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 <cursoragent@cursor.com>
This commit is contained in:
ved015 2026-07-03 20:50:35 +05:30
parent df671c6e41
commit f7fc662f15

View file

@ -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<File | null>(null)
const [fileError, setFileError] = useState<string | null>(null)
const [selectedTag, setSelectedTag] = useState<string | null>(
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) {
/>
</div>
) : (
<FileUpload
accept={ACCEPT}
description="Supports TXT, PDF, PNG, JPG, MP4"
onFile={setFile}
/>
<Stack gap="sm">
<FileUpload
accept={ACCEPT}
description={`Supports TXT, PDF, PNG, JPG, MP4 · up to ${formatFileSize(MAX_UPLOAD_BYTES)}`}
onFile={handleFileSelect}
/>
{fileError ? (
<p
className="text-(length:--text-xs) leading-relaxed text-error"
role="alert"
>
{fileError}
</p>
) : null}
</Stack>
)}
{writableTags.length > 0 ? (