进阶主题

代码模式 (Code Mode)

让大语言模型在单次 JavaScript 执行中编排多次工具调用。

什么是代码模式?

代码模式使用单个 code 工具替代了传统的多轮工具调用模式。大语言模型不再逐个调用工具(每次都需要一次往返通信),而是编写 JavaScript 代码,在单次执行中编排多个工具。

Prompt
在我的 Nuxt MCP 服务器 (@nuxtjs/mcp-toolkit) 中启用代码模式。

- 安装 secure-exec:pnpm add secure-exec
- 在 server/mcp/index.ts 的处理程序上设置 experimental_codeMode: true(通过 defineMcpHandler)
- 代码模式会将所有已注册的工具替换为单个 'code' 工具
- 大语言模型编写 JavaScript,通过 codemode 对象调用工具(例如 await codemode.listUsers())
- 这减少了往返通信和 Token 使用量——尤其是当工具较多时(10+ 个工具可节省 50%+ 的 Token)
- 代码在安全的 V8 沙箱(workerd)中运行——无法访问文件系统、网络或 Node API
- 如果工具很多(10+ 个),请考虑渐进模式(experimental_codeMode: { progressive: true })以同时保留独立工具和代码工具——对于较少的工具,标准模式已足够
- 代码模式目前处于实验阶段——API 可能会发生变化

文档:https://mcp-toolkit.nuxt.dev/advanced/code-mode
传统 MCP代码模式
模式大语言模型逐个调用工具大语言模型编写调用工具的 JS 代码
往返通信每次工具调用一次所有操作仅需一次
复杂逻辑条件/循环需要多轮对话原生 JS 控制流
Token 使用量较高(重复上下文)较低(单次调用)
代码模式目前处于实验阶段。随着我们根据反馈稳定此功能,API 可能会发生变化。

为什么使用代码模式?

每次大语言模型往返通信都会将所有工具描述作为上下文重新发送。在传统 MCP 中,一个需要 5 个步骤且拥有 50 个工具的任务会将完整的工具目录发送 5 次——仅工具描述就消耗 15,500 个 Token。代码模式在单个工具中发送紧凑的 TypeScript 签名,将其降至约 3,000 个 Token。

扩展性问题

在传统 MCP 中,工具描述开销按 工具数量 × 往返次数 增长。代码模式将所有工具替换为一个包含紧凑类型签名的 code 工具——并且通常需要更少的往返通信。

服务器规模传统 MCP代码模式节省比例
10 个工具,3 步任务工具描述约 1,860 Token约 920 Token-51%
25 个工具,4 步任务约 6,200 Token约 1,700 Token-73%
50 个工具,5 步任务约 15,500 Token约 3,000 Token-81%
100 个工具,5 步任务约 31,000 Token约 5,600 Token-82%

这些数字仅代表工具描述的开销。总节省量取决于具体任务,但趋势很明显:工具越多,节省越大

在使用 Claude Sonnet 的真实基准测试中(9 个工具,7 个任务),代码模式总体节省了 15% 的 Token,在复杂批量操作中最高节省 33%——将 8 次工具调用合并为单个 JavaScript 代码块。

超越 Token 节省

代码模式还解锁了传统 MCP 无法高效实现的模式:

  • 并行执行 — 使用 Promise.all() 进行独立调用,而非顺序往返通信
  • 条件逻辑 — 使用 if/else 分支,无需额外的 LLM 步骤
  • 循环 — 对数据使用 for 循环,而非逐个重复调用工具
  • 错误处理 — 使用 try/catch 在工作流中途处理失败情况

设置

1. 安装 secure-exec

代码模式使用 secure-exec 在安全的 V8 隔离环境中运行大语言模型生成的代码:

pnpm add secure-exec
代码模式需要 secure-exec 和 Node.js >=18.16.0。模块的其余部分仍支持 Node.js 18.x,但代码模式依赖 AsyncLocalStorage.snapshot() 来保留请求上下文。

2. 在处理程序中启用

向任意处理程序添加 experimental_codeMode

// server/mcp/index.ts
export default defineMcpHandler({
  experimental_codeMode: true,
})

就这么简单。该模块会将你所有的工具替换为单个 code 工具,供大语言模型用来编排它们。

工作原理

当启用代码模式时:

  1. 所有已注册的工具都会被转换为 TypeScript 类型定义
  2. 创建一个 code 工具,并将这些类型嵌入其描述中
  3. 大语言模型使用 codemode 对象编写 JavaScript 来调用工具
  4. 代码在 V8 隔离环境中运行,仅能通过 RPC 访问你的工具

