Getting Started
Get ZeroMCP running in under 3 minutes. Pick your language, build a tool, connect it to Claude Code or Cursor.
Install
npm install -g zeromcp pip install zeromcp go get github.com/antidrift-dev/zeromcp/pkg/zeromcp # Cargo.toml
[dependencies]
zeromcp = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] } // build.gradle.kts
plugins {
kotlin("jvm") version "2.0.0"
application
}
dependencies {
implementation("dev.antidrift:zeromcp:0.1.0")
}
application {
mainClass.set("MainKt")
} <!-- pom.xml -->
<dependency>
<groupId>dev.antidrift</groupId>
<artifactId>zeromcp</artifactId>
<version>0.1.0</version>
</dependency> dotnet new console -n MyMcpServer
cd MyMcpServer
dotnet add package ZeroMcp // Package.swift
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "MyMcpServer",
platforms: [.macOS(.v13)],
dependencies: [
.package(
url: "https://github.com/antidrift-dev/zeromcp-swift",
from: "0.1.0"
)
],
targets: [
.executableTarget(
name: "MyMcpServer",
dependencies: [
.product(
name: "ZeroMcp",
package: "zeromcp-swift"
)
]
)
]
) gem install zeromcp composer global require antidrift/zeromcp Requires Node.js 18+.
Requires Python 3.10+. Zero external dependencies.
Requires Go 1.22+.
Requires Kotlin 2.0+ and JVM 21.
Requires Java 17+.
Requires .NET 8+. Available on NuGet.
Requires Swift 5.9+ and macOS 13+.
Requires Ruby 3.0+. Zero external dependencies.
Requires PHP 8.1+. No extensions required.
Create a tool
Drop a file in ./tools/. ZeroMCP discovers it automatically.
Drop a file in ./tools/. ZeroMCP discovers it automatically.
Drop a file in ./tools/. ZeroMCP discovers it automatically.
Drop a file in ./tools/. ZeroMCP discovers it automatically.
Register tools in your main function. No file conventions, no code generation.
Register tools in your main function. No file conventions, no code generation.
Register tools in your main function. No file conventions, no code generation.
Register tools in your main function. No file conventions, no code generation.
Register tools in your main function. No file conventions, no code generation.
Register tools in your main function. No file conventions, no code generation.
mkdir tools
cat > tools/hello.js << 'EOF'
export default {
description: "Say hello to someone",
input: { name: "string" },
execute: async ({ name }) => `Hello, ${name}!`
}
EOF mkdir tools
cat > tools/hello.py << 'EOF'
tool = {
"description": "Say hello to someone",
"input": {
"name": "string"
},
}
async def execute(args, ctx):
return f"Hello, {args['name']}!"
EOF package main
import (
"fmt"
"github.com/antidrift-dev/zeromcp/pkg/zeromcp"
)
func main() {
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()
} use zeromcp::{Server, Tool, Input, Permissions, Ctx};
use serde_json::Value;
#[tokio::main]
async fn main() {
let mut server = Server::new();
server.tool("hello", Tool {
description: "Say hello to someone".to_string(),
input: Input::new()
.required_desc("name", "string", "Who to greet"),
permissions: Permissions::default(),
execute: Box::new(|args: Value, _ctx: Ctx| {
Box::pin(async move {
let name = args["name"]
.as_str()
.unwrap_or("world");
Ok(Value::String(
format!("Hello, {name}!")
))
})
}),
});
server.serve_stdio().await;
} import dev.antidrift.zeromcp.server
fun main() {
val server = server()
server.tool("hello") {
description = "Say hello to someone"
input {
"name" to "string"
}
execute { args, _ ->
"Hello, ${args.getString("name")}!"
}
}
server.serveStdio()
} import dev.antidrift.zeromcp.*;
public class Main {
public static void main(String[] args) {
var server = new Server();
server.tool("hello", Tool.builder()
.description("Say hello to someone")
.input(Input.required(
"name",
"string",
"The person's name"
))
.execute((a, ctx) ->
"Hello, " + a.get("name") + "!"
)
.build());
server.serveStdio();
}
} using ZeroMcp;
var server = new Server();
server.Tool("hello", new ToolDefinition {
Description = "Say hello to someone",
Input = new Dictionary<string, InputField> {
["name"] = new InputField(SimpleType.String)
},
Execute = async (args, ctx) => {
var name = args["name"].GetString() ?? "world";
return $"Hello, {name}!";
}
});
await server.ServeStdio(); import ZeroMcp
let server = Server()
server.tool("hello",
description: "Say hello to someone",
input: [
"name": .simple(.string)
]
) { args, ctx in
"Hello, \(args["name"] as? String ?? "world")!"
}
await server.serve() mkdir tools
cat > tools/hello.rb << 'EOF'
tool description: "Say hello to someone",
input: {
name: "string"
}
execute do |args, ctx|
"Hello, #{args['name']}!"
end
EOF mkdir tools
cat > tools/hello.php << 'EOF'
<?php
return [
'description' => 'Say hello to someone',
'input' => [
'name' => 'string'
],
'execute' => function ($args, $ctx) {
return "Hello, {$args['name']}!";
},
];
EOF That's it. No JSON Schema, no Zod, no server class. A file with description, input, and execute.
That's it. A tool dict and an execute function. No decorators, no classes, no framework.
No code generation, no reflection. Just a struct and a function.
Type-safe inputs, async execution, zero-cost abstractions. Standard Rust.
The DSL keeps tool definitions readable. Description, input, execute — that's the whole API.
Builder, description, input, execute. That's the whole API.
A ToolDefinition with description, input, and execute. That's the whole API.
Description, input, execute. That's it.
That's it. A DSL with tool and execute. No classes, no gems, no boilerplate.
That's it. Return an array with description, input, and execute. No framework, no classes, no Composer dependencies.
Serve
zeromcp serve python3 -m zeromcp serve ./tools go build -o my-server .
./my-server cargo build --release
./target/release/my-server ./gradlew run mvn package
java -jar target/my-server.jar dotnet run --project MyMcpServer swift build
swift run zeromcp serve ./tools php zeromcp.php serve ./tools ZeroMCP scans the tools/ directory, finds your files, and serves them over stdio. Any MCP client can now call your tools.
ZeroMCP scans the tools/ directory, finds your files, and serves them over stdio. Any MCP client can now call your tools.
ZeroMCP scans the tools/ directory, finds your files, and serves them over stdio. Any MCP client can now call your tools.
ZeroMCP scans the tools/ directory, finds your files, and serves them over stdio. Any MCP client can now call your tools.
You get a single binary. Ship it anywhere. Serves over stdio so any MCP client can connect.
You get a single binary. Ship it anywhere. Serves over stdio so any MCP client can connect.
You get a single binary. Ship it anywhere. Serves over stdio so any MCP client can connect.
You get a single binary. Ship it anywhere. Serves over stdio so any MCP client can connect.
You get a single binary. Ship it anywhere. Serves over stdio so any MCP client can connect.
You get a single binary. Ship it anywhere. Serves over stdio so any MCP client can connect.
Connect to Claude Code
Add to your Claude Code MCP config:
{
"mcpServers": {
"zeromcp": {
"command": "zeromcp",
"args": ["serve"]
}
}
} {
"mcpServers": {
"zeromcp": {
"command": "python3",
"args": ["-m", "zeromcp", "serve", "./tools"]
}
}
} {
"mcpServers": {
"zeromcp": {
"command": "./my-server"
}
}
} {
"mcpServers": {
"zeromcp": {
"command": "./target/release/my-server"
}
}
} {
"mcpServers": {
"zeromcp": {
"command": "./gradlew",
"args": ["run", "-q"]
}
}
} {
"mcpServers": {
"zeromcp": {
"command": "java",
"args": ["-jar", "target/my-server.jar"]
}
}
} {
"mcpServers": {
"zeromcp": {
"command": "dotnet",
"args": ["run", "--project", "MyMcpServer"]
}
}
} {
"mcpServers": {
"zeromcp": {
"command": "swift",
"args": ["run", "--package-path", "./MyMcpServer"]
}
}
} {
"mcpServers": {
"zeromcp": {
"command": "zeromcp",
"args": ["serve", "./tools"]
}
}
} {
"mcpServers": {
"zeromcp": {
"command": "php",
"args": ["zeromcp.php", "serve", "./tools"]
}
}
} A real tool
Here's a tool that lists Stripe customers with credential injection and sandboxed fetch:
// tools/stripe/list_customers.js
export default {
description: "List Stripe customers",
permissions: {
network: ["api.stripe.com"]
},
input: {
limit: {
type: 'number',
optional: true,
description: "Max results"
}
},
execute: async ({ limit = 10 }, ctx) => {
const res = await ctx.fetch(
`https://api.stripe.com/v1/customers?limit=${limit}`,
{
headers: {
'Authorization': `Bearer ${ctx.credentials.apiKey}`
}
}
);
return (await res.json()).data;
},
} # tools/stripe/list_customers.py
tool = {
"description": "List Stripe customers",
"permissions": {
"network": ["api.stripe.com"]
},
"input": {
"limit": {
"type": "number",
"optional": True,
"description": "Max results"
}
},
}
async def execute(args, ctx):
limit = args.get("limit", 10)
res = await ctx.fetch(
f"https://api.stripe.com/v1/customers?limit={limit}",
headers={
"Authorization": f"Bearer {ctx.credentials['apiKey']}"
}
)
data = await res.json()
return data["data"] s.Tool("stripe_list_customers", zeromcp.Tool{
Description: "List Stripe customers",
Permissions: zeromcp.Permissions{
Network: []string{"api.stripe.com"},
},
Input: zeromcp.Input{
"limit": zeromcp.Field{
Type: "number",
Optional: true,
Description: "Max results",
},
},
Execute: func(args map[string]any, ctx *zeromcp.Ctx) (any, error) {
limit := 10
if v, ok := args["limit"].(float64); ok {
limit = int(v)
}
url := fmt.Sprintf(
"https://api.stripe.com/v1/customers?limit=%d",
limit,
)
res, err := ctx.Fetch(url, zeromcp.FetchOpts{
Headers: map[string]string{
"Authorization": "Bearer " + ctx.Credentials["apiKey"],
},
})
if err != nil {
return nil, err
}
return res.JSON()
},
}) server.tool("stripe_list_customers", Tool {
description: "List Stripe customers".to_string(),
input: Input::new()
.optional_desc("limit", "number", "Max results"),
permissions: Permissions {
network: vec!["api.stripe.com".to_string()],
..Default::default()
},
execute: Box::new(|args: Value, ctx: Ctx| {
Box::pin(async move {
let limit = args["limit"]
.as_u64()
.unwrap_or(10);
let url = format!(
"https://api.stripe.com/v1/customers?limit={limit}"
);
let res = ctx.fetch(&url, FetchOpts {
headers: vec![(
"Authorization".to_string(),
format!(
"Bearer {}",
ctx.credentials["apiKey"]
),
)],
..Default::default()
}).await?;
res.json().await
})
}),
}); server.tool("stripe_list_customers") {
description = "List Stripe customers"
permissions {
network("api.stripe.com")
}
input {
"limit" to field {
type = "number"
optional = true
description = "Max results"
}
}
execute { args, ctx ->
val limit = args.getInt("limit") ?: 10
val res = ctx.fetch(
"https://api.stripe.com/v1/customers?limit=${limit}",
headers = mapOf(
"Authorization" to "Bearer ${ctx.credentials["apiKey"]}"
)
)
res.json()
}
} server.tool("stripe_list_customers", Tool.builder()
.description("List Stripe customers")
.permissions(Permissions.builder()
.network("api.stripe.com")
.build())
.input(Input.optional(
"limit",
"number",
"Max results"
))
.execute((a, ctx) -> {
var limit = a.getOrDefault("limit", 10);
var res = ctx.fetch(
"https://api.stripe.com/v1/customers?limit=" + limit,
FetchOpts.builder()
.header(
"Authorization",
"Bearer " + ctx.credentials().get("apiKey")
)
.build()
);
return res.json();
})
.build()); server.Tool("stripe_list_customers", new ToolDefinition {
Description = "List Stripe customers",
Permissions = new Permissions {
Network = new[] { "api.stripe.com" }
},
Input = new Dictionary<string, InputField> {
["limit"] = new InputField(SimpleType.Number) {
Optional = true,
Description = "Max results"
}
},
Execute = async (args, ctx) => {
var limit = args.TryGetValue("limit", out var v)
? v.GetInt32()
: 10;
var res = await ctx.Fetch(
$"https://api.stripe.com/v1/customers?limit={limit}",
new FetchOptions {
Headers = new Dictionary<string, string> {
["Authorization"] =
$"Bearer {ctx.Credentials["apiKey"]}"
}
}
);
return await res.Json();
}
}); server.tool("stripe_list_customers",
description: "List Stripe customers",
permissions: Permissions(
network: ["api.stripe.com"]
),
input: [
"limit": .field(
type: .number,
optional: true,
description: "Max results"
)
]
) { args, ctx in
let limit = args["limit"] as? Int ?? 10
let res = try await ctx.fetch(
"https://api.stripe.com/v1/customers?limit=\(limit)",
headers: [
"Authorization":
"Bearer \(ctx.credentials["apiKey"]!)"
]
)
return try await res.json()
} # tools/stripe/list_customers.rb
tool description: "List Stripe customers",
permissions: {
network: ["api.stripe.com"]
},
input: {
limit: {
type: "number",
optional: true,
description: "Max results"
}
}
execute do |args, ctx|
limit = args.fetch("limit", 10)
res = ctx.fetch(
"https://api.stripe.com/v1/customers?limit=#{limit}",
headers: {
"Authorization" => "Bearer #{ctx.credentials['apiKey']}"
}
)
res.json
end <?php
// tools/stripe/list_customers.php
return [
'description' => 'List Stripe customers',
'permissions' => [
'network' => ['api.stripe.com']
],
'input' => [
'limit' => [
'type' => 'number',
'optional' => true,
'description' => 'Max results'
]
],
'execute' => function ($args, $ctx) {
$limit = $args['limit'] ?? 10;
$res = $ctx->fetch(
"https://api.stripe.com/v1/customers?limit={$limit}",
[
'headers' => [
'Authorization' =>
"Bearer {$ctx->credentials['apiKey']}"
]
]
);
return $res->json();
},
]; The tool declares that it only needs network access to api.stripe.com. The runtime enforces this. Undeclared domains are blocked. Credentials come from the context object, mapped in your config file:
// zeromcp.config.json
{
"credentials": {
"stripe": { "env": "STRIPE_SECRET_KEY" }
}
} # zeromcp.config.json
{
"credentials": {
"stripe": {
"env": "STRIPE_SECRET_KEY"
}
}
} // zeromcp.config.json
{
"credentials": {
"stripe": {
"env": "STRIPE_SECRET_KEY"
}
}
} // zeromcp.config.json
{
"credentials": {
"stripe": {
"env": "STRIPE_SECRET_KEY"
}
}
} // zeromcp.config.json
{
"credentials": {
"stripe": {
"env": "STRIPE_SECRET_KEY"
}
}
} // zeromcp.config.json
{
"credentials": {
"stripe": {
"env": "STRIPE_SECRET_KEY"
}
}
} // zeromcp.config.json
{
"credentials": {
"stripe": {
"env": "STRIPE_SECRET_KEY"
}
}
} // zeromcp.config.json
{
"credentials": {
"stripe": {
"env": "STRIPE_SECRET_KEY"
}
}
} # zeromcp.config.json
{
"credentials": {
"stripe": {
"env": "STRIPE_SECRET_KEY"
}
}
} // zeromcp.config.json
{
"credentials": {
"stripe": {
"env": "STRIPE_SECRET_KEY"
}
}
} Next steps
- Tool Authoring Guide: schema reference, permissions, ctx API
- Configuration: full config file reference
- Composability: connect remote MCP servers
- Security: permission model, sandbox, audit CLI