From f7fc662f156419c8ea7626263fd4ebb7865033d8 Mon Sep 17 00:00:00 2001 From: ved015 Date: Fri, 3 Jul 2026 20:50:35 +0530 Subject: [PATCH] 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 ed71c856..63cd4a36 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 ? (