Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

85 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mcp-exec — reduce your token usage by as much as 99%

npm version node platform License: MIT works with docs contributing Discord

Implementation of "Code execution with MCP: building more efficient AI agents" — Anthropic Engineering, Nov 2025. The canonical reference for this pattern.

mcp-exec demo — rate limit pain vs exec fix

Install →    How it works →


52,000 tokens → 50 tokens.

Discord Join Discord

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.


You've been here.

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.


Before / After

Before and after token comparison


How it works

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.

Architecture diagram

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

Token savings

Token savings by workflow type


Scenarios

Overdue invoice triage — 27,000 → 80 tokens

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: ~80

Morning standup brief — 20,000 → 60 tokens

Without 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: ~60

Research loop — 40,000 → 55 tokens

Without 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: ~55

Stateful multi-step — data loaded once, queried many times

Session 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}, ...]

Python — data analysis with pandas

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, ...}

Single-tool filtering — control what enters context

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 tokens

The 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.


Reproducible case study

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).


Installation

Supported agents: Claude Code · Cursor · Windsurf · GitHub Copilot · Gemini CLI · Codex CLI · Cline · OpenCode


Claude Code

📖 Claude Code MCP docs

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-exec

Then 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)

Cursor

📖 Cursor MCP docs

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.

Windsurf

📖 Windsurf MCP docs

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.


GitHub Copilot

📖 VS Code MCP docs

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.

Gemini CLI

📖 Gemini CLI MCP docs

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 — use mcp-exec, not mcp_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.

Codex CLI

📖 Codex CLI MCP docs

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.

Cline

📖 Cline MCP docs

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.

OpenCode

📖 OpenCode plugin docs

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.js

Skills are auto-discovered. The using-mcp-exec skill is injected into the first user message automatically — no manual priming needed.


Skills

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

Requirements

  • 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)

Reference

tools(query)

tools("*")                    // all tools across all connected servers
tools("search emails")        // substring match across name + description
tools('"pull request"')       // exact phrase match

Returns { server, name, description, signature }[] — trimmed summaries, no full schemas.

exec(params)

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.

Session state

Implicit session per conversation (Node only). Explicit session_id for parallel isolation. Sessions expire after 10 minutes idle. Maximum 100 concurrent sessions.

Error handling

const { result } = await exec({ runtime: "node", code: `...` });
if (typeof result === 'object' && result !== null && 'error' in result) {
  const { error, line, column } = result;
}

When NOT to use mcp-exec

  • 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

Security

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 blocked

Node runtime uses vm.Context and does not spawn a subprocess, so its env exposure is limited to the parent process scope.

Plugin compatibility

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.

Roadmap

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

For app developers

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.

Development

npm install
npm run dev         # start server with tsx
npm test            # vitest
npm run typecheck   # tsc --noEmit
npm run lint        # biome check

Issue tracking: bd ready (requires beads).

License

MIT

About

Reference implementation of Anthropic's 'Code execution with MCP.' Reuses Claude Code's own sandbox and config to keep intermediate tool output out of the context window on every workflow — up to 99% fewer tokens.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages