fix(mcp-tools): add confirm_name parameter to destructive workspace tools (#58) #60
@@ -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()", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user