A TypeScript runtime that materializes MCP server tools as typed functions, then evaluates TypeScript snippets that compose those tools — keeping intermediate data in-memory and out of the model's context window.
Instead of the model shuttling data between tool calls, it declares intent:
const doc = await getDoc({ documentId: "doc-001" });
await emailDoc({ to: "boss@example.com", subject: doc.title, body: doc.body });The runtime handles data flow. The model never sees the document body.
mcp-compose
┌──────────────────────────────────────────────────────┐
│ │
│ TS snippet ──► esbuild ──► node:vm sandbox │
│ │ │
│ ┌────────────┼────────────┐ │
│ ▼ ▼ ▼ │
│ getDoc() listDocs() emailDoc() │
│ │ │ │ │
│ ┌─────┘ │ └────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐│
│ │doc-server│ │doc-server│ │email-srvr││
│ │(stdio) │ │(stdio) │ │(stdio) ││
│ └──────────┘ └──────────┘ └──────────┘│
└──────────────────────────────────────────────────────┘
Data flow: User code runs in a node:vm sandbox. Tool functions are injected as globals. Each call dispatches over stdio to the backing MCP server via a connection pool. Results stay in the sandbox — only a summary is returned.
src/
├── config/ # Zod-validated config loader (mcp-compose.json)
├── materializer/ # Introspects MCP servers, generates .d.ts declarations
├── transport/ # Connection pool + tool call dispatcher
├── runtime/ # esbuild transpile → vm sandbox execution
├── interface/ # CLI and MCP server frontends
└── index.ts # Public API
| Command | Description | Example |
|---|---|---|
just build |
Compile TypeScript (tsgo) | just build |
just test |
Build + run all tests (node:test) | just test |
just clean |
Remove dist/ and generated/ |
just clean |
just install |
Install npm dependencies | just install |
just materialize |
Generate .d.ts files for all configured servers |
just materialize |
just serve |
Start mcp-compose as an MCP server | just serve |
just eval CODE |
Evaluate a TypeScript expression | just eval 'return await listDocs()' |
just run FILE |
Run a TypeScript file in the sandbox | just run script.ts |
just inspect [SERVER] |
Open MCP inspector (default: compose) | just inspect doc-server |
just install
just test # 24 tests, ~0.4s
just materialize # generates typed declarations in generated/# List available documents
just eval 'return await listDocs()'
# Fetch a document
just eval 'return await getDoc({ documentId: "doc-001" })'
# Compose: fetch doc then email it
just eval '
const doc = await getDoc({ documentId: "doc-001" });
return await emailDoc({ to: "a@b.com", subject: doc.title, body: doc.body })
'Add to your MCP client config:
{
"mcpServers": {
"mcp-compose": {
"command": "node",
"args": ["dist/src/interface/mcp-server.js"],
"cwd": "/path/to/mcp-compose"
}
}
}This exposes two tools: compose (execute TS code) and listAvailableTools (show available tool signatures).
import { compose } from "mcp-compose";
const result = await compose(`
const doc = await getDoc({ documentId: "doc-001" });
return doc.title;
`);
console.log(result.value); // "Q4 Revenue Report"
console.log(result.callLog); // [{ serverId: "doc-server", toolName: "getDoc", ... }]
console.log(result.totalBytes); // bytes kept out of context windowmcp-compose.json declares which MCP servers to connect to:
{
"servers": {
"doc-server": {
"command": "node",
"args": ["--experimental-strip-types", "demo/doc-server/index.ts"]
},
"email-server": {
"command": "node",
"args": ["--experimental-strip-types", "demo/email-server/index.ts"]
}
}
}Environment variables can be referenced with ${VAR_NAME} syntax in command, args, and env values.