From 91c2f5a25ffae314f297c1c8e6b554063a02d579 Mon Sep 17 00:00:00 2001 From: Siddhesh Gawade Date: Wed, 14 Oct 2026 21:33:06 +0530 Subject: [PATCH] feat(mcp): support optional metadata in add_memory and save-memory tools --- apps/mcp/src/server/client/index.test.ts | 57 ++++++++++++++++++++++++ apps/mcp/src/server/client/index.ts | 3 +- apps/mcp/src/server/tools/add-memory.ts | 6 ++- apps/mcp/src/server/tools/compat.test.ts | 14 ++++++ apps/mcp/src/server/tools/save-memory.ts | 6 ++- 5 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 apps/mcp/src/server/client/index.test.ts diff --git a/apps/mcp/src/server/client/index.test.ts b/apps/mcp/src/server/client/index.test.ts new file mode 100644 index 00000000..b246faa6 --- /dev/null +++ b/apps/mcp/src/server/client/index.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it, vi } from "vitest" +import { SupermemoryClient } from "./index" + +describe("SupermemoryClient.createMemory", () => { + it("passes sm_source by default when no metadata is provided", async () => { + const client = new SupermemoryClient("test-token", "test-ns") + const addMock = vi.fn().mockResolvedValue({ + id: "doc-123", + status: "queued", + }) + ;(client as unknown as { client: { add: typeof addMock } }).client = { + add: addMock, + } + + const result = await client.createMemory("hello world") + + expect(addMock).toHaveBeenCalledWith("test-ns", { + content: "hello world", + metadata: { sm_source: "supermemory-mcp" }, + }) + expect(result).toEqual({ + id: "doc-123", + status: "queued", + namespace: "test-ns", + }) + }) + + it("merges custom metadata with sm_source", async () => { + const client = new SupermemoryClient("test-token", "test-ns") + const addMock = vi.fn().mockResolvedValue({ + id: "doc-456", + status: "queued", + }) + ;(client as unknown as { client: { add: typeof addMock } }).client = { + add: addMock, + } + + const result = await client.createMemory("project fact", { + client: "claude", + project: "supermemory-mcp", + }) + + expect(addMock).toHaveBeenCalledWith("test-ns", { + content: "project fact", + metadata: { + sm_source: "supermemory-mcp", + client: "claude", + project: "supermemory-mcp", + }, + }) + expect(result).toEqual({ + id: "doc-456", + status: "queued", + namespace: "test-ns", + }) + }) +}) diff --git a/apps/mcp/src/server/client/index.ts b/apps/mcp/src/server/client/index.ts index 39256cf7..c79ae1e8 100644 --- a/apps/mcp/src/server/client/index.ts +++ b/apps/mcp/src/server/client/index.ts @@ -191,11 +191,12 @@ export class SupermemoryClient { async createMemory( content: string, + metadata?: Record, ): Promise<{ id: string; status: string; namespace: string }> { try { const result = await this.client.add(this.namespace, { content, - metadata: { sm_source: MCP_SOURCE }, + metadata: { sm_source: MCP_SOURCE, ...metadata }, }) return { id: result.id, diff --git a/apps/mcp/src/server/tools/add-memory.ts b/apps/mcp/src/server/tools/add-memory.ts index 97bdd6da..d76c5eb2 100644 --- a/apps/mcp/src/server/tools/add-memory.ts +++ b/apps/mcp/src/server/tools/add-memory.ts @@ -23,6 +23,10 @@ export function register(deps: ToolDeps) { .describe( "Deprecated compatibility input. Use forget_memory to forget content; omit action to save.", ), + metadata: z + .record(z.string(), z.unknown()) + .optional() + .describe("Optional key-value metadata to attach to the memory"), namespace: optionalNamespaceSchema, ...legacyNamespaceInput, }) @@ -42,7 +46,7 @@ export function register(deps: ToolDeps) { const namespace = await deps.resolveNamespace(namespaceArg(args)) const client = deps.getClient(namespace) - const result = await client.createMemory(args.content) + const result = await client.createMemory(args.content, args.metadata) const message = `Memory saved (ID: ${result.id}, space: ${result.namespace})` const structuredContent: AddMemoryOutput = withLegacyNamespace({ action: "save", diff --git a/apps/mcp/src/server/tools/compat.test.ts b/apps/mcp/src/server/tools/compat.test.ts index 5eba1d1a..61bac420 100644 --- a/apps/mcp/src/server/tools/compat.test.ts +++ b/apps/mcp/src/server/tools/compat.test.ts @@ -113,6 +113,20 @@ describe("containerTag compatibility", () => { ).not.toThrow() }) + it("passes optional metadata through to client.createMemory", async () => { + const { deps, tools, client } = fakeDeps() + addMemory.register(deps) + const tool = tools.get("add_memory") + if (!tool) throw new Error("add_memory not registered") + const metadata = { client: "claude", project: "secret-project" } + const result = await tool.cb({ content: "save this note", metadata }, {}) + expect(client.createMemory).toHaveBeenCalledWith("save this note", metadata) + expect(result.structuredContent).toMatchObject({ + action: "save", + success: true, + }) + }) + it("treats containerTag as the required namespace for set-active-tag", async () => { const { deps, tools } = fakeDeps() setActiveTag.register(deps) diff --git a/apps/mcp/src/server/tools/save-memory.ts b/apps/mcp/src/server/tools/save-memory.ts index c844a3b6..036b0487 100644 --- a/apps/mcp/src/server/tools/save-memory.ts +++ b/apps/mcp/src/server/tools/save-memory.ts @@ -13,6 +13,10 @@ export function register(deps: ToolDeps) { description: "Save content to memory", inputSchema: z.object({ content: z.string().min(1), + metadata: z + .record(z.string(), z.unknown()) + .optional() + .describe("Optional key-value metadata to attach to the memory"), namespace: namespaceSchema.optional(), ...legacyNamespaceInput, viewId: z.string().uuid().optional(), @@ -28,7 +32,7 @@ export function register(deps: ToolDeps) { return deps.errorResult(new Error("namespace is required")) const viewId = args.viewId ?? crypto.randomUUID() const client = deps.getClient(namespace) - const result = await client.createMemory(args.content) + const result = await client.createMemory(args.content, args.metadata) const sc: ViewMessage = withLegacyView({ view: "save-success", viewId,