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

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

BeforeAfter
Server class + transport setupJust the tool
z.number().optional(){ type: "number", optional: true }
process.env.STRIPE_KEYctx.credentials.apiKey
Global fetchctx.fetch (sandboxed)
Wrap in { content: [{ type: "text" }] }Return the data directly
17 dependencies0 dependencies

Incremental approach

You don't have to migrate everything at once. Start with Path 1 (compose), then rewrite tools one at a time:

  1. Add existing servers as remotes
  2. Pick one tool to rewrite as a local file
  3. The local version automatically overrides the remote version (same name = local wins)
  4. Repeat until you've replaced all remote tools
  5. Shut down old servers