diff --git a/apps/mcp/src/server/tools/add-memory.ts b/apps/mcp/src/server/tools/add-memory.ts index c708deb50..fdae04ad8 100644 --- a/apps/mcp/src/server/tools/add-memory.ts +++ b/apps/mcp/src/server/tools/add-memory.ts @@ -1,5 +1,6 @@ import { z } from "zod" import { optionalContainerTagSchema } from "../container-tag" +import { effectiveContainerTagAccess } from "../auth/rbac" import { MEMORY_TOOL_ANNOTATIONS } from "./annotations" import { addMemoryOutputSchema, type AddMemoryOutput } from "./output-schemas" import { textContent, type ToolDeps } from "./types" @@ -26,6 +27,27 @@ export function register(deps: ToolDeps) { async (args) => { try { const effectiveTag = await deps.resolveContainerTag(args.containerTag) + + // Mirror the write gate applied by guided-save/upload-file: + // an explicitly passed containerTag must not bypass session + // RBAC (restricted/scoped-read sessions must not write into + // spaces their session cannot write to). + const [tags, session] = await Promise.all([ + deps.getClient().listContainerTags(), + deps.getSession(), + ]) + const canWrite = effectiveContainerTagAccess( + tags.map((tag) => tag.containerTag), + session, + ).some( + (access) => + access.permission === "write" && + access.containerTag === effectiveTag, + ) + if (!canWrite) { + throw new Error(`No write access to space "${effectiveTag}"`) + } + const client = deps.getClient(effectiveTag) if (args.action === "forget") { diff --git a/apps/mcp/src/server/tools/fetch-graph-data.ts b/apps/mcp/src/server/tools/fetch-graph-data.ts index 9c1345a8f..99068974a 100644 --- a/apps/mcp/src/server/tools/fetch-graph-data.ts +++ b/apps/mcp/src/server/tools/fetch-graph-data.ts @@ -12,8 +12,11 @@ export function register(deps: ToolDeps) { description: "Fetch documents with memories for graph display", inputSchema: z.object({ containerTag: optionalContainerTagSchema, - page: z.number().optional().default(1), - limit: z.number().optional().default(200), + // Bounded like the sibling list tools: values flow straight + // into a metered backend query, so negatives, fractions, and + // huge limits must be rejected at the schema. + page: z.number().int().min(1).max(10_000).optional().default(1), + limit: z.number().int().min(1).max(1_000).optional().default(200), }), outputSchema: documentsApiResponseSchema, annotations: READ_ONLY_TOOL_ANNOTATIONS, diff --git a/apps/mcp/src/server/tools/get-document.ts b/apps/mcp/src/server/tools/get-document.ts index 01535c732..61213c5c4 100644 --- a/apps/mcp/src/server/tools/get-document.ts +++ b/apps/mcp/src/server/tools/get-document.ts @@ -28,8 +28,23 @@ export function register(deps: ToolDeps) { }, async (args) => { try { + const effectiveTag = await deps.resolveContainerTag() const client = deps.getClient() const document = await client.getDocument(args.documentId) + // Space-scoping check: every sibling read tool filters by the + // resolved space, but get-by-ID fetched any document whose ID + // the caller learned elsewhere. When the backend returns tag + // metadata, enforce that the document belongs to the active + // space; report a generic miss otherwise (no existence + // oracle). Documents without tag metadata cannot be checked. + const docTags = document.containerTags + if ( + Array.isArray(docTags) && + docTags.length > 0 && + !docTags.includes(effectiveTag) + ) { + throw new Error("Document not found") + } const { content, truncated } = getDocumentContent(document) const structuredContent: GetDocumentOutput = { document: { diff --git a/apps/mcp/src/server/tools/guided-save.ts b/apps/mcp/src/server/tools/guided-save.ts index aac1e7ea0..7b886345d 100644 --- a/apps/mcp/src/server/tools/guided-save.ts +++ b/apps/mcp/src/server/tools/guided-save.ts @@ -13,7 +13,13 @@ export function register(deps: ToolDeps) { description: "Open an interactive form when the user wants to draft, review, edit, or choose the target space before saving information to Supermemory. Use this when the user wants to add a memory but has not supplied final content, or explicitly wants to review supplied content before saving. If the user provides the exact content and asks to save it immediately, use add_memory instead.", inputSchema: z.object({ - prefill: z.string().optional().describe("Optional content to prefill"), + // Capped like add_memory's content: an unbounded prefill would + // be allocated and echoed back verbatim in structured output. + prefill: z + .string() + .max(200000, "Prefill exceeds maximum length") + .optional() + .describe("Optional content to prefill"), }), outputSchema: saveViewSchema, _meta: appToolMeta(), diff --git a/apps/mcp/src/server/tools/output-schemas.ts b/apps/mcp/src/server/tools/output-schemas.ts index 6cb0bf66d..2ea8bb3b9 100644 --- a/apps/mcp/src/server/tools/output-schemas.ts +++ b/apps/mcp/src/server/tools/output-schemas.ts @@ -122,7 +122,6 @@ export const whoAmIOutputSchema = z.object({ version: z.string().optional(), }) .optional(), - sessionId: z.string().optional(), }) export type WhoAmIOutput = z.infer diff --git a/apps/mcp/src/server/tools/who-am-i.ts b/apps/mcp/src/server/tools/who-am-i.ts index 629d1a092..de7c814cf 100644 --- a/apps/mcp/src/server/tools/who-am-i.ts +++ b/apps/mcp/src/server/tools/who-am-i.ts @@ -20,7 +20,10 @@ export function register(deps: ToolDeps) { deps.getActiveContainerTag(), ]) const client = deps.getClientInfo(context) - const sessionId = context.sessionId + // Note: the MCP transport session id (context.sessionId) is + // deliberately NOT included — it is a bearer-style transport + // credential and would otherwise be persisted into chat + // transcripts and downstream LLM pipelines. const structuredContent: WhoAmIOutput = { userId: session.user.id, ...(session.user.email ? { email: session.user.email } : {}), @@ -34,7 +37,6 @@ export function register(deps: ToolDeps) { : null, ...(session.scope ? { scope: session.scope } : {}), ...(client ? { client } : {}), - ...(sessionId ? { sessionId } : {}), } return { content: [textContent(JSON.stringify(structuredContent))],