@nothumanwork/codemode
v0.1.2
Published
Run strictly typed code against configured MCP servers
Maintainers
Readme
codemode
codemode exposes configured MCP servers to ordinary JavaScript or TypeScript through a typed library. Its optional CLI can also generate exact types from each server's JSON Schemas, strictly check a complete program, and then execute it with Bun.
Codemode requires Bun 1.3 or newer. Run the published CLI without creating a package.json or installing it locally:
bunx --package @nothumanwork/[email protected] codemode --helpFrom this checkout:
bun install --frozen-lockfile
bun ./src/cli.ts --helpThe package bin is named codemode and points to the bundled dist/cli.js. The examples below assume that bin is on PATH; from this checkout, replace codemode with bun ./src/cli.ts.
Compile one standalone executable
To produce a single executable containing Bun, codemode, and its dependencies:
bun run compile
./dist/codemode --helpThe output is dist/codemode on macOS and Linux or dist/codemode.exe on Windows. It does not require Bun or node_modules at runtime. The compile workflow embeds the Bun runtime plus the Bun, Node, and TypeScript standard-library declarations that the strict runtime checker needs when it validates scripts.
On macOS, Bun 1.4 may leave an invalid embedded signature after attaching the standalone payload, causing the kernel to kill the executable before it reaches --help. The compile script ad-hoc signs the completed Mach-O and verifies that signature before reporting success. Ad-hoc signing is sufficient for local execution; distributing a trusted macOS binary still requires the appropriate Developer ID signing and notarization workflow.
Quick start: run an ordinary typed MCP script
The library loads the configured MCP servers, discovers their tools, makes the requested call, and closes its MCP connections. It does not require a local package.json, an init step, generated declarations, or the codemode command.
Point codemode at an MCP configuration. If this variable is omitted, codemode uses
~/.pi/agent/mcp.json.export MCP_SERVER_CONFIG_PATH=/absolute/path/to/mcp.jsonEnsure that the configuration names the server you want to call. This minimal example connects to a Streamable HTTP server as
mmr:{ "mcpServers": { "mmr": { "url": "http://127.0.0.1:3000/mcp" } } }Create
check-status.ts. Importing the package root provides editor types for the stablecall_toolandsearch_toolsAPI without an init command or generated declaration:import { call_tool, search_tools } from "@nothumanwork/codemode"; const hits = await search_tools("workspace status"); const result = await call_tool("mmr_status", { source: "codex", project: "codemode", }); if (result.isError) { console.error(result.content); process.exitCode = 1; } else { console.log(JSON.stringify(result.structuredContent, null, 2)); }Run the script:
bun run ./check-status.ts
When no local install exists, Bun resolves the bare package import without requiring a project manifest. Each direct search_tools or call_tool invocation discovers the current catalog from the configured servers, performs the operation, and closes all connections before resolving. The script is responsible for printing any result.
The published package can type only the stable API because each user's MCP tools come from local configuration. To make those configuration-specific tool names, inputs, and outputs visible in the editor and strictly check them before execution, use the optional CLI: run codemode init ./check-status.ts, generate a sidecar with codemode generate --out ./check-status.codemode.d.ts, or execute the script with codemode run ./check-status.ts.
For a quick stdin program:
printf '%s\n' 'const result = await call_tool("mmr_status", { source: "stdin" });' 'console.log(result.structuredContent);' | codemode run - --loader tsThe call syntax is independent of configured server names, including names that are not valid JavaScript identifiers.
An ordinary library script may use normal imports and exports. The stricter codemode run workflow accepts the Codemode package root through its bare, npm:, or jsr: forms, but rejects other static imports and all exports. Neither mode is a sandbox; scripts retain their normal filesystem, environment, process, and network authority.
Agent discovery and skill
For a large catalog, prefer lexical tool search before dumping the full inventory:
codemode search "workspace status"
codemode search "billing" --json --limit 10search loads the current artifact generation (regenerating when missing or stale), ranks tools with BM25F over name, description, parameter names/descriptions, and server name, and returns enough detail to call a match: name, server, optional description, score, full inputSchema, and a safe call_tool(...) snippet. Scores are corpus-relative implementation data; ranking and deterministic ties are the compatibility contract. Search is lexical only: it does not provide embeddings or semantic matching, stemming, fuzzy matching, synonym expansion, or live server search. Search is local after a fresh artifact is available and does not contact MCP servers. Regeneration contacts all configured servers the same way as generate/inspect. Remote catalog changes without a config edit still require codemode generate or a daemon-triggered refresh.
Use inspect when you need the complete catalog:
codemode inspectinspect prints JSON describing the MCP abstractions codemode currently accepts. It ensures a current artifact generation exists, then returns the resolved config path, artifact and declaration identifiers, and a sorted servers array. Each server includes its configured name and the persisted callable definition for each tool: name, optional description, inputSchema, and optional outputSchema. Pass the exact tool name to call_tool.
For example, the inventory shape is:
{
"schemaVersion": 2,
"configPath": "/absolute/path/to/mcp.json",
"artifactId": "...",
"declarationPath": "/absolute/cache/path/mcp.d.ts",
"servers": [
{
"name": "mmr",
"tools": [
{
"name": "mmr_status",
"description": "...",
"inputSchema": { "type": "object", "properties": {} },
"outputSchema": { "type": "object", "properties": {} }
}
]
}
]
}Transport configuration, headers, environment values, internal config fingerprints, and non-callable tool metadata such as _meta, icons, annotations, and execution hints are not included. inspect reuses a fresh generation; after a server changes its tools without a config change, run codemode generate before inspecting again to force live discovery.
The CLI also carries a self-contained agent skill:
codemode skillThis prints skills/codemode/SKILL.md, including the discovery workflow, binding rules, result handling, and runtime safety constraints. The Markdown is embedded in a standalone executable. To install it for an agent that discovers skills from a directory:
mkdir -p ~/.codex/skills/codemode
codemode skill > ~/.codex/skills/codemode/SKILL.mdcodemode skill does not read the MCP configuration or connect to any server.
Configure MCP servers
By default, codemode reads ~/.pi/agent/mcp.json. Set MCP_SERVER_CONFIG_PATH to use another file. Relative paths are resolved from the current working directory, and a leading ~/ is expanded.
The file must have one mcpServers object. A server selects exactly one transport by defining either command (stdio) or url (Streamable HTTP); do not add a transport field.
{
"mcpServers": {
"mmr": {
"command": "bun",
"args": ["/absolute/path/to/mmr-server.ts"],
"cwd": "/absolute/path/to/project",
"env": {
"MMR_TOKEN": "replace-me"
},
"lifecycle": "lazy",
"timeout": 10000
},
"remoteMmr": {
"url": "http://127.0.0.1:3000/mcp",
"headers": {
"Authorization": "Bearer replace-me"
},
"lifecycle": "eager",
"timeout": 15000
}
}
}Stdio entries accept command, plus optional string-array args, string cwd, string-valued env, lifecycle, and timeout. HTTP entries accept url, plus optional string-valued headers, lifecycle, and timeout. Unknown fields are rejected. lifecycle is lazy by default or eager; eager servers connect when a runtime client manager starts, while lazy servers connect on first use. Schema discovery still contacts every configured server. timeout is a positive integer in milliseconds and applies to connection, tool listing, and tool calls; the default is 10 seconds.
Configured stdio environment values are added to the MCP SDK's default child environment. HTTP headers are sent on transport requests. Treat both as credentials. Static HTTP configuration cannot override the SDK-owned Mcp-Session-Id, Mcp-Protocol-Version, Mcp-Method, or Mcp-Name headers. On shutdown, codemode asks a Streamable HTTP server to terminate its session before closing the transport; HTTP 405 remains an allowed server response under the MCP contract.
Generate and locate types
codemode generate
codemode generate --out ./check-status.codemode.d.ts
codemode types-pathgenerate connects to all configured servers, lists their tools, and atomically publishes one complete artifact generation. It prints the generated declaration path. With --out, it also atomically copies the declaration to a stable path suitable for a script's triple-slash reference and prints that resolved output path. types-path does not contact servers or generate anything; it validates the current artifact against the config path, config contents, generator version, installed MCP SDK version, and artifact hashes, then prints its declaration path. It fails if no valid current generation exists.
Artifacts are isolated by a 24-character SHA-256 prefix derived from the normalized config path:
${CODEMODE_CACHE_DIR:-${XDG_CACHE_HOME:-~/.cache}/codemode}/<config-key>/
current.json
generation-status.json
generations/<artifact-id>/
manifest.json
mcp.d.tsCODEMODE_CACHE_DIR replaces the entire default cache root. Relative cache paths are resolved from the current working directory. The printed mcp.d.ts path is generation-specific and can change after regeneration, so consumers should ask types-path again rather than retain it indefinitely.
Generation is all-or-nothing. A successful generation moves current.json to the new content-addressed files and records stale: false. A discovery or schema-generation failure preserves the previous generation and records a redacted stale: true status when generation has begun. A configuration file that cannot be read or parsed fails before that generation status can be written.
codemode run uses a current artifact when it matches the config. If it is missing or stale because the config path/content, generator, SDK, or last generation outcome changed, run regenerates synchronously and fails if it cannot obtain current schemas. Codemode does not poll remote tool schemas: after a server changes its tools without a config change, run codemode generate (or rewrite/touch the config while the daemon is running) before relying on new types. A failed refresh leaves the last-good files and pointer in place for recovery, but run and types-path reject them until a generation succeeds.
Run code with the strict CLI
File input infers js, jsx, ts, or tsx from the filename (.mts and .cts are TypeScript):
codemode run ./check-status.ts
codemode run ./check-status.txt --loader tsUse - for stdin. Stdin defaults to JavaScript, so pass --loader ts when it contains TypeScript syntax:
printf '%s\n' 'const result = await call_tool("mmr_status", { source: "stdin" });' 'console.log(result.structuredContent);' | codemode run - --loader tsPrograms may use top-level await. Named imports of call_tool and search_tools from the package root are checked and erased before execution:
import { call_tool, search_tools as search } from "@nothumanwork/codemode";Within codemode run, versioned npm and JSR specifiers allow a self-contained script without a local package.json; editor support for these specifiers depends on the corresponding runtime extension:
import { call_tool } from "npm:@nothumanwork/[email protected]";
import { call_tool } from "jsr:@nothumanwork/[email protected]";Use only one spelling in a script. The package supplies useful baseline input and MCP result types immediately. A generated *.codemode.d.ts is optional and adds exact, configuration-specific tool names and schemas that no registry package can know in advance.
The bindings may be aliased. Other static imports and all exports, including export modifiers and export assignments, are rejected with a CODEMODE_IMPORTS_UNSUPPORTED diagnostic; submitted source otherwise remains standalone. The legacy ambient globals remain available for existing scripts. Both TypeScript and JavaScript are checked with TypeScript's strict mode (checkJs is enabled) before runtime MCP clients are created. Invalid arguments, unknown tools, and invalid access to typed output therefore fail before a tool call. The CLI converts SIGINT/SIGTERM into a runtime interruption, closes MCP connections, reports a nonzero result, and then restores its signal handlers. This is cooperative lifecycle cleanup; synchronous trusted code cannot be forcibly preempted inside the same process.
The CLI does not automatically print the program's completion value. Use console.log, console.error, or other Bun APIs in the program when output is required.
MCP bindings and results
Codemode follows FastMCP Code Mode's compact call style and adds local BM25F discovery:
import { call_tool, search_tools } from "@nothumanwork/codemode";
const hits = await search_tools("workspace status", { limit: 5, minScore: 0 });
// hits[0]: { name, server, description?, score, inputSchema, snippet }
const output = await call_tool("mmr_status", {
source: "codex",
project: "codemode",
});In an ordinary imported script, search_tools discovers the live catalog from every configured server and closes those connections before resolving. Under codemode run, the injected binding instead reads the generated search-index.json artifact and does not issue an MCP request after artifacts are current. Empty, stopword-only, and unknown-term queries return an empty array. Invalid query/options types throw TypeError at runtime (and fail TypeScript checking). Adding discovery does not change the trusted, non-sandbox runtime model.
Tool names must be unique across all configured servers so the two-argument call is unambiguous. Artifact generation fails with both server names when two servers expose the same tool name.
Tool calls return the full MCP SDK CallToolResult, not only the structured payload. This preserves fields such as content, isError, and _meta, while structuredContent is narrowed from the tool's outputSchema:
const result = await call_tool("mmr_status", { source: "codex" });
console.log(result.content); // full MCP content blocks
console.log(result.isError); // tool-level failure, when present
console.log(result.structuredContent?.source); // typed from outputSchemaThe MCP SDK receives the discovered tool definition on each call and validates successful structuredContent against its outputSchema. Because structuredContent is optional in CallToolResult, the generated type remains optional. If a tool declares no outputSchema, its structuredContent type is unknown rather than an invented shape.
JSON Schema boundary
Codemode accepts JSON-serializable object inputSchema values and optional object outputSchema values that json-schema-to-typescript can compile without producing any. Standard nested object, array, composition, and local-reference shapes are supported subject to that compiler. Server-supplied schema titles are deterministically renamed so they cannot replace or collide with allocated TypeScript names, and the generated declaration is checked for its expected root type. External $ref values, tsType, tsEnumNames, invalid type keywords, non-object schemas, and schemas that require any fail the entire generation with server/tool context.
Generated TypeScript is a static approximation, not a replacement for server-side/runtime validation. Schema format does not narrow the TypeScript type, and tuple length constraints from minItems/maxItems are not represented. Codemode does not claim support for arbitrary JSON Schema dialects or extensions.
Daemon
codemode daemon start
codemode daemon status
codemode daemon restart
codemode daemon stopThe detached daemon watches the configured file's parent directory, generates immediately, and then regenerates after matching file changes (including atomic replacement). Changes are debounced and generations never overlap. It watches the config file, not remote servers.
start waits up to 15 seconds for the initial outcome and prints ready (pid ...) on success or degraded (pid ...): ... with exit code 1 on failure. Starting a live duplicate fails. restart stops the matching daemon first. stop prints stopped or not running and is idempotent.
status reports the live phase (starting, generating, ready, or degraded) and PID. It exits 1 for degraded, stale daemon state, or stopped; healthy live phases exit 0. A failed refresh retains the last-success timestamps/fingerprint in daemon status and can recover after the next valid config change. Graceful shutdown removes the PID and lock files, retains a stopped status and logs, and rotates the log near 1 MiB.
Daemon state is isolated by the same config-path key:
${CODEMODE_STATE_DIR:-${XDG_STATE_HOME:-~/.local/state}/codemode}/<config-key>/
daemon.lock
daemon.pid
daemon-status.json
daemon.log
daemon.log.1CODEMODE_STATE_DIR replaces the entire default state root; relative values are resolved from the current working directory. Daemon ownership checks currently rely on ps outside the daemon process, so reliable status and stop behavior is currently limited to non-Windows systems.
Troubleshooting
- Configuration cannot be read: print or set
MCP_SERVER_CONFIG_PATH, prefer an absolute path, and validate that the JSON has onlymcpServersand the transport-specific fields above. types-pathreports missing or stale artifacts: runcodemode generate. Check<cache>/<config-key>/generation-status.jsonif generation fails.- Daemon is degraded: fix the reported config/server/schema problem, then save the config again. Inspect
daemon-status.jsonanddaemon.logunder the state directory for bounded, redacted details. stale daemon state: the recorded process is gone or does not match its instance identity.daemon start,restart, orstopcleans stale PID/lock/status files before proceeding.- Artifact generation reports a duplicate tool name: rename or namespace that tool at one MCP server.
call_tool(name, params)requires tool names to be unique across the configured catalog. - A program fails before making a request: resolve the file/line TypeScript diagnostic. This is expected for schema mismatches and unsupported static module syntax.
- A server changed tools without a config edit: run
codemode generate, or trigger the watcher by saving the config, then obtain the new declaration path withtypes-path.
Design rationale and prior art
Codemode borrows the typed-tool idea while deliberately serving a different execution model:
- TanStack AI Code Mode turns a tool set into an
execute_typescripttool, generates stubs for the model prompt, and runs code through interchangeable Node, QuickJS, or Cloudflare isolate drivers. Its separate@tanstack/ai-mcppackage provides MCP discovery and one-shot code generation; it does not provide this project's config watcher. - Cloudflare Code Mode exposes compact typed APIs (including
search/executefor its large API surface) and runs generated code in isolated Dynamic Workers with outbound access blocked by default. The reusable implementation lives incloudflare/agents/packages/codemode. - FastMCP Code Mode uses BM25 discovery before
call_toolexecution inside its Python sandbox. This project adopts the compact call style plus local BM25F search while retaining generated TypeScript input/output types. - This project provides a direct typed library plus a local, trusted-code CLI. The library discovers and calls MCP tools over stdio or Streamable HTTP. The CLI additionally publishes content-addressed declarations and a search index, strictly checks a complete script, injects
search_toolsandcall_tool, and watches the MCP config for regeneration. Neither mode inherits the sandbox guarantees of FastMCP, TanStack's isolate drivers, or Cloudflare's Dynamic Workers.
Security
Codemode is not a sandbox. Ordinary imported scripts run as normal Bun programs. Programs submitted to codemode run are compiled into an async function and run in the codemode process. Both have full Bun, filesystem, network, environment, and process authority. Only run code you trust. The MCP configuration is also trusted: stdio entries can execute arbitrary commands, and HTTP entries can send configured credentials to their URLs.
Generated artifacts persist an allowlisted callable surface—server/tool names, descriptions, input/output schemas, derived search terms, and non-sensitive fingerprints—rather than transport configuration, environment, headers, or arbitrary tool metadata. Each generation directory holds manifest.json, mcp.d.ts, and search-index.json under the same content-addressed id. Surfaced generation errors are bounded and drop a secret-bearing cause. These are credential-hygiene defenses, not isolation from malicious code or servers.
Development
bun test
bun run typecheck
bun run format:check
bun run build
bun run compilePublish releases
The npm package contains two independent surfaces: the bundled codemode bin and the typed, executable script library exported from the package root. JSR publishes the same library source and its runtime dependency graph; the CLI remains an npm/Bun distribution.
Keep the versions in package.json and jsr.json identical, then validate both package payloads without uploading:
bun run publish:checkPublish npm after authenticating. The scoped package is configured as public:
bun run login:npm
bun run publish:npm:dry-run
bun run publish:npmJSR's publish command opens an interactive browser login when needed:
bun run publish:jsr:dry-run
bun run publish:jsrThe account used for each registry must control the nothumanwork scope. Registry versions are immutable, so bump both files before every subsequent release. This repository does not yet declare an open-source license; choose and add one before publishing if downstream users should be granted reuse rights.
