diff --git a/apps/docs/supermemory-mcp/mcp.mdx b/apps/docs/supermemory-mcp/mcp.mdx index f38845af..509619a8 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 one document and the memories extracted from it | `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 | @@ -70,6 +71,8 @@ Use the retrieval tools for different questions: `add_memory` saves the supplied `content` by default. Set `action` to `forget` when a fact is outdated or should be removed. There is no separate forget tool. +When the exact source is known, `deleteDocument` permanently removes that document and the memories extracted from it by `documentId` (from `listDocuments` or a memory result). Unlike `forget`, it does not rely on content matching, so it cannot remove the wrong memory. Deletion cannot be undone. + If the content is already final, the assistant should use `add_memory`. If you want to review, edit, or choose a space before saving, it should open the `guided-save` widget instead. ### Access control diff --git a/apps/mcp/src/server/client/index.ts b/apps/mcp/src/server/client/index.ts index cc45d438..4656845c 100644 --- a/apps/mcp/src/server/client/index.ts +++ b/apps/mcp/src/server/client/index.ts @@ -409,6 +409,15 @@ export class SupermemoryClient { } } + async deleteDocument(id: string): Promise<{ id: string }> { + try { + await this.client.documents.delete(id) + return { id } + } catch (error) { + this.handleOperationError("Delete document request", 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..1222cbe4 --- /dev/null +++ b/apps/mcp/src/server/tools/delete-document.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it, vi } from "vitest" +import type { SupermemoryClient } from "../client" +import * as deleteDocument from "./delete-document" +import { errorResult, type ToolDeps } from "./types" + +type ToolHandler = (args: { documentId: string }) => Promise<{ + content: Array<{ type: string; text: string }> + structuredContent?: { success: boolean; id: string } + isError?: boolean +}> + +function registerWithClient(client: Partial) { + let handler: ToolHandler | undefined + let config: Record | undefined + const registerTool = vi.fn( + (_name: string, toolConfig: Record, cb: ToolHandler) => { + config = toolConfig + handler = cb + return {} + }, + ) + + const deps = { + server: { registerTool }, + getClient: () => client as SupermemoryClient, + errorResult, + } as unknown as ToolDeps + + deleteDocument.register(deps) + if (!handler || !config) throw new Error("Tool was not registered") + return { handler, config, registerTool } +} + +describe("deleteDocument tool", () => { + it("deletes the document by id and reports success", async () => { + const deleteDocumentMock = vi.fn().mockResolvedValue({ id: "doc_123" }) + const { handler } = registerWithClient({ + deleteDocument: deleteDocumentMock, + }) + + const result = await handler({ documentId: "doc_123" }) + + expect(deleteDocumentMock).toHaveBeenCalledWith("doc_123") + expect(result.structuredContent).toEqual({ success: true, id: "doc_123" }) + expect(result.isError).toBeUndefined() + expect(result.content[0]?.text).toContain("doc_123") + }) + + it("returns an error result when deletion fails", async () => { + const { handler } = registerWithClient({ + deleteDocument: vi + .fn() + .mockRejectedValue(new Error("Document not found")), + }) + + const result = await handler({ documentId: "doc_missing" }) + + expect(result.isError).toBe(true) + expect(result.content[0]?.text).toContain("Document not found") + }) + + it("registers as a destructive, non-read-only tool", () => { + const { config, registerTool } = registerWithClient({ + deleteDocument: vi.fn(), + }) + + expect(registerTool).toHaveBeenCalledWith( + "deleteDocument", + expect.anything(), + expect.any(Function), + ) + expect(config?.annotations).toMatchObject({ + readOnlyHint: false, + destructiveHint: true, + }) + }) +}) 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..f1bd89c3 --- /dev/null +++ b/apps/mcp/src/server/tools/delete-document.ts @@ -0,0 +1,47 @@ +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 returned by listDocuments or a memory result"), + }) + + deps.server.registerTool( + "deleteDocument", + { + title: "Delete Document", + description: + "Permanently delete one stored document by ID, along with the memories extracted from it. This cannot be undone. Use listDocuments or getDocument first to confirm the target; prefer this over add_memory's forget action when the exact document is known.", + inputSchema, + outputSchema: deleteDocumentOutputSchema, + annotations: MEMORY_TOOL_ANNOTATIONS, + }, + async (args) => { + try { + const client = deps.getClient() + await client.deleteDocument(args.documentId) + const structuredContent: DeleteDocumentOutput = { + success: true, + id: args.documentId, + } + return { + content: [ + textContent(`Deleted document ${args.documentId} permanently.`), + ], + 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 0156d9d1..c1950393 100644 --- a/apps/mcp/src/server/tools/output-schemas.ts +++ b/apps/mcp/src/server/tools/output-schemas.ts @@ -80,6 +80,13 @@ export const addMemoryOutputSchema = z.object({ export type AddMemoryOutput = z.infer +export const deleteDocumentOutputSchema = z.object({ + success: z.boolean(), + id: z.string(), +}) + +export type DeleteDocumentOutput = z.infer + export const getDocumentOutputSchema = z.object({ document: z.object({ id: z.string(),