示例:大语言模型生成的内容

假设存在 get-userlist-todoscreate-todo 工具,大语言模型会接收类型定义并编写如下代码:

const user = await codemode.get_user({ id: "123" });
const todos = await codemode.list_todos({ userId: user.id });

if (todos.length === 0) {
  await codemode.create_todo({
    title: "Welcome task",
    userId: user.id,
  });
}

return { user, todos };

配置选项

传入选项对象而非 true 以实现更精细的控制:

server/mcp/index.ts
export default defineMcpHandler({
  experimental_codeMode: {
    memoryLimit: 64,
    cpuTimeLimitMs: 10_000,
    maxResultSize: 102_400,
    maxRequestBodyBytes: 1_048_576,
    maxToolResponseSize: 1_048_576,
    wallTimeLimitMs: 60_000,
    maxToolCalls: 200,
    progressive: false,
    description: undefined,
  },
})
memoryLimit
number
默认值:64V8 隔离环境的内存限制(单位:MB)。在首次执行时设置一次——调用 disposeCodeMode() 可更改。
cpuTimeLimitMs
number
默认值:10000每次执行的 CPU 时间限制(单位:毫秒)。超过此时长后沙箱将被终止。
maxResultSize
number
默认值:102400(100 KB)截断前的最大结果大小(单位:字节)。大型结果会被智能截断——数组按项目数量截断,对象按键数量截断。
maxRequestBodyBytes
number
默认值:1048576(1 MB)沙箱发出的单个 RPC 请求体允许的最大字节数。超出将返回 HTTP 413。防止因负载过大导致内存耗尽。与 memoryLimit 类似,此设置在 RPC 服务器首次启动时生效;更改前请调用 disposeCodeMode()
maxToolResponseSize
number
默认值:1048576(1 MB)单个工具 RPC 响应的最大字节数。大型结果使用与 maxResultSize 相同的策略进行截断。
wallTimeLimitMs
number
默认值:60000(60 秒)每次执行的截止时间,在每次沙箱→主机 RPC 调用(工具调用或返回值)开始时检查。超过截止时间后,下一次 RPC 将收到 HTTP 408。这限制了主机端的工作(例如缓慢的工具);隔离环境中的纯 CPU 循环主要受 cpuTimeLimitMs 限制。
maxToolCalls
number
默认值:200每次执行允许的最大工具 RPC 调用次数。防止反复调用昂贵工具的失控循环。超出时将返回 HTTP 429。
progressive
boolean
默认值:false启用渐进式披露模式。请参阅下方的 渐进模式
description
string
默认值:内置模板自定义 code 工具的描述。支持 {{types}}{{count}} 占位符。

渐进模式

当你的服务器暴露大量工具(50+)时,将所有类型定义嵌入 code 工具描述中会消耗大量 Token。渐进模式通过将其拆分为两个工具来解决此问题:

  1. search — 通过关键词发现工具,返回其签名
  2. code — 使用已发现的工具执行代码
server/mcp/index.ts
export default defineMcpHandler({
  experimental_codeMode: {
    progressive: true,
  },
})

大语言模型的工作流程变为:

大语言模型调用:search({ query: "user" })

→ 找到 2/12 个匹配 "user" 的工具:
  codemode.get_user: (input: { id: string }) => Promise<unknown>; // 根据 ID 获取用户
  codemode.list_users: () => Promise<unknown>; // 列出所有用户
渐进模式会增加一次额外的工具调用,但能显著减少初始提示词的大小。仅在工具数量庞大时使用——对于小型工具集,标准模式效率更高。

自定义描述

覆盖 code 工具的描述以自定义 LLM 指令:

server/mcp/index.ts
export default defineMcpHandler({
  experimental_codeMode: {
    description: `You have {{count}} tools available. Write JavaScript using the codemode object.

{{types}}

Always combine related operations into a single code block.`,
  },
})

{{types}} 占位符将被替换为生成的 TypeScript 类型定义。{{count}} 占位符将被替换为可用工具的数量。

在渐进模式(progressive mode)下,{{types}} 不可用,因为类型是通过 search 工具动态发现的。

安全性

运行 LLM 生成的代码需要严格的安全措施。Code Mode 在 7 个层级上实现了纵深防御(defense in depth),以确保沙箱无法逃逸、访问未授权资源或耗尽主机资源。

