代码模式 (Code Mode)
什么是代码模式?
代码模式使用单个 code 工具替代了传统的多轮工具调用模式。大语言模型不再逐个调用工具(每次都需要一次往返通信),而是编写 JavaScript 代码,在单次执行中编排多个工具。
在我的 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 使用量 | 较高(重复上下文) | 较低(单次调用) |
为什么使用代码模式?
每次大语言模型往返通信都会将所有工具描述作为上下文重新发送。在传统 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% |
这些数字仅代表工具描述的开销。总节省量取决于具体任务,但趋势很明显:工具越多,节省越大。
超越 Token 节省
代码模式还解锁了传统 MCP 无法高效实现的模式:
- 并行执行 — 使用
Promise.all()进行独立调用,而非顺序往返通信 - 条件逻辑 — 使用
if/else分支,无需额外的 LLM 步骤 - 循环 — 对数据使用
for循环,而非逐个重复调用工具 - 错误处理 — 使用
try/catch在工作流中途处理失败情况
设置
1. 安装 secure-exec
代码模式使用 secure-exec 在安全的 V8 隔离环境中运行大语言模型生成的代码:
pnpm add secure-exec
npm install secure-exec
yarn add secure-exec
bun 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,
})
// server/mcp/ai-agent.ts
export default defineMcpHandler({
name: 'ai-agent',
experimental_codeMode: true,
tools: [getUserTool, listTodosTool, createTodoTool],
})
就这么简单。该模块会将你所有的工具替换为单个 code 工具,供大语言模型用来编排它们。
工作原理
当启用代码模式时:
- 所有已注册的工具都会被转换为 TypeScript 类型定义
- 创建一个
code工具,并将这些类型嵌入其描述中 - 大语言模型使用
codemode对象编写 JavaScript 来调用工具 - 代码在 V8 隔离环境中运行,仅能通过 RPC 访问你的工具
示例:大语言模型生成的内容
假设存在 get-user、list-todos 和 create-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 };
const [users, products, orders] = await Promise.all([
codemode.list_users(),
codemode.list_products(),
codemode.list_orders({ status: "pending" }),
]);
return {
userCount: users.length,
productCount: products.length,
pendingOrders: orders.length,
};
const users = await codemode.list_users();
const results = [];
for (const user of users) {
const todos = await codemode.list_todos({ userId: user.id });
if (todos.some(t => t.overdue)) {
await codemode.send_reminder({ userId: user.id });
results.push(user.name);
}
}
return { reminded: results };
配置选项
传入选项对象而非 true 以实现更精细的控制:
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,
},
})
64V8 隔离环境的内存限制(单位:MB)。在首次执行时设置一次——调用 disposeCodeMode() 可更改。10000每次执行的 CPU 时间限制(单位:毫秒)。超过此时长后沙箱将被终止。102400(100 KB)截断前的最大结果大小(单位:字节)。大型结果会被智能截断——数组按项目数量截断,对象按键数量截断。1048576(1 MB)沙箱发出的单个 RPC 请求体允许的最大字节数。超出将返回 HTTP 413。防止因负载过大导致内存耗尽。与 memoryLimit 类似,此设置在 RPC 服务器首次启动时生效;更改前请调用 disposeCodeMode()。1048576(1 MB)单个工具 RPC 响应的最大字节数。大型结果使用与 maxResultSize 相同的策略进行截断。60000(60 秒)每次执行的截止时间,在每次沙箱→主机 RPC 调用(工具调用或返回值)开始时检查。超过截止时间后,下一次 RPC 将收到 HTTP 408。这限制了主机端的工作(例如缓慢的工具);隔离环境中的纯 CPU 循环主要受 cpuTimeLimitMs 限制。200每次执行允许的最大工具 RPC 调用次数。防止反复调用昂贵工具的失控循环。超出时将返回 HTTP 429。false启用渐进式披露模式。请参阅下方的 渐进模式。code 工具的描述。支持 {{types}} 和 {{count}} 占位符。渐进模式
当你的服务器暴露大量工具(50+)时,将所有类型定义嵌入 code 工具描述中会消耗大量 Token。渐进模式通过将其拆分为两个工具来解决此问题:
search— 通过关键词发现工具,返回其签名code— 使用已发现的工具执行代码
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({ code: "..." })
→ 使用已发现的工具执行代码
自定义描述
覆盖 code 工具的描述以自定义 LLM 指令:
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()、process、fs、child_process等 - 无环境变量 — 无法读取密钥或配置
- 无主机进程访问权限 — 无法以任何方式修改父进程
网络限制
沙箱只能与内部 RPC 服务器通信。所有其他网络访问均被阻止:
- 端口锁定 — 仅可访问随机分配的 RPC 端口。其他 localhost 服务(数据库、管理面板、其他应用)均被阻止。
- 主机锁定 — 仅允许
127.0.0.1和localhost。外部主机将被拒绝。 - 无 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 MB | memoryLimit | V8 isolate 硬性限制 — 防止 OOM 崩溃 |
| 结果大小 | 100 KB | maxResultSize | 智能截断(数组按元素,对象按键) |
| 工具响应大小 | 1 MB | maxToolResponseSize | 每次调用在返回前进行截断 |
| 请求体大小 | 1 MB | maxRequestBodyBytes | HTTP 413 提前拒绝 — 防止内存耗尽 |
| 每次执行的工具调用次数 | 200 | maxToolCalls | HTTP 429 速率限制 — 防止失控循环 |
| 日志条目 | 200 | 否 | 限制控制台输出 — 防止 console.log 泛滥 |
输入验证
工具名称会被插值到沙箱代码模板中。为防止代码注入:
- 严格的标识符正则 — 每个工具名称在注入沙箱模板前都会通过
/^[\w$]+$/进行验证。 - 清洗处理 — 名称在上游已进行清洗(
get-user→get_user),但在模板级别增加了第二层验证以确保纵深防御。 - 拒绝执行 — 如果名称验证失败,执行将立即抛出错误 — 不会进行部分注入。
错误信息清洗
基础设施错误(文件路径、堆栈跟踪)在返回给沙箱或 MCP 客户端之前会经过清洗。完整的错误详情会在服务器端以 [nuxt-mcp-toolkit] 为前缀记录日志,以便调试。
总结
与其他功能配合使用
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
},
})
// server/mcp/tools/admin-tool.ts
export default defineMcpTool({
name: 'admin-delete',
description: 'Delete a resource (admin only)',
enabled: event => event.context.user?.role === 'admin',
inputSchema: {
id: z.string(),
},
handler: async ({ id }) => {
// 仅当用户为管理员时在代码模式下可见
},
})
// server/mcp/index.ts
export default defineMcpHandler({
experimental_codeMode: {
progressive: true,
cpuTimeLimitMs: 15_000,
},
middleware: async (event) => {
const apiKey = getHeader(event, 'x-api-key')
if (!apiKey) throw createError({ statusCode: 401 })
event.context.user = await getUserByApiKey(apiKey)
},
})
中间件 在工具执行前运行 — 你的工具照常访问 event.context。带有 enabled 守卫 的工具将从生成的类型定义和 codemode 对象中排除。
工具名称清洗
MCP 工具名称(短横线命名法)会自动转换为 codemode 对象中有效的 JavaScript 标识符:
| MCP 名称 | JavaScript 名称 |
|---|---|
get-user | get_user |
list-todos | list_todos |
123-tool | _123_tool |
delete | delete_ |
保留的 JavaScript 关键字会添加 _ 后缀。以数字开头的名称会添加 _ 前缀。
清理资源
在关闭期间调用 disposeCodeMode() 以释放资源(V8 运行时、RPC 服务器):
import { disposeCodeMode } from '#imports'
// 在关闭钩子或清理函数中
disposeCodeMode()