-
Notifications
You must be signed in to change notification settings - Fork 92
Code Mode #533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Code Mode #533
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
1dd834b
Initial
0xKoller f6c739f
code mode working
0xKoller 49e9b0c
code mode w fuzzy search
0xKoller 4a3c0e9
typos
0xKoller b17acb0
Merge branch 'canary' into code-mode-xmcp-415
0xKoller 61e1feb
Update pnpm-lock.yaml
0xKoller 9dfcfad
greptile fix
0xKoller File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,269 @@ | ||
| --- | ||
| title: "Code Mode" | ||
| metadataTitle: "Code Mode Pattern Guide | xmcp Documentation" | ||
| publishedAt: "2026-03-26" | ||
| summary: "Reduce tool context bloat by exposing your entire API surface through just 2 meta-tools." | ||
| description: "Learn how to implement the Code Mode pattern in xmcp, replace hundreds of individual MCP tools with search and execute meta-tools for massive token savings." | ||
| --- | ||
|
|
||
| ## Overview | ||
|
|
||
| MCP servers with many tools create a problem: every tool definition (name, description, full schema) is loaded into the agent's context window upfront. A server with 100 tools can consume 50,000+ tokens before the agent starts working. | ||
|
|
||
| Code Mode solves this with a simple pattern: instead of exposing all tools directly, expose just **2 meta-tools**: | ||
|
|
||
| - **`search`**: Agents query tool metadata to discover what's available | ||
| - **`execute`**: Agents invoke any tool by name | ||
|
|
||
| This reduces the tool definition footprint from tens of thousands of tokens to under 1,000 — a 99%+ reduction. The pattern was [popularized by Cloudflare](https://blog.cloudflare.com/code-mode-mcp/) for their 2,500+ endpoint API. | ||
|
|
||
| ## When to use Code Mode | ||
|
|
||
| **Use Code Mode when** your server exposes many tools (20+) that an agent rarely needs all at once. Common cases: API wrappers, enterprise integrations, multi-service orchestrators, or any server where tool discovery is more efficient than upfront loading. | ||
|
|
||
| **Code Mode may not be necessary** for servers with a small, focused set of tools (under 10-15) where loading all definitions upfront is acceptable. | ||
|
|
||
| ## How it works | ||
|
|
||
| xmcp provides two primitives in every tool handler's `extra` parameter: | ||
|
|
||
| - **`extra.listTools()`**: Returns metadata and JSON Schema for all registered tools | ||
| - **`extra.callTool(name, args)`**: Invokes another tool by name, validates args, and returns the result | ||
|
|
||
| You use these to build your own `search` and `execute` tools. No plugins or special configuration needed. | ||
|
|
||
| ## Quickstart | ||
|
|
||
| Create two tool files in your xmcp project: | ||
|
|
||
| ### The search tool | ||
|
|
||
| ```typescript title="src/tools/search.ts" | ||
| import { z } from "zod"; | ||
| import type { ToolMetadata } from "xmcp"; | ||
|
|
||
| export const schema = { | ||
| query: z.string().describe("Search query to filter available tools"), | ||
| }; | ||
|
|
||
| export const metadata: ToolMetadata = { | ||
| name: "search", | ||
| description: | ||
| "Search available tools by name or description. Returns matching tools with their input schemas so you know how to call them.", | ||
| }; | ||
|
|
||
| export default async function search({ query }, extra) { | ||
| const tools = extra.listTools(); | ||
| const q = query.toLowerCase(); | ||
|
|
||
| const matches = tools.filter( | ||
| (t) => | ||
| t.name.toLowerCase().includes(q) || | ||
| t.description.toLowerCase().includes(q) | ||
| ); | ||
|
|
||
| return JSON.stringify(matches, null, 2); | ||
| } | ||
| ``` | ||
|
|
||
| ### The execute tool | ||
|
|
||
| ```typescript title="src/tools/execute.ts" | ||
| import { z } from "zod"; | ||
| import type { ToolMetadata } from "xmcp"; | ||
|
|
||
| export const schema = { | ||
| toolName: z.string().describe("Name of the tool to execute"), | ||
| args: z | ||
| .string() | ||
| .describe('JSON string of arguments (e.g. \'{"name": "World"}\')'), | ||
| }; | ||
|
|
||
| export const metadata: ToolMetadata = { | ||
| name: "execute", | ||
| description: | ||
| "Execute any available tool by name. Use the search tool first to discover available tools and their required arguments. Pass args as a JSON string.", | ||
| annotations: { | ||
| openWorldHint: true, | ||
| }, | ||
| }; | ||
|
|
||
| export default async function execute({ toolName, args }, extra) { | ||
| let parsedArgs; | ||
| try { | ||
| parsedArgs = JSON.parse(args); | ||
| } catch { | ||
| return { content: [{ type: "text", text: "Invalid JSON in args" }], isError: true }; | ||
| } | ||
| return await extra.callTool(toolName, parsedArgs); | ||
| } | ||
| ``` | ||
|
|
||
| ### Tools as `internal` | ||
|
|
||
| By default, all tools appear in MCP's `tools/list` response. When connecting to a server, clients see them upfront. To hide tools from `tools/list` while keeping them callable via `execute`, add `internal: true` to their annotations: | ||
|
|
||
| ```typescript title="src/tools/get-user.ts" | ||
| import { z } from "zod"; | ||
| import type { ToolMetadata } from "xmcp"; | ||
|
|
||
| export const schema = { | ||
| userId: z.string().describe("The user ID to look up"), | ||
| }; | ||
|
|
||
| export const metadata: ToolMetadata = { | ||
| name: "get-user", | ||
| description: "Retrieve user profile information by ID", | ||
| annotations: { | ||
| internal: true, | ||
| }, | ||
| }; | ||
|
|
||
| export default async function getUser({ userId }) { | ||
| const user = await db.users.findById(userId); | ||
| return JSON.stringify(user); | ||
| } | ||
| ``` | ||
|
|
||
| Now agents connecting to your server only see `search` + `execute` in `tools/list` (~1,000 tokens). The internal tools are discoverable via `search` and callable via `execute`. | ||
|
|
||
| ### Tags for richer discovery | ||
|
|
||
| Use `tags` in annotations to help agents filter tools by category: | ||
|
|
||
| ```typescript | ||
| annotations: { | ||
| internal: true, | ||
| tags: ["users", "admin"], | ||
| } | ||
| ``` | ||
|
|
||
| Then agents can search by tag: `search({ tag: "users" })` to find all user-related tools without scanning descriptions. | ||
|
|
||
| ## How agents interact with Code Mode | ||
|
|
||
| An agent connecting to your server sees only 2 tools. Here's a typical interaction: | ||
|
|
||
| **Step 1:** Agent calls `search` to find relevant tools: | ||
|
|
||
| ```json | ||
| { | ||
| "tool": "search", | ||
| "args": { "query": "user" } | ||
| } | ||
| ``` | ||
|
|
||
| Response: | ||
|
|
||
| ```json | ||
| [ | ||
| { | ||
| "name": "get-user", | ||
| "description": "Retrieve user profile information by ID", | ||
| "inputSchema": { | ||
| "type": "object", | ||
| "properties": { | ||
| "userId": { "type": "string", "description": "The user ID to look up" } | ||
| }, | ||
| "required": ["userId"] | ||
| } | ||
| }, | ||
| { | ||
| "name": "update-user", | ||
| "description": "Update user profile fields", | ||
| "inputSchema": { "..." } | ||
| } | ||
| ] | ||
| ``` | ||
|
|
||
| **Step 2:** Agent calls `execute` with the discovered tool: | ||
|
|
||
| ```json | ||
| { | ||
| "tool": "execute", | ||
| "args": { | ||
| "toolName": "get-user", | ||
| "args": "{\"userId\": \"usr_123\"}" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The agent gets the same result as if it had called `get-user` directly. | ||
|
|
||
| ## Tool chaining | ||
|
|
||
| Since `callTool` is available in any handler, you can also build orchestrator tools that chain multiple calls: | ||
|
|
||
| ```typescript title="src/tools/onboard-user.ts" | ||
| import { z } from "zod"; | ||
| import type { ToolMetadata } from "xmcp"; | ||
|
|
||
| export const schema = { | ||
| email: z.string().email(), | ||
| name: z.string(), | ||
| }; | ||
|
|
||
| export const metadata: ToolMetadata = { | ||
| name: "onboard-user", | ||
| description: "Create a user account and send welcome email", | ||
| }; | ||
|
|
||
| export default async function onboardUser({ email, name }, extra) { | ||
| const user = await extra.callTool("create-user", { email, name }); | ||
| await extra.callTool("send-welcome-email", { userId: user.id }); | ||
| return user; | ||
| } | ||
| ``` | ||
|
|
||
| ## Combining with authentication | ||
|
|
||
| Code Mode works with any xmcp auth middleware. The auth context automatically propagates through `callTool`: | ||
|
|
||
| ```typescript title="src/middleware.ts" | ||
| import { jwtAuthMiddleware } from "xmcp"; | ||
|
|
||
| export default jwtAuthMiddleware({ | ||
| secret: process.env.JWT_SECRET, | ||
| }); | ||
| ``` | ||
|
|
||
| When an authenticated agent calls `execute`, the called tool receives the same `authInfo`. | ||
|
|
||
| ## Smarter search | ||
|
|
||
| The default `search` example includes TF-IDF scoring and fuzzy matching. It handles typos ("crete" matches "create") and ranks results by relevance. For most use cases this is sufficient. | ||
|
|
||
| For **true semantic search** (where "remove" finds "delete"), you need embeddings. You can swap in any embedding provider in your search tool: | ||
|
|
||
| ```typescript title="src/tools/search.ts" | ||
| import { embed } from "your-embedding-lib"; // OpenAI, Cohere, local model, etc. | ||
|
|
||
| // Pre-compute embeddings at startup | ||
| let toolEmbeddings: Map<string, number[]>; | ||
|
|
||
| export default async function search({ query }, extra) { | ||
| if (!toolEmbeddings) { | ||
| toolEmbeddings = new Map(); | ||
| for (const tool of extra.listTools()) { | ||
| const vec = await embed(tool.description); | ||
| toolEmbeddings.set(tool.name, vec); | ||
| } | ||
| } | ||
|
|
||
| const queryVec = await embed(query); | ||
| const scored = extra.listTools().map((t) => ({ | ||
| ...t, | ||
| score: cosineSimilarity(queryVec, toolEmbeddings.get(t.name)!), | ||
| })); | ||
|
|
||
| return JSON.stringify(scored.sort((a, b) => b.score - a.score).slice(0, 10)); | ||
| } | ||
| ``` | ||
|
|
||
| Since `search` is just a regular xmcp tool, you have full control over the search strategy. | ||
|
|
||
| ## References | ||
|
|
||
| - [Cloudflare Code Mode announcement](https://blog.cloudflare.com/code-mode-mcp/): The original pattern | ||
| - [Tools documentation](/docs/core-concepts/tools): How xmcp tools work | ||
| - [Authentication guide](/docs/guides/authentication): Combining Code Mode with auth | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| { | ||
| "title": "Guides", | ||
| "pages": ["xmcp-mcp-server", "authentication", "monetization"], | ||
| "pages": ["xmcp-mcp-server", "authentication", "monetization", "code-mode"], | ||
| "defaultOpen": true, | ||
| "root": true | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "name": "Code Mode HTTP", | ||
| "description": "Expose your entire tool surface through 2 meta-tools using the Code Mode pattern", | ||
| "keywords": [ | ||
| "http", | ||
| "code-mode", | ||
| "introspection" | ||
| ], | ||
| "scripts": { | ||
| "build": "xmcp build", | ||
| "dev": "xmcp dev", | ||
| "start": "node dist/http.js" | ||
| }, | ||
| "dependencies": { | ||
| "xmcp": "workspace:*", | ||
| "zod": "^4.0.10" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { z } from "zod"; | ||
| import type { ToolMetadata } from "xmcp"; | ||
|
|
||
| export const schema = { | ||
| name: z.string().describe("The user's full name"), | ||
| email: z.string().email().describe("The user's email address"), | ||
| }; | ||
|
|
||
| export const metadata: ToolMetadata = { | ||
| name: "create-user", | ||
| description: "Create a new user account with name and email", | ||
| annotations: { | ||
| destructiveHint: true, | ||
| internal: true, | ||
| tags: ["users", "write"], | ||
| examples: [ | ||
| { | ||
| args: { name: "Alice", email: "alice@example.com" }, | ||
| description: "Create a new user", | ||
| }, | ||
| ], | ||
| }, | ||
| }; | ||
|
|
||
| export default async function createUser({ | ||
| name, | ||
| email, | ||
| }: { | ||
| name: string; | ||
| email: string; | ||
| }) { | ||
| const id = String(Math.floor(Math.random() * 10000)); | ||
| const user = { id, name, email }; | ||
| return JSON.stringify(user); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { z } from "zod"; | ||
| import type { ToolMetadata, ToolExtraArguments } from "xmcp"; | ||
|
|
||
| export const schema = { | ||
| toolName: z.string().describe("Name of the tool to execute"), | ||
| args: z | ||
| .string() | ||
| .describe( | ||
| 'JSON string of arguments to pass to the tool (e.g. \'{"name": "World"}\')' | ||
| ), | ||
| }; | ||
|
|
||
| export const metadata: ToolMetadata = { | ||
| name: "execute", | ||
| description: | ||
| "Execute any available tool by name. Use the search tool first to discover tools and their required arguments. Pass args as a JSON string.", | ||
| annotations: { openWorldHint: true }, | ||
| }; | ||
|
|
||
| export default async function execute( | ||
| { toolName, args }: { toolName: string; args: string }, | ||
| extra: ToolExtraArguments | ||
| ) { | ||
| let parsedArgs; | ||
| try { | ||
| parsedArgs = JSON.parse(args); | ||
| } catch { | ||
| return { content: [{ type: "text", text: "Invalid JSON in args" }], isError: true }; | ||
| } | ||
| return await extra.callTool(toolName, parsedArgs); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { z } from "zod"; | ||
| import type { ToolMetadata } from "xmcp"; | ||
|
|
||
| export const schema = { | ||
| userId: z.string().describe("The user ID to look up"), | ||
| }; | ||
|
|
||
| export const metadata: ToolMetadata = { | ||
| name: "get-user", | ||
| description: "Retrieve user profile information by ID", | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| internal: true, | ||
| tags: ["users", "read"], | ||
| examples: [{ args: { userId: "1" }, description: "Get Alice's profile" }], | ||
| }, | ||
| }; | ||
|
|
||
| const users: Record<string, { id: string; name: string; email: string }> = { | ||
| "1": { id: "1", name: "Alice", email: "alice@example.com" }, | ||
| "2": { id: "2", name: "Bob", email: "bob@example.com" }, | ||
| "3": { id: "3", name: "Charlie", email: "charlie@example.com" }, | ||
| }; | ||
|
|
||
| export default async function getUser({ userId }: { userId: string }) { | ||
| const user = users[userId]; | ||
| if (!user) { | ||
| return `User with ID "${userId}" not found`; | ||
| } | ||
| return JSON.stringify(user); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.