18 MCP servers to 1

How antidrift replaced 18 separate MCP connector packages with one ZeroMCP process. 262 tools. Zero runtime dependencies.

The problem

Antidrift ships MCP connectors for 18 services: Gmail, Drive, Calendar, Attio, HubSpot CRM, HubSpot Marketing, Pipedrive, Jira, Linear, ClickUp, Notion, GitHub, AWS, Stripe, Cloudflare, Vercel, Netlify, and a Google bundle.

Each connector was its own npm package. Its own MCP server class. Its own transport setup. Its own dependency tree. Its own process.

If a user connected 5 services, they ran 5 MCP server processes. Each one reimplemented the same boilerplate: JSON-RPC handling, stdio transport, schema conversion, error wrapping. The only thing that changed between packages was the API calls.

# 18 separate npm packages, 18 processes
@antidrift/mcp-gmail
@antidrift/mcp-drive
@antidrift/mcp-calendar
@antidrift/mcp-attio
@antidrift/mcp-hubspot-crm
@antidrift/mcp-hubspot-marketing
@antidrift/mcp-pipedrive
@antidrift/mcp-jira
@antidrift/mcp-linear
@antidrift/mcp-clickup
@antidrift/mcp-notion
@antidrift/mcp-github
@antidrift/mcp-aws
@antidrift/mcp-stripe
@antidrift/mcp-cloudflare
@antidrift/mcp-vercel
@antidrift/mcp-netlify
@antidrift/mcp-google   # bundle of gmail + drive + calendar

# Each one:
# - Its own MCP server class
# - Its own transport setup
# - Its own dependency tree
# - Its own process
# - Its own config entry in claude_desktop_config.json

What changed

ZeroMCP separates the runtime from the tools. The runtime handles MCP protocol, transport, schema conversion, credential injection, and sandboxing. The tools just make API calls.

Each connector package became a folder of tool files. The Stripe connector isn't a 500-line npm package anymore. It's 3 files in tools/stripe/, each under 30 lines. The tool imports the Stripe SDK. ZeroMCP handles everything else.

# 1 ZeroMCP process, tools loaded as needed
zeromcp serve

# Each connector is now a folder of tool files:
tools/
  gmail/
    search.js        # imports googleapis
    send.js
    read.js
  stripe/
    list_customers.js  # imports stripe
    create_charge.js
  github/
    list_issues.js     # imports @octokit/rest
    create_pr.js
  jira/
    list_tickets.js    # imports jira-client
    create_issue.js
  ...

# The runtime has 0 dependencies.
# Each tool imports its own SDK.
# Tools load when called, not at startup.

The numbers

Before (18 packages)After (ZeroMCP)
Processes1 per connector (up to 18)1 total
Runtime dependencies17 per package (@modelcontextprotocol/sdk)0
Boilerplate per connector~200 lines (server class, transport, schema)0 (runtime handles it)
Tool code per connectorBuried inside boilerplateStandalone files, 15-30 lines each
Config1 entry per connector in claude_desktop_config.json1 zeromcp.config.json
Credential handlingEach package reads env vars differentlyctx.credentials, mapped in one config
Total tools262262 (same tools, less infrastructure)

How it works

One config file maps credentials per service directory. Tools load when called, not at startup. A user who only uses Stripe and GitHub never loads the Gmail or Jira tools.

// zeromcp.config.json
{
  "tools": "./tools",
  "credentials": {
    "gmail": {
      "file": "~/.config/google/credentials.json"
    },
    "stripe": {
      "env": "STRIPE_SECRET_KEY"
    },
    "github": {
      "env": "GITHUB_TOKEN"
    },
    "jira": {
      "env": "JIRA_API_TOKEN"
    }
  }
}

Each tool is a standalone file. It imports whatever SDK it needs. The runtime provides ctx.credentials and ctx.fetch. That's the entire interface.

// tools/stripe/list_customers.js
import Stripe from 'stripe';

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 stripe = new Stripe(ctx.credentials);
    const customers = await stripe.customers.list({
      limit
    });
    return customers.data;
  },
}

What this means for users