Convert code-mode to the CodeMode plugin (#4002)

This commit is contained in:
Jeremiah Lowin 2026-04-22 09:46:29 -04:00 committed by GitHub
commit 19fa2fc33e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1019 additions and 645 deletions

View file

@ -10,9 +10,9 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.1.0" />
<Warning>
CodeMode is experimental. The core interface is stable, but the specific discovery tools and their parameters may evolve as we learn more about what works best in practice.
</Warning>
<Note>
The core interface is stable, but the specific discovery tools and their parameters may evolve as we learn more about what works best in practice.
</Note>
Standard MCP tool usage has two scaling problems. First, every tool in the catalog is loaded into the LLM's context upfront — with hundreds of tools, that's tens of thousands of tokens spent before the LLM even reads the user's request. Second, every tool call is a round-trip: the LLM calls a tool, the result passes back through the context window, the LLM reasons about it, calls another tool, and so on. Intermediate results that only exist to feed the next step still burn tokens flowing through the model.
@ -26,13 +26,13 @@ The approach was introduced by Cloudflare in [Code Mode](https://blog.cloudflare
CodeMode requires the `code-mode` extra for sandbox support. Install it with `pip install "fastmcp[code-mode]"`.
</Tip>
You take a normal server with normally registered tools and add a `CodeMode` transform. The transform wraps your existing tools in the code mode machinery — your tool functions don't change at all:
You take a normal server with normally registered tools and attach the `CodeMode` plugin. The plugin wraps your existing tools in the code mode machinery — your tool functions don't change at all:
```python
from fastmcp import FastMCP
from fastmcp.experimental.transforms.code_mode import CodeMode
from fastmcp.server.plugins.code_mode import CodeMode
mcp = FastMCP("Server", transforms=[CodeMode()])
mcp = FastMCP("Server", plugins=[CodeMode()])
@mcp.tool
def add(x: int, y: int) -> int:
@ -165,7 +165,7 @@ If your tools use [tags](/servers/tools#tags), Search also accepts a `tags` para
`ListTools` isn't included in the defaults — for large catalogs, search-based discovery is more token-efficient. But for smaller catalogs (under ~20 tools), letting the LLM see everything upfront can be faster than multiple search round-trips:
```python
from fastmcp.experimental.transforms.code_mode import CodeMode, ListTools, GetSchemas
from fastmcp.server.plugins.code_mode import CodeMode, ListTools, GetSchemas
code_mode = CodeMode(
discovery_tools=[ListTools(), GetSchemas()],
@ -182,23 +182,23 @@ The default. The LLM searches for candidates, inspects schemas for the ones it w
```python
from fastmcp import FastMCP
from fastmcp.experimental.transforms.code_mode import CodeMode
from fastmcp.server.plugins.code_mode import CodeMode
mcp = FastMCP("Server", transforms=[CodeMode()])
mcp = FastMCP("Server", plugins=[CodeMode()])
```
If your tools use [tags](/servers/tools#tags), add `GetTags` so the LLM can browse by category before searching — giving it four stages of progressive disclosure:
```python
from fastmcp import FastMCP
from fastmcp.experimental.transforms.code_mode import CodeMode
from fastmcp.experimental.transforms.code_mode import GetTags, Search, GetSchemas
from fastmcp.server.plugins.code_mode import CodeMode
from fastmcp.server.plugins.code_mode import GetTags, Search, GetSchemas
code_mode = CodeMode(
discovery_tools=[GetTags(), Search(), GetSchemas()],
)
mcp = FastMCP("Server", transforms=[code_mode])
mcp = FastMCP("Server", plugins=[code_mode])
```
### Two-Stage
@ -207,14 +207,14 @@ Search returns parameter schemas inline, so the LLM can go straight from search
```python
from fastmcp import FastMCP
from fastmcp.experimental.transforms.code_mode import CodeMode
from fastmcp.experimental.transforms.code_mode import Search, GetSchemas
from fastmcp.server.plugins.code_mode import CodeMode
from fastmcp.server.plugins.code_mode import Search, GetSchemas
code_mode = CodeMode(
discovery_tools=[Search(default_detail="detailed"), GetSchemas()],
)
mcp = FastMCP("Server", transforms=[code_mode])
mcp = FastMCP("Server", plugins=[code_mode])
```
`GetSchemas` is still available as a fallback — the LLM can call it with `detail="full"` if it encounters a tool with complex nested parameters where the compact markdown isn't enough.
@ -225,7 +225,7 @@ Skip discovery entirely and bake tool instructions into the execute tool's descr
```python
from fastmcp import FastMCP
from fastmcp.experimental.transforms.code_mode import CodeMode
from fastmcp.server.plugins.code_mode import CodeMode
code_mode = CodeMode(
discovery_tools=[],
@ -237,7 +237,7 @@ code_mode = CodeMode(
),
)
mcp = FastMCP("Server", transforms=[code_mode])
mcp = FastMCP("Server", plugins=[code_mode])
```
## Custom Discovery Tools
@ -247,8 +247,8 @@ Discovery tools are composable — you can mix the built-ins with your own. Each
Here's a minimal example:
```python
from fastmcp.experimental.transforms.code_mode import CodeMode
from fastmcp.experimental.transforms.code_mode import GetToolCatalog, GetSchemas
from fastmcp.server.plugins.code_mode import CodeMode
from fastmcp.server.plugins.code_mode import GetToolCatalog, GetSchemas
from fastmcp.server.context import Context
from fastmcp.tools.tool import Tool
@ -268,7 +268,7 @@ The LLM sees the docstring of each discovery tool's inner function as its descri
Discovery tools and the execute tool can also have custom names:
```python
from fastmcp.experimental.transforms.code_mode import Search, GetSchemas
from fastmcp.server.plugins.code_mode import Search, GetSchemas
code_mode = CodeMode(
discovery_tools=[
@ -278,7 +278,7 @@ code_mode = CodeMode(
execute_tool_name="run_workflow",
)
mcp = FastMCP("Server", transforms=[code_mode])
mcp = FastMCP("Server", plugins=[code_mode])
```
## Sandbox Configuration
@ -288,14 +288,14 @@ mcp = FastMCP("Server", transforms=[code_mode])
The default `MontySandboxProvider` can enforce execution limits — timeouts, memory caps, recursion depth, and more. Without limits, LLM-generated scripts can run indefinitely.
```python
from fastmcp.experimental.transforms.code_mode import CodeMode
from fastmcp.experimental.transforms.code_mode import MontySandboxProvider
from fastmcp.server.plugins.code_mode import CodeMode
from fastmcp.server.plugins.code_mode import MontySandboxProvider
sandbox = MontySandboxProvider(
limits={"max_duration_secs": 10, "max_memory": 50_000_000},
)
mcp = FastMCP("Server", transforms=[CodeMode(sandbox_provider=sandbox)])
mcp = FastMCP("Server", plugins=[CodeMode(sandbox_provider=sandbox)])
```
All keys are optional — omit any to leave that dimension uncapped:
@ -316,8 +316,8 @@ You can replace the default sandbox with any object implementing the `SandboxPro
from collections.abc import Callable
from typing import Any
from fastmcp.experimental.transforms.code_mode import CodeMode
from fastmcp.experimental.transforms.code_mode import SandboxProvider
from fastmcp.server.plugins.code_mode import CodeMode
from fastmcp.server.plugins.code_mode import SandboxProvider
class RemoteSandboxProvider:
async def run(
@ -332,7 +332,7 @@ class RemoteSandboxProvider:
mcp = FastMCP(
"Server",
transforms=[CodeMode(sandbox_provider=RemoteSandboxProvider())],
plugins=[CodeMode(sandbox_provider=RemoteSandboxProvider())],
)
```