代码模式:使用MCP的更好方法

MCP是为工具调用设计的,但它实际上不一定要以这种方式使用。

代码模式:使用MCP的更好方法

结果证明,我们一直都在错误地使用MCP。

如今大多数代理通过直接向LLM暴露“工具”来使用MCP。

我们尝试了不同的方法:将MCP工具转换为TypeScript API,然后让LLM编写调用该API的代码。

结果非常显著:

  1. 我们发现当这些工具以TypeScript API的形式呈现时,代理能够处理更多的工具和更复杂的工具。也许这是因为LLM在其训练集中有大量的真实世界TypeScript代码,但只有少量构造的工具调用示例。
  2. 当代理需要串联多个调用时,这种方法特别有效。传统的做法中,每个工具调用的输出必须输入到LLM的神经网络中,然后再复制到下一个调用的输入中,这会浪费时间、能量和令牌。当LLM可以编写代码时,它可以跳过所有这些步骤,只需读取它需要的最终结果即可。

简而言之,LLM在编写调用MCP的代码方面比直接调用MCP更擅长。

1、什么是MCP?

对于不熟悉的人来说:Model Context Protocol 是一种标准协议,用于给AI代理访问外部工具,这样它们就可以直接执行工作,而不仅仅是与您聊天。

从另一个角度来看,MCP是一种统一的方式:

  • 暴露一个用于做某事的API,
  • 以及LLM理解它所需的文档,
  • 并通过带外方式处理授权。

MCP在2025年引起了广泛关注,因为它突然大大扩展了AI代理的能力。

MCP服务器暴露的“API”是一组“工具”。每个工具基本上是一个远程过程调用(RPC)函数——它带有某些参数并返回响应。大多数现代LLM都有使用“工具”(有时称为“函数调用”)的能力,这意味着它们被训练成在想要调用工具时输出特定格式的文本。调用LLM的程序看到这种格式后,会按照指定调用工具,然后将结果作为输入反馈给LLM。

2、工具调用的结构

在内部,LLM生成一系列代表其输出的“标记”。一个标记可能代表一个单词、一个音节、某种标点符号或其他文本组件。

然而,工具调用涉及一个没有文本等效的标记。LLM被训练(或更常见的是微调)以理解一个特殊的标记,它可以输出表示“以下内容应解释为工具调用”,另一个特殊标记表示“这是工具调用的结束”。在这两个标记之间,LLM通常会写入对应于某种JSON消息的标记,描述调用。

例如,假设您已将代理连接到提供天气信息的MCP服务器,然后询问代理德克萨斯州奥斯汀的天气情况。在内部,LLM可能会生成如下输出。请注意,这里我们使用<||>中的单词来表示我们的特殊标记,但实际上这些标记并不表示文本;这只是为了说明。

我会使用天气MCP服务器来查找德克萨斯州奥斯汀的天气。

我会使用天气MCP服务器来查找德克萨西州奥斯汀的天气。

<|tool_call|>
{
  "name": "get_current_weather",
  "arguments": {
    "location": "Austin, TX, USA"
  }
}
<|end_tool_call|>

当看到输出中的这些特殊标记时,LLM的框架会将其解释为工具调用。在看到结束标记后,框架暂停LLM的执行。它解析JSON消息,并将其作为结构化API结果的独立组件返回。调用LLM API的代理会看到工具调用,调用相关的MCP服务器,然后将结果返回给LLM API。LLM的框架随后使用另一组特殊标记将结果反馈给LLM:

<|tool_result|>
{
  "location": "Austin, TX, USA",
  "temperature": 93,
  "unit": "fahrenheit",
  "conditions": "sunny"
}
<|end_tool_result|>

LLM以与读取用户输入相同的方式读取这些标记——只是用户无法生成这些特殊标记,因此LLM知道这是工具调用的结果。然后LLM像正常一样继续生成输出。

不同的LLM可能使用不同的工具调用格式,但这是基本概念。

3、这有什么问题吗?

工具调用中使用的特殊标记是LLM在现实中从未见过的东西。它们必须基于合成训练数据专门训练来使用工具。它们并不总是那么好。如果你向LLM展示太多工具,或者过于复杂的工具,它可能会难以选择正确的工具或正确使用它。因此,MCP服务器设计者被鼓励相比他们通常向开发者暴露的传统API,提供大大简化后的API。

同时,LLM在编写代码方面变得非常出色。事实上,要求LLM针对通常向开发者暴露的完整、复杂API编写代码似乎并没有太大的困难。那么为什么MCP接口必须“简化”呢?编写代码和调用工具几乎是同一件事,但看起来LLM在一个方面做得比另一个好得多?

