Code Mode:使用MCP的更好方式
原文:Code Mode: the better way to use MCP 作者:Kenton Varda, Sunil Pai 发布时间:2025年9月26日
事实证明,我们一直以来使用MCP的方式都是错误的。
目前大多数智能体使用MCP的方式是直接将”工具”暴露给大语言模型(LLM)。
我们尝试了一种不同的方法:将MCP工具转换为TypeScript API,然后让LLM编写调用该API的代码。
结果令人震撼:
-
我们发现,当工具以TypeScript API的形式呈现时,智能体能够处理更多数量和更复杂的工具,而不是直接暴露工具。 这可能是因为LLM的训练集中包含了大量真实世界的TypeScript代码,但只有少量人为构造的工具调用示例。
-
当智能体需要串联多个调用时,这种方法尤其出色。 使用传统方法时,每个工具调用的输出必须先输入到LLM的神经网络中,然后才能复制到下一个调用的输入中,浪费了时间、能源和token。而当LLM可以编写代码时,它可以跳过所有这些步骤,只读取它需要的最终结果。
简而言之,LLM在编写代码调用MCP方面比直接调用MCP更出色。
什么是MCP?
对于不熟悉的人来说:模型上下文协议(Model Context Protocol)是一个标准协议,用于为AI智能体提供外部工具访问权限,使它们能够直接执行工作,而不仅仅是与你聊天。
换个角度看,MCP是一种统一的方式来:
- 暴露执行某项操作的API
- 提供LLM理解它所需的文档
- 在带外处理授权
MCP在2025年掀起了巨大波澜,因为它突然大幅扩展了AI智能体的能力。
MCP服务器暴露的”API”表达为一组”工具”。每个工具本质上是一个远程过程调用(RPC)函数——使用一些参数调用它,并返回响应。大多数现代LLM都具有使用”工具”(有时称为”函数调用”)的能力,这意味着它们经过训练,当想要调用工具时,会以特定格式输出文本。调用LLM的程序看到这种格式后,会按照指定调用工具,然后将结果作为输入反馈回LLM。
工具调用的解剖
在底层,LLM生成表示其输出的”token”流。一个token可能代表一个单词、一个音节、某种标点符号或文本的其他组成部分。
然而,工具调用涉及一个没有任何文本等效物的token。LLM经过训练(或者更常见的是微调)来理解一个特殊的token,它可以输出该token来表示”以下内容应被解释为工具调用”,以及另一个特殊token来表示”这是工具调用的结束”。在这两个token之间,LLM通常会写入对应于某种JSON消息的token,该消息描述调用。
例如,假设你已将智能体连接到提供天气信息的MCP服务器,然后询问智能体Austin, TX的天气如何。在底层,LLM可能会生成如下输出。请注意,这里我们使用<|和|>中的单词来表示我们的特殊token,但实际上这些token根本不代表文本;这只是为了说明。
我将使用天气MCP服务器查找Austin, TX的天气。
<|tool_call|>
{
"name": "get_current_weather",
"arguments": {
"location": "Austin, TX, USA"
}
}
<|end_tool_call|>
在输出中看到这些特殊token后,LLM的线束(harness)会将该序列解释为工具调用。看到结束token后,线束暂停LLM的执行。它解析JSON消息并将其作为结构化API结果的单独组件返回。调用LLM API的智能体看到工具调用,调用相关的MCP服务器,然后将结果发送回LLM API。然后,LLM的线束将使用另一组特殊token将结果反馈回LLM:
<|tool_result|>
{
"location": "Austin, TX, USA",
"temperature": 93,
"unit": "fahrenheit",
"conditions": "sunny"
}
<|end_tool_result|>
LLM读取这些token的方式与读取用户输入完全相同——只是用户无法产生这些特殊token,因此LLM知道这是工具调用的结果。然后LLM继续像往常一样生成输出。
不同的LLM可能使用不同的工具调用格式,但基本思想就是这样。
这有什么问题?
工具调用中使用的特殊token是LLM在实际环境中从未见过的东西。它们必须基于合成训练数据进行专门训练才能使用工具。它们并不总是那么擅长。如果你向LLM提供太多工具或过于复杂的工具,它可能很难选择正确的工具或正确使用它。因此,鼓励MCP服务器设计者提供大大简化的API,而不是他们可能向开发者暴露的更传统的API。
与此同时,LLM在编写代码方面变得非常出色。实际上,当LLM被要求针对通常暴露给开发者的完整、复杂的API编写代码时,似乎并没有太大困难。那么,为什么MCP接口必须”降低智商”呢?编写代码和调用工具几乎是同一件事,但似乎LLM在其中一项上的表现要好得多?
答案很简单:LLM见过很多代码。它们没有见过很多”工具调用”。 实际上,它们见过的工具调用可能仅限于LLM自己的开发者构建的人为训练集,目的是尝试训练它。而它们见过来自数百万个开源项目的真实世界代码。
让LLM使用工具调用执行任务,就像让莎士比亚上一个月的汉语速成班,然后要求他用汉语写一部戏剧。这不会是他的最佳作品。
但MCP仍然有用,因为它是统一的
MCP是为工具调用设计的,但实际上它不必以这种方式使用。
MCP服务器暴露的”工具”实际上只是带有附加文档的RPC接口。我们真的不必将它们作为工具呈现。我们可以获取这些工具,并将它们转换为编程语言API。
但是,当编程语言API已经独立存在时,我们为什么要这样做呢?几乎每个MCP服务器只是现有传统API的包装器——为什么不暴露那些API呢?
嗯,事实证明MCP还做了另一件非常有用的事情:它提供了一种统一的方式来连接和学习API。
即使智能体的开发者从未听说过特定的MCP服务器,并且MCP服务器的开发者从未听说过特定的智能体,AI智能体也可以使用MCP服务器。这在过去传统API中很少出现。通常,客户端开发者总是确切地知道他们为哪个API编码。因此,每个API都能够以略微不同的方式处理基本连接、授权和文档等事项。
即使当AI智能体编写代码时,这种统一性也很有用。我们希望AI智能体在沙箱中运行,这样它只能访问我们提供给它的工具。MCP使智能框架能够实现这一点,通过以标准方式处理连接和授权,独立于AI代码。我们也不希望AI必须在互联网上搜索文档;MCP直接在协议中提供它。
好的,它是如何工作的?
我们已经扩展了Cloudflare Agents SDK以支持这种新模型!
例如,假设你有一个使用ai-sdk构建的应用程序,如下所示:
const stream = streamText({
model: openai("gpt-5"),
system: "You are a helpful assistant",
messages: [
{ role: "user", content: "Write a function that adds two numbers" }
],
tools: {
// 工具定义
}
})
你可以使用codemode辅助函数包装工具和提示,并在应用程序中使用它们:
import { codemode } from "agents/codemode/ai";
const {system, tools} = codemode({
system: "You are a helpful assistant",
tools: {
// 工具定义
},
// ...config
})
const stream = streamText({
model: openai("gpt-5"),
system,
tools,
messages: [
{ role: "user", content: "Write a function that adds two numbers" }
]
})
通过这个更改,你的应用程序现在将开始生成并运行代码,这些代码本身将调用你定义的工具,包括MCP服务器。我们将在不久的将来为其他库引入变体。阅读文档以获取更多详细信息和示例。
将MCP转换为TypeScript
当你在”代码模式”下连接到MCP服务器时,Agents SDK将获取MCP服务器的架构,然后将其转换为TypeScript API,包括基于架构的文档注释。
例如,连接到https://gitmcp.io/cloudflare/agents的MCP服务器将生成如下TypeScript定义:
interface FetchAgentsDocumentationInput {
[k: string]: unknown;
}
interface FetchAgentsDocumentationOutput {
[key: string]: any;
}
interface SearchAgentsDocumentationInput {
/**
* 用于查找相关文档的搜索查询
*/
query: string;
}
interface SearchAgentsDocumentationOutput {
[key: string]: any;
}
interface SearchAgentsCodeInput {
/**
* 用于查找相关代码文件的搜索查询
*/
query: string;
/**
* 要检索的页码(从1开始)。每页包含30个结果。
*/
page?: number;
}
interface SearchAgentsCodeOutput {
[key: string]: any;
}
interface FetchGenericUrlContentInput {
/**
* 要获取的文档或页面的URL
*/
url: string;
}
interface FetchGenericUrlContentOutput {
[key: string]: any;
}
declare const codemode: {
/**
* 从GitHub仓库cloudflare/agents获取完整的文档文件。
* 适用于一般性问题。如果被问到cloudflare/agents,请始终首先调用此工具。
*/
fetch_agents_documentation: (
input: FetchAgentsDocumentationInput
) => Promise<FetchAgentsDocumentationOutput>;
/**
* 在从GitHub仓库cloudflare/agents获取的文档中进行语义搜索。
* 适用于特定查询。
*/
search_agents_documentation: (
input: SearchAgentsDocumentationInput
) => Promise<SearchAgentsDocumentationOutput>;
/**
* 使用GitHub Search API(精确匹配)在GitHub仓库"cloudflare/agents"中搜索代码。
* 返回匹配的文件,以便你进一步查询(如果相关)。
*/
search_agents_code: (
input: SearchAgentsCodeInput
) => Promise<SearchAgentsCodeOutput>;
/**
* 通用工具,用于从任何绝对URL获取内容,遵守robots.txt规则。
* 使用此工具检索先前获取的文档中提到的引用URL(绝对URL)。
*/
fetch_generic_url_content: (
input: FetchGenericUrlContentInput
) => Promise<FetchGenericUrlContentOutput>;
};
然后,这个TypeScript被加载到智能体的上下文中。目前,整个API都被加载,但未来的改进可以允许智能体更动态地搜索和浏览API——很像智能编码助手那样。
在沙箱中运行代码
我们的智能体不再看到所有已连接MCP服务器的所有工具,而是只看到一个工具,它只是执行一些TypeScript代码。
然后代码在安全沙箱中执行。沙箱与互联网完全隔离。它访问外部世界的唯一途径是通过表示其已连接MCP服务器的TypeScript API。
这些API由RPC调用支持,该调用回调到智能体循环。在那里,Agents SDK将调用分派到适当的MCP服务器。
沙箱代码以明显的方式将结果返回给智能体:通过调用console.log()。当脚本完成时,所有输出日志都会传递回智能体。

