ZeroMCP
Drop a tool file, get a sandboxed MCP server. Stdio out of the box, 10 languages, zero setup.
Why ZeroMCP?
Drop a tool and go
With the official MCP SDKs, you write a tool, then you write the server around it — instantiate a server class, pick a transport, wire them together, configure schemas. With ZeroMCP, you skip all of that.
This is a complete MCP server:
// tools/hello.js
export default {
description: "Say hello to someone",
input: { name: 'string' },
execute: async ({ name }) => `Hello, ${name}!`,
};node bin/mcp.js serve ./toolsStdio transport works immediately. No server class to instantiate, no transport to configure, no schema library to learn. Drop a file in the tools directory, run the command, connect a client.
Want another tool? Drop another file. Want to remove one? Delete it. Hot reload picks up changes automatically.
Built-in sandbox
The official MCP SDKs give tools unrestricted access to the network, filesystem, environment variables, and child processes. ZeroMCP is the only MCP runtime that sandboxes tool execution:
- Network allowlists — tools declare which domains they can reach. Requests to anything else are blocked.
-
Credential isolation — tools receive secrets through
ctx.credentials, notprocess.env. A tool only sees the credentials it was given. -
Filesystem controls — tools must declare
fs: 'read'orfs: 'write'. Unauthorized access is blocked. - Exec prevention — subprocess spawning is denied by default.
- Permission logging — every permission decision is logged, so you can see exactly what your tools are doing.
// tools/fetch_data.js
export default {
description: "Fetch data from our API",
input: { endpoint: 'string' },
permissions: {
network: ['api.example.com'],
},
execute: async ({ endpoint }, ctx) => {
const res = await ctx.fetch(`https://api.example.com/${endpoint}`);
return res.body;
},
};[zeromcp] fetch_data → GET api.example.com (allowed)
[zeromcp] fetch_data ✗ GET evil.com (not in allowlist) (denied)
What's different
| Official SDKs | ZeroMCP | |
|---|---|---|
| Getting started | Instantiate server, configure transport, register tools | Drop a file, run the command |
| Adding a tool | Write code, register with server, restart | Drop a file in the directory |
| Stdio transport | Manual setup | Works out of the box |
| Network sandboxing | None | Per-tool domain allowlists |
| Credential isolation | None | Injected via ctx.credentials
|
| Filesystem/exec control | None | Declared per tool, enforced at runtime |
| Cross-language conformance | Each SDK tested independently | 10 suites, 67 implementations, 0 failures |
| Performance | No built-in benchmarking | 1.1x to 740x faster, benchmarked against all 10 official SDKs |
| Chaos resilience | Not tested | 21-22/22 attacks survived across all 10 languages |
Quick start
Node.js — tools/hello.js
export default {
description: "Say hello to someone",
input: { name: 'string' },
execute: async ({ name }) => `Hello, ${name}!`,
};cd nodejs && npm run build
node bin/mcp.js serve ./examples/toolsPython — tools/hello.py
tool = {
"description": "Say hello to someone",
"input": {"name": "string"},
}
async def execute(args, ctx):
return f"Hello, {args['name']}!"cd python
python3 -m zeromcp serve ./examples/toolsGo
s := zeromcp.NewServer()
s.Tool("hello", zeromcp.Tool{
Description: "Say hello to someone",
Input: zeromcp.Input{"name": "string"},
Execute: func(args map[string]any, ctx *zeromcp.Ctx) (any, error) {
return fmt.Sprintf("Hello, %s!", args["name"]), nil
},
})
s.ServeStdio()Rust / Java / Kotlin / Swift / C# / Ruby / PHP — see language-specific READMEs below.
Languages
| Language | Directory | Runtime | Tool model | Performance |
|---|---|---|---|---|
| Node.js | nodejs/ |
Node 14+ (also Bun, Deno) | Drop a .js file, it's a tool |
1.6x faster, 23% less memory |
| Python | python/ |
Python 3.9+ | Drop a .py file, it's a tool |
12.7x faster, 59% less memory |
| Ruby | ruby/ |
Ruby 3.0+ | Drop a .rb file, it's a tool |
1.2x faster, 50% less memory |
| PHP | php/ |
PHP 8.1+ | Drop a .php file, it's a tool |
740x faster |
| Go | go/ |
Go 1.22+ | Register tools in code | 12.8x faster |
| Rust | rust/ |
Rust 1.78+ | Register tools in code | 1.1x faster, 50% less memory |
| Java | java/ |
Java 17+ | Register tools in code (builder API) | 2.5x faster, 53% less memory |
| Kotlin | kotlin/ |
Kotlin 2.0 / JVM 21 | Register tools in code (DSL) | 8.4x faster |
| Swift | swift/ |
Swift 5.9+ / macOS 13+ | Register tools in code | 99x faster |
| C# | csharp/ |
.NET 8 | Register tools in code | 1.4x faster, 34% less memory |
Sandbox
ZeroMCP's sandbox is a runtime permission layer that the official MCP SDKs don't have.
Per-tool permissions
export default {
description: "Search our CRM",
input: { query: 'string' },
permissions: {
network: ['crm.example.com'], // only this domain
fs: 'read', // read-only filesystem
exec: false, // no subprocess spawning (default)
},
execute: async ({ query }, ctx) => {
const res = await ctx.fetch(`https://crm.example.com/search?q=${query}`);
return res.body;
},
};Credential injection
Credentials are configured once and injected per namespace. Tools never access env vars directly:
{
"credentials": {
"stripe": { "env": "STRIPE_SECRET_KEY" },
"internal": { "file": "~/.config/internal-api.json" }
}
}Tools in the stripe/ directory receive ctx.credentials with the Stripe key. Tools in other directories don't see it.
Bypass mode
For local development, "bypass_permissions": true allows all access with warning logs:
[zeromcp] ⚠ fetch_data → GET unknown-host.com (not in allowlist — bypassed)
Performance
Benchmarked against every official MCP SDK across all 10 languages.
| Language | vs. Official SDK | HTTP throughput | Memory |
|---|---|---|---|
| Node.js | 1.6x faster | 4,539 rps | 22-26 MB |
| Python | 12.7x faster | 2,623 rps | 27 MB |
| Go | 12.8x faster | 4,024 rps | 21-23 MB |
| Rust | 1.1x faster | 5,111 rps | 3-4 MB |
| Java | 2.5x faster | 3,791 rps | 184-207 MB |
| Kotlin | 8.4x faster | 2,848 rps | 188-194 MB |
| Ruby | 1.2x faster | 3,217 rps | 26 MB |
| PHP | 740x faster | 1,561 rps | 11-33 MB |
| Swift | 99x faster | 1,730 rps | 49-96 MB |
| C# | 1.4x faster | 4,421 rps | 126-314 MB |
The "vs. Official SDK" column is stdio throughput (1,000 requests). ZeroMCP runs native inside your HTTP framework — no proxy, no subprocess. The official SDKs route HTTP through a stdio proxy, serializing every request through a single child process.
Over sustained HTTP load (5 minutes), ZeroMCP is 1.4x to 92x faster than official SDKs across all 10 languages. The official Rust SDK leaks to 2.4 GB in 5 minutes; ZeroMCP Rust uses 3 MB.
A 22-attack chaos monkey suite tests resilience against malformed input, giant payloads, concurrent floods, and protocol abuse. ZeroMCP survives 21-22/22 attacks across all 10 languages. Official SDKs survive 19-20/21.
Configuration
All implementations support an optional zeromcp.config.json:
{
"tools": ["./tools"],
"transport": [
{ "type": "stdio" },
{ "type": "http", "port": 4242, "auth": "env:TOKEN" }
],
"logging": true,
"bypass_permissions": false,
"credentials": {
"api": { "env": "API_KEY" }
},
"remote": [
{ "name": "other-server", "url": "http://localhost:5000", "auth": "env:TOKEN" }
]
}HTTP / Streamable HTTP
ZeroMCP doesn't own the HTTP layer — you bring your own framework. ZeroMCP gives you a handler function that takes JSON-RPC in and returns JSON-RPC out.
import { createHandler } from 'zeromcp/handler';
const handler = await createHandler('./tools');
// Express, Fastify, Hono, Cloudflare Workers, Lambda — anything
app.post('/mcp', async (req, res) => res.json(await handler(req.body)));This is compatible with MCP's Streamable HTTP transport. The handler processes initialize, tools/list, tools/call, and ping. Your framework handles the HTTP.
See Node.js README for examples with Express, Fastify, Hono, Cloudflare Workers, and Lambda.
Features
- Drop and go — stdio out of the box, no server setup, no transport config
-
Framework-agnostic HTTP —
createHandlerworks with any HTTP framework or serverless runtime - Built-in sandbox — network allowlists, filesystem controls, exec prevention, credential isolation
- File-based tools — add a tool by creating a file, remove it by deleting (Node.js, Python, Ruby, PHP)
- Hot reload — tool changes detected automatically
-
Input validation — JSON Schema generated from shorthand types (
string,number,boolean,object,array) - Execute timeout — default 30s, configurable per tool
- Remote federation — proxy tools from other MCP servers over HTTP
Testing
A cross-language conformance suite validates all implementations across 10 suites and 67 test implementations covering protocol compliance, sandbox enforcement, timeout handling, credential isolation, namespace resolution, and HTTP transport. 0 failures, 0 skipped. Additionally, a 22-attack chaos monkey suite tests resilience against malformed input, giant payloads, concurrent floods, and protocol abuse.
Run with Docker (all languages)
docker build -t zeromcp .
docker run zeromcpRun locally (single language)
node tests/conformance/run-all.jsLanguages whose binaries aren't built locally are skipped automatically.
Project structure
zeromcp/
nodejs/ TypeScript/Node.js implementation
python/ Python implementation
go/ Go implementation
rust/ Rust implementation
java/ Java implementation (Maven)
kotlin/ Kotlin implementation (Gradle)
swift/ Swift implementation (SPM)
csharp/ C# implementation (.NET 8)
ruby/ Ruby implementation (gem)
php/ PHP implementation
tests/ Cross-language conformance suite
Dockerfile Multi-language build & test container
License
MIT