From 5a6425b5e954ebad9aad87bee086da0a0fbc3e87 Mon Sep 17 00:00:00 2001 From: Dhravya Date: Sun, 11 Oct 2026 02:37:14 +0000 Subject: [PATCH] fix(mcp): expose forget_memory as a separate tool Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- apps/docs/supermemory-mcp/mcp.mdx | 5 +- apps/mcp/README.md | 3 +- apps/mcp/e2e/discovery.test.ts | 1 + apps/mcp/src/server/tools/add-memory.ts | 28 ++-- .../src/server/tools/forget-memory.test.ts | 154 ++++++++++++++++++ apps/mcp/src/server/tools/forget-memory.ts | 52 ++++++ apps/mcp/src/server/tools/index.ts | 2 + apps/mcp/src/server/tools/output-schemas.ts | 4 + 8 files changed, 229 insertions(+), 20 deletions(-) create mode 100644 apps/mcp/src/server/tools/forget-memory.test.ts create mode 100644 apps/mcp/src/server/tools/forget-memory.ts diff --git a/apps/docs/supermemory-mcp/mcp.mdx b/apps/docs/supermemory-mcp/mcp.mdx index c7f5f6dc..2835f497 100644 --- a/apps/docs/supermemory-mcp/mcp.mdx +++ b/apps/docs/supermemory-mcp/mcp.mdx @@ -48,7 +48,8 @@ Your assistant chooses these tools automatically. Use this table when you need t | --- | --- | --- | --- | | `search_memory` | Semantic recall from one space | `query` (required), `containerTag` | Matching memories | | `get_profile` | Stable and recent profile context for one space | `containerTag` | Profile facts | -| `add_memory` | Save information or forget outdated information | `content` (required), `action` (`save` or `forget`), `containerTag` | Save or forget confirmation | +| `add_memory` | Save information | `content` (required), `namespace` | Save confirmation | +| `forget_memory` | Forget outdated information | `content` (required), `namespace` | Forget confirmation | | `list_documents` | Browse stored source documents and their summaries | `page`, `limit`, `containerTag` | Document IDs, titles, types, status, dates, and summaries | | `get_document` | Read the available content of one document | `documentId` (required) | Document metadata, summary, and available content | | `list_memories` | Browse recent extracted memory entries and their source document IDs | `page`, `limit`, `containerTag` | Memory IDs, text, versions, and source document IDs | @@ -70,7 +71,7 @@ Use the retrieval tools for different questions: ### Save or forget -`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. +`add_memory` saves the supplied `content`. Use `forget_memory` when a fact is outdated or should be removed. Both tools use the active space unless you pass `namespace`. The deprecated `containerTag` input and `add_memory` with `action: "forget"` remain supported for existing clients. Because the legacy action can remove information, `add_memory` retains its destructive safety annotation. 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. diff --git a/apps/mcp/README.md b/apps/mcp/README.md index 9608db6e..26476920 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -55,7 +55,8 @@ The client discovers the OAuth authorization server through | `list_memories` | List extracted memory entries and their source document IDs | | `list_spaces` | List spaces visible to the authenticated account | | `who_am_i` | Return identity, access, and active-space context | -| `add_memory` | Save or forget a memory | +| `add_memory` | Save a memory (legacy `action: "forget"` remains supported) | +| `forget_memory` | Forget a memory matching the supplied content | ### MCP App launchers diff --git a/apps/mcp/e2e/discovery.test.ts b/apps/mcp/e2e/discovery.test.ts index c252105a..2260fc7d 100644 --- a/apps/mcp/e2e/discovery.test.ts +++ b/apps/mcp/e2e/discovery.test.ts @@ -10,6 +10,7 @@ import { const EXPECTED_TOOLS = [ "add_memory", "fetch-graph-data", + "forget_memory", "getDocument", "get_document", "get_profile", diff --git a/apps/mcp/src/server/tools/add-memory.ts b/apps/mcp/src/server/tools/add-memory.ts index 104dd87b..97bdd6da 100644 --- a/apps/mcp/src/server/tools/add-memory.ts +++ b/apps/mcp/src/server/tools/add-memory.ts @@ -6,6 +6,7 @@ import { withLegacyNamespace, } from "./compat" import { MEMORY_TOOL_ANNOTATIONS } from "./annotations" +import { forgetMemory } from "./forget-memory" import { addMemoryOutputSchema, type AddMemoryOutput } from "./output-schemas" import { textContent, type ToolDeps } from "./types" @@ -14,8 +15,14 @@ export function register(deps: ToolDeps) { content: z .string() .max(200000, "Content exceeds maximum length") - .describe("The memory content to save or forget"), - action: z.enum(["save", "forget"]).optional().default("save"), + .describe("The memory content to save"), + action: z + .enum(["save", "forget"]) + .optional() + .default("save") + .describe( + "Deprecated compatibility input. Use forget_memory to forget content; omit action to save.", + ), namespace: optionalNamespaceSchema, ...legacyNamespaceInput, }) @@ -24,30 +31,17 @@ export function register(deps: ToolDeps) { "add_memory", { description: - "Add (save) or forget a memory in the user's ACTIVE space. Defaults to 'save'. The target space is the one the user selected via select-space; pass namespace only to override it. Use 'forget' when information is outdated or the user asks to remove it.", + "Save a memory in the user's ACTIVE space. The target space is the one the user selected via select-space; pass namespace only to override it. To remove outdated information, use the separately exposed forget_memory tool. Legacy action='forget' remains supported for existing clients.", inputSchema, outputSchema: addMemoryOutputSchema, annotations: MEMORY_TOOL_ANNOTATIONS, }, async (args) => { + if (args.action === "forget") return forgetMemory(deps, args) try { const namespace = await deps.resolveNamespace(namespaceArg(args)) const client = deps.getClient(namespace) - if (args.action === "forget") { - const result = await client.forgetMemory(args.content) - const structuredContent: AddMemoryOutput = withLegacyNamespace({ - action: "forget", - success: result.success, - namespace: result.namespace, - message: result.message, - }) - return { - content: [textContent(result.message)], - structuredContent, - } - } - const result = await client.createMemory(args.content) const message = `Memory saved (ID: ${result.id}, space: ${result.namespace})` const structuredContent: AddMemoryOutput = withLegacyNamespace({ diff --git a/apps/mcp/src/server/tools/forget-memory.test.ts b/apps/mcp/src/server/tools/forget-memory.test.ts new file mode 100644 index 00000000..6e1b773c --- /dev/null +++ b/apps/mcp/src/server/tools/forget-memory.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, it, vi } from "vitest" +import { z } from "zod" +import * as addMemory from "./add-memory" +import { MEMORY_TOOL_ANNOTATIONS } from "./annotations" +import { namespaceArg } from "./compat" +import * as forgetMemory from "./forget-memory" +import { errorResult, type ToolDeps } from "./types" + +function setup() { + const tools = new Map< + string, + { + config: { + description: string + inputSchema: z.ZodObject + outputSchema: z.ZodTypeAny + annotations: typeof MEMORY_TOOL_ANNOTATIONS + } + cb: (args: Record) => Promise + } + >() + const client = { + createMemory: vi.fn().mockResolvedValue({ + id: "d1", + status: "queued", + namespace: "active", + }), + forgetMemory: vi.fn().mockResolvedValue({ + success: true, + namespace: "active", + message: "Memory forgotten", + }), + } + const deps = { + server: { + registerTool: (name: string, config: unknown, cb: unknown) => { + tools.set(name, { config, cb } as NonNullable< + ReturnType + >) + }, + }, + getClient: vi.fn(() => client), + resolveNamespace: vi.fn(async (explicit?: string) => explicit ?? "active"), + errorResult, + } as unknown as ToolDeps + addMemory.register(deps) + forgetMemory.register(deps) + const getTool = (name: string) => { + const tool = tools.get(name) + if (!tool) throw new Error(`${name} not registered`) + return tool + } + return { tools, getTool, deps, client } +} + +describe("separate memory operations", () => { + it("exposes save and forget with descriptions, schemas, and safety hints", () => { + const { tools, getTool } = setup() + expect([...tools.keys()]).toEqual(["add_memory", "forget_memory"]) + for (const tool of tools.values()) { + expect(tool.config.description.length).toBeGreaterThan(0) + expect(tool.config.annotations).toEqual(MEMORY_TOOL_ANNOTATIONS) + expect(tool.config.inputSchema.safeParse({}).success).toBe(false) + expect( + tool.config.inputSchema.safeParse({ content: "x".repeat(200001) }) + .success, + ).toBe(false) + } + const schema = z.toJSONSchema(getTool("forget_memory").config.inputSchema) + expect(schema.properties).not.toHaveProperty("action") + }) + + it("saves by default without forgetting", async () => { + const { getTool, client } = setup() + const tool = getTool("add_memory") + const result = await tool.cb( + tool.config.inputSchema.parse({ content: "A fact" }), + ) + expect(client.createMemory).toHaveBeenCalledWith("A fact") + expect(client.forgetMemory).not.toHaveBeenCalled() + expect(result).toMatchObject({ + structuredContent: { action: "save", id: "d1" }, + }) + }) + + it.each([ + [{}, "active"], + [{ namespace: "explicit" }, "explicit"], + [{ containerTag: "legacy" }, "legacy"], + [{ namespace: "explicit", containerTag: "legacy" }, "explicit"], + ])("forgets in the resolved space for %j", async (scope, namespace) => { + const { getTool, deps, client } = setup() + const tool = getTool("forget_memory") + const result = await tool.cb( + tool.config.inputSchema.parse({ content: "Old fact", ...scope }), + ) + expect(deps.resolveNamespace).toHaveBeenCalledWith(namespaceArg(scope)) + expect(deps.getClient).toHaveBeenCalledWith(namespace) + expect(client.forgetMemory).toHaveBeenCalledWith("Old fact") + expect(client.createMemory).not.toHaveBeenCalled() + expect(result).toMatchObject({ + structuredContent: { + action: "forget", + containerTag: "active", + success: true, + }, + }) + expect( + tool.config.outputSchema.safeParse( + (result as { structuredContent: unknown }).structuredContent, + ).success, + ).toBe(true) + }) + + it("preserves the legacy forget action and response", async () => { + const { getTool, client } = setup() + const invoke = (name: string, args: Record) => { + const tool = getTool(name) + return tool.cb(tool.config.inputSchema.parse(args)) + } + const legacy = await invoke("add_memory", { + content: "Old fact", + action: "forget", + }) + const separate = await invoke("forget_memory", { content: "Old fact" }) + expect(legacy).toEqual(separate) + expect(client.createMemory).not.toHaveBeenCalled() + }) + + it("returns unsuccessful forget results without saving", async () => { + const { getTool, client } = setup() + client.forgetMemory.mockResolvedValueOnce({ + success: false, + namespace: "active", + message: "No matching memory found", + }) + const result = await getTool("forget_memory").cb({ content: "Missing" }) + expect(result).toMatchObject({ structuredContent: { success: false } }) + expect(client.createMemory).not.toHaveBeenCalled() + }) + + it("returns errors from namespace resolution and the client", async () => { + const { getTool, deps, client } = setup() + vi.mocked(deps.resolveNamespace).mockRejectedValueOnce(new Error("Denied")) + expect(await getTool("forget_memory").cb({ content: "Old fact" })).toEqual( + errorResult(new Error("Denied")), + ) + expect(client.forgetMemory).not.toHaveBeenCalled() + client.forgetMemory.mockRejectedValueOnce(new Error("Unavailable")) + expect(await getTool("forget_memory").cb({ content: "Old fact" })).toEqual( + errorResult(new Error("Unavailable")), + ) + }) +}) diff --git a/apps/mcp/src/server/tools/forget-memory.ts b/apps/mcp/src/server/tools/forget-memory.ts new file mode 100644 index 00000000..1369fbde --- /dev/null +++ b/apps/mcp/src/server/tools/forget-memory.ts @@ -0,0 +1,52 @@ +import { z } from "zod" +import { optionalNamespaceSchema } from "../namespace" +import { MEMORY_TOOL_ANNOTATIONS } from "./annotations" +import { + legacyNamespaceInput, + namespaceArg, + withLegacyNamespace, +} from "./compat" +import { forgetMemoryOutputSchema } from "./output-schemas" +import { textContent, type ToolDeps } from "./types" + +export async function forgetMemory( + deps: ToolDeps, + args: { content: string; namespace?: string; containerTag?: string }, +) { + try { + const namespace = await deps.resolveNamespace(namespaceArg(args)) + const result = await deps.getClient(namespace).forgetMemory(args.content) + return { + content: [textContent(result.message)], + structuredContent: withLegacyNamespace({ + action: "forget" as const, + success: result.success, + namespace: result.namespace, + message: result.message, + }), + } + } catch (error) { + return deps.errorResult(error) + } +} + +export function register(deps: ToolDeps) { + deps.server.registerTool( + "forget_memory", + { + description: + "Forget a memory matching the supplied content in the user's ACTIVE space. Use when information is outdated or the user asks to remove it. The target space is the one selected via select-space; pass namespace only to override it.", + inputSchema: z.object({ + content: z + .string() + .max(200000, "Content exceeds maximum length") + .describe("The memory content to forget"), + namespace: optionalNamespaceSchema, + ...legacyNamespaceInput, + }), + outputSchema: forgetMemoryOutputSchema, + annotations: MEMORY_TOOL_ANNOTATIONS, + }, + async (args) => forgetMemory(deps, args), + ) +} diff --git a/apps/mcp/src/server/tools/index.ts b/apps/mcp/src/server/tools/index.ts index 26eb454c..15d786ff 100644 --- a/apps/mcp/src/server/tools/index.ts +++ b/apps/mcp/src/server/tools/index.ts @@ -1,5 +1,6 @@ import * as addMemory from "./add-memory" import * as fetchGraphData from "./fetch-graph-data" +import * as forgetMemory from "./forget-memory" import * as getDocument from "./get-document" import * as getProfile from "./get-profile" import * as guidedSave from "./guided-save" @@ -68,6 +69,7 @@ export function registerAllTools(toolDeps: ToolDeps) { memoryGraph.register(deps) fetchGraphData.register(deps) addMemory.register(deps) + forgetMemory.register(deps) guidedSave.register(deps) saveMemory.register(deps) uploadFile.register(deps) diff --git a/apps/mcp/src/server/tools/output-schemas.ts b/apps/mcp/src/server/tools/output-schemas.ts index 72a9065d..0fe1e96e 100644 --- a/apps/mcp/src/server/tools/output-schemas.ts +++ b/apps/mcp/src/server/tools/output-schemas.ts @@ -29,6 +29,10 @@ export const addMemoryOutputSchema = z.object({ export type AddMemoryOutput = z.infer +export const forgetMemoryOutputSchema = addMemoryOutputSchema.extend({ + action: z.literal("forget"), +}) + export const getDocumentOutputSchema = z.object({ namespace: z.string(), ...legacyNamespaceOutput,