mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
feat(mcp): add deleteDocument tool for permanent deletion by ID
The MCP server exposes every document operation except deletion: a connector user who spots a stale document via listDocuments has to open the web app to remove it. The only removal path, add_memory's forget action, matches by content similarity above 0.85 and can therefore delete the wrong memory when several are close. Add a deleteDocument tool that removes one document (and its extracted memories) by exact documentId via the existing documents.delete SDK call. It carries destructive-tool annotations so MCP hosts that surface them prompt before running it. Docs table and forget section updated.
This commit is contained in:
parent
2731de5c06
commit
bf1390ae82
6 changed files with 145 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
77
apps/mcp/src/server/tools/delete-document.test.ts
Normal file
77
apps/mcp/src/server/tools/delete-document.test.ts
Normal file
|
|
@ -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<SupermemoryClient>) {
|
||||
let handler: ToolHandler | undefined
|
||||
let config: Record<string, unknown> | undefined
|
||||
const registerTool = vi.fn(
|
||||
(_name: string, toolConfig: Record<string, unknown>, 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
47
apps/mcp/src/server/tools/delete-document.ts
Normal file
47
apps/mcp/src/server/tools/delete-document.ts
Normal file
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -80,6 +80,13 @@ export const addMemoryOutputSchema = z.object({
|
|||
|
||||
export type AddMemoryOutput = z.infer<typeof addMemoryOutputSchema>
|
||||
|
||||
export const deleteDocumentOutputSchema = z.object({
|
||||
success: z.boolean(),
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export type DeleteDocumentOutput = z.infer<typeof deleteDocumentOutputSchema>
|
||||
|
||||
export const getDocumentOutputSchema = z.object({
|
||||
document: z.object({
|
||||
id: z.string(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue