Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 269 additions & 0 deletions apps/website/content/docs/guides/code-mode.mdx
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:
Comment thread
0xKoller marked this conversation as resolved.

```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
2 changes: 1 addition & 1 deletion apps/website/content/docs/guides/meta.json
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
}
18 changes: 18 additions & 0 deletions examples/code-mode-http/package.json
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"
}
}
35 changes: 35 additions & 0 deletions examples/code-mode-http/src/tools/create-user.ts
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);
}
31 changes: 31 additions & 0 deletions examples/code-mode-http/src/tools/execute.ts
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);
}
31 changes: 31 additions & 0 deletions examples/code-mode-http/src/tools/get-user.ts
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);
}
Loading
Loading