FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

janwilmake/agent-codemode: Let the scripts your coding agent writes call your connected MCP servers (no extra auth) · GitHub

Repository files navigation

agent-codemode

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.

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

npm install -g agent-codemode    # also installs a shorter `codemode` alias
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+.

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/. Copy it into your project, or into ~/.claude/skills/ to have it everywhere:

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:

// 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<ToolResult>;
  // …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:

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" });
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<string, unknown> 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:

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 — it reads three servers, prints the digest, and only posts to Slack behind --post:

npx tsx examples/standup.ts

How this differs from the other code modes

Anthropic PTC Cloudflare Code Mode TanStack AI 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 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 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 — showed how far that goes: 2,500 Cloudflare endpoints behind search() and execute(), at a fixed context cost. Matt Carey builds the Code Mode SDK 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: 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 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.

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.

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 <serverName>|<urlHash> 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), 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 <server>.
  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.

CLI

agent-codemode servers                       list every server (config files + Keychain)
agent-codemode tools <server>                list a server's tools
agent-codemode call <server> <tool> [args]   call a tool
agent-codemode types <server> [--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 <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

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

About

Let the scripts your coding agent writes call your connected MCP servers (no extra auth)

Topics

Resources

Contributing

Security policy

Stars

27 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages


Back | FazBrowse Home | New Git URL