From ed3767deb7b0ab4139649fe8305abd38b50f5680 Mon Sep 17 00:00:00 2001 From: devaniketh Date: Tue, 1 Sep 2026 19:04:32 +0530 Subject: [PATCH] feat(mcp): add deleteDocument tool for exact-ID removal (#1442) --- apps/docs/supermemory-mcp/mcp.mdx | 1 + apps/mcp/README.md | 1 + apps/mcp/e2e/discovery.test.ts | 1 + apps/mcp/src/server/analytics.ts | 1 + apps/mcp/src/server/client/index.ts | 8 ++ .../src/server/tools/delete-document.test.ts | 111 ++++++++++++++++++ apps/mcp/src/server/tools/delete-document.ts | 60 ++++++++++ apps/mcp/src/server/tools/index.ts | 2 + apps/mcp/src/server/tools/output-schemas.ts | 8 ++ 9 files changed, 193 insertions(+) create mode 100644 apps/mcp/src/server/tools/delete-document.test.ts create mode 100644 apps/mcp/src/server/tools/delete-document.ts diff --git a/apps/docs/supermemory-mcp/mcp.mdx b/apps/docs/supermemory-mcp/mcp.mdx index f38845af..af987fa4 100644 --- a/apps/docs/supermemory-mcp/mcp.mdx +++ b/apps/docs/supermemory-mcp/mcp.mdx @@ -50,6 +50,7 @@ Your assistant chooses these tools automatically. Use this table when you need t | `add_memory` | Save information or forget outdated information | `content` (required), `action` (`save` or `forget`), `containerTag` | Save or forget confirmation | | `listDocuments` | Browse stored source documents and their summaries | `page`, `limit`, `containerTag` | Document IDs, titles, types, status, dates, and summaries | | `getDocument` | Read the available content of one document | `documentId` (required) | Document metadata, summary, and available content | +| `deleteDocument` | Permanently delete a document by ID | `documentId` (required) | Deletion confirmation | | `listMemories` | Browse recent extracted memory entries and their source document IDs | `page`, `limit`, `containerTag` | Memory IDs, text, versions, and source document IDs | | `listSpaces` | List accessible spaces and resolve a space name to its key | None | Formatted list plus structured `spaces` and `count` fields | | `whoAmI` | Inspect the authenticated account, permissions, scope, and active space | None | Account and access context | diff --git a/apps/mcp/README.md b/apps/mcp/README.md index 1cf39bc1..bf65aaa0 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -51,6 +51,7 @@ The client discovers the OAuth authorization server through | `search_memory` | Search memories and optionally include profile context | | `listDocuments` | List document metadata and summaries in a space | | `getDocument` | Read one document's available content by ID | +| `deleteDocument` | Permanently delete a document by ID | | `listMemories` | List extracted memory entries and their source document IDs | | `listSpaces` | List spaces visible to the authenticated account | | `whoAmI` | Return identity, access, and active-space context | diff --git a/apps/mcp/e2e/discovery.test.ts b/apps/mcp/e2e/discovery.test.ts index 3da201c2..a5edc48a 100644 --- a/apps/mcp/e2e/discovery.test.ts +++ b/apps/mcp/e2e/discovery.test.ts @@ -9,6 +9,7 @@ import { const EXPECTED_TOOLS = [ "add_memory", + "deleteDocument", "fetch-graph-data", "getDocument", "guided-save", diff --git a/apps/mcp/src/server/analytics.ts b/apps/mcp/src/server/analytics.ts index aac932d1..87df3f7b 100644 --- a/apps/mcp/src/server/analytics.ts +++ b/apps/mcp/src/server/analytics.ts @@ -36,6 +36,7 @@ const TOOL_SURFACES: Record = { search_memory: "model_tool", listDocuments: "model_tool", getDocument: "model_tool", + deleteDocument: "model_tool", listMemories: "model_tool", listSpaces: "model_tool", whoAmI: "model_tool", diff --git a/apps/mcp/src/server/client/index.ts b/apps/mcp/src/server/client/index.ts index a2ccc8e7..4ea4e331 100644 --- a/apps/mcp/src/server/client/index.ts +++ b/apps/mcp/src/server/client/index.ts @@ -393,6 +393,14 @@ export class SupermemoryClient { } } + async deleteDocument(id: string): Promise { + try { + await this.client.documents.delete(id) + } catch (error) { + this.handleError(error) + } + } + async listMemoryEntries( page = 1, limit = 50, diff --git a/apps/mcp/src/server/tools/delete-document.test.ts b/apps/mcp/src/server/tools/delete-document.test.ts new file mode 100644 index 00000000..dc6165e0 --- /dev/null +++ b/apps/mcp/src/server/tools/delete-document.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it, vi } from "vitest" +import type { SupermemoryClient } from "../client" +import { register } from "./delete-document" +import { errorResult, type ToolDeps } from "./types" + +function createMockDeps(overrides: Partial = {}): { + deps: ToolDeps + registeredTool: { + name: string + config: any + handler: (args: any) => Promise + } + mockClient: { + getDocument: ReturnType + deleteDocument: ReturnType + } +} { + let registeredTool: any = null + const registerTool = vi.fn((name, config, handler) => { + registeredTool = { name, config, handler } + }) + + const mockClient = { + getDocument: vi.fn(), + deleteDocument: vi.fn(), + } + + const deps: ToolDeps = { + server: { registerTool } as any, + actor: { + userId: "user_123", + organizationId: "org_123", + bearerToken: "token_123", + scopes: [], + }, + getClient: vi.fn(() => mockClient as unknown as SupermemoryClient), + getSession: vi.fn(), + resolveContainerTag: vi.fn().mockResolvedValue("space_default"), + getActiveContainerTag: vi.fn().mockResolvedValue("space_default"), + setActiveContainerTag: vi.fn(), + createUploadSession: vi.fn(), + getClientInfo: vi.fn(), + errorResult, + ...overrides, + } + + register(deps) + + return { deps, registeredTool, mockClient } +} + +describe("deleteDocument tool", () => { + it("registers with destructiveHint: true and correct schema", () => { + const { registeredTool } = createMockDeps() + + expect(registeredTool.name).toBe("deleteDocument") + expect(registeredTool.config.annotations.destructiveHint).toBe(true) + expect(registeredTool.config.annotations.readOnlyHint).toBe(false) + expect(registeredTool.config.inputSchema).toBeDefined() + }) + + it("successfully deletes a document belonging to the active space", async () => { + const { registeredTool, mockClient } = createMockDeps() + + mockClient.getDocument.mockResolvedValue({ + id: "doc_123", + containerTags: ["space_default"], + title: "My Document", + }) + mockClient.deleteDocument.mockResolvedValue(undefined) + + const result = await registeredTool.handler({ documentId: "doc_123" }) + + expect(mockClient.getDocument).toHaveBeenCalledWith("doc_123") + expect(mockClient.deleteDocument).toHaveBeenCalledWith("doc_123") + expect(result.structuredContent).toEqual({ + documentId: "doc_123", + success: true, + message: "Document doc_123 permanently deleted.", + }) + expect(result.content[0].text).toContain("permanently deleted") + }) + + it("rejects deletion of a document from a different space with 'Document not found'", async () => { + const { registeredTool, mockClient } = createMockDeps() + + mockClient.getDocument.mockResolvedValue({ + id: "doc_other", + containerTags: ["other_space"], + title: "Private Document", + }) + + const result = await registeredTool.handler({ documentId: "doc_other" }) + + expect(mockClient.getDocument).toHaveBeenCalledWith("doc_other") + expect(mockClient.deleteDocument).not.toHaveBeenCalled() + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain("Document not found") + }) + + it("propagates client errors via errorResult", async () => { + const { registeredTool, mockClient } = createMockDeps() + + mockClient.getDocument.mockRejectedValue(new Error("Network timeout")) + + const result = await registeredTool.handler({ documentId: "doc_123" }) + + expect(result.isError).toBe(true) + expect(result.content[0].text).toContain("Error: Network timeout") + }) +}) diff --git a/apps/mcp/src/server/tools/delete-document.ts b/apps/mcp/src/server/tools/delete-document.ts new file mode 100644 index 00000000..34ae5691 --- /dev/null +++ b/apps/mcp/src/server/tools/delete-document.ts @@ -0,0 +1,60 @@ +import { z } from "zod" +import { MEMORY_TOOL_ANNOTATIONS } from "./annotations" +import { + deleteDocumentOutputSchema, + type DeleteDocumentOutput, +} from "./output-schemas" +import { textContent, type ToolDeps } from "./types" + +export function register(deps: ToolDeps) { + const inputSchema = z.object({ + documentId: z + .string() + .min(1, "Document ID is required") + .max(255, "Document ID exceeds maximum length") + .describe("Document ID to permanently delete, as returned by listDocuments or getDocument"), + }) + + deps.server.registerTool( + "deleteDocument", + { + title: "Delete Document", + description: + "Permanently delete a document by its exact ID. Use listDocuments to discover document IDs before calling this tool.", + inputSchema, + outputSchema: deleteDocumentOutputSchema, + annotations: MEMORY_TOOL_ANNOTATIONS, + }, + async (args) => { + try { + const effectiveTag = await deps.resolveContainerTag() + const client = deps.getClient() + const document = await client.getDocument(args.documentId) + const docTags = document.containerTags + if ( + Array.isArray(docTags) && + docTags.length > 0 && + !docTags.includes(effectiveTag) + ) { + throw new Error("Document not found") + } + + await client.deleteDocument(args.documentId) + + const message = `Document ${args.documentId} permanently deleted.` + const structuredContent: DeleteDocumentOutput = { + documentId: args.documentId, + success: true, + message, + } + + return { + content: [textContent(message)], + structuredContent, + } + } catch (error) { + return deps.errorResult(error) + } + }, + ) +} diff --git a/apps/mcp/src/server/tools/index.ts b/apps/mcp/src/server/tools/index.ts index 6effe28d..3ba70d15 100644 --- a/apps/mcp/src/server/tools/index.ts +++ b/apps/mcp/src/server/tools/index.ts @@ -1,4 +1,5 @@ import * as addMemory from "./add-memory" +import * as deleteDocument from "./delete-document" import * as fetchGraphData from "./fetch-graph-data" import * as getDocument from "./get-document" import * as guidedSave from "./guided-save" @@ -19,6 +20,7 @@ export function registerAllTools(deps: ToolDeps) { searchMemory.register(deps) listDocuments.register(deps) getDocument.register(deps) + deleteDocument.register(deps) listMemories.register(deps) listContainerTags.register(deps) whoAmI.register(deps) diff --git a/apps/mcp/src/server/tools/output-schemas.ts b/apps/mcp/src/server/tools/output-schemas.ts index 2ea8bb3b..0aaa32b4 100644 --- a/apps/mcp/src/server/tools/output-schemas.ts +++ b/apps/mcp/src/server/tools/output-schemas.ts @@ -71,6 +71,14 @@ export const getDocumentOutputSchema = z.object({ export type GetDocumentOutput = z.infer +export const deleteDocumentOutputSchema = z.object({ + documentId: z.string(), + success: z.boolean(), + message: z.string(), +}) + +export type DeleteDocumentOutput = z.infer + export const listDocumentsOutputSchema = z.object({ documents: z.array(documentSummarySchema), pagination: paginationSchema,