FastMCP CodeMode 实战:用 search + execute 两个元工具把整个工具目录折叠进沙箱
【免费下载链接】fastmcp🚀 The fast, Pythonic way to build MCP servers and clients.项目地址: https://gitcode.com/GitHub_Trending/fa/fastmcp
CodeMode 是 FastMCP 提供的一种服务端 Transform:它把完整的工具目录折叠成search(关键词发现)与execute(沙箱内执行 Python 脚本、链式调用工具)两个元工具。LLM 不再为每个中间结果消耗上下文 token,而是编写一段在服务端沙箱中运行的脚本,只把最终答案传回。本文以仓库中的 examples/code_mode 示例为主线,结合源码与官方文档,讲清 CodeMode 的原理、运行方式、配置手段与安全边界。
CodeMode 要解决的两个扩展性问题
标准 MCP 的工具调用模式存在两个规模化瓶颈:
- 目录全量加载:整个工具目录会在开始时全部装入 LLM 上下文。当工具数量达到数百个时,LLM 在读取用户请求之前就已经消耗了数万 token。
- 逐次往返:每一次
call_tool都是一次"LLM 调用工具 → 结果穿回上下文 → LLM 推理 → 再调用下一个工具"的完整往返,仅用于喂给下一步的中间结果也在反复烧 token。
CodeMode 同时解决这两个问题:LLM 看到的不是完整工具目录,而是"发现工具"和"编写并执行代码"两类元工具。它按需发现(on demand discovery),在沙箱里写一段链式调用工具的脚本,最终只拿到最终答案。该思路最初由 Cloudflare 在 Code Mode 中提出,后被 Anthropic 在 Code Execution with MCP 中进一步探索(详见 docs/servers/transforms/code-mode.mdx)。
快速运行:两个终端即可体验
仓库中的 examples/code_mode 目录包含三个文件:
- README.md:示例说明与运行方式
- server.py:定义 8 个普通工具并挂载 CodeMode Transform
- client.py:演示
search与execute的完整调用链
启动方式(两个终端):
uv run python server.py # 终端一:启动服务 uv run python client.py # 终端二:运行客户端也可以按 server.py 文件头注释中的方式运行:uv run python examples/code_mode/client.py。
依赖前提:沙箱依赖pydantic-monty,需要安装code-modeextra:
pip install "fastmcp[code-mode]"安装后uv或pip均可解析依赖。如果未安装pydantic-monty,execute会抛出ImportError,提示"Install it withfastmcp[code-mode]or pass a custom SandboxProvider"(见 code_mode.py)。
服务端:一行 Transform 折叠 8 个工具
server.py 用@mcp.tool定义了 8 个普通工具:add、multiply、fibonacci、reverse_string、word_count、to_uppercase、list_files、read_file。这些函数与标准 FastMCP 服务没有任何区别,关键在最后一行:
from fastmcp import FastMCP from fastmcp.experimental.transforms.code_mode import CodeMode mcp = FastMCP("CodeMode Demo") # ... 8 个 @mcp.tool 定义 ... mcp.add_transform(CodeMode())也可以采用官方文档推荐的构造方式:mcp = FastMCP("Server", transforms=[CodeMode()])(见 docs/servers/transforms/code-mode.mdx)。Transform 包裹已有工具,工具函数本身完全不用改动。
客户端:发现、检索、一次往返执行
client.py 完整演示了三步流程:
第一步:list_tools()只返回两个合成元工具
async with Client(Path("examples/code_mode/server.py")) as client: tools = await client.list_tools()服务端明明有 8 个工具,但客户端此时只能看到两个:
┌────────────── list_tools() ──────────────┐ │ Tool Description │ │ search Search for available tools ... │ │ execute Chain `await call_tool(...)` ... │ └── 8 backend tools collapsed into 2 ──────┘第二步:search按关键词发现工具
result = await client.call_tool("search", {"query": "add multiply numbers"})返回按相关度排序的匹配结果(示例输出中命中 3 个):
┌──── search(query="math arithmetic") ─────┐ │ # Tool Description │ │ 1 add Add two numbers together. │ │ 2 multiply Multiply two numbers. │ │ 3 fibonacci Generate the first n ... │ └── 3 results ─────────────────────────────┘第三步:execute一段脚本完成全部链式调用
code = """\ a = await call_tool("add", {"a": 3, "b": 4}) b = await call_tool("multiply", {"x": a["result"], "y": 2}) fib = await call_tool("fibonacci", {"n": b["result"]}) return {"sum": a["result"], "product": b["result"], "fibonacci": fib["result"]} """ result = await client.call_tool("execute", {"code": code})整段脚本在服务端沙箱中运行,只有最终return的值穿回上下文:
┌────────────── execute ───────────────────┐ │ a = await call_tool("add", {"a": 3 ... │ │ b = await call_tool("multiply", ... │ │ return b │ └── result: 14.0 ──────────────────────────┘核心洞察(README 原话):标准 MCP 中每次call_tool都是经过 LLM 的一次往返;而 CodeMode 下 LLM 只写一段脚本,所有工具调用在服务端完成,中间数据永远不会进入上下文窗口。
源码级原理:CodeMode 是如何折叠目录的
code_mode.py 是完整实现,核心类CodeMode继承自CatalogTransform。从源码结构看,它通过两个钩子改造工具暴露面:
transform_tools(tools):把完整目录替换为发现工具列表 + execute,即[search, get_schema, execute](默认配置),见 code_mode.py;get_tool(name, call_next):拦截对合成工具的查找,其余名字继续走下游 Transform 链,见 code_mode.py。
execute工具的内部实现值得细看(code_mode.py):
- 沙箱内只注入一个异步函数
call_tool(tool_name, params),它先通过transform.get_tool_catalog(ctx)拿到(经过鉴权过滤的)工具目录,找不到工具时抛NotFoundError("Unknown tool: ..."); - 每次调用先检查
max_tool_calls计数(默认 50),超限抛ToolError,防止一段 LLM 生成的代码里的循环扇出成大量后端操作; - 工具返回值经
_unwrap_tool_result规范化:有输出 schema 时返回structured_content,否则把文本内容拼接成字符串返回,见 code_mode.py。
沙箱:默认 MontySandboxProvider 与资源限制
默认沙箱MontySandboxProvider由pydantic-monty驱动。源码中的_DEFAULT_LIMITS给出了开箱即用的保守基线(code_mode.py):
_DEFAULT_LIMITS = { "max_duration_secs": 30.0, "max_memory": 100_000_000, # 100 MB }即:不传limits时默认 30 秒超时、100 MB 内存上限,确保开箱配置不会无界运行。三个构造形态:
MontySandboxProvider() # 基线:30s、100 MB MontySandboxProvider(limits={...}) # 自定义 MontySandboxProvider(limits=None) # 显式不限limits支持的键全部可选,省略即表示该维度不设上限(见 docs/servers/transforms/code-mode.mdx):
| Key | 类型 | 说明 |
|---|---|---|
max_duration_secs | float | 最大墙钟执行时间 |
max_memory | int | 内存上限(字节) |
max_allocations | int | 对象分配总数上限 |
max_recursion_depth | int | 最大递归深度 |
gc_interval | int | 垃圾回收频率 |
安全警告:SandboxProvider协议注释明确强调,run收到的code是不可信的 LLM 生成代码,实现必须将其放入隔离沙箱执行,绝不能直接exec();生产环境应使用MontySandboxProvider(见 code_mode.py)。仓库测试中的_UnsafeTestSandboxProvider也自注释为 "UNSAFE: Uses exec() for testing only"(见 tests/experimental/transforms/test_code_mode.py)。
工具调用上限同样可调:
CodeMode() # 默认:每次 execute 最多 50 次 call_tool() CodeMode(max_tool_calls=200) # 调高上限 CodeMode(max_tool_calls=None) # 不设上限发现工具:Search / GetSchemas / GetTags / ListTools
CodeMode 内置 4 个发现工具工厂,默认只启用Search和GetSchemas(code_mode.py)。每个工具都支持default_detail,LLM 也可在单次调用中覆盖。
Search与GetSchemas共享同一套三种详细度,输出格式一致:
| 级别 | 输出 | Token 成本 |
|---|---|---|
"brief" | 工具名 + 一行描述 | 最低,适合扫描 |
"detailed" | 紧凑 Markdown,含参数名、类型、必填标记 | 中等,通常足够写代码 |
"full" | 完整 JSON schema | 最高 |
Search:基于 BM25 排序做自然语言检索(默认最大 50 条)。命中数少于目录总数时,结果带"N of M tools:"前缀提示 LLM 还有更多可发现;支持default_limit限制结果数、tags参数先按标签过滤再检索。示例:Search(default_limit=5)。GetSchemas:按工具名返回参数细节,detail="full"时给出完整 JSON schema,适合参数深度嵌套的场景。GetTags:按标签浏览目录,brief 输出- math (3 tools)这类计数,full 输出各标签下的工具列表。默认不启用,适合大目录让 LLM 先按类别定位。ListTools:整体倾倒目录,默认"brief"。默认不启用——对小型目录(约 20 个工具以内)"一次性看全"比多次 search 往返更快,大目录则 search 更省 token:
from fastmcp.experimental.transforms.code_mode import CodeMode, ListTools, GetSchemas code_mode = CodeMode( discovery_tools=[ListTools(), GetSchemas()], )三种发现模式:按目录规模取舍
三阶段(默认):search → get_schema → execute。适合大型/复杂工具集,LLM 只为真正用到的 schema 付费:
mcp = FastMCP("Server", transforms=[CodeMode()])如果工具带标签,可加GetTags形成四阶段渐进式披露:
code_mode = CodeMode( discovery_tools=[GetTags(), Search(), GetSchemas()], ) mcp = FastMCP("Server", transforms=[code_mode])两阶段:让 Search 直接返回参数 schema,少一次往返,适合中小目录:
code_mode = CodeMode( discovery_tools=[Search(default_detail="detailed"), GetSchemas()], ) mcp = FastMCP("Server", transforms=[code_mode])单阶段:跳过发现,把工具说明写进 execute 的描述里,适合工具很少、LLM 已知晓目录的场景:
code_mode = CodeMode( discovery_tools=[], execute_description=( "Available tools:\n" "- add(x: int, y: int) -> int: Add two numbers\n" "- multiply(x: int, y: int) -> int: Multiply two numbers\n\n" "Write Python using `await call_tool(name, params)` and `return` the result." ), ) mcp = FastMCP("Server", transforms=[code_mode])自定义发现工具与命名
发现工具是"工厂可组合"的:每个工厂接收GetToolCatalog(目录访问函数,而非目录本身——因为目录是请求级作用域的,不同用户基于鉴权可能看到不同工具),返回一个Tool:
from fastmcp.experimental.transforms.code_mode import CodeMode, GetToolCatalog, GetSchemas from fastmcp.server.context import Context from fastmcp.tools import Tool def list_all_tools(get_catalog: GetToolCatalog) -> Tool: async def list_tools(ctx: Context) -> str: """List all available tool names.""" tools = await get_catalog(ctx) return ", ".join(t.name for t in tools) return Tool.from_function(fn=list_tools, name="list_tools") code_mode = CodeMode(discovery_tools=[list_all_tools, GetSchemas()])注意:LLM 看到的工具描述来自发现工具内部函数的 docstring,因此 docstring 应写清"返回什么、何时该调用"。工具名也可自定义:
code_mode = CodeMode( discovery_tools=[ Search(name="find_tools"), GetSchemas(name="describe"), ], execute_tool_name="run_workflow", )源码中_build_discovery_tools会校验名称唯一性,并禁止发现工具与execute_tool_name重名(见 code_mode.py)。
何时使用 CodeMode
结合官方文档的指引,CodeMode 特别适合工具数量多、LLM 需要编排多步工具调用的服务:上下文占用从"目录全量 + 每步中间结果"降为"按需发现 + 最终答案",往返次数从"每步一次"降为"一段脚本一次"。官方文档同时给出经验之谈:对于真正复杂的服务器,分阶段发现往往效果更好——把用不上的工具详细 schema 一次性灌给 LLM,其代价可能超过多一次往返的成本。
需要注意的是,CodeMode 目前属于实验性功能(官方文档标注版本 3.1.0),核心接口稳定,但具体发现工具及其参数可能随实践演进。生产使用务必先落实code-modeextra 安装与沙箱资源限制,并针对自己的工具目录跑通端到端调用链(可参考 tests/experimental/transforms/test_code_mode.py 中的测试模式与 docs/servers/transforms/code-mode.mdx 的完整配置说明)。
【免费下载链接】fastmcp🚀 The fast, Pythonic way to build MCP servers and clients.项目地址: https://gitcode.com/GitHub_Trending/fa/fastmcp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考