fix(mcp-tools): add confirm_name parameter to destructive workspace tools (#58) #60

Merged
agent-reviewer merged 1 commits from fix/mcp-58-confirm-name-destructive-actions into main 2026-06-11 03:12:59 +00:00
5 changed files with 39 additions and 7 deletions
+12
View File
@@ -543,6 +543,18 @@ describe("handleDeleteWorkspace()", () => {
expect.objectContaining({ method: "DELETE" })
);
});
test("sends X-Confirm-Name header when confirm_name is provided", async () => {
global.fetch = mockFetch({ deleted: true });
await handleDeleteWorkspace({ workspace_id: "ws-del", confirm_name: "Test-PM" });
expect(global.fetch).toHaveBeenCalledWith(
`${PLATFORM_URL}/workspaces/ws-del?confirm=true`,
expect.objectContaining({
method: "DELETE",
headers: expect.objectContaining({ "X-Confirm-Name": "Test-PM" }),
})
);
});
});
describe("handleRestartWorkspace()", () => {
+10
View File
@@ -234,6 +234,16 @@ describe("workspace lifecycle tools", () => {
expect(url).toBe(`${HOST}/workspaces/w1`);
expect(init.method).toBe("DELETE");
});
it("deprovision_workspace sends X-Confirm-Name when confirm_name is provided", async () => {
const f = mockFetch({ ok: true });
global.fetch = f as unknown as typeof fetch;
await handleDeprovisionWorkspace({ workspace_id: "w1", confirm_name: "Test-PM" });
const { url, init } = lastCall(f);
expect(url).toBe(`${HOST}/workspaces/w1`);
expect(init.method).toBe("DELETE");
expect(headersOf(init)["X-Confirm-Name"]).toBe("Test-PM");
});
});
describe("budget + billing tools", () => {
+2 -1
View File
@@ -99,6 +99,7 @@ export async function mgmtCall<T = unknown>(
method: string,
path: string,
body?: unknown,
extraHeaders?: Record<string, string>,
): Promise<T | ApiError> {
const headers = managementHeaders();
if (!isHeaders(headers)) return headers;
@@ -106,7 +107,7 @@ export async function mgmtCall<T = unknown>(
const base = managementUrl();
const res = await fetch(`${base}${path}`, {
method,
headers,
headers: { ...headers, ...(extraHeaders ?? {}) },
body: body !== undefined ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
+7 -2
View File
@@ -44,6 +44,7 @@ const ProvisionWorkspaceSchema = z.object({
const DeprovisionWorkspaceSchema = z.object({
workspace_id: z.string().describe("Workspace UUID"),
confirm_name: z.string().optional().describe("Echo the workspace's exact name to confirm destructive action (maps to X-Confirm-Name header)"),
});
const WorkspaceLifecycleSchema = z.object({
@@ -197,7 +198,8 @@ export async function handleProvisionWorkspace(args: unknown) {
export async function handleDeprovisionWorkspace(args: unknown) {
const p = validate(args, DeprovisionWorkspaceSchema);
return toMcpResult(await mgmtCall("DELETE", `/workspaces/${encodeURIComponent(p.workspace_id)}`));
const headers = p.confirm_name ? { "X-Confirm-Name": p.confirm_name } : undefined;
return toMcpResult(await mgmtCall("DELETE", `/workspaces/${encodeURIComponent(p.workspace_id)}`, undefined, headers));
}
export async function handleRestartWorkspace(args: unknown) {
@@ -413,7 +415,10 @@ export function registerManagementTools(srv: McpServer) {
srv.tool(
"deprovision_workspace",
"Management: delete/deprovision a workspace (cascades to children).",
{ workspace_id: z.string().describe("Workspace UUID") },
{
workspace_id: z.string().describe("Workspace UUID"),
confirm_name: z.string().optional().describe("Echo the workspace's exact name to confirm destructive action"),
},
handleDeprovisionWorkspace,
);
srv.tool(
+8 -4
View File
@@ -329,8 +329,9 @@ export async function handleGetWorkspace(params: { workspace_id: string }) {
return toMcpResult(data);
}
export async function handleDeleteWorkspace(params: { workspace_id: string }) {
const data = await apiCall("DELETE", `/workspaces/${params.workspace_id}?confirm=true`);
export async function handleDeleteWorkspace(params: { workspace_id: string; confirm_name?: string }) {
const headers = params.confirm_name ? { "X-Confirm-Name": params.confirm_name } : undefined;
const data = await apiCall("DELETE", `/workspaces/${params.workspace_id}?confirm=true`, undefined, headers);
return toMcpResult(data);
}
@@ -434,8 +435,11 @@ export function registerWorkspaceTools(srv: McpServer) {
srv.tool(
"delete_workspace",
"Delete a workspace (cascades to children)",
{ workspace_id: z.string().describe("Workspace ID") },
"Delete a workspace (cascades to children).",
{
workspace_id: z.string().describe("Workspace ID"),
confirm_name: z.string().optional().describe("Echo the workspace's exact name to confirm destructive action"),
},
handleDeleteWorkspace
);