Skip to content

Commit 3a02393

Browse files
ved015cursoragent
andcommitted
fix(mcp): clear oversized-upload error and graph view for fetch-graph-data
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. fetch-graph-data returned raw DocumentsApiResponse as structuredContent, which the widget cannot dispatch on, rendering "Received unrecognized response from server" despite the call succeeding. It now returns a proper graph ViewMessage so the widget renders the memory graph. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent df671c6 commit 3a02393

2 files changed

Lines changed: 75 additions & 10 deletions

File tree

apps/mcp/src/server/tools/fetch-graph-data.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server"
22
import { z } from "zod"
3-
import { SUPERMEMORY_RESOURCE_URI } from "../../shared/types"
3+
import { SUPERMEMORY_RESOURCE_URI, type ViewMessage } from "../../shared/types"
44
import type { ToolDeps } from "./types"
55

66
export function register(deps: ToolDeps) {
@@ -44,9 +44,23 @@ export function register(deps: ToolDeps) {
4444
args.limit,
4545
)
4646

47+
// structuredContent must be a ViewMessage — the widget dispatches on
48+
// `view` and renders anything else as an "unrecognized response" error.
49+
const sc: ViewMessage = {
50+
view: "graph",
51+
containerTag: effectiveTag,
52+
documents: data.documents,
53+
totalCount: data.pagination.totalItems,
54+
}
55+
4756
return {
48-
content: [{ type: "text" as const, text: JSON.stringify(data) }],
49-
structuredContent: data,
57+
content: [
58+
{
59+
type: "text" as const,
60+
text: `Fetched ${data.documents.length} documents (page ${data.pagination.currentPage} of ${data.pagination.totalPages}, ${data.pagination.totalItems} total)${effectiveTag ? `. Workspace: ${effectiveTag}` : ""}`,
61+
},
62+
],
63+
structuredContent: sc,
5064
}
5165
} catch (error) {
5266
return deps.errorResult(error)

apps/mcp/src/widget/views/Upload.tsx

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,52 @@ function formatFileSize(bytes: number): string {
2929

3030
const ACCEPT = ".txt,.pdf,.png,.jpg,.jpeg,.mp4"
3131

32+
// The MCP transport rejects JSON-RPC bodies over 4 MiB with a bare 413
33+
// (MAXIMUM_MESSAGE_SIZE_BYTES in the agents SDK). The file travels base64
34+
// encoded (~4/3 inflation) inside that body, so cap the raw file size such
35+
// that the encoded payload plus the JSON-RPC envelope stays under the limit.
36+
const TRANSPORT_MESSAGE_LIMIT_BYTES = 4 * 1024 * 1024
37+
const ENVELOPE_ALLOWANCE_BYTES = 64 * 1024
38+
const MAX_UPLOAD_BYTES = Math.floor(
39+
((TRANSPORT_MESSAGE_LIMIT_BYTES - ENVELOPE_ALLOWANCE_BYTES) * 3) / 4,
40+
)
41+
42+
const FILE_TOO_LARGE_MESSAGE = (size: number) =>
43+
`This file is ${formatFileSize(size)}. The maximum upload size is ${formatFileSize(MAX_UPLOAD_BYTES)} — please choose a smaller file.`
44+
45+
// The transport-level 413 surfaces as an opaque error string; translate it
46+
// so the user sees the size limit instead of a generic failure.
47+
function friendlyUploadError(raw: string, fileSize: number): string {
48+
if (/413|too large|payload/i.test(raw)) {
49+
return FILE_TOO_LARGE_MESSAGE(fileSize)
50+
}
51+
return raw
52+
}
53+
3254
export function Upload({ activeTag, writableTags, onAdvance, onError }: Props) {
3355
const { callTool } = useApp()
3456
const log = useLog()
3557
const [file, setFile] = useState<File | null>(null)
58+
const [fileError, setFileError] = useState<string | null>(null)
3659
const [selectedTag, setSelectedTag] = useState<string | null>(
3760
activeTag ?? writableTags[0] ?? null,
3861
)
3962
const [uploading, setUploading] = useState(false)
4063

64+
const handleFileSelect = (selected: File) => {
65+
if (selected.size > MAX_UPLOAD_BYTES) {
66+
log(
67+
"warning",
68+
`[upload] rejected oversized file: ${selected.name} (${selected.size}B > ${MAX_UPLOAD_BYTES}B)`,
69+
)
70+
setFileError(FILE_TOO_LARGE_MESSAGE(selected.size))
71+
setFile(null)
72+
return
73+
}
74+
setFileError(null)
75+
setFile(selected)
76+
}
77+
4178
const options = useMemo(
4279
() => writableTags.map((tag) => ({ value: tag, label: tag })),
4380
[writableTags],
@@ -59,13 +96,17 @@ export function Upload({ activeTag, writableTags, onAdvance, onError }: Props) {
5996
})
6097
if (!result.ok || !result.data) {
6198
log("error", `[upload] failed: ${result.error}`)
62-
onError(result.error ?? "Upload failed")
99+
onError(
100+
result.error
101+
? friendlyUploadError(result.error, file.size)
102+
: "Upload failed",
103+
)
63104
return
64105
}
65106
onAdvance(result.data)
66107
} catch (err) {
67108
log("error", `[upload] threw: ${err}`)
68-
onError(String(err))
109+
onError(friendlyUploadError(String(err), file.size))
69110
} finally {
70111
setUploading(false)
71112
}
@@ -102,11 +143,21 @@ export function Upload({ activeTag, writableTags, onAdvance, onError }: Props) {
102143
/>
103144
</div>
104145
) : (
105-
<FileUpload
106-
accept={ACCEPT}
107-
description="Supports TXT, PDF, PNG, JPG, MP4"
108-
onFile={setFile}
109-
/>
146+
<Stack gap="sm">
147+
<FileUpload
148+
accept={ACCEPT}
149+
description={`Supports TXT, PDF, PNG, JPG, MP4 · up to ${formatFileSize(MAX_UPLOAD_BYTES)}`}
150+
onFile={handleFileSelect}
151+
/>
152+
{fileError ? (
153+
<p
154+
className="text-(length:--text-xs) leading-relaxed text-error"
155+
role="alert"
156+
>
157+
{fileError}
158+
</p>
159+
) : null}
160+
</Stack>
110161
)}
111162

112163
{writableTags.length > 0 ? (

0 commit comments

Comments
 (0)