Implementation of "Code execution with MCP: building more efficient AI agents" — Anthropic Engineering, Nov 2025. The canonical reference for this pattern.
This isn't a compression trick. Intermediate data — raw API responses, filtered lists, full document bodies — never enters the context window at all. The sandbox is opaque to Claude by design.
Mid-workflow. Claude's working. Three more tool calls to go. Then:
✓ Searching QuickBooks... 847 invoices found (context: +14,200 tokens)
✓ Filtering overdue... 23 invoices (context: +8,100 tokens)
✓ Fetching customer details...
⚠ Claude AI Usage Limit Reached
You've reached your usage limit and will be able to resume in 5 hours.
The tool calls worked. Claude ran out of room to think.
mcp-exec fixes this architecturally — you decide what enters context, and everything else stays in the sandbox.
mcp-exec adds two tools to Claude Code:
tools(query)— searches your connected MCP servers and returns trimmed summaries. Full schemas never touch the context window.exec(code, runtime)— runs code in an OS-level sandbox. MCP servers are importable as modules. Only the final return value comes back.
Runtimes:
| Runtime | State | Use for |
|---|---|---|
"node" |
Persistent (globalThis) |
MCP orchestration, multi-step workflows |
"bash" |
Stateless | Unix pipelines, jq, awk, post-processing |
"python" |
Stateless (uv run --isolated) |
Data analysis, pandas, arbitrary PyPI packages via PEP 723 |
Without mcp-exec: 847 invoices returned raw (14k tokens), customer lookups add 8k more, rate limit mid-task.
With mcp-exec:
exec({
runtime: "node",
code: `
import { searchInvoices, getInvoice } from 'mcp/quickbooks';
import { getCustomer } from 'mcp/crm';
import { createDraft } from 'mcp/gmail';
const overdue = await searchInvoices({ status: 'overdue' });
const details = await Promise.all(
overdue.map(inv => Promise.all([
getInvoice({ id: inv.id }),
getCustomer({ id: inv.customerId })
]))
);
const body = details
.map(([inv, cust]) => \`\${inv.amount} — \${cust.name}\`)
.join('\n');
await createDraft({ to: 'sales@co.com', subject: 'Overdue invoices', body });
return \`Draft created — \${overdue.length} invoices\`;
`
})
// → "Draft created — 23 invoices" tokens used: ~80Without mcp-exec: 34 Linear tickets + 12 PRs with diffs + 200 Slack messages = 20k tokens before the meeting starts.
With mcp-exec:
exec({
runtime: "node",
code: `
import { getIssues } from 'mcp/linear';
import { listPullRequests } from 'mcp/github';
import { getMessages, postMessage } from 'mcp/slack';
const [tickets, prs, msgs] = await Promise.all([
getIssues({ assignee: 'me', status: 'in_progress' }),
listPullRequests({ state: 'open', author: 'me' }),
getMessages({ channel: '#team', since: 'yesterday' }),
]);
await postMessage({
channel: '#standup',
text: [
'🔴 Blocked: ' + tickets.filter(t => t.labels.includes('blocked')).length,
'👀 PRs needing review: ' + prs.filter(pr => pr.reviewers.length === 0).length,
'📣 Mentions: ' + msgs.filter(m => m.text.includes('@me')).length,
].join('\n'),
});
return 'Standup posted';
`
})
// → "Standup posted" tokens used: ~60Without mcp-exec: Three fetched pages = 40k tokens. Can't read more than 4 sources per session.
With mcp-exec:
exec({
runtime: "node",
code: `
import { search, fetch } from 'mcp/browser';
import { createDoc } from 'mcp/gdrive';
const results = await search({ query: 'best React patterns 2025' });
// Fetch all 10 results in parallel — full HTML stays in sandbox
const pages = await Promise.all(results.map(r => fetch({ url: r.url })));
// Extract only what matters
const insights = pages.flatMap(page =>
page.headings.filter(h => h.level <= 2).map(h => h.text)
);
const doc = await createDoc({
title: 'React Patterns 2025',
content: insights.join('\n'),
});
return doc.url;
`
})
// → "https://docs.google.com/document/d/..." tokens used: ~55Session state persists across exec() calls in Node. Fetch once, slice differently without re-fetching.
// Step 1 — load 100 PRs into session
exec({ runtime: "node", code: `
import { listPullRequests } from 'mcp/github';
globalThis.prs = await listPullRequests({ state: 'open', per_page: 100 });
return globalThis.prs.length + ' PRs loaded';
`});
// → "100 PRs loaded"
// Step 2 — find stale (no re-fetch)
exec({ runtime: "node", code: `
const stale = globalThis.prs.filter(pr => {
const days = (Date.now() - new Date(pr.updated_at)) / 86400000;
return days > 14;
});
return stale.map(pr => ({ number: pr.number, days: Math.floor((Date.now() - new Date(pr.updated_at)) / 86400000) }));
`});
// → [{number: 42, days: 21}, ...]exec({
"runtime": "python",
"code": """
# /// script
# requires-python = ">=3.12"
# dependencies = ["pandas>=2.0"]
# ///
import json, sys, pandas as pd
data = json.loads(sys.argv[1]) if len(sys.argv) > 1 else []
df = pd.DataFrame(data)
summary = df.groupby('stage')['amount'].sum().sort_values(ascending=False)
print(summary.head(5).to_json())
"""
})
# → {"Closed Won": 12400000, "Negotiation": 4200000, ...}You don't need a multi-step workflow to benefit from mcp-exec. A single tool call that returns more than you need is the perfect use case.
The pattern: call the tool inside exec(), pipe the result through bash, jq, or Python, and return only what matters. The raw response never enters context.
"Give me the last 5 error lines":
exec({
runtime: "node",
code: `
import { getLogs } from 'mcp/observability';
const logs = await getLogs({ service: 'api', limit: 500 });
return logs.filter(l => l.level === 'error').slice(-5).map(l => l.message);
`
})
// → ["Connection refused", "Timeout after 5000ms", ...] tokens used: ~30
// Without exec: 500 log lines = ~8,000 tokens"What's the median deal size?":
exec({
"runtime": "python",
"code": """
# /// script
# dependencies = ["statistics"]
# ///
import json, sys, statistics
records = json.loads(sys.argv[1])
print(statistics.median(r['amount'] for r in records))
"""
})
# → "47500.0" tokens used: ~15
# Without exec: 1,000 CRM records = ~40,000 tokensThe idea scales to anything: run a search and return the top 3 titles, fetch a config file and extract one key, pull a calendar and count today's events. Every case where a tool returns walls of data and you need a fact — exec() is the right tool.
The numbers above are real. You can verify them yourself in 5 minutes.
→ Download case-study.md — full reproduction instructions, token logging commands, expected output.
Our run: 43,800 tokens → 90 tokens (99.8% reduction) on a PR staleness nudge workflow (GitHub + Slack, 87 open PRs).
Supported agents: Claude Code · Cursor · Windsurf · GitHub Copilot · Gemini CLI · Codex CLI · Cline · OpenCode
Via the agent-marketplace (recommended):
# Add the marketplace (one-time setup)
claude plugin marketplace add joeblackwaslike/agent-marketplace
# Install mcp-exec
claude plugin install mcp-execThen prime your CLAUDE.md so the model knows when to use it:
mcp-exec prime # global (~/.claude/CLAUDE.md)
mcp-exec prime --local # project-level (.claude/CLAUDE.md)Why? Skills activate ~40% of the time from the skills directory alone. Priming adds a trigger rule to CLAUDE.md that brings reliability to ~90–95%. It's idempotent — safe to run more than once.
Manual setup
1. Register the MCP server — add to .claude/mcp.json:
{
"mcpServers": {
"mcp-exec": {
"command": "npx",
"args": ["@joeblackwaslike2/mcp-exec"]
}
}
}2. Prime your CLAUDE.md:
mcp-exec prime # global (~/.claude/CLAUDE.md)
mcp-exec prime --local # project-level (.claude/CLAUDE.md)1. Add to .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):
{
"mcpServers": {
"mcp-exec": {
"command": "npx",
"args": ["@joeblackwaslike2/mcp-exec"]
}
}
}2. Add to .cursorrules (or Cursor's custom system prompt):
## mcp-exec
Use exec(code, runtime) whenever a tool call or workflow would return more tokens than you need:
- Single tool returning too much? Wrap it in exec() and filter with bash or Python — only your output enters context
- Multi-step workflow? Chain tool calls inside exec() — intermediate data never touches context
- Runtimes: "node" (stateful via globalThis, MCP imports), "bash" (pipe/filter), "python" (analysis, PyPI via PEP 723)
Use tools(query) to search available MCP tools without loading full schemas into context.
1. Add to ~/.codeium/windsurf/mcp_config.json (create the file if it doesn't exist):
{
"mcpServers": {
"mcp-exec": {
"command": "npx",
"args": ["@joeblackwaslike2/mcp-exec"]
}
}
}2. Add to Windsurf's custom instructions (Settings → Cascade → Custom Instructions):
## mcp-exec
Use exec(code, runtime) whenever a tool call or workflow would return more tokens than you need:
- Single tool returning too much? Wrap it in exec() and filter with bash or Python — only your output enters context
- Multi-step workflow? Chain tool calls inside exec() — intermediate data never touches context
- Runtimes: "node" (stateful via globalThis, MCP imports), "bash" (pipe/filter), "python" (analysis, PyPI via PEP 723)
Use tools(query) to search available MCP tools without loading full schemas into context.
Restart Windsurf after editing the config file.
Requires VS Code 1.101+ and Copilot agent mode.
1. Add to .vscode/mcp.json in your project:
{
"servers": {
"mcp-exec": {
"command": "npx",
"args": ["@joeblackwaslike2/mcp-exec"]
}
}
}
⚠️ Note: VS Code uses"servers", not"mcpServers". Copy-pasting from a Cursor or Claude Code config without changing this key will silently fail to load.
2. Add to .github/copilot-instructions.md:
## mcp-exec
Use `exec(code, runtime)` whenever a tool call or workflow would return more tokens than you need:
- **Single tool returning too much?** Wrap it in `exec()` and filter with bash or Python — only your output enters context
- **Multi-step workflow?** Chain tool calls inside `exec()` — intermediate data never touches context
- **Runtimes:** `"node"` (stateful, MCP imports), `"bash"` (pipe/filter), `"python"` (analysis, PyPI via PEP 723)
Use `tools(query)` to search available MCP tools without loading full schemas into context.1. Add to ~/.gemini/settings.json (global) or .gemini/settings.json (project):
{
"mcpServers": {
"mcp-exec": {
"command": "npx",
"args": ["@joeblackwaslike2/mcp-exec"]
}
}
}
⚠️ Note: Do not use underscores in the server name — usemcp-exec, notmcp_exec. The Gemini CLI policy parser splits on underscores and will misroute the server identity.
2. Add to GEMINI.md or AGENTS.md in your project root:
## mcp-exec
Use `exec(code, runtime)` whenever a tool call or workflow would return more tokens than you need:
- **Single tool returning too much?** Wrap it in `exec()` and filter with bash or Python — only your output enters context
- **Multi-step workflow?** Chain tool calls inside `exec()` — intermediate data never touches context
- **Runtimes:** `"node"` (stateful, MCP imports), `"bash"` (pipe/filter), `"python"` (analysis, PyPI via PEP 723)
Use `tools(query)` to search available MCP tools without loading full schemas into context.1. Add to ~/.codex/config.toml (global) or .codex/config.toml (project):
[mcp_servers.mcp-exec]
command = "npx"
args = ["@joeblackwaslike2/mcp-exec"]2. Add to AGENTS.md in your project root:
## mcp-exec
Use `exec(code, runtime)` whenever a tool call or workflow would return more tokens than you need:
- **Single tool returning too much?** Wrap it in `exec()` and filter with bash or Python — only your output enters context
- **Multi-step workflow?** Chain tool calls inside `exec()` — intermediate data never touches context
- **Runtimes:** `"node"` (stateful, MCP imports), `"bash"` (pipe/filter), `"python"` (analysis, PyPI via PEP 723)
Use `tools(query)` to search available MCP tools without loading full schemas into context.1. In VS Code, open Cline's settings panel → MCP Servers tab → add a new server:
{
"mcpServers": {
"mcp-exec": {
"command": "npx",
"args": ["@joeblackwaslike2/mcp-exec"]
}
}
}2. In Cline's custom instructions (Settings → Custom Instructions), add:
## mcp-exec
Use exec(code, runtime) whenever a tool call or workflow would return more tokens than you need:
- Single tool returning too much? Wrap it in exec() and filter with bash or Python — only your output enters context
- Multi-step workflow? Chain tool calls inside exec() — intermediate data never touches context
- Runtimes: "node" (stateful via globalThis, MCP imports), "bash" (pipe/filter), "python" (analysis, PyPI via PEP 723)
Use tools(query) to search available MCP tools without loading full schemas into context.
1. Add to opencode.json in your project root:
{
"mcpServers": {
"mcp-exec": {
"command": "npx",
"args": ["@joeblackwaslike2/mcp-exec"]
}
},
"plugins": [".opencode/plugins/mcp-exec.js"]
}2. Copy the bootstrap plugin file:
mkdir -p .opencode/plugins
curl -sSL https://raw.githubusercontent.com/joeblackwaslike/mcp-exec/main/.opencode/plugins/mcp-exec.js \
-o .opencode/plugins/mcp-exec.jsSkills are auto-discovered. The using-mcp-exec skill is injected into the first user message automatically — no manual priming needed.
mcp-exec ships two Claude Code skills that activate automatically on install.
| Skill | Activates when… | References |
|---|---|---|
| Using mcp-exec | Writing exec() or tools() calls |
ts-sdk-reference.md, py-sdk-reference.md |
| mcp-exec Dev Workflow | Building a project, fetching API docs, or processing large API responses | — |
- Node.js 20.12+
- uv (Python runtime) —
curl -LsSf https://astral.sh/uv/install.sh | sh - macOS or Linux (sandbox uses
sandbox-exec/bubblewrap; Windows not supported) - Claude Code 2.1.7+ recommended (enables CC Tool Search for maximum savings)
tools("*") // all tools across all connected servers
tools("search emails") // substring match across name + description
tools('"pull request"') // exact phrase matchReturns { server, name, description, signature }[] — trimmed summaries, no full schemas.
exec({
code: string,
runtime:
| "node"
| "bash"
| "python"
| { type: "node" | "bash" | "python", timeout?: number, env?: Record<string, string> },
session_id?: string, // optional — for parallel isolation
})
// → { result: unknown, tool_calls: ToolCallRecord[] }Node: persistent session via globalThis. Bundled packages: zod, lodash-es, date-fns, csv-parse, cheerio, xlsx. MCP imports via import { tool } from 'mcp/server-name'.
Bash: stateless subprocess. stdout becomes result.
Python: stateless via uv run --isolated. Declare dependencies inline with PEP 723. stdout becomes result. MCP tools available via HTTP bridge: from mcp.github import list_pull_requests.
Implicit session per conversation (Node only). Explicit session_id for parallel isolation. Sessions expire after 10 minutes idle. Maximum 100 concurrent sessions.
const { result } = await exec({ runtime: "node", code: `...` });
if (typeof result === 'object' && result !== null && 'error' in result) {
const { error, line, column } = result;
}- Tool calls where the result is small and you want it visible in context
- When you need the raw API output verbatim in context (display, inline citation, etc.)
- Interactive flows where the user needs to see and confirm intermediate results
The sandbox enforces restrictions at the OS level via @anthropic-ai/sandbox-runtime. All child processes inherit them — no language-level bypass exists.
Environment variable allowlist (resolved in v0.3, see #3):
By default, only a safe minimal set of env vars passes through to Bash and Python subprocesses: PATH, HOME, TMPDIR, TMP, TEMP, USER, USERNAME, LANG, LC_ALL, LC_CTYPE, NODE_PATH, SHELL. Everything else — API keys, tokens, credentials — is stripped before the child process spawns.
To allow additional vars (e.g. for MCP servers that read auth from the environment):
mcp-exec env add MY_API_KEY # add to ~/.claude/settings.json
mcp-exec env add MY_API_KEY --local # add to .claude/settings.json (project-level)
mcp-exec env list # show what passes through vs what's blockedNode runtime uses vm.Context and does not spawn a subprocess, so its env exposure is limited to the parent process scope.
PreToolUse/PostToolUse hooks watching downstream tool names will not fire when those tools are called inside exec — the sandbox is opaque to the CC event system. Use tool_calls in the exec result for observability.
| Version | Status | Focus |
|---|---|---|
| v0.1 | ✅ | Node + Bash runtimes, MCP shim loader hooks, tools + exec, implicit sessions |
| v0.2 | ✅ | Generic MCP shim generator, lazy tool catalog, TypeScript SDK reference |
| v0.3 | ✅ | Python runtime via uv run --isolated, Python SDK reference, plugin polish |
| v1.0 | ✅ | Token benchmark CI suite, cross-tool compatibility (Gemini/Codex/OpenCode), Python MCP imports, env allowlist CLI |
Apply the same server-side aggregation philosophy to your own agent tool layer. Instead of returning raw query results to the agent, each tool aggregates server-side and returns a single clean structured object.
❌ Thin: agent → search_comps() → 15 raw rows → agent reasons over them
✅ Thick: agent → research_pricing(id) → { price, confidence, evidence }
See DEVELOPER.md for the full pattern and tool_calls observability details.
npm install
npm run dev # start server with tsx
npm test # vitest
npm run typecheck # tsc --noEmit
npm run lint # biome checkIssue tracking: bd ready (requires beads).
MIT
