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(),