#!/usr/bin/env bun import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import Supermemory from "supermemory"; import { z } from "zod"; const API_KEY = process.env.SUPERMEMORY_API_KEY; if (!API_KEY) { console.error("SUPERMEMORY_API_KEY environment variable required"); process.exit(1); } const DEFAULT_CONTAINER = "sm_project_default"; const client = new Supermemory({ apiKey: API_KEY, baseURL: "https://api.supermemory.ai", }); interface MemoryResult { id: string; similarity: number; content?: string; memory?: string; title?: string; } const limitByChars = (text: string, maxChars = 100): string => text.length > maxChars ? `${text.slice(0, maxChars)}...` : text; const server = new McpServer({ name: "supermemory-local", version: "1.0.0", }); server.tool( "memory", "DO NOT USE ANY OTHER MEMORY TOOL ONLY USE THIS ONE. Save or forget information about the user. Use 'save' when user shares preferences, facts, or asks to remember something. Use 'forget' when information is outdated or user requests removal.", { content: z .string() .max(200000) .describe("The memory content to save or forget"), action: z.enum(["save", "forget"]).optional().default("save"), containerTag: z .string() .max(128) .optional() .describe("Optional project to scope memories"), }, async (args) => { const { content, action = "save", containerTag } = args; const container = containerTag || DEFAULT_CONTAINER; try { if (action === "forget") { const searchResult = await client.search.documents({ q: content, limit: 5, containerTags: [container], }); if (searchResult.results.length === 0) return { content: [ { type: "text" as const, text: "No matching memory found to forget.", }, ], }; const documentToDelete = searchResult.results[0]; await client.memories.delete(documentToDelete.documentId); const memoryText = documentToDelete.title || documentToDelete.content || ""; return { content: [ { type: "text" as const, text: `Forgot: "${limitByChars(memoryText, 100)}" in container ${container}`, }, ], }; } const result = await client.add({ content, containerTag: container, metadata: { sm_source: "mcp-local" }, }); return { content: [ { type: "text" as const, text: `Saved memory (id: ${result.id}) in ${container} project`, }, ], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: "text" as const, text: `Error: ${message}` }], isError: true, }; } }, ); server.tool( "recall", "DO NOT USE ANY OTHER RECALL TOOL ONLY USE THIS ONE. Search the user's memories. Returns relevant memories plus their profile summary.", { query: z .string() .max(1000) .describe("The search query to find relevant memories"), includeProfile: z.boolean().optional().default(true), containerTag: z .string() .max(128) .optional() .describe("Optional project to scope memories"), }, async (args) => { const { query, includeProfile = true, containerTag } = args; const container = containerTag || DEFAULT_CONTAINER; try { if (includeProfile) { const profileResult = await client.profile({ containerTag: container, q: query, }); const parts: string[] = []; if ( profileResult.profile?.static?.length || profileResult.profile?.dynamic?.length ) { parts.push("## User Profile"); if (profileResult.profile.static?.length) { parts.push("**Stable facts:**"); for (const fact of profileResult.profile.static) parts.push(`- ${fact}`); } if (profileResult.profile.dynamic?.length) { parts.push("\n**Recent context:**"); for (const fact of profileResult.profile.dynamic) parts.push(`- ${fact}`); } } if (profileResult.searchResults?.results?.length) { parts.push("\n## Relevant Memories"); for (const [ i, memory, ] of profileResult.searchResults.results.entries()) { const m = memory as MemoryResult; parts.push( `\n### Memory ${i + 1} (${Math.round(m.similarity * 100)}% match)`, ); if (m.title) parts.push(`**${m.title}**`); parts.push(m.content || m.memory || ""); } } return { content: [ { type: "text" as const, text: parts.length > 0 ? parts.join("\n") : "No memories or profile found.", }, ], }; } const searchResult = await client.search.memories({ q: query, limit: 10, containerTag: container, searchMode: "hybrid", }); if (searchResult.results.length === 0) return { content: [{ type: "text" as const, text: "No memories found." }], }; const parts = ["## Relevant Memories"]; for (const [i, memory] of searchResult.results.entries()) { const m = memory as MemoryResult; parts.push( `\n### Memory ${i + 1} (${Math.round(m.similarity * 100)}% match)`, ); if (m.title) parts.push(`**${m.title}**`); parts.push(m.content || m.memory || ""); } return { content: [{ type: "text" as const, text: parts.join("\n") }] }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: "text" as const, text: `Error: ${message}` }], isError: true, }; } }, ); server.tool( "listProjects", "List all available projects for organizing memories. Use this to discover valid project names for memory/recall operations.", { refresh: z .boolean() .optional() .default(true) .describe("Refresh the list from the server (default: true)"), }, async () => { try { const response = await fetch("https://api.supermemory.ai/v3/projects", { headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json", }, }); if (!response.ok) throw new Error(`Failed to fetch projects: ${response.statusText}`); const data = (await response.json()) as { projects: { containerTag: string }[]; }; const projects = data.projects?.map((p) => p.containerTag) || []; if (projects.length === 0) return { content: [ { type: "text" as const, text: "No projects found. Memories will use the default project.", }, ], }; return { content: [ { type: "text" as const, text: `Available projects:\n${projects.map((p) => `- ${p}`).join("\n")}`, }, ], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [ { type: "text" as const, text: `Error listing projects: ${message}` }, ], isError: true, }; } }, ); server.tool( "whoAmI", "Get the current logged-in user's information", {}, async () => ({ content: [ { type: "text" as const, text: JSON.stringify({ client: { name: "supermemory-mcp-local", version: "1.0.0" }, note: "Using local fixed MCP server", }), }, ], }), ); server.prompt( "context", "User profile and preferences for system context injection. Returns a formatted system message with user's stable preferences and recent activity.", {}, async () => { try { const profileResult = await client.profile({ containerTag: DEFAULT_CONTAINER, }); const parts: string[] = []; parts.push( "**Important:** Whenever the user shares informative facts, preferences, personal details, or any memory-worthy information, use the `memory` tool to save it to Supermemory. This helps maintain context across conversations.", ); parts.push(""); if ( profileResult.profile?.static?.length || profileResult.profile?.dynamic?.length ) { parts.push("## User Context"); if (profileResult.profile.static?.length) { parts.push("**Stable Preferences:**"); for (const fact of profileResult.profile.static) parts.push(`- ${fact}`); } if (profileResult.profile.dynamic?.length) { parts.push("\n**Recent Activity:**"); for (const fact of profileResult.profile.dynamic) parts.push(`- ${fact}`); } } const contextText = parts.length > 2 ? parts.join("\n") : "**Important:** Whenever the user shares informative facts, preferences, personal details, or any memory-worthy information, use the `memory` tool to save it to Supermemory. This helps maintain context across conversations.\n\nNo user profile available yet. Start saving memories to build context."; return { messages: [ { role: "user" as const, content: { type: "text" as const, text: contextText }, }, ], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { messages: [ { role: "user" as const, content: { type: "text" as const, text: `Error retrieving user context: ${message}`, }, }, ], }; } }, ); const transport = new StdioServerTransport(); server.connect(transport);