Migration Guide
Already using MCP? Two ways to move to ZeroMCP. Compose first, rewrite later.
Path 1: Compose (zero effort)
Keep your existing MCP servers running. Add them as remotes in ZeroMCP. Done in 30 seconds:
// zeromcp.config.json
{
"remote": [
{
"name": "github",
"url": "http://localhost:3001/mcp"
},
{
"name": "jira",
"url": "http://localhost:3002/mcp"
}
]
} Your existing servers keep running. ZeroMCP proxies them into one process with auto-namespacing. Your MCP client connects to ZeroMCP instead of each server individually.
You can add local tools on top at any time. They merge with the remote tools into a single tool surface.
Why start here
- Zero rewriting. Your existing tools work as-is.
- Immediate benefit. One process, one connection, auto-namespacing.
- Incremental migration. Rewrite tools to local files one at a time.
Path 2: Rewrite as native tools
For when you want to kill the old servers entirely. Extract each tool into a native ZeroMCP tool:
Before: Official SDK
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0"
});
server.tool(
"list_customers",
{
limit: z.number().optional().describe("Max results")
},
async ({ limit }) => {
const res = await fetch(
`https://api.stripe.com/v1/customers?limit=${limit || 10}`,
{
headers: {
Authorization: `Bearer ${process.env.STRIPE_KEY}`
}
}
);
const data = await res.json();
return {
content: [
{ type: "text", text: JSON.stringify(data) }
]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport); The official MCP SDK is JavaScript. ZeroMCP gives you a native implementation:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0"
});
server.tool(
"list_customers",
{
limit: z.number().optional().describe("Max results")
},
async ({ limit }) => {
const res = await fetch(
`https://api.stripe.com/v1/customers?limit=${limit || 10}`,
{
headers: {
Authorization: `Bearer ${process.env.STRIPE_KEY}`
}
}
);
const data = await res.json();
return {
content: [
{ type: "text", text: JSON.stringify(data) }
]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport); The official MCP SDK is JavaScript. ZeroMCP gives you a native implementation:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0"
});
server.tool(
"list_customers",
{
limit: z.number().optional().describe("Max results")
},
async ({ limit }) => {
const res = await fetch(
`https://api.stripe.com/v1/customers?limit=${limit || 10}`,
{
headers: {
Authorization: `Bearer ${process.env.STRIPE_KEY}`
}
}
);
const data = await res.json();
return {
content: [
{ type: "text", text: JSON.stringify(data) }
]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport); The official MCP SDK is JavaScript. ZeroMCP gives you a native implementation:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0"
});
server.tool(
"list_customers",
{
limit: z.number().optional().describe("Max results")
},
async ({ limit }) => {
const res = await fetch(
`https://api.stripe.com/v1/customers?limit=${limit || 10}`,
{
headers: {
Authorization: `Bearer ${process.env.STRIPE_KEY}`
}
}
);
const data = await res.json();
return {
content: [
{ type: "text", text: JSON.stringify(data) }
]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport); The official MCP SDK is JavaScript. ZeroMCP gives you a native implementation:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0"
});
server.tool(
"list_customers",
{
limit: z.number().optional().describe("Max results")
},
async ({ limit }) => {
const res = await fetch(
`https://api.stripe.com/v1/customers?limit=${limit || 10}`,
{
headers: {
Authorization: `Bearer ${process.env.STRIPE_KEY}`
}
}
);
const data = await res.json();
return {
content: [
{ type: "text", text: JSON.stringify(data) }
]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport); The official MCP SDK is JavaScript. ZeroMCP gives you a native implementation:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0"
});
server.tool(
"list_customers",
{
limit: z.number().optional().describe("Max results")
},
async ({ limit }) => {
const res = await fetch(
`https://api.stripe.com/v1/customers?limit=${limit || 10}`,
{
headers: {
Authorization: `Bearer ${process.env.STRIPE_KEY}`
}
}
);
const data = await res.json();
return {
content: [
{ type: "text", text: JSON.stringify(data) }
]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport); The official MCP SDK is JavaScript. ZeroMCP gives you a native implementation:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0"
});
server.tool(
"list_customers",
{
limit: z.number().optional().describe("Max results")
},
async ({ limit }) => {
const res = await fetch(
`https://api.stripe.com/v1/customers?limit=${limit || 10}`,
{
headers: {
Authorization: `Bearer ${process.env.STRIPE_KEY}`
}
}
);
const data = await res.json();
return {
content: [
{ type: "text", text: JSON.stringify(data) }
]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport); The official MCP SDK is JavaScript. ZeroMCP gives you a native implementation:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0"
});
server.tool(
"list_customers",
{
limit: z.number().optional().describe("Max results")
},
async ({ limit }) => {
const res = await fetch(
`https://api.stripe.com/v1/customers?limit=${limit || 10}`,
{
headers: {
Authorization: `Bearer ${process.env.STRIPE_KEY}`
}
}
);
const data = await res.json();
return {
content: [
{ type: "text", text: JSON.stringify(data) }
]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport); The official MCP SDK is JavaScript. ZeroMCP gives you a native implementation:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0"
});
server.tool(
"list_customers",
{
limit: z.number().optional().describe("Max results")
},
async ({ limit }) => {
const res = await fetch(
`https://api.stripe.com/v1/customers?limit=${limit || 10}`,
{
headers: {
Authorization: `Bearer ${process.env.STRIPE_KEY}`
}
}
);
const data = await res.json();
return {
content: [
{ type: "text", text: JSON.stringify(data) }
]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport); The official MCP SDK is JavaScript. ZeroMCP gives you a native implementation:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0"
});
server.tool(
"list_customers",
{
limit: z.number().optional().describe("Max results")
},
async ({ limit }) => {
const res = await fetch(
`https://api.stripe.com/v1/customers?limit=${limit || 10}`,
{
headers: {
Authorization: `Bearer ${process.env.STRIPE_KEY}`
}
}
);
const data = await res.json();
return {
content: [
{ type: "text", text: JSON.stringify(data) }
]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport); After: ZeroMCP
// 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();
},
]; What changes
| Before | After |
|---|---|
| Server class + transport setup | Just the tool |
z.number().optional() | { type: "number", optional: true } |
process.env.STRIPE_KEY | ctx.credentials.apiKey |
Global fetch | ctx.fetch (sandboxed) |
Wrap in { content: [{ type: "text" }] } | Return the data directly |
| 17 dependencies | 0 dependencies |
Incremental approach
You don't have to migrate everything at once. Start with Path 1 (compose), then rewrite tools one at a time:
- Add existing servers as remotes
- Pick one tool to rewrite as a local file
- The local version automatically overrides the remote version (same name = local wins)
- Repeat until you've replaced all remote tools
- Shut down old servers