Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/mcp/src/server/tools/add-memory.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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") {
Expand Down
7 changes: 5 additions & 2 deletions apps/mcp/src/server/tools/fetch-graph-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 15 additions & 0 deletions apps/mcp/src/server/tools/get-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
8 changes: 7 additions & 1 deletion apps/mcp/src/server/tools/guided-save.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
1 change: 0 additions & 1 deletion apps/mcp/src/server/tools/output-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ export const whoAmIOutputSchema = z.object({
version: z.string().optional(),
})
.optional(),
sessionId: z.string().optional(),
})

export type WhoAmIOutput = z.infer<typeof whoAmIOutputSchema>
6 changes: 4 additions & 2 deletions apps/mcp/src/server/tools/who-am-i.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } : {}),
Expand All @@ -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))],
Expand Down