动态Worker加载:这里没有容器
这种新方法需要访问一个安全沙箱,可以在其中运行任意代码。那么我们在哪里找到一个呢?我们必须运行容器吗?那很贵吗?
不。这里没有容器。我们有更好的东西:isolates(隔离区)。
Cloudflare Workers平台一直基于V8 isolates,即由V8 JavaScript引擎提供支持的隔离JavaScript运行时。
Isolates比容器轻量得多。 一个isolate可以在几毫秒内启动,只使用几兆字节的内存。
Isolates如此之快,以至于我们可以为智能体运行的每段代码创建一个新的。无需重用它们。无需预热它们。只需按需创建它,运行代码,然后丢弃它。这一切发生得如此之快,以至于开销可以忽略不计;就好像你只是直接eval()代码一样。但有安全保障。
Worker Loader API
然而,到目前为止,还没有办法让Worker直接加载包含任意代码的isolate。所有Worker代码都必须通过Cloudflare API上传,然后全球部署,以便它可以在任何地方运行。这不是我们想要用于Agents的!我们希望代码就在智能体所在的位置运行。
为此,我们为Workers平台添加了一个新的API:Worker Loader API。使用它,你可以按需加载Worker代码。它的样子如下:
// 获取具有给定ID的Worker,如果不存在这样的Worker则创建它。
let worker = env.LOADER.get(id, async () => {
// 如果Worker尚不存在,则调用此回调来获取其代码。
return {
compatibilityDate: "2025-06-01",
// 指定worker的代码(模块文件)。
mainModule: "foo.js",
modules: {
"foo.js":
"export default {\n" +
" fetch(req, env, ctx) { return new Response('Hello'); }\n" +
"}\n",
},
// 指定动态Worker的环境(`env`)。
env: {
// 它可以包含基本的可序列化数据类型...
SOME_NUMBER: 123,
// ...以及使用新的`ctx.exports`环回绑定API绑定回父worker导出的RPC接口。
SOME_RPC_BINDING: ctx.exports.MyBindingImpl({props})
},
// 将Worker的`fetch()`和`connect()`重定向到通过父worker代理,
// 以监视或过滤所有互联网访问。你也可以通过传递`null`完全阻止互联网访问。
globalOutbound: ctx.exports.OutboundProxy({props}),
};
});
// 现在你可以获取Worker的入口点并向其发送请求。
let defaultEntrypoint = worker.getEntrypoint();
await defaultEntrypoint.fetch("http://example.com");
// 你也可以获取非默认入口点,并指定要传递给入口点的`ctx.props`值。
let someEntrypoint = worker.getEntrypoint("SomeEntrypointClass", {
props: {someProp: 123}
});
你现在可以在使用Wrangler本地运行workerd时开始使用这个API(查看文档),并且你可以注册beta访问权限以在生产环境中使用它。
Workers是更好的沙箱
Workers的设计使其在沙箱方面异常出色,特别是对于这个用例,原因有几个:
更快、更便宜、一次性的沙箱
Workers平台使用isolates而不是容器。Isolates更轻量,启动速度更快。启动一个新的isolate只需几毫秒,而且非常便宜,我们可以为智能体生成的每个代码片段创建一个新的isolate。无需担心池化isolates以重用、预热等。
我们尚未最终确定Worker Loader API的定价,但由于它基于isolates,我们将能够以显著低于基于容器的解决方案的成本提供它。
默认隔离,但通过绑定连接
Workers在处理隔离方面就是更好。
在Code Mode中,我们禁止沙箱worker与互联网通信。全局fetch()和connect()函数会抛出错误。
但在大多数平台上,这会是一个问题。在大多数平台上,访问私有资源的方式是,你首先拥有一般网络访问权限。然后,使用该网络访问权限,你向特定服务发送请求,向它们传递某种API密钥以授权私有访问。
但Workers一直有更好的答案。在Workers中,“环境”(env对象)不仅包含字符串,它包含活动对象,也称为”绑定”。这些对象可以提供对私有资源的直接访问,而无需涉及通用网络请求。
在Code Mode中,我们为沙箱提供了表示它连接到的MCP服务器的绑定访问权限。因此,智能体可以特别访问那些MCP服务器,而无需一般网络访问权限。
通过绑定限制访问比通过网络级过滤或HTTP代理等方式更清晰。过滤对LLM和监督者都很困难,因为边界通常不清楚:监督者可能很难准确识别哪些流量是与API通信所必需的。同时,LLM可能很难猜测哪些类型的请求会被阻止。使用绑定方法,它是明确定义的:绑定提供JavaScript接口,该接口允许被使用。这样更好。
没有API密钥泄露
绑定的另一个好处是它们隐藏了API密钥。绑定本身提供了一个已授权的客户端接口到MCP服务器。对它进行的所有调用首先到达智能体监督者,监督者持有访问令牌并将它们添加到发送到MCP的请求中。
这意味着AI不可能编写泄露任何密钥的代码,解决了当今AI编写的代码中常见的安全问题。
实践:code-mode-mcp
想要体验 Code Mode 的强大?查看这个开源实践项目:https://github.com/cexll/code-mode-mcp
🚀 如何在 Claude Code 中使用
方式一:独立运行(推荐新手)
# 1. 克隆项目
git clone git@github.com:cexll/code-mode-mcp.git
cd code-mode-mcp
# 2. 一键安装
./setup.sh
# 3. 生成 MCP API
npm run generate-api
# 4. 设置 API key
export ANTHROPIC_API_KEY='sk-ant-api03-your-key'
# 5. 运行交互式 Agent
tsx examples/chat.ts
现在你可以对话了:
You: 列出当前目录的所有 TypeScript 文件
Agent: [生成代码 → 沙箱执行 → 返回结果]
方式二:在 Claude Desktop 中集成
1. 创建 MCP Server 包装器
创建 examples/mcp-server.js(详见 USAGE.md)
2. 配置 Claude Desktop
编辑配置文件:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"code-mode": {
"command": "node",
"args": ["/path/to/code-mode-mcp/examples/mcp-server.js"],
"env": {
"ANTHROPIC_API_KEY": "sk-ant-api03-your-key"
}
}
}
}
3. 重启 Claude Desktop
现在在 Claude Desktop 中,你可以使用 execute_code_mode 工具!
方式三:在你的项目中集成
// your-project/agent.ts
import { CodeModeAgent } from 'code-mode-mcp/src/agent.js';
const agent = new CodeModeAgent(
process.env.ANTHROPIC_API_KEY!,
'./node_modules/code-mode-mcp/generated-api'
);
await agent.connectMCPServer('filesystem', 'npx', [...]);
await agent.connectMCPServer('fetch', 'npx', [...]);
const response = await agent.chat('你的任务');
console.log(response);
💡 实际使用示例
示例 1:数据分析
You: 读取 sales.csv,计算总销售额,找出前 5 名产品
Agent 自动生成并执行代码:
- 读取 CSV
- 解析数据
- 计算总额
- 排序找 Top 5
- 返回结果
Token 消耗: ~3,000(vs 传统模式 ~20,000)
示例 2:GitHub 批量操作
You: 读取 users.json,为每个用户查询 GitHub 资料并汇总
Agent 自动执行:
- 读取用户列表
- for 循环调用 API
- 汇总数据
- 返回结果
1 轮完成(vs 传统模式 100 轮)
示例 3:复杂工作流
You: 从 Google Drive 下载文档,转换格式,上传到 GitHub
Agent 管道式处理:
Google Drive → 格式转换 → GitHub
数据在沙箱内流动,不经过 LLM
🎯 核心优势
| 指标 | 传统 MCP | Code Mode | 提升 |
|---|---|---|---|
| Token 消耗 | ~150K | ~2K | 98.7% ↓ |
| 响应时间 | 6-8秒 | 2秒 | 75% ↓ |
| 往返次数 | 3-100轮 | 1轮 | 99% ↓ |
| 大文件 | 容易超限 | 无限制 | ✅ |
| 复杂逻辑 | 困难 | for/if/try-catch | ✅ |
📋 快速命令参考
# 查看核心概念演示(无需安装)
node quick-demo.js
# 安装依赖
./setup.sh
# 生成 MCP TypeScript API
npm run generate-api
# 运行交互式 Agent
export ANTHROPIC_API_KEY='your-key'
tsx examples/chat.ts
# 查看生成的文件
tree generated-api/
⚙️ 沙箱配置
编辑 ~/.srt-settings.json:
{
"network": {
"allowedDomains": ["github.com", "*.github.com", "api.github.com"]
},
"filesystem": {
"denyRead": ["~/.ssh", "~/.aws"],
"allowWrite": [".", "/tmp"],
"denyWrite": [".env", ".git"]
}
}
🆘 常见问题
Q: 如何添加更多 MCP servers?
编辑 examples/generate-api.ts:
await generator.connectServer('my-server', 'npx', [
'-y', '@modelcontextprotocol/server-my-server'
]);
Q: 生成的 API 在哪里?
generated-api/servers/
├── filesystem/
│ ├── readFile.ts
│ └── writeFile.ts
└── fetch/
└── fetch.ts
Q: 如何调试?
查看 USAGE.md 的”调试技巧”章节。
Q: 生产环境如何部署?
查看 USAGE.md 的”生产环境部署”章节(包含 Docker 配置)。
🎓 学习路径
初学者(10 分钟)
node quick-demo.js- 查看核心概念cat QUICKSTART.md- 阅读快速开始./setup.sh && npm run generate-api- 安装和生成 API
进阶使用(30 分钟)
tsx examples/chat.ts- 运行交互式 Agentcat USAGE.md- 阅读完整使用指南- 尝试 3 个实际示例
深度集成(1-2 小时)
cat MIGRATION.md- 阅读迁移指南- 集成到你的项目
- 配置自定义 MCP servers