作者在实际集成MCP到会计工作流时发现文件上传全部失败,逐一验证7个主流服务(MCP服务器),结果无一在MCP协议层面支持文件上传,需绕过协议才能实现。
那张收据附件是一切的起点
我正在把 MCP 接入一个会计工作流。整个流程运行得很顺畅,直到最后一步——而这恰恰是整个工作流存在的目的:将收据 PDF 附加到交易记录上。Agent 产生了一个漂亮且规范的工具调用请求。然后服务器基本上回了一句:"不行。"
我以为是某个 MCP 服务器的 bug。于是换了一个试。结果是相同形状的失败,只是错误信息不同。
那一刻我不再怪罪厂商,而是开始阅读 MCP 规范。
我挑选了七项服务,在这些服务里"上传文件"不是可选项,而是某些场景下工作流存在的全部理由。
freee(会计系统)——收据、发票
Jira / Confluence——问题单上的截图、日志
Notion——页面中的图片、PDF
GitHub——PR 上的截图
Gmail——外发邮件的附件
Google Drive——顾名思义
Slack——频道里的文件
每项服务都有一个官方或知名的 MCP 服务器。每项服务的底层 HTTP API 都支持文件上传。我真正关心的问题更窄:MCP 客户端通过 MCP 协议,能否把一个文件送进那个服务?
最终的统计结果是:
✅ 完整协议支持:0
△ 可以工作,但通过绕开协议:4
七项服务中零个完整实现。这就是结论。

