Skip to main content
Glama

node-code-mode

CI

One MCP tool in front of all your MCP servers. Cloudflare's Code Mode pattern, running on Node instead of Workers.

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:

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.

Related MCP server: CodeForge MCP

Install

npx -y node-code-mode

Claude Desktop / Claude Code:

{
  "mcpServers": {
    "codemode": {
      "command": "npx",
      "args": ["-y", "node-code-mode"]
    }
  }
}

OpenCode:

{
  "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.

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). 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, which is more ambitious and framework-agnostic.

MIT.

Available Tools

1 tool
codeA

Execute one JavaScript program that achieves the whole goal. This replaces chains of separate tool calls: one call here does the work of N, and intermediate data never enters your context.

Available: type ShellInput = { cmd: string; cwd?: string; timeout_ms?: number; } type ShellOutput = unknown type FsReadInput = { path: string; } type FsReadOutput = unknown type FsWriteInput = { path: string; content: string; } type FsWriteOutput = unknown type FsAppendInput = { path: string; content: string; } type FsAppendOutput = unknown type FsExistsInput = { path: string; } type FsExistsOutput = unknown type FsStatInput = { path: string; } type FsStatOutput = unknown type FsListInput = { dir?: string; } type FsListOutput = unknown type FsGlobInput = { pattern: string; cwd?: string; limit?: number; } type FsGlobOutput = unknown type FsGrepInput = { pattern: string; cwd?: string; include?: string; limit?: number; ignore_case?: boolean; } type FsGrepOutput = unknown type FsRemoveInput = { path: string; } type FsRemoveOutput = unknown type FsMkdirInput = { path: string; } type FsMkdirOutput = unknown type HttpInput = { url: string; method?: string; headers?: { [key: string]: string; }; body?: string; timeout_ms?: number; } type HttpOutput = unknown type HttpJsonInput = { url: string; method?: string; headers?: { [key: string]: string; }; body?: string; timeout_ms?: number; } type HttpJsonOutput = unknown

declare const codemode: { /** * Run a PowerShell (Windows) or POSIX shell command. Returns {code,stdout,stderr,ms}. */ shell: (input: ShellInput) => Promise;

/**
 * Read a UTF-8 text file. Relative paths resolve under the sandbox ROOT.
 */
fs_read: (input: FsReadInput) => Promise<FsReadOutput>;

/**
 * Write a file (creates parent dirs).
 */
fs_write: (input: FsWriteInput) => Promise<FsWriteOutput>;

/**
 * Append to a file.
 */
fs_append: (input: FsAppendInput) => Promise<FsAppendOutput>;

/**
 * True if the path exists.
 */
fs_exists: (input: FsExistsInput) => Promise<FsExistsOutput>;

/**
 * Size / dir / mtime.
 */
fs_stat: (input: FsStatInput) => Promise<FsStatOutput>;

/**
 * List a directory.
 */
fs_list: (input: FsListInput) => Promise<FsListOutput>;

/**
 * Glob files under cwd (forward slashes, *, ?). Skips node_modules/.git/dist.
 */
fs_glob: (input: FsGlobInput) => Promise<FsGlobOutput>;

/**
 * Regex search under cwd. Returns ["file:line: text"].
 */
fs_grep: (input: FsGrepInput) => Promise<FsGrepOutput>;

/**
 * Delete a file or directory tree.
 */
fs_remove: (input: FsRemoveInput) => Promise<FsRemoveOutput>;

/**
 * Create a directory tree.
 */
fs_mkdir: (input: FsMkdirInput) => Promise<FsMkdirOutput>;

/**
 * HTTP request. Returns {status, ok, headers, text, ms}.
 */
http: (input: HttpInput) => Promise<HttpOutput>;

/**
 * HTTP request parsed as JSON. Returns {status, ok, json}.
 */
http_json: (input: HttpJsonInput) => Promise<HttpJsonOutput>;

}

Rules:

  • Write an async arrow function that returns the final value. Do NOT use TypeScript syntax.

  • Use forward slashes in paths. A backslash inside a JS string is an escape ("\p" becomes "p") and silently corrupts the path.

  • Return a small JSON-able summary. Anything you do not return is discarded, which is the point: filter and aggregate inside the code, not in the conversation.

  • console.log output is captured host-side but is NOT returned — collect progress in an array and return it.

  • Loop freely: many files, many URLs, many tool calls per execution.

  • Default timeout 120s (CODEMODE_TIMEOUT_MS). Errors throw; catch them inside the code if you want to continue.

Example: async () => { const r = await codemode.shell({ cmd: "...", cwd: "...", timeout_ms: 0 }); return r; }

ParametersJSON Schema
NameRequiredDescriptionDefault
codeYesJavaScript async arrow function to execute

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden and does so: default 120s timeout with the CODEMODE_TIMEOUT_MS override, errors throw rather than return, console.log is captured but not returned, unreturned data is discarded, relative paths resolve under a sandbox ROOT, and glob skips node_modules/.git/dist. The return shapes for each helper (shell → {code,stdout,stderr,ms}, http → {status,ok,headers,text,ms}) are spelled out inline.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The value proposition and execution rules are front-loaded, and the bullet rules each carry a distinct, non-obvious constraint. It is long, and the helper API is presented twice (TypeScript type blocks plus the declare const with JSDoc), which is mild redundancy, though the second listing adds return shapes and path semantics.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, and the description compensates by explaining what is returned versus discarded. Combined with the enumerated helper surface and the timeout/error model, an agent has everything needed to write a correct call on the first attempt.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% for the single 'code' parameter, so the baseline is a 3-4, and the description exceeds it by specifying the required shape: an async arrow function returning the final value, no TypeScript syntax, forward slashes only because backslashes are escape sequences. The concrete example anchors the expected form.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence states a specific verb and resource ('Execute one JavaScript program') and frames the scope as 'achieves the whole goal,' which tells an agent exactly what unit of work this tool covers. The follow-up contrast with 'chains of separate tool calls' pins down the abstraction level unambiguously.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It clearly indicates when to prefer this over sequential tool calls ('one call here does the work of N, and intermediate data never enters your context') and gives operational guidance such as looping freely and catching errors inside the code. It stops short of stating when not to use it, e.g. a single trivial operation where a direct tool would be simpler.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev0.1.0
    • First observedcode

TDQS

A4.4/5.0

Scored across 1 tool

Disambiguation4/5

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.

Naming Consistency4/5

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.

Tool Count4/5

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.

Completeness4/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Universal MCP server for executing TypeScript and Python code with progressive disclosure, reducing token usage by 98% by enabling on-demand access to all other MCP tools through code execution rather than loading tool definitions directly.
    37 npm
    129
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A self-hosted MCP server that provides a single execute_code tool, enabling agents to write TypeScript to call multiple REST APIs via fetch() with transparent credential injection, reducing token usage by keeping intermediate results in the sandbox.
    11
    BSD 3-Clause
  • A
    license
    A
    quality
    A
    maintenance
    Code-mode MCP server (docs_search + execute_code two-tool surface) backed by a unified capability manifest across three sandbox kernels (in-process node:vm, WASM via QuickJS / Pyodide / Wasmtime, and remote microVM via E2B / Cloudflare Sandbox). At N=30 tools the bootstrap-context cost drops to 13.6% of direct tool-use. Apache-2.0.
    2
    6
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Fronts every MCP server you have with two tools: search shortlists relevant tools, and execute runs sandboxed Python that calls upstream tools as mcp..(). It enables agents to make bulk, repeated, or conditional tool calls using calibrated decision-model choices instead of per-item LLM generation.
    3
    MIT