现有 MCP 存在的问题
工具定义挤占上下文窗口空间
agent 为了使用 MCP 工具,必须实现加载所有的 tools 到上下文中,当只有一两个 server 的时候,这对上下文的负担不大,但如果有数百乃至数千种工具同时出现在上下文窗口时,agent 的运行速度下降和成本攀升
使用 MCP tool 时,模型需要生成 tool call 的参数,然后接收返回值,这一来一回,可能会非常消耗 token,比如下面这种场景,我们想要给 server 发送一个很大的文件,就需要让模型首先读取文件,然后重新复述文件的内容来作为参数……这也太蠢了,我们可以生成文件的路径来避免这种情况,但 Remote 类型的 server 该如何获取这个文件呢?
NOTE
输入端确实是一个问题,输出端也存在内容量过大的问题,不过我们是否可以通过分页来解决这个问题……似乎也不是很优雅
下图描述了 agent 发现和使用 MCP tools 的流程
一个可能的解决方案,基于代码执行的 MCP
Anthropic 这篇文章给出的解决方案是:将 MCP 服务器呈现为代码 API 而非让 agent 直接进行工具调用。agent 需要通过编写代码与 MCP 服务器交互。实现这一目标有多种方法,其中一种方案是从连接的 MCP 服务器生成所有可用工具的文件树结构。
servers
├── google-drive
│ ├── getDocument.ts
│ ├── ... (other tools)
│ └── index.ts
├── salesforce
│ ├── updateRecord.ts
│ ├── ... (other tools)
│ └── index.ts
└── ... (other servers)在上面代码可以看到每个工具都对应一个文件,文件内部大致是:
// ./servers/google-drive/getDocument.ts
import { callMCPTool } from "../../../client.js";
interface GetDocumentInput {
documentId: string;
}
interface GetDocumentResponse {
content: string;
}
/* Read a document from Google Drive */
export async function getDocument(
input: GetDocumentInput,
): Promise<GetDocumentResponse> {
return callMCPTool<GetDocumentResponse>("google_drive__get_document", input);
}模型使用这个 tool 时,实际上是生成一段代码:
import * as gdrive from "./servers/google-drive";
import * as salesforce from "./servers/salesforce";
const transcript = (await gdrive.getDocument({ documentId: "abc123" })).content;
await salesforce.updateRecord({
objectType: "SalesMeeting",
recordId: "00Q5f000001abcXYZ",
data: { Notes: transcript },
});整个流程变成了下面这样:
- 发现工具:先列出
./servers/目录以查找可用服务器(如google-drive和salesforce) - 读取定义:读取所需的特定工具文件(如
getDocument.ts和updateRecord.ts)来理解每个工具的接口 - 执行工具:生成上面的代码,然后执行
Anthropic 和 Cloudflare 称这种方式非常节省 token,使用量从 15 万降至 2000 个,节省了 98.7%
INFO
这种方式需要让很多 server 开发者去调整代码……我感觉这是不现实的,这种方式似乎比较适合我们编写的内置 MCP 或者官方对 MCP 协议进行调整,能够在开发者不修改代码的前提下,完成 tools 的按需暴露,他们提到了另一种暴露的方式,即提供一个 search_tools 工具,让模型输入所需的工具关键词,然后由 server 执行搜索
这里提到的搜索是放置到 server 端,有没有可能放置到 client 端?mcp zero 提到他们的实践是分两级进行检索,先搜索 server,再搜索他们的 tools
优势
渐进式发现工具:将工具以代码形式呈现在文件系统中,可使模型按需读取工具定义,而非一次性读取所有内容
上下文友好:在处理大型数据集时,智能体可以先通过代码对结果进行筛选和转换(比如做个 slice 或者 map 之类的操作),再将其返回
typescript// 不使用 code execution 的情况 TOOL CALL: gdrive.getSheet(sheetId: 'abc123') → 返回完整的 100000 行数据 // 使用 code execution 的情况 const allRows = await gdrive.getSheet({ sheetId: 'abc123' }); const pendingOrders = allRows.filter(row => row["Status"] === 'pending' ); console.log(`Found ${pendingOrders.length} pending orders`); console.log(pendingOrders.slice(0, 5)); // 只展示五行结果更强大的流程控制:循环、条件判断和错误处理可以通过代码实现,无须让模型去决断
typescriptlet found = false; while (!found) { const messages = await slack.getChannelHistory({ channel: "C123456" }); found = messages.some((m) => m.text.includes("deployment complete")); if (!found) await new Promise((r) => setTimeout(r, 5000)); } console.log("Deployment notification received");如果不通过代码实现,agent 就需要循环交替调用 MCP 工具和休眠命令,这对执行时间非常长的后台 MCP tool 非常有用
状态持久化:agent 可将中间结果写入文件,从而能够恢复工作并追踪进度
typescriptconst leads = await salesforce.query({ query: "SELECT Id, Email FROM Lead LIMIT 1000", }); const csvData = leads.map((l) => `${l.Id},${l.Email}`).join("\n"); await fs.writeFile("./workspace/leads.csv", csvData); // 稍后执行就可以读取文件,不会丢失状态 const saved = await fs.readFile("./workspace/leads.csv", "utf-8");agent 还能将这类代码保存为可复用函数,以供未来使用。这部分就和 Cluade Code 的 skill 关系非常密切!
尾声
WARNING
With code execution environments becoming more common for agents, a solution is to present MCP servers as code APIs rather than direct tool calls
文章里提到了这一句话,“代码执行环境在智能体中日益普及”,我不太明白这里的执行环境指的是什么,node 或者 python 运行环境?但我看主流工具似乎并没有内置运行时(比如 Cluade Code)
原文链接:https://www.anthropic.com/engineering/code-execution-with-mcp