沙箱隔离

LLM 生成的代码通过 secure-exec 在独立的 V8 isolate 中运行。这与 Cloudflare Workers 及类似平台使用的隔离技术相同。沙箱具有以下限制:

  • 无文件系统访问权限 — 无法读取、写入或列出文件
  • 无 Node.js API — 不支持 require()import()processfschild_process
  • 无环境变量 — 无法读取密钥或配置
  • 无主机进程访问权限 — 无法以任何方式修改父进程

网络限制

沙箱只能与内部 RPC 服务器通信。所有其他网络访问均被阻止:

  • 端口锁定 — 仅可访问随机分配的 RPC 端口。其他 localhost 服务(数据库、管理面板、其他应用)均被阻止。
  • 主机锁定 — 仅允许 127.0.0.1localhost。外部主机将被拒绝。
  • 无 DNS — 完全禁用 DNS 解析。
  • 无重定向 — HTTP 重定向将被拒绝(redirect: 'error'),防止通过开放重定向引发 SSRF 攻击。

RPC 身份验证

沙箱与主机之间的通信使用基于会话的加密令牌:

  • 256 位令牌 — 在 RPC 服务器启动时使用 crypto.randomBytes(32) 生成。
  • 基于请求头的认证 — 每个请求都必须通过 x-rpc-token 请求头携带该令牌。
  • 不匹配返回 403 — 没有有效令牌的请求将被立即拒绝。

这可以防止其他本地进程通过 RPC 端口调用你的 MCP 工具。

资源限制

资源默认值可配置保护机制
CPU 时间10 秒cpuTimeLimitMs超时后终止沙箱 — 防止无限循环
挂钟时间(绝对时间)限制60 秒wallTimeLimitMs对沙箱发出的每次 RPC 强制执行 — 超过截止时间后停止后续的工具/返回 RPC 调用
内存64 MBmemoryLimitV8 isolate 硬性限制 — 防止 OOM 崩溃
结果大小100 KBmaxResultSize智能截断(数组按元素,对象按键)
工具响应大小1 MBmaxToolResponseSize每次调用在返回前进行截断
请求体大小1 MBmaxRequestBodyBytesHTTP 413 提前拒绝 — 防止内存耗尽
每次执行的工具调用次数200maxToolCallsHTTP 429 速率限制 — 防止失控循环
日志条目200限制控制台输出 — 防止 console.log 泛滥

输入验证

工具名称会被插值到沙箱代码模板中。为防止代码注入:

  • 严格的标识符正则 — 每个工具名称在注入沙箱模板前都会通过 /^[\w$]+$/ 进行验证。
  • 清洗处理 — 名称在上游已进行清洗(get-userget_user),但在模板级别增加了第二层验证以确保纵深防御。
  • 拒绝执行 — 如果名称验证失败,执行将立即抛出错误 — 不会进行部分注入。

错误信息清洗

基础设施错误(文件路径、堆栈跟踪)在返回给沙箱或 MCP 客户端之前会经过清洗。完整的错误详情会在服务器端以 [nuxt-mcp-toolkit] 为前缀记录日志,以便调试。

总结

沙箱只能通过经过身份验证的 RPC 桥接器与你注册的 MCP 工具通信。它无法访问文件系统、网络、环境或任何其他主机资源。

与其他功能配合使用

Code Mode 与模块的其他功能完全兼容。你的工具保持不变 — 仅改变它们向 LLM 暴露的方式。

// server/mcp/index.ts
export default defineMcpHandler({
  experimental_codeMode: true,
  middleware: async (event) => {
    const user = await getUser(event)
    if (!user) {
      throw createError({ statusCode: 401 })
    }
    event.context.user = user
  },
})

中间件 在工具执行前运行 — 你的工具照常访问 event.context。带有 enabled 守卫 的工具将从生成的类型定义和 codemode 对象中排除。

工具名称清洗

MCP 工具名称(短横线命名法)会自动转换为 codemode 对象中有效的 JavaScript 标识符:

MCP 名称JavaScript 名称
get-userget_user
list-todoslist_todos
123-tool_123_tool
deletedelete_

保留的 JavaScript 关键字会添加 _ 后缀。以数字开头的名称会添加 _ 前缀。

清理资源

在关闭期间调用 disposeCodeMode() 以释放资源(V8 运行时、RPC 服务器):

import { disposeCodeMode } from '#imports'

// 在关闭钩子或清理函数中
disposeCodeMode()

下一步