From 360fc3ef911c347094508a8e0a820fc563f74711 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Wed, 7 Oct 2026 21:02:06 -0700 Subject: [PATCH] fix(mcp): accept containerTag as a deprecated alias and emit both keys for clients with a cached tool list Every namespace input also takes containerTag (described as deprecated), structured outputs and view messages carry both the new and the old field names, output schemas declare the legacy keys as optional so cached clients still validate, and the server sends tools/list_changed once a client finishes initializing. Compat lives in src/server/tools/compat.ts with unit tests; remove it once PostHog shows no containerTag arguments for two weeks. --- apps/mcp/src/server/server.ts | 7 + apps/mcp/src/server/tools/add-memory.ts | 16 +- apps/mcp/src/server/tools/compat.test.ts | 168 ++++++++++++++++++ apps/mcp/src/server/tools/compat.ts | 62 +++++++ apps/mcp/src/server/tools/fetch-graph-data.ts | 4 +- apps/mcp/src/server/tools/get-document.ts | 12 +- apps/mcp/src/server/tools/get-profile.ts | 12 +- apps/mcp/src/server/tools/guided-save.ts | 5 +- apps/mcp/src/server/tools/list-documents.ts | 4 +- apps/mcp/src/server/tools/list-memories.ts | 4 +- apps/mcp/src/server/tools/list-namespaces.ts | 17 +- apps/mcp/src/server/tools/memory-graph.ts | 8 +- apps/mcp/src/server/tools/output-schemas.ts | 5 + .../src/server/tools/prepare-file-upload.ts | 11 +- apps/mcp/src/server/tools/save-memory.ts | 15 +- apps/mcp/src/server/tools/search-memory.ts | 12 +- apps/mcp/src/server/tools/select-space.ts | 5 +- apps/mcp/src/server/tools/set-active-tag.ts | 12 +- apps/mcp/src/server/tools/upload-file.ts | 5 +- apps/mcp/src/server/tools/who-am-i.ts | 5 +- apps/mcp/src/shared/types.ts | 14 ++ 21 files changed, 357 insertions(+), 46 deletions(-) create mode 100644 apps/mcp/src/server/tools/compat.test.ts create mode 100644 apps/mcp/src/server/tools/compat.ts diff --git a/apps/mcp/src/server/server.ts b/apps/mcp/src/server/server.ts index f3dc7ca5..809b9f04 100644 --- a/apps/mcp/src/server/server.ts +++ b/apps/mcp/src/server/server.ts @@ -99,6 +99,13 @@ export function createSupermemoryServer( errorResult, }) + // Clients that cached the pre-v5 tool list refetch it on this notice + server.server.oninitialized = () => { + try { + server.sendToolListChanged() + } catch {} + } + registerProfileResource(server, getClient, resolveSelectedNamespace) registerNamespacesResource( server, diff --git a/apps/mcp/src/server/tools/add-memory.ts b/apps/mcp/src/server/tools/add-memory.ts index 04608301..104dd87b 100644 --- a/apps/mcp/src/server/tools/add-memory.ts +++ b/apps/mcp/src/server/tools/add-memory.ts @@ -1,5 +1,10 @@ import { z } from "zod" import { optionalNamespaceSchema } from "../namespace" +import { + legacyNamespaceInput, + namespaceArg, + withLegacyNamespace, +} from "./compat" import { MEMORY_TOOL_ANNOTATIONS } from "./annotations" import { addMemoryOutputSchema, type AddMemoryOutput } from "./output-schemas" import { textContent, type ToolDeps } from "./types" @@ -12,6 +17,7 @@ export function register(deps: ToolDeps) { .describe("The memory content to save or forget"), action: z.enum(["save", "forget"]).optional().default("save"), namespace: optionalNamespaceSchema, + ...legacyNamespaceInput, }) deps.server.registerTool( @@ -25,17 +31,17 @@ export function register(deps: ToolDeps) { }, async (args) => { try { - const namespace = await deps.resolveNamespace(args.namespace) + 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 = { + const structuredContent: AddMemoryOutput = withLegacyNamespace({ action: "forget", success: result.success, namespace: result.namespace, message: result.message, - } + }) return { content: [textContent(result.message)], structuredContent, @@ -44,14 +50,14 @@ export function register(deps: ToolDeps) { const result = await client.createMemory(args.content) const message = `Memory saved (ID: ${result.id}, space: ${result.namespace})` - const structuredContent: AddMemoryOutput = { + const structuredContent: AddMemoryOutput = withLegacyNamespace({ action: "save", success: true, namespace: result.namespace, message, id: result.id, status: result.status, - } + }) return { content: [textContent(message)], structuredContent, diff --git a/apps/mcp/src/server/tools/compat.test.ts b/apps/mcp/src/server/tools/compat.test.ts new file mode 100644 index 00000000..bdaa1aee --- /dev/null +++ b/apps/mcp/src/server/tools/compat.test.ts @@ -0,0 +1,168 @@ +import { describe, expect, it, vi } from "vitest" +import { z } from "zod" +import * as addMemory from "./add-memory" +import { namespaceArg, withLegacyView } from "./compat" +import * as listNamespaces from "./list-namespaces" +import * as setActiveTag from "./set-active-tag" +import type { ToolDeps } from "./types" +import { errorResult } from "./types" + +type Registered = { + config: { inputSchema: z.ZodTypeAny; outputSchema: z.ZodTypeAny } + cb: ( + args: Record, + context: unknown, + ) => Promise<{ + structuredContent?: Record + isError?: boolean + }> +} + +function fakeDeps() { + const tools = new Map() + const client = { + createMemory: vi.fn().mockResolvedValue({ + id: "d1", + status: "queued", + namespace: "legacy_ns", + }), + listNamespaces: vi.fn().mockResolvedValue([ + { + id: "1", + namespace: "legacy_ns", + description: null, + documentCount: 2, + memoryCount: 1, + createdAt: "2026-01-01", + updatedAt: "2026-01-02", + }, + ]), + } + const deps = { + server: { + registerTool: (name: string, config: unknown, cb: unknown) => { + tools.set(name, { config, cb } as Registered) + }, + }, + getClient: vi.fn(() => client), + resolveNamespace: vi.fn(async (explicit?: string) => explicit ?? "active"), + setActiveNamespace: vi.fn(async () => {}), + errorResult, + } as unknown as ToolDeps + return { deps, tools, client } +} + +// Shape a client that cached the pre-v5 tool list validates against +const legacyAddMemoryOutput = z.object({ + action: z.enum(["save", "forget"]), + success: z.boolean(), + containerTag: z.string(), + message: z.string(), +}) + +describe("containerTag compatibility", () => { + it("prefers namespace and falls back to the deprecated containerTag", () => { + expect(namespaceArg({ namespace: "a", containerTag: "b" })).toBe("a") + expect(namespaceArg({ containerTag: "b" })).toBe("b") + expect(namespaceArg({})).toBeUndefined() + }) + + it("accepts containerTag input and ignores unknown keys", () => { + const { deps, tools } = fakeDeps() + addMemory.register(deps) + const tool = tools.get("add_memory") + if (!tool) throw new Error("add_memory not registered") + const parsed = tool.config.inputSchema.parse({ + content: "hi", + containerTag: "legacy_ns", + somethingElse: 1, + }) as { namespace?: string; containerTag?: string } + expect(parsed.containerTag).toBe("legacy_ns") + expect(parsed.namespace).toBeUndefined() + }) + + it("routes a containerTag-only call to that namespace and emits both keys", async () => { + const { deps, tools } = fakeDeps() + addMemory.register(deps) + const tool = tools.get("add_memory") + if (!tool) throw new Error("add_memory not registered") + const result = await tool.cb( + { content: "hi", action: "save", containerTag: "legacy_ns" }, + {}, + ) + expect(deps.getClient).toHaveBeenCalledWith("legacy_ns") + expect(result.structuredContent).toMatchObject({ + namespace: "legacy_ns", + containerTag: "legacy_ns", + }) + expect(() => + tool.config.outputSchema.parse(result.structuredContent), + ).not.toThrow() + expect(() => + legacyAddMemoryOutput.parse(result.structuredContent), + ).not.toThrow() + }) + + it("treats containerTag as the required namespace for set-active-tag", async () => { + const { deps, tools } = fakeDeps() + setActiveTag.register(deps) + const tool = tools.get("set-active-tag") + if (!tool) throw new Error("set-active-tag not registered") + const result = await tool.cb({ containerTag: "legacy_ns" }, {}) + expect(deps.setActiveNamespace).toHaveBeenCalledWith("legacy_ns") + expect(result.structuredContent).toMatchObject({ + view: "confirmation", + namespace: "legacy_ns", + containerTag: "legacy_ns", + }) + const missing = await tool.cb({}, {}) + expect(missing.isError).toBe(true) + }) + + it("lists spaces with both keys per item", async () => { + const { deps, tools } = fakeDeps() + listNamespaces.register(deps) + const tool = tools.get("list_spaces") + if (!tool) throw new Error("list_spaces not registered") + const result = await tool.cb({}, {}) + expect(result.structuredContent?.spaces).toEqual([ + expect.objectContaining({ + namespace: "legacy_ns", + containerTag: "legacy_ns", + }), + ]) + expect(() => + tool.config.outputSchema.parse(result.structuredContent), + ).not.toThrow() + }) + + it("mirrors renamed view fields under their old names", () => { + const picker = withLegacyView({ + view: "picker", + namespaces: [ + { + id: "1", + namespace: "ns", + documentCount: 0, + memoryCount: 0, + createdAt: "a", + updatedAt: "b", + }, + ], + activeNamespace: "ns", + assignedNamespaces: [{ namespace: "ns", permission: "write" }], + }) + expect(picker).toMatchObject({ + activeTag: "ns", + containerTags: [{ namespace: "ns", containerTag: "ns" }], + assignedTags: [{ namespace: "ns", containerTag: "ns" }], + }) + expect( + withLegacyView({ + view: "save", + activeNamespace: "ns", + writableNamespaces: ["ns"], + }), + ).toMatchObject({ activeTag: "ns", writableTags: ["ns"] }) + }) +}) diff --git a/apps/mcp/src/server/tools/compat.ts b/apps/mcp/src/server/tools/compat.ts new file mode 100644 index 00000000..37d9e452 --- /dev/null +++ b/apps/mcp/src/server/tools/compat.ts @@ -0,0 +1,62 @@ +import { z } from "zod" +import type { + NamespaceAccess, + NamespaceInfo, + ViewMessage, +} from "../../shared/types" +import { namespaceSchema } from "../namespace" + +// Clients that cached the pre-v5 tool list still send and expect containerTag; drop once PostHog shows no containerTag args for 2 weeks. +export const legacyNamespaceInput = { + containerTag: namespaceSchema + .optional() + .describe("Deprecated alias of namespace. Prefer namespace."), +} + +export const legacyNamespaceOutput = { containerTag: z.string().optional() } + +export const namespaceArg = (args: { + namespace?: string + containerTag?: string +}) => args.namespace ?? args.containerTag + +export function withLegacyNamespace(value: T) { + return { ...value, containerTag: value.namespace } +} + +export const withLegacyNamespaces = ( + values: T[], +) => values.map(withLegacyNamespace) + +const legacyAccess = (values: NamespaceAccess[] | null | undefined) => + values ? withLegacyNamespaces(values) : values + +const legacyInfos = (values: NamespaceInfo[]) => withLegacyNamespaces(values) + +// Mirrors every renamed view field under its pre-v5 name so cached output schemas still validate. +export function withLegacyView(view: ViewMessage): ViewMessage { + switch (view.view) { + case "picker": + return { + ...view, + namespaces: legacyInfos(view.namespaces), + containerTags: legacyInfos(view.namespaces), + activeTag: view.activeNamespace, + assignedNamespaces: legacyAccess(view.assignedNamespaces), + assignedTags: legacyAccess(view.assignedNamespaces), + } + case "save": + case "upload": + return { + ...view, + activeTag: view.activeNamespace, + writableTags: view.writableNamespaces, + } + case "confirmation": + case "save-success": + case "upload-success": + return { ...view, containerTag: view.namespace } + case "graph": + return { ...view, containerTag: view.namespace } + } +} diff --git a/apps/mcp/src/server/tools/fetch-graph-data.ts b/apps/mcp/src/server/tools/fetch-graph-data.ts index 585fdf75..cfdc7da6 100644 --- a/apps/mcp/src/server/tools/fetch-graph-data.ts +++ b/apps/mcp/src/server/tools/fetch-graph-data.ts @@ -2,6 +2,7 @@ import { z } from "zod" import { documentsApiResponseSchema } from "../../shared/types" import { appToolMeta } from "../app-metadata" import { optionalNamespaceSchema } from "../namespace" +import { legacyNamespaceInput, namespaceArg } from "./compat" import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations" import { textContent, type ToolDeps } from "./types" @@ -12,6 +13,7 @@ export function register(deps: ToolDeps) { description: "Fetch documents with memories for graph display", inputSchema: z.object({ namespace: optionalNamespaceSchema, + ...legacyNamespaceInput, page: z.number().int().min(1).max(10_000).optional().default(1), limit: z.number().int().min(1).max(1_000).optional().default(200), }), @@ -21,7 +23,7 @@ export function register(deps: ToolDeps) { }, async (args) => { try { - const namespace = await deps.resolveNamespace(args.namespace) + const namespace = await deps.resolveNamespace(namespaceArg(args)) const client = deps.getClient(namespace) const data = await client.getGraphDocuments(args.page, args.limit) diff --git a/apps/mcp/src/server/tools/get-document.ts b/apps/mcp/src/server/tools/get-document.ts index 76edd939..166bd135 100644 --- a/apps/mcp/src/server/tools/get-document.ts +++ b/apps/mcp/src/server/tools/get-document.ts @@ -1,6 +1,11 @@ import { z } from "zod" import { formatDocument, getDocumentContent } from "../format" import { optionalNamespaceSchema } from "../namespace" +import { + legacyNamespaceInput, + namespaceArg, + withLegacyNamespace, +} from "./compat" import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations" import { getDocumentOutputSchema, @@ -16,6 +21,7 @@ export function register(deps: ToolDeps) { .max(255, "Document ID exceeds maximum length") .describe("Document ID returned by list_documents or a memory result"), namespace: optionalNamespaceSchema, + ...legacyNamespaceInput, }) deps.server.registerTool( @@ -30,11 +36,11 @@ export function register(deps: ToolDeps) { }, async (args) => { try { - const namespace = await deps.resolveNamespace(args.namespace) + const namespace = await deps.resolveNamespace(namespaceArg(args)) const client = deps.getClient(namespace) const document = await client.getDocument(args.documentId) const { content, truncated } = getDocumentContent(document) - const structuredContent: GetDocumentOutput = { + const structuredContent: GetDocumentOutput = withLegacyNamespace({ namespace, document: { id: document.id, @@ -47,7 +53,7 @@ export function register(deps: ToolDeps) { content, contentTruncated: truncated, }, - } + }) return { content: [textContent(formatDocument(document))], diff --git a/apps/mcp/src/server/tools/get-profile.ts b/apps/mcp/src/server/tools/get-profile.ts index cbbcf991..5764642a 100644 --- a/apps/mcp/src/server/tools/get-profile.ts +++ b/apps/mcp/src/server/tools/get-profile.ts @@ -1,5 +1,10 @@ import { z } from "zod" import { optionalNamespaceSchema } from "../namespace" +import { + legacyNamespaceInput, + namespaceArg, + withLegacyNamespace, +} from "./compat" import { formatFactSection } from "../space-presentation" import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations" import { getProfileOutputSchema, type GetProfileOutput } from "./output-schemas" @@ -8,6 +13,7 @@ import { textContent, type ToolDeps } from "./types" export function register(deps: ToolDeps) { const inputSchema = z.object({ namespace: optionalNamespaceSchema, + ...legacyNamespaceInput, }) deps.server.registerTool( @@ -22,7 +28,7 @@ export function register(deps: ToolDeps) { }, async (args) => { try { - const namespace = await deps.resolveNamespace(args.namespace) + const namespace = await deps.resolveNamespace(namespaceArg(args)) const profileResult = await deps.getClient(namespace).getProfile() const profile = { static: profileResult.profile.static, @@ -46,10 +52,10 @@ export function register(deps: ToolDeps) { parts.push("No profile facts are available for this space yet.") } - const structuredContent: GetProfileOutput = { + const structuredContent: GetProfileOutput = withLegacyNamespace({ namespace, profile, - } + }) return { content: [textContent(parts.join("\n"))], diff --git a/apps/mcp/src/server/tools/guided-save.ts b/apps/mcp/src/server/tools/guided-save.ts index fb5427c8..495fb5ea 100644 --- a/apps/mcp/src/server/tools/guided-save.ts +++ b/apps/mcp/src/server/tools/guided-save.ts @@ -3,6 +3,7 @@ import { saveViewSchema, type ViewMessage } from "../../shared/types" import { appResultMeta, appToolMeta } from "../app-metadata" import { effectiveNamespaceAccess } from "../auth/rbac" import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations" +import { withLegacyView } from "./compat" import { textContent, type ToolDeps } from "./types" export function register(deps: ToolDeps) { @@ -39,13 +40,13 @@ export function register(deps: ToolDeps) { .filter((access) => access.permission === "write") .map((access) => access.namespace) - const sc: ViewMessage = { + const sc: ViewMessage = withLegacyView({ view: "save", viewId, activeNamespace, writableNamespaces, prefill, - } + }) return { content: [textContent("Opening memory save form...")], diff --git a/apps/mcp/src/server/tools/list-documents.ts b/apps/mcp/src/server/tools/list-documents.ts index 7f7f86a2..c886f364 100644 --- a/apps/mcp/src/server/tools/list-documents.ts +++ b/apps/mcp/src/server/tools/list-documents.ts @@ -1,5 +1,6 @@ import { z } from "zod" import { optionalNamespaceSchema } from "../namespace" +import { legacyNamespaceInput, namespaceArg } from "./compat" import { formatDocumentsList } from "../format" import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations" import { @@ -26,6 +27,7 @@ export function register(deps: ToolDeps) { .default(10) .describe("Documents per page (default 10, max 50)"), namespace: optionalNamespaceSchema, + ...legacyNamespaceInput, }) deps.server.registerTool( @@ -40,7 +42,7 @@ export function register(deps: ToolDeps) { }, async (args) => { try { - const namespace = await deps.resolveNamespace(args.namespace) + const namespace = await deps.resolveNamespace(namespaceArg(args)) const client = deps.getClient(namespace) const data = await client.listDocuments( args.page ?? 1, diff --git a/apps/mcp/src/server/tools/list-memories.ts b/apps/mcp/src/server/tools/list-memories.ts index 18e208a3..b2aa017d 100644 --- a/apps/mcp/src/server/tools/list-memories.ts +++ b/apps/mcp/src/server/tools/list-memories.ts @@ -1,5 +1,6 @@ import { z } from "zod" import { optionalNamespaceSchema } from "../namespace" +import { legacyNamespaceInput, namespaceArg } from "./compat" import { formatMemoryEntriesList } from "../format" import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations" import { @@ -26,6 +27,7 @@ export function register(deps: ToolDeps) { .default(10) .describe("Memory entries per page (default 10, max 50)"), namespace: optionalNamespaceSchema, + ...legacyNamespaceInput, }) deps.server.registerTool( @@ -40,7 +42,7 @@ export function register(deps: ToolDeps) { }, async (args) => { try { - const namespace = await deps.resolveNamespace(args.namespace) + const namespace = await deps.resolveNamespace(namespaceArg(args)) const client = deps.getClient(namespace) const data = await client.listMemoryEntries( args.page ?? 1, diff --git a/apps/mcp/src/server/tools/list-namespaces.ts b/apps/mcp/src/server/tools/list-namespaces.ts index f49e920d..eeff9b96 100644 --- a/apps/mcp/src/server/tools/list-namespaces.ts +++ b/apps/mcp/src/server/tools/list-namespaces.ts @@ -1,6 +1,7 @@ import { z } from "zod" import { listSpacesOutputSchema } from "../../shared/types" import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations" +import { withLegacyNamespaces } from "./compat" import { textContent, type ToolDeps } from "./types" export function register(deps: ToolDeps) { @@ -16,13 +17,15 @@ export function register(deps: ToolDeps) { async () => { try { const namespaces = await deps.getClient().listNamespaces() - const spaces = namespaces.map((entry) => ({ - namespace: entry.namespace, - description: entry.description, - documentCount: entry.documentCount, - memoryCount: entry.memoryCount, - updatedAt: entry.updatedAt, - })) + const spaces = withLegacyNamespaces( + namespaces.map((entry) => ({ + namespace: entry.namespace, + description: entry.description, + documentCount: entry.documentCount, + memoryCount: entry.memoryCount, + updatedAt: entry.updatedAt, + })), + ) if (spaces.length === 0) { return { diff --git a/apps/mcp/src/server/tools/memory-graph.ts b/apps/mcp/src/server/tools/memory-graph.ts index 47c1a5d5..73cc2b2d 100644 --- a/apps/mcp/src/server/tools/memory-graph.ts +++ b/apps/mcp/src/server/tools/memory-graph.ts @@ -2,12 +2,14 @@ import { z } from "zod" import { graphViewSchema, type ViewMessage } from "../../shared/types" import { appResultMeta, appToolMeta } from "../app-metadata" import { optionalNamespaceSchema } from "../namespace" +import { legacyNamespaceInput, namespaceArg, withLegacyView } from "./compat" import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations" import { textContent, type ToolDeps } from "./types" export function register(deps: ToolDeps) { const inputSchema = z.object({ namespace: optionalNamespaceSchema, + ...legacyNamespaceInput, }) deps.server.registerTool( @@ -24,14 +26,14 @@ export function register(deps: ToolDeps) { async (args) => { try { const viewId = crypto.randomUUID() - const namespace = await deps.resolveNamespace(args.namespace) + const namespace = await deps.resolveNamespace(namespaceArg(args)) const client = deps.getClient(namespace) const result = await client.getGraphDocuments(1, 200) const memoryCount = result.documents.reduce( (sum, document) => sum + document.memoryEntries.length, 0, ) - const sc: ViewMessage = { + const sc: ViewMessage = withLegacyView({ view: "graph", viewId, namespace, @@ -42,7 +44,7 @@ export function register(deps: ToolDeps) { totalDocumentCount: result.pagination.totalItems, truncated: result.documents.length < result.pagination.totalItems, rendered: true, - } + }) return { content: [ textContent( diff --git a/apps/mcp/src/server/tools/output-schemas.ts b/apps/mcp/src/server/tools/output-schemas.ts index df071a84..72a9065d 100644 --- a/apps/mcp/src/server/tools/output-schemas.ts +++ b/apps/mcp/src/server/tools/output-schemas.ts @@ -1,4 +1,5 @@ import { z } from "zod" +import { legacyNamespaceOutput } from "./compat" import { namespaceAccessSchema, memoriesListSchema, @@ -20,6 +21,7 @@ export const addMemoryOutputSchema = z.object({ action: z.enum(["save", "forget"]), success: z.boolean(), namespace: z.string(), + ...legacyNamespaceOutput, message: z.string(), id: z.string().optional(), status: z.string().optional(), @@ -29,6 +31,7 @@ export type AddMemoryOutput = z.infer export const getDocumentOutputSchema = z.object({ namespace: z.string(), + ...legacyNamespaceOutput, document: z.object({ id: z.string(), title: z.string().nullable(), @@ -58,6 +61,7 @@ export type ListMemoriesOutput = z.infer export const searchMemoryOutputSchema = z.object({ query: z.string(), namespace: z.string(), + ...legacyNamespaceOutput, profile: z .object({ static: z.array(z.string()), @@ -80,6 +84,7 @@ export type SearchMemoryOutput = z.infer export const getProfileOutputSchema = z.object({ namespace: z.string(), + ...legacyNamespaceOutput, profile: z.object({ static: z.array(z.string()), dynamic: z.array(z.string()), diff --git a/apps/mcp/src/server/tools/prepare-file-upload.ts b/apps/mcp/src/server/tools/prepare-file-upload.ts index ca20020f..d01e415b 100644 --- a/apps/mcp/src/server/tools/prepare-file-upload.ts +++ b/apps/mcp/src/server/tools/prepare-file-upload.ts @@ -2,6 +2,7 @@ import { z } from "zod" import { uploadPreparationSchema } from "../../shared/types" import { appToolMeta } from "../app-metadata" import { namespaceSchema } from "../namespace" +import { legacyNamespaceInput, namespaceArg } from "./compat" import { ADDITIVE_MEMORY_TOOL_ANNOTATIONS } from "./annotations" import { textContent, type ToolDeps } from "./types" @@ -10,14 +11,20 @@ export function register(deps: ToolDeps) { "prepare-file-upload", { description: "Prepare a direct file upload", - inputSchema: z.object({ namespace: namespaceSchema }), + inputSchema: z.object({ + namespace: namespaceSchema.optional(), + ...legacyNamespaceInput, + }), outputSchema: uploadPreparationSchema, annotations: ADDITIVE_MEMORY_TOOL_ANNOTATIONS, _meta: appToolMeta(["app"]), }, async (args) => { try { - const preparation = await deps.createUploadSession(args.namespace) + const namespace = namespaceArg(args) + if (!namespace) + return deps.errorResult(new Error("namespace is required")) + const preparation = await deps.createUploadSession(namespace) return { content: [textContent("Upload session prepared")], diff --git a/apps/mcp/src/server/tools/save-memory.ts b/apps/mcp/src/server/tools/save-memory.ts index 09ca642a..c844a3b6 100644 --- a/apps/mcp/src/server/tools/save-memory.ts +++ b/apps/mcp/src/server/tools/save-memory.ts @@ -2,6 +2,7 @@ import { z } from "zod" import { saveSuccessViewSchema, type ViewMessage } from "../../shared/types" import { appResultMeta, appToolMeta } from "../app-metadata" import { namespaceSchema } from "../namespace" +import { legacyNamespaceInput, namespaceArg, withLegacyView } from "./compat" import { ADDITIVE_MEMORY_TOOL_ANNOTATIONS } from "./annotations" import { textContent, type ToolDeps } from "./types" @@ -12,7 +13,8 @@ export function register(deps: ToolDeps) { description: "Save content to memory", inputSchema: z.object({ content: z.string().min(1), - namespace: namespaceSchema, + namespace: namespaceSchema.optional(), + ...legacyNamespaceInput, viewId: z.string().uuid().optional(), }), outputSchema: saveSuccessViewSchema, @@ -21,15 +23,18 @@ export function register(deps: ToolDeps) { }, async (args) => { try { + const namespace = namespaceArg(args) + if (!namespace) + return deps.errorResult(new Error("namespace is required")) const viewId = args.viewId ?? crypto.randomUUID() - const client = deps.getClient(args.namespace) + const client = deps.getClient(namespace) const result = await client.createMemory(args.content) - const sc: ViewMessage = { + const sc: ViewMessage = withLegacyView({ view: "save-success", viewId, id: result.id, - namespace: args.namespace, - } + namespace, + }) return { content: [textContent(`Memory saved: ${result.id}`)], structuredContent: sc, diff --git a/apps/mcp/src/server/tools/search-memory.ts b/apps/mcp/src/server/tools/search-memory.ts index 4fbf14af..e4a257a5 100644 --- a/apps/mcp/src/server/tools/search-memory.ts +++ b/apps/mcp/src/server/tools/search-memory.ts @@ -1,6 +1,11 @@ import { z } from "zod" import { getMemoryText } from "../client" import { optionalNamespaceSchema } from "../namespace" +import { + legacyNamespaceInput, + namespaceArg, + withLegacyNamespace, +} from "./compat" import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations" import { searchMemoryOutputSchema, @@ -15,6 +20,7 @@ export function register(deps: ToolDeps) { .max(1000, "Query exceeds maximum length") .describe("The search query to find relevant memories"), namespace: optionalNamespaceSchema, + ...legacyNamespaceInput, }) deps.server.registerTool( @@ -28,7 +34,7 @@ export function register(deps: ToolDeps) { }, async (args) => { try { - const namespace = await deps.resolveNamespace(args.namespace) + const namespace = await deps.resolveNamespace(namespaceArg(args)) const client = deps.getClient(namespace) const searchResult = await client.search(args.query) @@ -50,13 +56,13 @@ export function register(deps: ToolDeps) { parts.push("No matching memories found.") } - const structuredContent: SearchMemoryOutput = { + const structuredContent: SearchMemoryOutput = withLegacyNamespace({ query: args.query, namespace, results, total: searchResult.total, timing: searchResult.timing, - } + }) return { content: [textContent(parts.join("\n"))], diff --git a/apps/mcp/src/server/tools/select-space.ts b/apps/mcp/src/server/tools/select-space.ts index c64a4f33..905c2ea7 100644 --- a/apps/mcp/src/server/tools/select-space.ts +++ b/apps/mcp/src/server/tools/select-space.ts @@ -3,6 +3,7 @@ import { pickerViewSchema, type ViewMessage } from "../../shared/types" import { appResultMeta, appToolMeta } from "../app-metadata" import { effectiveNamespaceAccess } from "../auth/rbac" import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations" +import { withLegacyView } from "./compat" import { textContent, type ToolDeps } from "./types" export function register(deps: ToolDeps) { @@ -31,13 +32,13 @@ export function register(deps: ToolDeps) { session, ) - const sc: ViewMessage = { + const sc: ViewMessage = withLegacyView({ view: "picker", viewId, namespaces, activeNamespace, assignedNamespaces, - } + }) return { content: [ diff --git a/apps/mcp/src/server/tools/set-active-tag.ts b/apps/mcp/src/server/tools/set-active-tag.ts index 62512f28..1b1fb729 100644 --- a/apps/mcp/src/server/tools/set-active-tag.ts +++ b/apps/mcp/src/server/tools/set-active-tag.ts @@ -2,6 +2,7 @@ import { z } from "zod" import { confirmationViewSchema, type ViewMessage } from "../../shared/types" import { appResultMeta, appToolMeta } from "../app-metadata" import { namespaceSchema } from "../namespace" +import { legacyNamespaceInput, namespaceArg, withLegacyView } from "./compat" import { SETTINGS_TOOL_ANNOTATIONS } from "./annotations" import { textContent, type ToolDeps } from "./types" @@ -11,7 +12,8 @@ export function register(deps: ToolDeps) { { description: "Set the active Supermemory space for this account", inputSchema: z.object({ - namespace: namespaceSchema, + namespace: namespaceSchema.optional(), + ...legacyNamespaceInput, viewId: z.string().uuid().optional(), }), outputSchema: confirmationViewSchema, @@ -19,7 +21,9 @@ export function register(deps: ToolDeps) { annotations: SETTINGS_TOOL_ANNOTATIONS, }, async (args) => { - const { namespace } = args + const namespace = namespaceArg(args) + if (!namespace) + return deps.errorResult(new Error("namespace is required")) try { const viewId = args.viewId ?? crypto.randomUUID() const namespaces = await deps.getClient().listNamespaces() @@ -29,11 +33,11 @@ export function register(deps: ToolDeps) { ) } await deps.setActiveNamespace(namespace) - const sc: ViewMessage = { + const sc: ViewMessage = withLegacyView({ view: "confirmation", viewId, namespace, - } + }) return { content: [textContent(`Active space set to ${namespace}`)], structuredContent: sc, diff --git a/apps/mcp/src/server/tools/upload-file.ts b/apps/mcp/src/server/tools/upload-file.ts index 4e333b51..3cf3b7f4 100644 --- a/apps/mcp/src/server/tools/upload-file.ts +++ b/apps/mcp/src/server/tools/upload-file.ts @@ -3,6 +3,7 @@ import { uploadViewSchema, type ViewMessage } from "../../shared/types" import { appResultMeta, appToolMeta } from "../app-metadata" import { effectiveNamespaceAccess } from "../auth/rbac" import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations" +import { withLegacyView } from "./compat" import { textContent, type ToolDeps } from "./types" export function register(deps: ToolDeps) { @@ -32,12 +33,12 @@ export function register(deps: ToolDeps) { .filter((access) => access.permission === "write") .map((access) => access.namespace) - const sc: ViewMessage = { + const sc: ViewMessage = withLegacyView({ view: "upload", viewId, activeNamespace, writableNamespaces, - } + }) return { content: [textContent("Opening file upload form...")], diff --git a/apps/mcp/src/server/tools/who-am-i.ts b/apps/mcp/src/server/tools/who-am-i.ts index 8ecba05a..08fcea8e 100644 --- a/apps/mcp/src/server/tools/who-am-i.ts +++ b/apps/mcp/src/server/tools/who-am-i.ts @@ -1,5 +1,6 @@ import { z } from "zod" import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations" +import { withLegacyNamespaces } from "./compat" import { whoAmIOutputSchema, type WhoAmIOutput } from "./output-schemas" import { textContent, type ToolDeps } from "./types" @@ -28,8 +29,8 @@ export function register(deps: ToolDeps) { accessType: session.accessType ?? "full", activeSpace: activeNamespace ?? null, assignedSpaces: - session.accessType === "restricted" - ? (session.namespaces ?? null) + session.accessType === "restricted" && session.namespaces + ? withLegacyNamespaces(session.namespaces) : null, ...(session.scope ? { scope: session.scope } : {}), ...(client ? { client } : {}), diff --git a/apps/mcp/src/shared/types.ts b/apps/mcp/src/shared/types.ts index fd52691a..ad59a02b 100644 --- a/apps/mcp/src/shared/types.ts +++ b/apps/mcp/src/shared/types.ts @@ -5,6 +5,7 @@ import { z } from "zod" export const namespaceAccessSchema = z.object({ namespace: z.string(), + containerTag: z.string().optional(), permission: z.enum(["read", "write"]), }) @@ -57,6 +58,7 @@ export type SessionInfo = z.output export const namespaceInfoSchema = z.object({ id: z.string(), namespace: z.string(), + containerTag: z.string().optional(), description: z.string().nullish(), documentCount: z.number().int().nonnegative(), memoryCount: z.number().int().nonnegative(), @@ -68,6 +70,7 @@ export type NamespaceInfo = z.infer export const spaceSummarySchema = z.object({ namespace: z.string(), + containerTag: z.string().optional(), description: z.string().nullish(), documentCount: z.number().int().nonnegative(), memoryCount: z.number().int().nonnegative(), @@ -176,12 +179,16 @@ export const pickerViewSchema = z.object({ namespaces: z.array(namespaceInfoSchema), activeNamespace: z.string().nullish(), assignedNamespaces: z.array(namespaceAccessSchema).nullable().optional(), + containerTags: z.array(namespaceInfoSchema).optional(), + activeTag: z.string().nullish(), + assignedTags: z.array(namespaceAccessSchema).nullable().optional(), }) export const confirmationViewSchema = z.object({ view: z.literal("confirmation"), viewId: viewIdSchema, namespace: z.string(), + containerTag: z.string().optional(), }) export const saveViewSchema = z.object({ @@ -190,6 +197,8 @@ export const saveViewSchema = z.object({ activeNamespace: z.string().nullish(), writableNamespaces: z.array(z.string()), prefill: z.string().optional(), + activeTag: z.string().nullish(), + writableTags: z.array(z.string()).optional(), }) export const saveSuccessViewSchema = z.object({ @@ -197,6 +206,7 @@ export const saveSuccessViewSchema = z.object({ viewId: viewIdSchema, id: z.string(), namespace: z.string(), + containerTag: z.string().optional(), }) export const uploadViewSchema = z.object({ @@ -204,6 +214,8 @@ export const uploadViewSchema = z.object({ viewId: viewIdSchema, activeNamespace: z.string().nullish(), writableNamespaces: z.array(z.string()), + activeTag: z.string().nullish(), + writableTags: z.array(z.string()).optional(), }) export const uploadSuccessViewSchema = z.object({ @@ -212,6 +224,7 @@ export const uploadSuccessViewSchema = z.object({ id: z.string(), fileName: z.string(), namespace: z.string(), + containerTag: z.string().optional(), }) export const uploadPreparationSchema = z.object({ @@ -229,6 +242,7 @@ export const graphViewSchema = z.object({ view: z.literal("graph"), viewId: viewIdSchema, namespace: z.string().optional(), + containerTag: z.string().optional(), documents: z.array(documentWithMemoriesSchema), totalCount: z.number().int().nonnegative(), documentCount: z.number().int().nonnegative(),