答案很简单:LLM看到了很多代码。它们没有看到很多“工具调用”。事实上,它们看到的工具调用可能仅限于由LLM自己的开发人员构建的构造性训练集,以尝试训练它。而它们却看到了数百万开源项目的实际代码。

让LLM通过工具调用来完成任务就像把莎士比亚送进一个月的中文课程,然后让他用中文写一部戏剧。这绝不会是他的最佳作品。

4、但MCP仍然有用,因为它具有统一性

MCP是为工具调用设计的,但它实际上不一定要以这种方式使用。

MCP服务器暴露的“工具”实际上只是一个带有附加文档的RPC接口。我们并不真的必须将它们作为工具呈现。我们可以将这些工具转换为编程语言API。

但是为什么当我们已经有独立的编程语言API时还要这样做呢?几乎每个MCP服务器只是对现有传统API的包装——为什么不直接暴露这些API?

嗯,事实证明MCP还做了另一件很有用的事情:它提供了一种统一的方式来连接和了解API。

即使代理的开发人员从未听说过特定的MCP服务器,代理也可以使用MCP服务器。同样,MCP服务器的开发人员也从未听说过特定的代理。过去这种情况很少见。通常,客户端开发人员总是确切知道他们正在编码的API是什么。因此,每个API都能以稍微不同的方式实现基本的连接、授权和文档。

即使在AI代理编写代码时,这种统一性也很有用。我们希望AI代理运行在一个沙箱中,只能访问我们提供的工具。MCP使代理框架能够通过以标准方式处理连接和授权来实现这一点,与AI代码无关。我们也不希望AI代理搜索互联网寻找文档;MCP在协议中直接提供它。

5、那么它是如何工作的?

我们已经扩展了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: {
    // tool definitions 
  }
})

你可以用codemode助手包装工具和提示,并在你的应用程序中使用它们:

import { codemode } from "agents/codemode/ai";

const {system, tools} = codemode({
  system: "You are a helpful assistant",
  tools: {
    // tool definitions 
  },
  // ...config
})

const stream = streamText({
  model: openai("gpt-5"),
  system,
  tools,
  messages: [
    { role: "user", content: "Write a function that adds two numbers" }
  ]
})

通过这个更改,你的应用程序现在开始生成并运行代码,这些代码本身将调用你定义的工具,包括MCP服务器。我们将在不久的将来推出其他库的变体。阅读文档获取更多细节和示例。

6、将MCP转换为TypeScript

当你在“代码模式”下连接到MCP服务器时,Agents SDK会获取MCP服务器的Schema,然后将其转换为TypeScript API,包括基于模式的文档注释。

例如,连接到MCP服务器https://gitmcp.io/cloudflare/agents,将生成类似这样的TypeScript定义:

interface FetchAgentsDocumentationInput {
  [k: string]: unknown;
}
interface FetchAgentsDocumentationOutput {
  [key: string]: any;
}

interface SearchAgentsDocumentationInput {
  /**
   * The search query to find relevant documentation
   */
  query: string;
}
interface SearchAgentsDocumentationOutput {
  [key: string]: any;
}

interface SearchAgentsCodeInput {
  /**
   * The search query to find relevant code files
   */
  query: string;
  /**
   * Page number to retrieve (starting from 1). Each page contains 30
   * results.
   */
  page?: number;
}
interface SearchAgentsCodeOutput {
  [key: string]: any;
}

interface FetchGenericUrlContentInput {
  /**
   * The URL of the document or page to fetch
   */
  url: string;
}
interface FetchGenericUrlContentOutput {
  [key: string]: any;
}

declare const codemode: {
  /**
   * Fetch entire documentation file from GitHub repository:
   * cloudflare/agents. Useful for general questions. Always call
   * this tool first if asked about cloudflare/agents.
   */
  fetch_agents_documentation: (
    input: FetchAgentsDocumentationInput
  ) => Promise<FetchAgentsDocumentationOutput>;

  /**
   * Semantically search within the fetched documentation from
   * GitHub repository: cloudflare/agents. Useful for specific queries.
   */
  search_agents_documentation: (
    input: SearchAgentsDocumentationInput
  ) => Promise<SearchAgentsDocumentationOutput>;

  /**
   * Search for code within the GitHub repository: "cloudflare/agents"
   * using the GitHub Search API (exact match). Returns matching files
   * for you to query further if relevant.
   */
  search_agents_code: (
    input: SearchAgentsCodeInput
  ) => Promise<SearchAgentsCodeOutput>;

  /**
   * Generic tool to fetch content from any absolute URL, respecting
   * robots.txt rules. Use this to retrieve referenced urls (absolute
   * urls) that were mentioned in previously fetched documentation.
   */
  fetch_generic_url_content: (
    input: FetchGenericUrlContentInput
  ) => Promise<FetchGenericUrlContentOutput>;
};

