diff --git a/apps/mcp/src/server/tools/fetch-graph-data.ts b/apps/mcp/src/server/tools/fetch-graph-data.ts index 9c1345a8..d86fe12e 100644 --- a/apps/mcp/src/server/tools/fetch-graph-data.ts +++ b/apps/mcp/src/server/tools/fetch-graph-data.ts @@ -12,8 +12,8 @@ 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), + 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 01535c73..ab5f8565 100644 --- a/apps/mcp/src/server/tools/get-document.ts +++ b/apps/mcp/src/server/tools/get-document.ts @@ -7,6 +7,7 @@ import { } from "./output-schemas" import { textContent, type ToolDeps } from "./types" +// An out-of-space document reports "not found" on purpose, so the id is not an existence oracle. export function register(deps: ToolDeps) { const inputSchema = z.object({ documentId: z @@ -28,8 +29,17 @@ export function register(deps: ToolDeps) { }, async (args) => { try { + const effectiveTag = await deps.resolveContainerTag() const client = deps.getClient() const document = await client.getDocument(args.documentId) + 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 aac1e7ea..c3f3c042 100644 --- a/apps/mcp/src/server/tools/guided-save.ts +++ b/apps/mcp/src/server/tools/guided-save.ts @@ -13,7 +13,11 @@ 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"), + 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 6cb0bf66..2ea8bb3b 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 629d1a09..83d33040 100644 --- a/apps/mcp/src/server/tools/who-am-i.ts +++ b/apps/mcp/src/server/tools/who-am-i.ts @@ -20,7 +20,6 @@ export function register(deps: ToolDeps) { deps.getActiveContainerTag(), ]) const client = deps.getClientInfo(context) - const sessionId = context.sessionId const structuredContent: WhoAmIOutput = { userId: session.user.id, ...(session.user.email ? { email: session.user.email } : {}), @@ -34,7 +33,6 @@ export function register(deps: ToolDeps) { : null, ...(session.scope ? { scope: session.scope } : {}), ...(client ? { client } : {}), - ...(sessionId ? { sessionId } : {}), } return { content: [textContent(JSON.stringify(structuredContent))], diff --git a/apps/raycast-extension/src/search-memories.tsx b/apps/raycast-extension/src/search-memories.tsx index dbc47ceb..db6c1f27 100644 --- a/apps/raycast-extension/src/search-memories.tsx +++ b/apps/raycast-extension/src/search-memories.tsx @@ -40,9 +40,18 @@ const extractContent = (memory: SearchResult) => { return "No content available" } +// metadata.url comes from ingested content, so only http(s) reaches the OS opener. const extractUrl = (memory: SearchResult) => { if (memory.metadata?.url && typeof memory.metadata.url === "string") { - return memory.metadata.url + const url = memory.metadata.url + try { + const parsed = new URL(url) + if (parsed.protocol === "https:" || parsed.protocol === "http:") { + return url + } + } catch { + return null + } } return null } diff --git a/packages/ai-sdk/src/tools.ts b/packages/ai-sdk/src/tools.ts index 7d0b837c..82ea39b1 100644 --- a/packages/ai-sdk/src/tools.ts +++ b/packages/ai-sdk/src/tools.ts @@ -21,6 +21,13 @@ type AddMemoryInput = { memory: string } +// The schema constrains well-behaved models; a prompt-injected one can still send anything. +function clampSearchLimit(value: unknown): number { + const parsed = Number(value) + if (!Number.isFinite(parsed)) return 10 + return Math.min(50, Math.max(1, Math.floor(parsed))) +} + /** * Create Supermemory tools for AI SDK */ @@ -30,6 +37,8 @@ export function supermemoryTools( ) { const client = new Supermemory({ apiKey, + timeout: 30_000, + maxRetries: 2, ...(config?.baseUrl ? { baseURL: config.baseUrl } : {}), }) @@ -54,8 +63,10 @@ export function supermemoryTools( default: true, }, limit: { - type: "number", - description: "Maximum number of results to return", + type: "integer", + minimum: 1, + maximum: 50, + description: "Maximum number of results to return (1-50)", default: 10, }, }, @@ -67,10 +78,11 @@ export function supermemoryTools( limit = 10, }) => { try { + const safeLimit = clampSearchLimit(limit) const response = await client.search.execute({ q: informationToGet, containerTags, - limit, + limit: safeLimit, chunkThreshold: 0.6, includeFullDocs, }) diff --git a/packages/validation/api.test.ts b/packages/validation/api.test.ts index e186af88..fa52ed89 100644 --- a/packages/validation/api.test.ts +++ b/packages/validation/api.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "bun:test" import { readFileSync } from "node:fs" import { + BulkDeleteMemoriesSchema, DocumentsWithMemoriesQuerySchema, ListMemoriesQuerySchema, SearchRequestSchema, @@ -151,4 +152,26 @@ describe("pagination query schemas", () => { expect(parsed.page).toBe(2) expect(parsed.limit).toBe(50) }) + + it("DocumentsWithMemoriesQuerySchema caps limit at 1000", () => { + expect( + DocumentsWithMemoriesQuerySchema.safeParse({ limit: 1001 }).success, + ).toBe(false) + expect( + DocumentsWithMemoriesQuerySchema.safeParse({ limit: 200 }).success, + ).toBe(true) + }) + + it("BulkDeleteMemoriesSchema caps containerTags at 100 entries of bounded length", () => { + const tooMany = { + containerTags: Array.from({ length: 101 }, (_, i) => `tag_${i}`), + } + expect(BulkDeleteMemoriesSchema.safeParse(tooMany).success).toBe(false) + + const tagTooLong = { containerTags: ["x".repeat(257)] } + expect(BulkDeleteMemoriesSchema.safeParse(tagTooLong).success).toBe(false) + + const ok = { containerTags: ["tag_a", "tag_b"] } + expect(BulkDeleteMemoriesSchema.safeParse(ok).success).toBe(true) + }) }) diff --git a/packages/validation/api.ts b/packages/validation/api.ts index f066bfcd..ae6ac319 100644 --- a/packages/validation/api.ts +++ b/packages/validation/api.ts @@ -1102,8 +1102,8 @@ export const DocumentsWithMemoriesQuerySchema = z description: "Page number to fetch", example: 1, }), - limit: z.number().int().min(1).default(10).openapi({ - description: "Number of items per page", + limit: z.number().int().min(1).max(1000).default(10).openapi({ + description: "Number of items per page (max 1000)", example: 10, }), sort: z.enum(["createdAt", "updatedAt"]).default("createdAt").openapi({ @@ -1409,12 +1409,13 @@ export const BulkDeleteMemoriesSchema = z example: ["acxV5LHMEsG2hMSNb4umbn", "bxcV5LHMEsG2hMSNb4umbn"], }), containerTags: z - .array(z.string()) + .array(z.string().max(256)) .min(1) + .max(100) .optional() .openapi({ description: - "Array of container tags - all memories in these containers will be deleted", + "Array of container tags - all memories in these containers will be deleted (max 100 at once)", example: ["user_123", "project_123"], }), })