# agent-codemode [![npm](https://img.shields.io/npm/v/agent-codemode)](https://www.npmjs.com/package/agent-codemode) [![license](https://img.shields.io/npm/l/agent-codemode)](./LICENSE) [![node](https://img.shields.io/node/v/agent-codemode)](https://nodejs.org) **Code mode for the coding agent you already run — 40 tool calls in, 1 script out.** Your coding agent has already logged into every MCP server you use. This lets it stop calling them one tool at a time and write a script instead. *[Why I built it, in a thread](https://x.com/janwilmake/status/2089730708713472326).* ![the same task, both ways](./docs/cost.gif) ## What it costs One real prompt — *fetch every In Progress Linear ticket with its full body and count how many times "mcp" appears* — measured both ways against a live workspace of 39 tickets: | | into the model | round trips | | --- | --- | --- | | as tool calls | 262,159 chars (~65,500 tokens) | 40, in sequence | | as one script | 903 chars (~226 tokens) | 1 | **290× less into context, 99.66% saved** — and that is the floor, because in a tool-call loop those 262,159 characters are re-read on every subsequent turn, while the script pays once. The round-trip column is the one you feel. Forty sequential calls each wait for a model to decide what to ask next; the script makes the same forty requests without stopping to think between them, so it finishes in the time the tool-call version spends on its first few. And the answer is exact. Counting occurrences across 262,159 characters by reading them is something a model does approximately — the tool-call path would spend the 65,500 tokens and still be liable to return a wrong number. Character counts are exact; token figures use the rough four-characters-per-token heuristic. Your own numbers will differ with your data — the ratio is the durable part. ### Who this is for The first user is the coding agent itself. Ask it to reconcile two trackers today and it does twenty tool calls, paying for every intermediate result twice — once into the model, once back out — and burning context on data it only needed in order to hand to the next call. Give it this and it writes one script: a loop, a filter, a join, and one answer read back at the end. That is code mode in the plain sense. The model writes code, the code calls the tools. The script it leaves behind is the second win, and the one that compounds. It is a file. It runs again tomorrow from cron, or as a CI gate, with no model and no tokens at all — which is also the answer to the reasonable objection that code mode on top of MCP on top of code mode is a useless extra layer. This is not a layer. It is the model *leaving*. A job that moves three tickets does not need reasoning; it needs the token, and the token is already there. ## Quickstart ```bash npm install -g agent-codemode # also installs a shorter `codemode` alias ``` ```bash agent-codemode servers # who has a live token agent-codemode tools linear # what can it do agent-codemode call linear list_issues --arg assignee=me --arg state="In Progress" --text agent-codemode types --all # typed TypeScript for every server you're logged into ``` No API keys. No OAuth flow. No sandbox to deploy. No model in the loop. Requires Node 18+. ![agent-codemode in a terminal](./docs/demo.gif) ## Give it to your agent This is the main way to use it, so do this first. The repo ships a skill at [`.claude/skills/agent-codemode/`](./.claude/skills/agent-codemode/SKILL.md). Copy it into your project, or into `~/.claude/skills/` to have it everywhere: ```bash mkdir -p ~/.claude/skills && cp -r .claude/skills/agent-codemode ~/.claude/skills/ ``` An agent that knows this exists writes one script. An agent that doesn't keeps doing what it knows, one tool call at a time. The skill is what turns 262,159 characters into 903. ## The typed API `agent-codemode types` reads a server's live `tools/list` and emits a `.ts` module — every tool as a method, every input schema as an interface, every description as JSDoc: ```ts // Generated by agent-codemode from linear's tools/list. Do not edit by hand. export interface LinearListIssuesArgs { /** Max results (default 50, max 250) */ limit?: number; /** State type, name, or ID */ state?: string; /** User ID, name, email, or "me" */ assignee?: string | null; /** 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low */ priority?: number; } export interface LinearClient { listIssues(args?: LinearListIssuesArgs): Promise; // …52 more } ``` Each generated module also registers itself with `mcp`, so importing it is all it takes — there is no cast and no type argument at the call site: ```ts import "./mcp-types"; // side-effect import; registers every server import { mcp } from "agent-codemode"; const issues = await mcp.linear.listIssues({ assignee: "me", state: "In Progress" }); ``` ```ts mcp.linear.listIssues({ bogus: 1 }); // ✗ 'bogus' does not exist in LinearListIssuesArgs mcp.linear.listIssues({ limit: "fifty" }); // ✗ string is not assignable to number mcp.linear.noSuchTool({}); // ✗ does not exist on LinearClient ``` Those types cost your agent nothing at runtime — they live in your editor, not in a context window. And they stay optional: without the import, `mcp.linear.listIssues({ … })` still runs, just with `Record` arguments. A server you never generated types for keeps working. ## One script, three servers, zero setup Plenty of tools will call three MCP servers from one script. What none of them skip is the credential step: you either bring an API key, or complete an OAuth flow into that tool's own store, before the first call works. Every server below authenticated itself once, through your coding agent, and this borrows that. There is no `.env` in this example because there is nothing to put in it, and no login to run first: ```ts import { mcp } from "agent-codemode"; const [issues, events, channel] = await Promise.all([ mcp.linear.listIssues({ assignee: "me", limit: 50 }), mcp.axiom.queryDataset({ apl: "['prod'] | where _time > ago(24h) | summarize count()" }), mcp.slack.slackSearchChannels({ query: "general" }), ]); ``` ``` linear 49 open · 12 In Progress · 36 In review · 1 Planned axiom 24,385,957 events in sample-http-logs / 24h slack #general → C06CS3MUQN9 *Standup* — 49 open, 18 high priority 24,385,957 events in the last 24h. Watch: HYR2-948, HYR2-947, HYR2-881, HYR2-625, HYR2-923 (+13 more) ``` The runnable version is [`examples/standup.ts`](./examples/standup.ts) — it reads three servers, prints the digest, and only posts to Slack behind `--post`: ```bash npx tsx examples/standup.ts ``` ## How this differs from the other code modes | | [Anthropic PTC](https://www.anthropic.com/engineering/advanced-tool-use) | [Cloudflare Code Mode](https://github.com/cloudflare/agents/tree/main/packages/codemode) | TanStack AI | [MCPorter](https://github.com/openclaw/mcporter) | **agent-codemode** | | --- | --- | --- | --- | --- | --- | | Where the code runs | Anthropic's sandbox | Worker sandbox | your app | your shell | **your shell** | | Needs a model | yes | yes | yes | no | **no** | | Needs a deploy | no | yes | no | no | **no** | | Several servers in one script | yes | yes | yes | yes | yes | | Typed clients | Python functions | yes | yes | yes (`emit-ts`) | yes (`types --all`) | | Credentials | your API keys | your API keys | your API keys | its own vault | **your agent's, inherited** | | Setup before the first call | opt each tool in, relay results | deploy a Worker | wire up the SDK | `mcporter auth` per server | **none** | **Be clear about what is and isn't new here.** Multi-server scripts are not new — Cloudflare's sandbox takes bindings for every server it's connected to, [VoidMCP](https://github.com/voidmind-io/voidmcp) orchestrates several in one WASM runtime, and `mcp-use` and LangChain's `MultiServerMCPClient` have done it for a while. Typed clients are not new either. Reading other tools' MCP config is not new: MCPorter imports server definitions from Cursor, Claude, Codex, Windsurf, OpenCode and VS Code. The one thing that is different is the row second from the bottom. Every other tool has a credential step — bring an API key, or run its OAuth flow into its own store. MCPorter, the closest neighbour, reads Claude Code's server *definitions* but keeps its own vault at `~/.mcporter/credentials.json` and makes you authenticate again. This reads the token your agent already minted, so there is no step at all: `npm install`, then call. That is a narrow difference. It is also the whole reason the script gets written — an agent mid-task will reach for something that works right now, and not for something that first needs you to go and complete an OAuth flow. ## Prior art Code mode is not my idea. [Kenton Varda and Sunil Pai named it](https://blog.cloudflare.com/code-mode/) at Cloudflare in September 2025, resting the whole pattern on one observation: > LLMs are better at writing code to call MCP, than at calling MCP directly. The follow-up — [give agents an entire API in 1,000 tokens](https://blog.cloudflare.com/code-mode-mcp/) — showed how far that goes: 2,500 Cloudflare endpoints behind `search()` and `execute()`, at a fixed context cost. [Matt Carey](https://github.com/mattzcarey) builds the [Code Mode SDK](https://www.npmjs.com/package/@cloudflare/codemode) that packages it, and his `codeMcpServer` — wrap a server, get one code tool, every upstream tool a typed method — is the shape this codegen imitates. Anthropic ships the same pattern inside the API as [Programmatic Tool Calling](https://www.anthropic.com/engineering/advanced-tool-use): the model writes Python in a sandbox, the code calls the tools, and only the final result comes back into context — 37% fewer tokens on their research tasks, and a small accuracy gain on top, which is the "the answer is exact" argument above with numbers attached. It is the API-side sibling of this: for people building on the platform, with tools they wire up and results they relay into the sandbox themselves. This is the same idea for the coding agent you already run, with credentials it already has, and a script that outlives the session. [MCPorter](https://github.com/openclaw/mcporter) reached the runtime-and-CLI-for-MCP idea first, and does more than this does. What is left for this package is small, and it is the last mile: the credentials are already sitting on your machine. Discussion: [the launch thread](https://x.com/janwilmake/status/2089730708713472326). ## Why An MCP server is a JSON-RPC endpoint. `claude mcp` can add, list and log in to servers — it cannot *call* them. So the OAuth dance is already done and the result sits unused, reachable only by a model deciding to reach for it, one tool call at a time. A script can talk to those servers directly. That is what you want any time paying a model to relay a request is slow and expensive: while the agent works, because a written script beats twenty round trips, and after it stops, because the script keeps running from cron or CI on its own. ## What works Verified end to end against a live install: | Kind | Auth source | Example | Status | | --- | --- | --- | --- | | Remote HTTP MCP (OAuth) | Keychain | `linear`, `axiom`, `fastmail` | ✅ works | | Self-hosted remote MCP | Keychain | `hyre-prod`, `hyre-staging` | ✅ works | | Plugin MCP | Keychain | `plugin:slack:slack` | ✅ works | | API-key HTTP/SSE MCP | config `headers` | `{ "type": "http", "headers": { … } }` | ✅ works | | stdio MCP (local subprocess) | config `env` | `{ "command": "npx", … }` | ✅ works | | claude.ai connector | claude.ai backend | `claude.ai Gmail`, `Google Calendar` | ❌ not yet — see below | macOS is fully supported. On Linux/Windows the config-based servers (stdio, API-key) work as-is, and Claude's OAuth tokens are read from `~/.claude/.credentials.json`; the macOS Keychain path is skipped. Help verifying Linux/Windows is welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md). **It reads other coding clients too.** The config files scanned aren't only Claude's — Cursor, Windsurf, VS Code, and Gemini CLI keep MCP servers in the same schema, and this reads them all (`agent-codemode servers` shows the `client` each came from). So it inherits every client's **stdio and API-key** servers. OAuth inheritance is Claude-only for now — other clients' OAuth servers show as `unsupported` and are easy to add. Adding a client's config is one entry in `src/clients.ts`. **claude.ai connectors** are configured under a `claude.ai config` scope and have no entry in the `mcpOAuth` store; they authenticate through the account-level `claudeAiOauth` token, and the connector endpoint rejects any client whose name contains "claude". Supporting them would mean impersonating Claude Code itself — out of scope on purpose. ## How it works Claude Code stores what a server needs in two places, and this package reads both: - **The macOS Keychain**, service `Claude Code-credentials`. Its `mcpOAuth` map is keyed `|` and each entry carries `serverUrl`, `accessToken`, `refreshToken`, `clientId`, `issuer` and `expiresAt`. This is the OAuth HTTP servers. - **The config files**, `~/.claude.json` (top-level `mcpServers` and per-project) and `.mcp.json`. A stdio server keeps its `command`, `args` and `env` here; an API-key server keeps its `url` and static `headers` here. `${VAR}` references in those fields are expanded from the environment, as Claude Code does. For an HTTP server it speaks Streamable HTTP MCP — `initialize`, carry the returned `Mcp-Session-Id`, `notifications/initialized`, then `tools/list` or `tools/call`, accepting a JSON or an SSE response. For a stdio server it spawns the subprocess and speaks the same JSON-RPC newline-framed over stdin/stdout. One `McpSession` interface covers both. ## Three rules, learned the hard way 1. **Never cache the token.** Claude Code refreshes on its own schedule. This package re-reads the Keychain on every call, and so should you. 2. **Never try to refresh it yourself.** The `refreshToken` and `clientId` are right there, but Claude Code re-runs dynamic client registration on launch and orphans the previous pair ([claude-code#59460](https://github.com/anthropics/claude-code/issues/59460)), so your refresh may be rejected by a client the issuer has forgotten. If a token has expired, the fix is to start Claude Code, or `claude mcp login `. 3. **An expired token is a loud error, never an empty result.** A gate that silently returns nothing because auth broke is indistinguishable from a gate that found no work — which is the failure that costs you a night. ## Security Tokens are read fresh and never printed; this package only ever *reads* credentials, and never writes, refreshes or transmits them anywhere except to the server they belong to. There is one thing about your machine worth knowing before you use this, and it is true whether or not you install it. See **[SECURITY.md](./SECURITY.md)**. ## CLI ``` agent-codemode servers list every server (config files + Keychain) agent-codemode tools list a server's tools agent-codemode call [args] call a tool agent-codemode types [--out file] generate a .ts types module for a server agent-codemode types --all [--out dir] generate a module per authed server + index barrel agent-codemode types --url [--name n] generate for a raw endpoint (unauthenticated tools/list) --json '{"a":1}' whole argument object as JSON --arg key=value one string argument --arg key:=1 one JSON-valued argument (number, bool, array, object) --text print only the text content of the result ``` Server names can be shortened when unambiguous: `agent-codemode call slack …` reaches `plugin:slack:slack`. Exit codes: `0` success, `1` error (credential, transport, usage), `2` the tool itself reported `isError`. ## API ```ts import { mcp, McpClient, callTool, listTools, resultText, listCredentials, getCredential, isExpired, } from "agent-codemode"; // the proxy — no codegen needed const issues = await mcp.linear.listIssues({ assignee: "me" }); // one-shot const tools = await listTools("linear"); // or hold a session open across several calls const client = await McpClient.fromClaudeCode("linear"); await client.connect(); const a = await client.callTool("list_issues", { team: "Hyre Ops" }); const b = await client.callTool("list_teams", {}); ``` Set `MCP_PROTOCOL_VERSION` to advertise a different protocol revision. ## Licence MIT © Jan Wilmake