然后将此TypeScript加载到代理的上下文中。目前,整个API都被加载,但未来的改进可以使代理更动态地搜索和浏览API——就像一个代理编码助手那样。

7、在沙箱中运行代码

我们的代理只被呈现一个工具,而不是被呈现所有连接的MCP服务器的所有工具,它只是执行一些TypeScript代码。

然后代码在安全的沙箱中执行。沙箱完全隔离于互联网。它对外界的唯一访问是通过代表其连接的MCP服务器的TypeScript API。

这些API由RPC调用支持,调用回调到代理循环。在那里,Agents SDK将调用分派到适当的MCP服务器。

沙箱代码通过调用console.log()将结果返回给代理。当脚本完成后,所有输出日志都会返回给代理。

8、动态Worker加载:这里没有容器

这种新方法需要访问一个安全的沙箱,其中可以运行任意代码。那么我们从哪里找到这样的沙箱?我们需要运行容器吗?那很昂贵吗?

不需要。这里没有容器。我们有更好的东西:隔离。

Cloudflare Workers平台始终基于V8隔离,即由V8 JavaScript引擎驱动的隔离JavaScript运行时。

隔离比容器轻量得多。 一个隔离可以在仅使用几兆字节内存的情况下,在几十毫秒内启动。

隔离如此快速,我们可以为代理运行的每一段代码创建一个新的隔离。不需要重复使用它们。不需要预热它们。只需按需创建,运行代码,然后丢弃。这一切发生得如此迅速,开销可以忽略不计;几乎就像你直接eval()代码一样。但有安全性。

9、Worker加载器API

到目前为止,还没有办法让Worker直接加载包含任意代码的隔离。所有的Worker代码都必须通过Cloudflare API上传,然后部署到全球,以便它可以运行在任何地方。这不是我们想要的!我们希望代码就在代理所在的地方运行。

为此,我们为Workers平台添加了一个新的API:Worker加载器API。借助它,您可以按需加载Worker代码。以下是它的样子:

// 获取具有给定ID的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,

      // ...以及绑定回父Worker的导出RPC接口,使用新的`ctx.exports`回环绑定API。
      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}
});

您现在可以在本地运行workerd时使用这个API(查看文档),并且可以注册参加测试以在生产环境中使用它。

10、Workers是更好的沙箱

Workers的设计使其特别适合沙箱化,尤其是对于这种用例,原因有几个:

更快、更便宜、可丢弃的沙箱

Workers平台使用隔离而不是容器。 隔离轻量级且启动速度快。启动一个新的隔离只需要几毫秒,而且成本很低,我们可以为代理生成的每一段代码都创建一个新的隔离。不需要担心隔离池的复用、预热等。

我们尚未最终确定Worker加载器API的定价,但由于它基于隔离,我们将能够以显著低于基于容器的解决方案的成本提供它。

默认隔离,但通过绑定连接

Workers在处理隔离方面表现得更好。

在代码模式下,我们禁止沙箱化的Worker与互联网通信。全局的fetch()connect()函数会抛出错误。

但在大多数平台上,这会成为一个问题。在大多数平台上,获得对私有资源的访问权限的方法是,你一开始就有通用的网络访问权限。然后,使用这个网络访问权限,你向特定服务发送请求,传递某种API密钥以授权私有访问。

但Workers一直有一个更好的答案。在Workers中,“环境”(env对象)不仅包含字符串,它包含实时对象,也称为“绑定”。这些对象可以直接访问私有资源,而不涉及通用的网络请求。

在代码模式下,我们给予沙箱对它连接的MCP服务器的绑定访问权限。因此,代理可以具体访问这些MCP服务器,而无需一般性的网络访问。

通过绑定限制访问比通过网络级过滤或HTTP代理更干净。过滤对LLM和监督者来说都很困难,因为边界往往不清楚:监督者可能很难准确识别哪些流量是合法必要的与API通信。同时,LLM可能很难猜测哪些请求会被阻止。通过绑定的方法,界限是明确的:绑定提供了一个JavaScript接口,允许使用该接口。这种方式更好。

没有泄露API密钥的风险

绑定的另一个好处是隐藏API密钥。绑定本身提供了一个已授权的客户端接口给MCP服务器。所有对其的调用首先由代理监督者处理,该监督者持有访问令牌并将它们添加到发送到MCP的请求中。

这意味着AI不可能写出泄露任何密钥的代码,解决了当今AI编写的代码中常见的安全问题。


原文链接:Code Mode: the better way to use MCP

汇智网翻译整理,转载请标明出处