codemode-node
# node-code-mode
[](https://github.com/a-saeedia/node-code-mode/actions/workflows/ci.yml)
One MCP tool in front of all your MCP servers. Cloudflare's [Code Mode](https://github.com/cloudflare/agents/tree/main/packages/codemode) pattern, running on Node instead of Workers.
```sh
npx -y node-code-mode --stats
```
Status: published on npm, running on my own machine. Unpolished in places, but every number below was measured on a real config.
## The problem
Every MCP server you connect gets its schemas pasted into the context on every request. From my own config, with four servers turned off:
```
8 MCP servers 113 tools 174,406 bytes ~43,602 tokens
node-code-mode 1 tool 6,524 bytes ~1,631 tokens
```
43k tokens before the model has done anything, and it grows with each server you install (`github` alone was 120 KB). The `--stats` command below measures your own config instead of trusting mine.
With the `code` tool the model writes one program per task:
```js
const files = await codemode.fs_glob({ pattern: '*/package.json' });
const stale = [];
for (const f of files) {
const pkg = JSON.parse(await codemode.fs_read({ path: f }));
if (!pkg.description) stale.push(f);
}
return { total: files.length, stale };
```
One round trip, one small object back. Everything else stays on disk and in the process.
## Install
```sh
npx -y node-code-mode
```
Claude Desktop / Claude Code:
```json
{
"mcpServers": {
"codemode": {
"command": "npx",
"args": ["-y", "node-code-mode"]
}
}
}
```
OpenCode:
```jsonc
{
"mcp": {
"codemode": {
"type": "local",
"command": ["npx", "-y", "node-code-mode"],
"enabled": true
}
}
}
```
Cursor and VS Code take the same shape (`servers` in `.vscode/mcp.json`, `mcpServers` in `.mcp.json`).
Then stop advertising the servers you don't want in context. They remain reachable through `codemode.mcp_call({ server, tool, args })`. Disabled means hidden, not gone.
## The audit
`--stats` reads your real config, boots each server, and counts what you're actually sending:
```
server tools bytes tokens
----------------------------------------------------------------------------
· higgsfield 48 182757 45689 ██████████████████████████████
github 44 120572 30143 ████████████████████
director 26 19725 4931 ███
· playwright 24 18502 4626 ███
----------------------------------------------------------------------------
TOTAL POOLED 282 441280 110320
· = not advertised (still reachable through codemode.mcp_call)
ADVERTISED: 113 tools, 174,406 bytes ≈ 43,602 tokens
WITH CODEMODE: 1 tool ≈ 1,631 tokens
You are paying ~43,602 tokens per turn for tools you did not call.
```
`--stats --json` for anything that isn't a human.
## Sandbox API
Every function takes one object. All of it hangs off a single `codemode` namespace inside the sandbox.
| group | functions |
|---|---|
| shell / files | `shell`, `fs_read`, `fs_write`, `fs_append`, `fs_exists`, `fs_stat`, `fs_list`, `fs_glob`, `fs_grep`, `fs_remove`, `fs_mkdir` |
| network | `http`, `http_json` |
| MCP | `mcp_list`, `mcp_tools`, `mcp_call` |
| Cloudflare | `cf_get`, `cf_post`, `cf_patch`, `cf_put`, `cf_del`, `cf_request` (only with `CLOUDFLARE_API_TOKEN`) |
| Moli | `moli_fetch` (only with `MOLI_BIN`) |
| globals | `sleep(ms)`, `env`, `ROOT`, `log()` |
Whatever the program returns is the only thing sent back. `console.log` is captured in `logs`, never returned to the model, so progress has to be part of the return value.
## Config
Looks for the first of these:
1. `--config <path>` or `CODEMODE_CONFIG`
2. `./.mcp.json`
3. `~/.config/opencode/opencode.jsonc`
4. Claude Desktop config, any platform
It reads `mcpServers` (Claude), `mcp` (OpenCode) and `servers` (VS Code), including JSONC comments and `{env:VAR}` interpolation.
```sh
npx -y node-code-mode --print-config
```
| env | effect |
|---|---|
| `CODEMODE_ROOT` | base directory for relative fs paths (default: home) |
| `CODEMODE_TIMEOUT_MS` | wall clock limit per call (default 120000) |
| `CODEMODE_DEBUG=1` | forward pooled-server stderr |
| `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID` | enables `cf_*` |
| `MOLI_BIN` | enables `moli_fetch` |
## Security
Each call gets a fresh Node child process that exits afterwards. The sandbox is `node:vm` with no `require`, `import`, `process` or filesystem; the proxies above are its entire capability set.
`node:vm` is not a security boundary. Escapes exist, and the proxies are real: `shell` runs commands, `fs_write` writes files, `http` opens sockets. The model can do anything you can do in a terminal, which is the same deal every coding agent offers. If that's not acceptable, run the whole thing in a container.
No credentials are read from disk. They come from the environment.
## Numbers
Same task both ways, three runs, median, Windows 11 / Node 22:
| | round trips | bytes to the model | tokens | wall time |
|---|---:|---:|---:|---:|
| 5 tool calls | 5 | 41,812 | 10,453 | 2,240 ms |
| 1 `code` call | 1 | 145 | 36 | 2,733 ms |
About half a second slower locally, 288× less context. Each skipped round trip is a full model call, so on a real task the difference is seconds, not milliseconds.
## Limitations
- No streaming. One result at the end; long jobs look like a hang.
- Plain JavaScript. No TypeScript, no imports.
- Cold start around 1.5 s, then 250 to 500 ms per call from process spawn and IPC.
- stdio on the server side only. Upstream servers can be stdio or HTTP.
- Pinned to `@cloudflare/codemode@0.5.5`. Upstream is Workers-first and a minor bump can add a `cloudflare:workers` import that Node cannot load ([#2142](https://github.com/cloudflare/agents/issues/2142)). Upgrade deliberately and run the tests.
## Prior art
`@cloudflare/codemode` supplies the type generation, tool description, result unwrapping and truncation. This repo supplies the executor and the server. The protocol idea comes from [UTCP's code-mode](https://github.com/universal-tool-calling-protocol/code-mode), which is more ambitious and framework-agnostic.
MIT.
TDQS
Scored across 1 tool
There is only one MCP tool, so an agent cannot misselect between tools. The only ambiguity is inside the exposed runtime API, where `shell` can do everything the `fs_*` methods and even `http` (via curl) can do, so multiple paths exist for the same task.
With a single tool there is no cross-tool pattern to violate, and the embedded API uses predictable snake_case with `fs_`/`http` prefixes for most methods. The outliers are bare names like `shell` and `http`/`http_json`, which break the prefix convention slightly but remain obvious.
A single entry point is the deliberate design of a code-mode server, and it is far from trivial—it encapsulates shell, filesystem, and HTTP capability. The tradeoff is that all functionality is compressed behind one untyped-looking call, so per-capability discovery relies entirely on reading the description.
The surface covers the full file lifecycle (write/read/append/exists/stat/list/glob/grep/mkdir/remove) plus shell and HTTP, with `shell` acting as a universal escape hatch for anything missing. Minor gaps exist for rename/copy, binary file handling, and line-level edits, all of which must be hand-rolled via read+write or shell.