From 276d35457de1111276e7acdc3eec5d1389c2c248 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Sat, 4 Jul 2026 20:13:59 +0530 Subject: [PATCH 1/6] fix(mcp): show user-facing widget errors for invalid container tags Return structured error views from set-active-tag so the widget shows clear user-facing copy instead of a generic unrecognized-response error. Validate the resolved workspace in search_memory, and distinguish tag list refresh failures from genuinely missing tags. Co-authored-by: Cursor --- apps/mcp/src/server/agent.ts | 7 +++-- apps/mcp/src/server/tools/search-memory.ts | 23 +++++++++----- apps/mcp/src/server/tools/set-active-tag.ts | 23 ++++++++++---- apps/mcp/src/server/tools/types.ts | 31 ++++++++++++++++--- .../server/tools/validate-container-tag.ts | 17 +++++++--- apps/mcp/src/shared/types.ts | 6 ++++ apps/mcp/src/widget/App.tsx | 8 +++++ apps/mcp/src/widget/hooks/useViewState.ts | 17 ++++++++++ apps/mcp/src/widget/views/Error.tsx | 16 ++++++++-- 9 files changed, 120 insertions(+), 28 deletions(-) diff --git a/apps/mcp/src/server/agent.ts b/apps/mcp/src/server/agent.ts index 258fc0600..a5dec716e 100644 --- a/apps/mcp/src/server/agent.ts +++ b/apps/mcp/src/server/agent.ts @@ -8,7 +8,7 @@ import { registerContainerTagsResource } from "./resources/container-tags" import { registerProfileResource } from "./resources/profile" import { registerWidgetResource } from "./resources/widget" import { registerAllTools } from "./tools" -import { errorResult } from "./tools/types" +import { appErrorResult, errorResult } from "./tools/types" type Env = { MCP_SERVER: DurableObjectNamespace @@ -71,6 +71,7 @@ export class SupermemoryMCP extends McpAgent { getClientInfo: () => this.clientInfo, getMcpSessionId: () => this.ctx.id.name ?? "unknown", errorResult, + appErrorResult, } registerAllTools(deps) @@ -103,13 +104,15 @@ export class SupermemoryMCP extends McpAgent { return activeTag || this.props?.containerTag } - private async refreshContainerTags(): Promise { + private async refreshContainerTags(): Promise { try { const client = this.getClient() const tags = await client.listContainerTags() this.cachedContainerTagsList = tags.map((t) => t.containerTag) + return true } catch (error) { console.error("Failed to refresh container tags:", error) + return false } } } diff --git a/apps/mcp/src/server/tools/search-memory.ts b/apps/mcp/src/server/tools/search-memory.ts index a4b7431b1..823f2f44e 100644 --- a/apps/mcp/src/server/tools/search-memory.ts +++ b/apps/mcp/src/server/tools/search-memory.ts @@ -2,8 +2,9 @@ import { z } from "zod" import { getMemoryText } from "../client" import type { ToolDeps } from "./types" import { - containerTagExists, + containerTagValidationUnavailableError, unknownContainerTagError, + validateContainerTag, } from "./validate-container-tag" export function register(deps: ToolDeps) { @@ -47,13 +48,21 @@ export function register(deps: ToolDeps) { ), ) } - if ( - args.containerTag && - !(await containerTagExists(deps, args.containerTag)) - ) { - return deps.errorResult(unknownContainerTagError(args.containerTag)) - } const effectiveTag = await deps.resolveContainerTag(args.containerTag) + if (effectiveTag && !deps.rbac.canRead(effectiveTag)) { + return deps.errorResult( + new Error(`No read access to container tag '${effectiveTag}'.`), + ) + } + if (effectiveTag) { + const validation = await validateContainerTag(deps, effectiveTag) + if (validation === "missing") { + return deps.errorResult(unknownContainerTagError(effectiveTag)) + } + if (validation === "unavailable") { + return deps.errorResult(containerTagValidationUnavailableError()) + } + } const client = deps.getClient(effectiveTag) const parts: string[] = [] diff --git a/apps/mcp/src/server/tools/set-active-tag.ts b/apps/mcp/src/server/tools/set-active-tag.ts index 31e654418..8c1a19677 100644 --- a/apps/mcp/src/server/tools/set-active-tag.ts +++ b/apps/mcp/src/server/tools/set-active-tag.ts @@ -3,8 +3,9 @@ import { z } from "zod" import { SUPERMEMORY_RESOURCE_URI, type ViewMessage } from "../../shared/types" import type { ToolDeps } from "./types" import { - containerTagExists, + containerTagValidationUnavailableError, unknownContainerTagError, + validateContainerTag, } from "./validate-container-tag" export function register(deps: ToolDeps) { @@ -26,16 +27,26 @@ export function register(deps: ToolDeps) { async (args) => { const containerTag = (args as { containerTag: string }).containerTag if (!deps.rbac.canRead(containerTag)) { - return deps.errorResult( - new Error(`No access to container tag '${containerTag}'.`), + return deps.appErrorResult( + new Error( + `You don't have access to '${containerTag}'. Choose a workspace from listSpaces.`, + ), + { kind: "user", title: "No access to this workspace" }, ) } try { - if (!(await containerTagExists(deps, containerTag))) { - return deps.errorResult(unknownContainerTagError(containerTag)) + const validation = await validateContainerTag(deps, containerTag) + if (validation === "missing") { + return deps.appErrorResult(unknownContainerTagError(containerTag), { + kind: "user", + title: "Workspace not found", + }) + } + if (validation === "unavailable") { + return deps.appErrorResult(containerTagValidationUnavailableError()) } } catch (error) { - return deps.errorResult(error) + return deps.appErrorResult(error) } await deps.storage.put("activeContainerTag", containerTag) const sc: ViewMessage = { diff --git a/apps/mcp/src/server/tools/types.ts b/apps/mcp/src/server/tools/types.ts index eb005b0c6..5be7297a0 100644 --- a/apps/mcp/src/server/tools/types.ts +++ b/apps/mcp/src/server/tools/types.ts @@ -1,5 +1,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" import type { Props } from "../../shared/types" +import type { ViewMessage } from "../../shared/types" import type { RbacContext } from "../auth/rbac" import type { SupermemoryClient } from "../client" @@ -16,13 +17,11 @@ export interface ToolDeps { put: (key: string, value: T) => Promise } cachedContainerTags: () => string[] - refreshContainerTags: () => Promise + refreshContainerTags: () => Promise getClientInfo: () => { name: string; version?: string } | null getMcpSessionId: () => string - errorResult: (error: unknown) => { - content: { type: "text"; text: string }[] - isError: true - } + errorResult: typeof errorResult + appErrorResult: typeof appErrorResult } export function errorResult(error: unknown) { @@ -33,3 +32,25 @@ export function errorResult(error: unknown) { isError: true as const, } } + +/** Like errorResult, but includes structuredContent so app widgets render the message. */ +export function appErrorResult( + error: unknown, + options?: { kind?: "user" | "system"; title?: string }, +) { + const message = + error instanceof Error ? error.message : "An unexpected error occurred" + const kind = options?.kind ?? "system" + const text = `Error: ${message}` + const structuredContent: ViewMessage = { + view: "error", + message, + kind, + title: options?.title, + } + return { + content: [{ type: "text" as const, text }], + isError: true as const, + structuredContent, + } +} diff --git a/apps/mcp/src/server/tools/validate-container-tag.ts b/apps/mcp/src/server/tools/validate-container-tag.ts index 5ba0c8602..7a6034ac6 100644 --- a/apps/mcp/src/server/tools/validate-container-tag.ts +++ b/apps/mcp/src/server/tools/validate-container-tag.ts @@ -1,17 +1,20 @@ import type { ToolDeps } from "./types" +export type ContainerTagValidation = "exists" | "missing" | "unavailable" + /** * Checks whether a container tag actually exists for this user. * Fast path is the cached tag list from init; on a miss we re-fetch once so * tags created after the session started are not falsely rejected. */ -export async function containerTagExists( +export async function validateContainerTag( deps: ToolDeps, containerTag: string, -): Promise { - if (deps.cachedContainerTags().includes(containerTag)) return true - await deps.refreshContainerTags() - return deps.cachedContainerTags().includes(containerTag) +): Promise { + if (deps.cachedContainerTags().includes(containerTag)) return "exists" + const refreshed = await deps.refreshContainerTags() + if (!refreshed) return "unavailable" + return deps.cachedContainerTags().includes(containerTag) ? "exists" : "missing" } export function unknownContainerTagError(containerTag: string): Error { @@ -19,3 +22,7 @@ export function unknownContainerTagError(containerTag: string): Error { `Container tag '${containerTag}' does not exist. Use listSpaces to see the available container tags.`, ) } + +export function containerTagValidationUnavailableError(): Error { + return new Error("Could not verify workspace. Please try again.") +} diff --git a/apps/mcp/src/shared/types.ts b/apps/mcp/src/shared/types.ts index 1b5bf9cd2..5f580cdcd 100644 --- a/apps/mcp/src/shared/types.ts +++ b/apps/mcp/src/shared/types.ts @@ -92,6 +92,12 @@ export type ViewMessage = totalCount: number containerTag?: string } + | { + view: "error" + message: string + kind?: "user" | "system" + title?: string + } export type ViewName = ViewMessage["view"] diff --git a/apps/mcp/src/widget/App.tsx b/apps/mcp/src/widget/App.tsx index 736b9a651..03eb93722 100644 --- a/apps/mcp/src/widget/App.tsx +++ b/apps/mcp/src/widget/App.tsx @@ -139,6 +139,14 @@ function renderView( ) case "confirmation": return + case "error": + return ( + + ) case "save-success": return case "upload-success": diff --git a/apps/mcp/src/widget/hooks/useViewState.ts b/apps/mcp/src/widget/hooks/useViewState.ts index 57d35c330..687458f80 100644 --- a/apps/mcp/src/widget/hooks/useViewState.ts +++ b/apps/mcp/src/widget/hooks/useViewState.ts @@ -14,6 +14,11 @@ function safeLog( } } +function toolResultText(result: CallToolResult): string | null { + const item = result.content?.[0] + return item?.type === "text" ? item.text : null +} + type ViewState = | { kind: "loading" } | { kind: "view"; message: ViewMessage } @@ -54,8 +59,20 @@ export function useViewState(): { setState({ kind: "loading" }) } app.ontoolresult = (result: CallToolResult) => { + if (result.isError) { + const text = toolResultText(result) ?? "Tool returned an error" + safeLog("error", `[host] ontoolresult: tool error: ${text}`) + setState({ kind: "error", message: text }) + return + } const sc = (result as { structuredContent?: unknown }).structuredContent if (!sc || typeof sc !== "object") { + const text = toolResultText(result) + if (text) { + safeLog("warning", `[host] ontoolresult: text-only result: ${text}`) + setState({ kind: "error", message: text }) + return + } safeLog("warning", "[host] ontoolresult: no structuredContent") setState({ kind: "raw", structuredContent: sc }) return diff --git a/apps/mcp/src/widget/views/Error.tsx b/apps/mcp/src/widget/views/Error.tsx index fc6fa120f..b1b70221f 100644 --- a/apps/mcp/src/widget/views/Error.tsx +++ b/apps/mcp/src/widget/views/Error.tsx @@ -3,18 +3,28 @@ import { WarningCircle } from "../lib/icons" interface Props { message: string + kind?: "user" | "system" + title?: string } -export function ErrorView({ message }: Props) { +export function ErrorView({ message, kind = "system", title }: Props) { + const heading = + title ?? + (kind === "user" ? "Couldn't complete that" : "Something went wrong") + return ( - +
- Something went wrong + {heading}

{message} From f52da0f6c5b957f4af09453633b7bc6319dd9c72 Mon Sep 17 00:00:00 2001 From: ved015 Date: Fri, 3 Jul 2026 20:50:35 +0530 Subject: [PATCH 2/6] fix(mcp): show clear error for oversized file uploads The MCP transport rejects JSON-RPC bodies over 4 MiB with a bare 413, so large file uploads died with a generic "Upload failed" widget error. The upload widget now rejects oversized files at selection time with the size limit spelled out, advertises the limit in the dropzone copy, and maps any transport 413 that still occurs to the same friendly message. Co-authored-by: Cursor --- apps/mcp/src/widget/views/Upload.tsx | 65 +++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/apps/mcp/src/widget/views/Upload.tsx b/apps/mcp/src/widget/views/Upload.tsx index ed71c856a..63cd4a36c 100644 --- a/apps/mcp/src/widget/views/Upload.tsx +++ b/apps/mcp/src/widget/views/Upload.tsx @@ -29,15 +29,52 @@ function formatFileSize(bytes: number): string { const ACCEPT = ".txt,.pdf,.png,.jpg,.jpeg,.mp4" +// The MCP transport rejects JSON-RPC bodies over 4 MiB with a bare 413 +// (MAXIMUM_MESSAGE_SIZE_BYTES in the agents SDK). The file travels base64 +// encoded (~4/3 inflation) inside that body, so cap the raw file size such +// that the encoded payload plus the JSON-RPC envelope stays under the limit. +const TRANSPORT_MESSAGE_LIMIT_BYTES = 4 * 1024 * 1024 +const ENVELOPE_ALLOWANCE_BYTES = 64 * 1024 +const MAX_UPLOAD_BYTES = Math.floor( + ((TRANSPORT_MESSAGE_LIMIT_BYTES - ENVELOPE_ALLOWANCE_BYTES) * 3) / 4, +) + +const FILE_TOO_LARGE_MESSAGE = (size: number) => + `This file is ${formatFileSize(size)}. The maximum upload size is ${formatFileSize(MAX_UPLOAD_BYTES)} โ€” please choose a smaller file.` + +// The transport-level 413 surfaces as an opaque error string; translate it +// so the user sees the size limit instead of a generic failure. +function friendlyUploadError(raw: string, fileSize: number): string { + if (/413|too large|payload/i.test(raw)) { + return FILE_TOO_LARGE_MESSAGE(fileSize) + } + return raw +} + export function Upload({ activeTag, writableTags, onAdvance, onError }: Props) { const { callTool } = useApp() const log = useLog() const [file, setFile] = useState(null) + const [fileError, setFileError] = useState(null) const [selectedTag, setSelectedTag] = useState( activeTag ?? writableTags[0] ?? null, ) const [uploading, setUploading] = useState(false) + const handleFileSelect = (selected: File) => { + if (selected.size > MAX_UPLOAD_BYTES) { + log( + "warning", + `[upload] rejected oversized file: ${selected.name} (${selected.size}B > ${MAX_UPLOAD_BYTES}B)`, + ) + setFileError(FILE_TOO_LARGE_MESSAGE(selected.size)) + setFile(null) + return + } + setFileError(null) + setFile(selected) + } + const options = useMemo( () => writableTags.map((tag) => ({ value: tag, label: tag })), [writableTags], @@ -59,13 +96,17 @@ export function Upload({ activeTag, writableTags, onAdvance, onError }: Props) { }) if (!result.ok || !result.data) { log("error", `[upload] failed: ${result.error}`) - onError(result.error ?? "Upload failed") + onError( + result.error + ? friendlyUploadError(result.error, file.size) + : "Upload failed", + ) return } onAdvance(result.data) } catch (err) { log("error", `[upload] threw: ${err}`) - onError(String(err)) + onError(friendlyUploadError(String(err), file.size)) } finally { setUploading(false) } @@ -102,11 +143,21 @@ export function Upload({ activeTag, writableTags, onAdvance, onError }: Props) { /> ) : ( - + + + {fileError ? ( +

+ {fileError} +

+ ) : null} +
)} {writableTags.length > 0 ? ( From 521db2b61504f435998224c8142b631d39d500ab Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Sat, 4 Jul 2026 20:15:25 +0530 Subject: [PATCH 3/6] fix(mcp): use function declarations in upload widget Address Graphite style review on Upload.tsx. Co-authored-by: Cursor --- apps/mcp/src/widget/views/Upload.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/mcp/src/widget/views/Upload.tsx b/apps/mcp/src/widget/views/Upload.tsx index 63cd4a36c..6e34490b2 100644 --- a/apps/mcp/src/widget/views/Upload.tsx +++ b/apps/mcp/src/widget/views/Upload.tsx @@ -39,14 +39,15 @@ const MAX_UPLOAD_BYTES = Math.floor( ((TRANSPORT_MESSAGE_LIMIT_BYTES - ENVELOPE_ALLOWANCE_BYTES) * 3) / 4, ) -const FILE_TOO_LARGE_MESSAGE = (size: number) => - `This file is ${formatFileSize(size)}. The maximum upload size is ${formatFileSize(MAX_UPLOAD_BYTES)} โ€” please choose a smaller file.` +function fileTooLargeMessage(size: number): string { + return `This file is ${formatFileSize(size)}. The maximum upload size is ${formatFileSize(MAX_UPLOAD_BYTES)} โ€” please choose a smaller file.` +} // The transport-level 413 surfaces as an opaque error string; translate it // so the user sees the size limit instead of a generic failure. function friendlyUploadError(raw: string, fileSize: number): string { if (/413|too large|payload/i.test(raw)) { - return FILE_TOO_LARGE_MESSAGE(fileSize) + return fileTooLargeMessage(fileSize) } return raw } @@ -61,13 +62,13 @@ export function Upload({ activeTag, writableTags, onAdvance, onError }: Props) { ) const [uploading, setUploading] = useState(false) - const handleFileSelect = (selected: File) => { + function handleFileSelect(selected: File) { if (selected.size > MAX_UPLOAD_BYTES) { log( "warning", `[upload] rejected oversized file: ${selected.name} (${selected.size}B > ${MAX_UPLOAD_BYTES}B)`, ) - setFileError(FILE_TOO_LARGE_MESSAGE(selected.size)) + setFileError(fileTooLargeMessage(selected.size)) setFile(null) return } From 9d80b75c0d0edbe610d274bbf8c7b79283660781 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 3 Jul 2026 21:08:47 +0530 Subject: [PATCH 4/6] fix(mcp): return graph view message from fetch-graph-data --- apps/mcp/src/server/tools/fetch-graph-data.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/mcp/src/server/tools/fetch-graph-data.ts b/apps/mcp/src/server/tools/fetch-graph-data.ts index e2d2b083b..770a0996a 100644 --- a/apps/mcp/src/server/tools/fetch-graph-data.ts +++ b/apps/mcp/src/server/tools/fetch-graph-data.ts @@ -1,6 +1,6 @@ import { registerAppTool } from "@modelcontextprotocol/ext-apps/server" import { z } from "zod" -import { SUPERMEMORY_RESOURCE_URI } from "../../shared/types" +import { SUPERMEMORY_RESOURCE_URI, type ViewMessage } from "../../shared/types" import type { ToolDeps } from "./types" export function register(deps: ToolDeps) { @@ -44,9 +44,23 @@ export function register(deps: ToolDeps) { args.limit, ) + // structuredContent must be a ViewMessage - the widget dispatches on + // `view` and renders anything else as an "unrecognized response" error. + const sc: ViewMessage = { + view: "graph", + containerTag: effectiveTag, + documents: data.documents, + totalCount: data.pagination.totalItems, + } + return { - content: [{ type: "text" as const, text: JSON.stringify(data) }], - structuredContent: data, + content: [ + { + type: "text" as const, + text: `Fetched ${data.documents.length} documents (page ${data.pagination.currentPage} of ${data.pagination.totalPages}, ${data.pagination.totalItems} total)${effectiveTag ? `. Workspace: ${effectiveTag}` : ""}`, + }, + ], + structuredContent: sc, } } catch (error) { return deps.errorResult(error) From ddfc4cda236c962199b1d2d630427a961ee00389 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Sat, 4 Jul 2026 20:18:56 +0530 Subject: [PATCH 5/6] fix(mcp): preserve pagination in fetch-graph-data structuredContent Keep pagination metadata in the graph view response for paginated callers and e2e tests. Co-authored-by: Cursor --- apps/mcp/src/server/tools/fetch-graph-data.ts | 1 + apps/mcp/src/shared/types.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/mcp/src/server/tools/fetch-graph-data.ts b/apps/mcp/src/server/tools/fetch-graph-data.ts index 770a0996a..63aa9ccbd 100644 --- a/apps/mcp/src/server/tools/fetch-graph-data.ts +++ b/apps/mcp/src/server/tools/fetch-graph-data.ts @@ -51,6 +51,7 @@ export function register(deps: ToolDeps) { containerTag: effectiveTag, documents: data.documents, totalCount: data.pagination.totalItems, + pagination: data.pagination, } return { diff --git a/apps/mcp/src/shared/types.ts b/apps/mcp/src/shared/types.ts index 5f580cdcd..835676f85 100644 --- a/apps/mcp/src/shared/types.ts +++ b/apps/mcp/src/shared/types.ts @@ -91,6 +91,7 @@ export type ViewMessage = documents: DocumentWithMemories[] totalCount: number containerTag?: string + pagination?: DocumentsApiResponse["pagination"] } | { view: "error" From 1f74da175601ae94a68e9fea15f2721b0093fe55 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:20:24 +0000 Subject: [PATCH 6/6] style: fix Biome formatting issues Apply Biome auto-formatting to fix CI failures: - Split ternary operator across lines in validate-container-tag.ts - Collapse ErrorView props to single line in App.tsx Co-Authored-By: Claude Opus 4.5 --- apps/mcp/src/server/tools/validate-container-tag.ts | 4 +++- apps/mcp/src/widget/App.tsx | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/mcp/src/server/tools/validate-container-tag.ts b/apps/mcp/src/server/tools/validate-container-tag.ts index 7a6034ac6..fcc2e7386 100644 --- a/apps/mcp/src/server/tools/validate-container-tag.ts +++ b/apps/mcp/src/server/tools/validate-container-tag.ts @@ -14,7 +14,9 @@ export async function validateContainerTag( if (deps.cachedContainerTags().includes(containerTag)) return "exists" const refreshed = await deps.refreshContainerTags() if (!refreshed) return "unavailable" - return deps.cachedContainerTags().includes(containerTag) ? "exists" : "missing" + return deps.cachedContainerTags().includes(containerTag) + ? "exists" + : "missing" } export function unknownContainerTagError(containerTag: string): Error { diff --git a/apps/mcp/src/widget/App.tsx b/apps/mcp/src/widget/App.tsx index 03eb93722..18d0c9ec8 100644 --- a/apps/mcp/src/widget/App.tsx +++ b/apps/mcp/src/widget/App.tsx @@ -141,11 +141,7 @@ function renderView( return case "error": return ( - + ) case "save-success": return