From bc7796df3fcaf8ebbb119e0e7b90f7436e8b01b9 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:04:23 -0700 Subject: [PATCH] feat: add listMemories tool to the Supermemory MCP server --- apps/mcp/src/client.ts | 197 ++++++++++++++++++++++++++++++++++++++++- apps/mcp/src/server.ts | 110 +++++++++++++++++++++++ 2 files changed, 306 insertions(+), 1 deletion(-) diff --git a/apps/mcp/src/client.ts b/apps/mcp/src/client.ts index 8fdb6748d..762414e28 100644 --- a/apps/mcp/src/client.ts +++ b/apps/mcp/src/client.ts @@ -2,6 +2,8 @@ import Supermemory from "supermemory" const MAX_CHARS = 200000 // ~50k tokens (character-based limit) const DEFAULT_PROJECT_ID = "sm_project_default" +const DEFAULT_LIST_LIMIT = 50 +const MAX_LIST_LIMIT = 200 export type Memory = | { @@ -25,6 +27,48 @@ export interface SearchResult { timing: number } +type ListMemoryBase = { + id: string + title?: string + content?: string + summary?: string + createdAt?: string + updatedAt?: string + metadata?: unknown + status?: string + containerTags?: string[] + customId?: string | null + type?: string + connectionId?: string | null +} + +export type ListedMemory = + | (ListMemoryBase & { memory: string }) + | (ListMemoryBase & { chunk: string }) + +export type ListMemoriesSort = + | "createdAt" + | "updatedAt" + | "-createdAt" + | "-updatedAt" + | "createdAt:asc" + | "createdAt:desc" + | "updatedAt:asc" + | "updatedAt:desc" + +export interface ListMemoriesOptions { + containerTag?: string + limit?: number + cursor?: string + sort?: ListMemoriesSort + filter?: string +} + +export interface ListMemoriesResult { + memories: ListedMemory[] + nextCursor: string | null +} + export interface Profile { static: string[] dynamic: string[] @@ -82,7 +126,7 @@ export interface DocumentsApiResponse { } } -export function getMemoryText(m: Memory): string { +export function getMemoryText(m: Memory | ListedMemory): string { return "memory" in m ? m.memory : m.chunk } @@ -101,6 +145,121 @@ interface SDKResult { context?: string } +interface SDKListMemory { + id: string + memory?: string + chunk?: string + content?: string | null + summary?: string | null + title?: string | null + createdAt?: string + updatedAt?: string + metadata?: unknown + status?: string + containerTags?: string[] + customId?: string | null + type?: string + connectionId?: string | null +} + +interface SDKListResponse { + memories?: SDKListMemory[] + results?: SDKListMemory[] + pagination?: { + currentPage: number + totalPages: number + } + nextCursor?: string | null +} + +function clampListLimit(limit = DEFAULT_LIST_LIMIT): number { + return Math.min(Math.max(Math.trunc(limit), 1), MAX_LIST_LIMIT) +} + +function parseListCursor(cursor?: string): number { + if (!cursor) return 1 + + const page = Number(cursor) + if (!Number.isInteger(page) || page < 1) { + throw new Error( + "Invalid cursor. Use the nextCursor value from listMemories.", + ) + } + return page +} + +function parseListSort(sort: ListMemoriesSort = "-createdAt"): { + sort: "createdAt" | "updatedAt" + order: "asc" | "desc" +} { + if (sort.startsWith("-")) { + return { + sort: sort.slice(1) as "createdAt" | "updatedAt", + order: "desc", + } + } + + if (sort.includes(":")) { + const [field, order] = sort.split(":") as [ + "createdAt" | "updatedAt", + "asc" | "desc", + ] + return { sort: field, order } + } + + return { + sort: sort as "createdAt" | "updatedAt", + order: "desc", + } +} + +function normalizeListedMemory(memory: SDKListMemory): ListedMemory { + const content = + typeof memory.content === "string" + ? limitByChars(memory.content) + : undefined + const summary = + typeof memory.summary === "string" + ? limitByChars(memory.summary) + : undefined + const text = limitByChars( + memory.content || + memory.memory || + memory.chunk || + memory.summary || + memory.title || + "", + ) + const base: ListMemoryBase = { + id: memory.id, + title: memory.title || undefined, + content, + summary, + createdAt: memory.createdAt, + updatedAt: memory.updatedAt, + metadata: memory.metadata, + status: memory.status, + containerTags: memory.containerTags, + customId: memory.customId, + type: memory.type, + connectionId: memory.connectionId, + } + + if (memory.chunk && !memory.memory) { + return { ...base, chunk: text } + } + return { ...base, memory: text } +} + +function matchesFilter(memory: ListedMemory, filter?: string): boolean { + const normalizedFilter = filter?.trim().toLowerCase() + if (!normalizedFilter) return true + + return [getMemoryText(memory), memory.content, memory.summary, memory.title] + .filter((value): value is string => typeof value === "string") + .some((value) => value.toLowerCase().includes(normalizedFilter)) +} + export class SupermemoryClient { private client: Supermemory private containerTag: string @@ -255,6 +414,42 @@ export class SupermemoryClient { } } + // List memories/documents with pagination using SDK + async listMemories( + options: ListMemoriesOptions = {}, + ): Promise { + try { + const limit = clampListLimit(options.limit) + const page = parseListCursor(options.cursor) + const { sort, order } = parseListSort(options.sort) + const result = (await this.client.documents.list({ + containerTags: [options.containerTag || this.containerTag], + includeContent: true, + limit, + page, + sort, + order, + })) as SDKListResponse + const rawMemories = result.memories || result.results || [] + const memories = rawMemories + .map(normalizeListedMemory) + .filter((memory) => matchesFilter(memory, options.filter)) + const nextCursor = + result.nextCursor ?? + (result.pagination && + result.pagination.currentPage < result.pagination.totalPages + ? String(result.pagination.currentPage + 1) + : null) + + return { + memories, + nextCursor, + } + } catch (error) { + this.handleError(error) + } + } + // Get user profile using SDK async getProfile(query?: string): Promise { try { diff --git a/apps/mcp/src/server.ts b/apps/mcp/src/server.ts index 510c1481a..00d3692df 100644 --- a/apps/mcp/src/server.ts +++ b/apps/mcp/src/server.ts @@ -89,6 +89,45 @@ export class SupermemoryMCP extends McpAgent { ...(hasRootContainerTag ? {} : containerTagField), }) + const listMemoriesSchema = z.object({ + limit: z + .number() + .int("Limit must be an integer") + .min(1, "Limit must be at least 1") + .max(200, "Limit cannot exceed 200") + .optional() + .default(50) + .describe( + "Maximum number of memories to return (default: 50, max: 200)", + ), + cursor: z + .string() + .optional() + .describe("Cursor from a previous listMemories response"), + sort: z + .enum([ + "createdAt", + "updatedAt", + "-createdAt", + "-updatedAt", + "createdAt:asc", + "createdAt:desc", + "updatedAt:asc", + "updatedAt:desc", + ]) + .optional() + .default("-createdAt") + .describe("Sort order for listed memories"), + filter: z + .string() + .max(1000, "Filter exceeds maximum length of 1,000 characters") + .optional() + .describe( + "Case-insensitive substring filter applied to memory content", + ), + ...(hasRootContainerTag ? {} : containerTagField), + }) + const contextPromptSchema = z.object({ includeRecent: z .boolean() @@ -101,6 +140,7 @@ export class SupermemoryMCP extends McpAgent { type ContextPromptArgs = z.infer type MemoryArgs = z.infer type RecallArgs = z.infer + type ListMemoriesArgs = z.infer // Register memory tool this.server.registerTool( @@ -126,6 +166,18 @@ export class SupermemoryMCP extends McpAgent { (args: RecallArgs) => this.handleRecall(args), ) + // Register list memories tool + this.server.registerTool( + "listMemories", + { + description: + "List the user's stored memories with pagination. Use this to audit, clean, or diff memories before save/forget operations.", + inputSchema: listMemoriesSchema, + }, + // @ts-expect-error - zod type inference issue with MCP SDK + (args: ListMemoriesArgs) => this.handleListMemories(args), + ) + // Register profile resource this.server.registerResource( "User Profile", @@ -747,6 +799,64 @@ export class SupermemoryMCP extends McpAgent { } } + private async handleListMemories(args: { + limit?: number + cursor?: string + sort?: + | "createdAt" + | "updatedAt" + | "-createdAt" + | "-updatedAt" + | "createdAt:asc" + | "createdAt:desc" + | "updatedAt:asc" + | "updatedAt:desc" + filter?: string + containerTag?: string + }) { + const { + containerTag, + cursor, + filter, + limit = 50, + sort = "-createdAt", + } = args + + try { + const client = this.getClient(containerTag) + const result = await client.listMemories({ + containerTag: containerTag || this.props?.containerTag, + cursor, + filter, + limit, + sort, + }) + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(result, null, 2), + }, + ], + structuredContent: result, + } + } catch (error) { + const message = + error instanceof Error ? error.message : "An unexpected error occurred" + console.error("List memories operation failed:", error) + return { + content: [ + { + type: "text" as const, + text: `Error: ${message}`, + }, + ], + isError: true, + } + } + } + private async getClientInfo(): Promise< { name: string; version?: string } | undefined > {