原因:MCP 没有 FileContent 类型
原因就在规范里的一段话,不是任何一个服务器的问题。
MCP 工具结果可以携带 TextContent、ImageContent(base64)、AudioContent、ResourceLink 和 EmbeddedResource。没有 FileContent。任何不是文本、base64 图片或 base64 音频 blob 的东西都走在协议的愉快路径之外。EmbeddedResource 允许你指向一个 URI,这听起来像是一个变通方案,直到你注意到它只是把问题转移了——客户端和服务器仍然需要就如何实际移动字节达成一致,而 MCP 对此没有任何说法。
最清晰的确认来自 GitHub Discussion #1197 上的一位 Anthropic 维护者,回复给一个恰好撞上这堵墙的人:"我不认为你漏看了什么,你的用例在协议当前状态下确实不太好用。"
这就是全部。协议在这里故意"不太好用"。这不是一个缺陷,而是设计上的取舍。
提议的修复方案,以及后续发展
有一个关于此事的活跃讨论线程。SEP-1306("文件上传的二进制模式引出")于 2025 年 8 月提出,现已关闭——不过"草稿"标签还挂着,如果你只在搜索结果里瞄一眼会以为还活着。核心思路是:扩展引出功能,增加第三种模式,让服务器可以向用户请求文件,维持现有的、与文本和确认框相同的用户同意模型。
截至 2026 年中期规范周期的状态大致是:
SEP-1306 在 2026 年初作为提案搁置。
2026 年 7 月的发布候选版本完全推迟了二进制传输(后续有 SEP-2631 草案也被推后了)。
工作已转移到一个文件上传工作组,锚定在 SEP-2356 上,设计讨论现在在那里进行。
解读:有一个计划,但没有已交付的协议方案。如果你有客户在等着上传今天的收据 pending 的规范帮不了你。
三个直接拒绝的实际情况
不是转述——这是当你追问时,维护者和文档给出的原话。
freee。底层 API 接受 multipart/form-data。MCP 的 JSON-RPC 传输不支持 multipart。所以这个 API 里有这个工具,但通过 MCP 走不通。如果你的合规制度要求收据图片必须存档,这是最让人痛的一个。
Jira / Confluence。Atlassian 的社区答案是直接的:"不支持通过 MCP 远程 Agent 上传文件或图片附件。"更糟的是,社区的 mcp-atlassian 服务器需要文件先放在 MCP 服务器的文件系统上才能附件——这在服务器运行在 Docker 容器而文件在客户端磁盘上的那一刻就炸了。Issue #618 有确切的堆栈跟踪:"File not found: /home/user/jira-mcp/grafana.png"。
GitHub。在 github-mcp-server Issue #738 中提出:"MCP 需要能够上传图片,而目前看起来还做不到。这样我们就能拥有更具描述性/可视化的 PR。"显而易见的用例——"这是 UI 回归的截图"——根本不存在。
四个通过绕开协议来工作的方案
Gmail。一些社区 Gmail MCP 服务器接受本地文件路径作为工具参数,然后服务器自己读取文件并附件。字节从未穿过 MCP。如果 MCP 服务器和文件不在同一个文件系统上,你就又回到原点了。
Google Drive。同样的思路。服务器从本地路径读取然后调用 Drive API。在笔记本上很方便。一旦"服务器"在别的地方就脆弱了。
Slack。CData Slack MCP 有一个 UploadFile 工具,行为与此类似。Slack 自己的 MCP 专注于消息和搜索——那里没有上传原语。
这三个的共同模式:MCP 服务器是代理,位于一个客户端已经信任的文件系统之上。它能工作,而且有特定的原因可以工作,但把它称为"MCP 文件上传支持"是夸张的。它其实是"MCP 文本调用触发了一个文件系统读取"。
Notion。这个是另一种形状,而且更好。Notion 的 MCP 暴露了 notion-create-file-upload:工具返回一个短期上传 URL,以及客户端必须使用的请求头和表单字段,然后客户端自己发送字节。最大 20 MiB。看清楚这是什么——这是我在这篇文章底部落脚点的预签名 URL 模式,已经交付了。字节仍然没有通过 MCP 传输,这就是它放在这组而不是第一组的原因。但没有人需要等待规范。
为什么协议被设计成这个样子
三个原因,都站得住脚,即便它们带来不便。
JSON-RPC 优先。MCP 是一个 JSON/文本协议。二进制传输在设计上是超出范围的。添加它不是改一行的问题,而是形状的改变。
安全。一旦你允许任意路径穿越工具边界,路径注入命令执行、"请读取这个文件"的数据外泄、恶意软件夹带都变得更简单。上面的 Jira 变通方案恰好说明了为什么"直接接受路径"模式有安全隐患。
上下文成本。Base64 编码使 payload 膨胀大约 33%。一张 1 MB 的图片变成 ~1.33 MB 的文本——更糟的是——变成模型在每一轮仍能看到的 token。即使你作为 ImageContent 发出,你也在消耗无法回收的上下文。
这些都不能让收据自己附件。但它们确实让"为什么这事这么难"的答案变得合理,而不令人尴尬。
当前真正可行的方案
既然协议不搬运字节,就别再让它搬了。三个模式在生产环境中站得住脚,大致按从整洁到勉强接受的顺序排列。
// Tool 1: 找个地方放字节
const { uploadUrl, fileRef } = await mcp.call("prepare_upload", {
filename: "receipt-2026-08.pdf",
size: fileSize,
});
// 字节通过纯 HTTPS 传输,不是 MCP
await fetch(uploadUrl, { method: "PUT", body: fileBuffer });
// Tool 2: 引用上传的对象
await mcp.call("attach_receipt", { transactionId, fileRef });
共享对象存储作为媒介。客户端和服务器都认证到同一个桶。客户端上传;MCP 调用只携带对象键。当客户端和服务器在同一个信任区域时没问题。不在的时候就很丑。
通过 ImageContent 传 base64,而且只用于图片。对小截图确实有用。在 500 KB 以上就崩溃了,既因为 token 成本,也因为你会有请求大小限制。PDF 永远不要这样做。就别。
我最不推荐的一个:把原始文件系统路径穿越边界扔过去然后祈祷。它"能用",直到服务器迁移到容器然后就不行了,而且是无声的。
MCP 没有文件上传原语,而且修复方案至少被推迟了一个主要规范周期。在它落地之前,把 MCP 当作协调者,让字节通过你已经信任的带外通道移动。你读到的每一个"MCP 文件上传能用!"教程,要么是在底层做了我说的这些,要么是即将让复制它的人踩坑。
如果你想要完整的剖析——7 服务拆解含确切错误信息、SEP-1306 时间线、OWASP MCP Top 10 威胁模型,以及生产就绪的预签名 URL 服务器模板——我写在了 MCP Security in Practice 里。