Composing MCP Tools with TypeScript

Large language models are great at calling tools — but when a task requires chaining several tools together, the model ends up shuttling data back and forth, burning tokens on intermediate results it doesn’t need to see. mcp-compose fixes this by letting models write TypeScript that declares the composition, while the runtime handles the data flow.

The Problem

Consider a simple workflow: fetch a document, then email it to someone. With standard tool use, the model:

  1. Calls getDoc → receives the full document body
  2. Passes the body back in a emailDoc call

The document content travels through the model’s context window even though the model doesn’t need to reason about it. Multiply this across longer chains and larger payloads, and you’re wasting significant tokens.

The Solution

With mcp-compose, the model writes a small TypeScript snippet instead:

const doc = await getDoc({ documentId: "doc-001" });
return await emailDoc({ to: "[email protected]", subject: doc.title, body: doc.body });

The runtime executes this in a sandboxed VM. The document content stays in memory — it never hits the model’s context. Only the final result comes back.

How It Works

mcp-compose connects to any number of MCP servers, introspects their available tools, and exposes them as typed global functions inside a secure sandbox.

The pipeline:

  1. Configure your MCP servers in mcp-compose.json (supports both stdio and HTTP transports)
  2. Materialize — introspect all servers and generate .d.ts declaration files so models know what tools are available and their type signatures
  3. Compose — write TypeScript that chains tools together; the runtime handles connections, dispatching, and result shaping

Architecture

The project is organized into clean modules:

  • Config — validates mcp-compose.json and resolves environment variable references
  • Transport — manages a connection pool with lazy connect and deduplication
  • Materializer — introspects servers and generates TypeScript declarations
  • Runtime — transpiles TypeScript via esbuild, executes in a node:vm sandbox with tool functions injected as globals
  • Interface — a CLI (materialize, run, eval) and an MCP server that exposes compose itself as a tool

The Sandbox

Code runs in a locked-down VM context. It gets safe builtins (JSON, Math, Promise, etc.) and the tool functions — nothing else. No process, no require, no filesystem access. There’s a configurable timeout (default 30s) so runaway scripts don’t hang.

Each tool call is logged with timing and byte counts, so you get a clear picture of what happened:

2 tool call(s) | 690 bytes | 36ms
  doc-server/getDoc (3ms, 519B)
  email-server/emailDoc (32ms, 171B)

Exposing mcp-compose as an MCP Server

Here’s where it gets interesting: mcp-compose itself is an MCP server. It exposes two tools:

  • compose — accepts TypeScript code, executes it against all configured servers, returns the result
  • listAvailableTools — returns typed function signatures so the calling model knows what’s available

This means you can add mcp-compose to any MCP-compatible client (like Claude Code) and it becomes a meta-tool — a single tool that can orchestrate any number of other tools.

Getting Started

npm install
just materialize   # generate .d.ts files from configured servers
just eval 'await listDocs()'   # run a quick expression
just run script.ts             # run a full script

Configure your servers in mcp-compose.json:

{
  "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"]
    }
  }
}

Why This Matters

As MCP adoption grows, models will routinely interact with dozens of tools across multiple servers. Composing those tools efficiently — without bloating context windows — is essential. mcp-compose provides a minimal, typed, sandboxed runtime that lets models declare what they want to happen, while keeping the data plumbing out of sight.

Repository: https://github.com/splusq/mcp-compose

Comments