Skip to main content
Glama

mcp-multiplexer

Formerly dokploy-bundle-mcp.

One MCP server for all your Dokploy instances, or for all your accounts on any other MCP server, such as several Linear workspaces.

It runs one upstream connection per instance and adds a required argument to every tool (instance, workspace or a name you pick) that says which instance the call goes to. Your MCP client (Claude Desktop, Claude Code or any other) sees one set of tools, however many instances you have.

For Dokploy, the bundle wraps the official @dokploy/mcp. Other MCP servers can be any stdio command or remote URL, with per-instance headers or a per-instance OAuth login.

Why

If you register an MCP server once per instance, every instance adds its own copy of all its tools. Claude Code lists every tool name in context at session start, even when it defers the schemas. @dokploy/mcp has 604 tools, so each Dokploy instance costs about 8k tokens before you've typed anything.

Setup

Tool names

≈ Tokens

6 separate @dokploy/mcp servers

3,624

50k

This bundle, all tools

606

6k

This bundle, tags from config.example.json

159

1.6k

With the bundle, that cost stays the same however many instances you add. Built-in connectors often reach only one account; Linear's, for example, connects to a single workspace. The bundle reaches all of them through one connection.

Related MCP server: MCP Gateway

Requirements

  • Node.js 20 or newer, and pnpm (npm works too)

  • For Dokploy: an API key for each instance. You generate these in Dokploy under your profile settings.

Install

git clone https://github.com/paulschwahn/mcp-multiplexer.git
cd mcp-multiplexer && pnpm install && pnpm build

Configure

Each bundle has its own config file. The server reads the file you pass with --config, then MCP_MULTIPLEXER_CONFIG, then ~/.config/mcp-multiplexer/config.json. The examples below are for Dokploy, the default type; see Other MCP servers for the rest.

{
  "tools": { "enabledTags": ["project", "environment", "application", "compose", "domain", "deployment"] },
  "instances": {
    "staging": {
      "url": "https://dokploy-staging.example.com",
      "apiKeyEnv": "DOKPLOY_STAGING_API_KEY",
      "description": "Staging; hosts the PR preview deployments"
    },
    "production": {
      "url": "https://dokploy.example.com",
      "apiKey": "paste-key-here",
      "description": "Production",
      "protected": true
    }
  }
}

If you put keys in the file, make it readable only by you (chmod 600). If you'd rather keep keys out of the file, use apiKeyEnv and set the variable in your MCP client's server entry.

Already using @dokploy/mcp in Claude Desktop?

pnpm import-desktop-config

This copies every @dokploy/mcp entry from Claude Desktop's config into the bundle config: URL, key and any extra DOKPLOY_* variables. Instance names come from the entry keys, so dokploy-acme-prod-mcp becomes acme-prod. Names that contain prod are marked protected.

The script:

  • writes the file with mode 600;

  • refuses to overwrite an existing config unless you pass --force;

  • prints instance names only, never keys;

  • leaves the Desktop config alone and prints the entry to replace them with.

It finds the Desktop config on macOS, Windows and Linux. To read a different file, pass its path as an argument.

Config reference

Field

Meaning

type

dokploy (default), command or url. See Other MCP servers.

name

Names this bundle's cache files. Defaults to the config file's name, e.g. linear for linear.json.

argument

The routing argument added to every tool. Defaults to instance.

instances.<name>

One entry per instance. Names may only use a-z, 0-9 and -.

.description

Shown by bundle-instances. It helps Claude pick the right instance, so say what it is.

.protected

Changes need your approval (see Protect production).

.env

Extra environment for this instance's upstream process.

.url (dokploy)

Dokploy base URL. A trailing /api is stripped, because upstream adds it.

.apiKey / .apiKeyEnv (dokploy)

The key itself, or the name of an environment variable that holds it. Exactly one is required.

env

Environment for every instance's upstream, e.g. DOKPLOY_REDACT_ENV.

tools (dokploy)

Which tools to expose (see below). It applies to all instances.

upstream.version (dokploy)

Which @dokploy/mcp to run: bundled (default), latest, a range or an exact version (see Upgrading).

Unknown fields are rejected, so a typo fails at startup rather than being silently ignored.

Choosing tools

The bundle passes upstream's own filter through. A tool's tag is the part of its name before the dash: application-deploy has the tag application. Use either a preset or a list of enabledTags, and optionally disabledTags to drop tags from it.

tools

Tags

Tools

{ "preset": "minimal" }

project, application

43

{ "preset": "core" }

minimal + server

62

{ "preset": "deploy" }

core + environment, compose, domain, deployment

119

{ "preset": "databases" }

postgres, redis, mysql, mariadb, mongo, libsql

94

{ "preset": "git" }

github, gitlab, bitbucket, gitea, gitProvider, registry, sshKey

46

{} (default)

everything

604

Upgrading @dokploy/mcp

By default the bundle runs the @dokploy/mcp version pinned in package.json. To follow upstream releases instead, set upstream.version:

{ "upstream": { "version": "latest" } }

version

Runs

"bundled" (default)

the version in package.json, updated when you pull this repo and rebuild

"latest"

the newest release

"~0.30.7"

the newest 0.30.x patch release

"0.31.2"

exactly that version

How it works:

  • Checked once per start. When your client starts the server, the bundle asks npm which version the spec resolves to. A running session never switches versions.

  • Installed once per version. A new version is installed into ~/.cache/mcp-multiplexer/upstream/<version> with --ignore-scripts. The three newest versions are kept.

  • Falls back when offline. If npm or the registry can't be reached, the bundle uses the newest cached version, or else the bundled one.

  • Easy to check. bundle-instances shows which version is running and why.

Before you switch, know two things:

  • Every API key goes to whatever version runs. With latest, a new upstream release gets your keys without anyone reviewing it. For production instances, a patch range or bundled is the more careful choice.

  • Upstream is still 0.x, so a minor release can rename or remove tools. This repo's tests only cover the bundled version.

Connect your client

The server talks MCP over stdio. The name you register it under becomes the tool prefix, so Dokploy registered as dokploy gives mcp__dokploy__…. The approval hook below matches on that name.

Claude Desktop. Quit the app completely first, because it rewrites its config while running. Then add this to claude_desktop_config.json and start the app again:

"mcpServers": {
  "dokploy": {
    "command": "/absolute/path/to/node",
    "args": ["/path/to/mcp-multiplexer/dist/index.js"]
  }
}

For a second bundle, add another entry with "--config", "/path/to/its-config.json" after index.js in args. Claude Desktop doesn't load your shell's PATH, so give node's absolute path (which node). This matters especially if you use nvm.

Claude Code:

claude mcp add dokploy --scope user -- "$(which node)" /path/to/mcp-multiplexer/dist/index.js

Any other MCP client: run node /path/to/mcp-multiplexer/dist/index.js [--config <file>] as a stdio server.

Use it

Start with a health check. Ask Claude to "run bundle-check". Each instance should come back with its Dokploy version, or with the reason it failed.

After that, just ask for what you want and name the instance:

  • "Show the last deployments of the api app on staging."

  • "Redeploy the web compose on production." On a protected instance, this asks for your approval first.

Every tool takes the required routing argument, and there is no default. Claude has to choose an instance on every call, and an unknown name is rejected with the list of valid ones. Two tools come from the bundle itself:

Tool

What it does

bundle-instances

Lists name, description, whether it's protected, whether a connection is open, and what serves the instances. Never shows keys or header values.

bundle-check

Checks one instance, or all of them. For Dokploy it returns the version; for other types it calls checkTool or pings.

Other MCP servers

Set type to bundle a server other than Dokploy. Every instance must serve the same tools: the bundle lists them from the first instance that connects.

type

upstream

Per instance

command

{ "command": "…", "args": [ … ] }: any stdio server

args (appended), env

url

{ "url": "…" }: a remote server, using headers

headers; ${VAR} in a value is read from the environment

url

{ "url": "…", "auth": "oauth" }: a remote server with an OAuth login

authDir: where its tokens live

checkTool names a read-only tool that bundle-check calls on each instance. Pick one that shows which account the instance reached. Without it, bundle-check only pings.

OAuth logins go through mcp-remote, pinned in package.json, which keeps each instance's tokens in its own directory. By default that's ~/.mcp-auth/mcp-multiplexer/<bundle>/<instance>. Point authDir at an existing MCP_REMOTE_CONFIG_DIR to reuse a login you already have.

Example: Linear across several workspaces

~/.config/mcp-multiplexer/linear.json:

{
  "type": "url",
  "argument": "workspace",
  "upstream": { "url": "https://mcp.linear.app/mcp", "auth": "oauth" },
  "checkTool": "get_workspace",
  "instances": {
    "acme": { "description": "Acme (linear.app/acme)" },
    "side-project": { "description": "Side project (linear.app/side)" }
  }
}

Register it as a second server:

claude mcp add linear --scope user -- "$(which node)" /path/to/mcp-multiplexer/dist/index.js --config ~/.config/mcp-multiplexer/linear.json

Then ask Claude to "run bundle-check". It goes through the workspaces one at a time:

  • A workspace without a login opens Linear's sign-in in your browser.

  • Choose the workspace that instance is for on Linear's consent screen. Linear preselects the one you used last.

  • get_workspace then shows which workspace each login reached. If one is wrong, delete that instance's token directory and run the check again.

Linear's MCP server may also accept a personal API key instead of OAuth. Third-party docs say so, but Linear's own docs don't confirm it:

"upstream": { "url": "https://mcp.linear.app/mcp" },
"instances": { "acme": { "headers": { "Authorization": "Bearer ${LINEAR_ACME_API_KEY}" } } }

Protect production

A Claude Code PreToolUse hook asks for your approval before any tool that isn't read-only runs against an instance marked "protected": true. Read-only tools still run without asking. Add one entry per bundle to ~/.claude/settings.json. Give each the same --config as its server, and a matcher with the name you registered that server under:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__dokploy__.*",
        "hooks": [
          { "type": "command", "command": "/absolute/path/to/node /path/to/mcp-multiplexer/dist/hook.js" }
        ]
      },
      {
        "matcher": "mcp__linear__.*",
        "hooks": [
          { "type": "command", "command": "/absolute/path/to/node /path/to/mcp-multiplexer/dist/hook.js --config ~/.config/mcp-multiplexer/linear.json" }
        ]
      }
    ]
  }
}

What counts as read-only comes from upstream's readOnlyHint annotations: 226 of Dokploy's 604 tools, and 43 of Linear's 82. Each time the server lists tools, it writes that list for the upstream it runs to ~/.cache/mcp-multiplexer/catalogs/<bundle>.json. Until a Dokploy server has started, the hook uses dist/catalog.json from the build. If the hook can't read the config or doesn't know a tool, it asks.

Hooks only run in Claude Code: the CLI, the IDE extensions and the Code tab in Claude Desktop. In other clients, protected is only a note in the tool description.

Troubleshooting

  • The tools don't appear in Claude Desktop. Check ~/Library/Logs/Claude/mcp-server-<name>.log on macOS. The usual cause is a node path that isn't absolute. If the config is invalid, the server exits at startup with a message naming the field.

  • bundle-check reports HTTP 401 (API key rejected). The key is wrong or was revoked. A connection error means the URL is wrong; it should be the base URL you open in the browser.

  • One instance fails and the others work. That's by design, for example when an apiKeyEnv or ${VAR} variable isn't set, or a login is missing. The error says which.

  • An OAuth login doesn't open a browser. mcp-remote writes the sign-in URL to the server log; open it from there. The login waits for up to five minutes.

  • A config change has no effect. The server reads the config at startup, so restart your client.

  • upstream.version isn't the version you expected. Ask for bundle-instances: its upstream field shows the running version, and if npm failed, the reason it fell back.

  • The hook doesn't prompt. Check that its matcher uses the server name from your client config and that it has the same --config as the server. Then run it by hand, adjusting the tool and instance to a protected instance of yours:

    echo '{"tool_name":"mcp__dokploy__application-deploy","tool_input":{"instance":"production"}}' | node dist/hook.js

    For a protected instance this prints a permissionDecision: "ask".

How it works

  • Listing tools: the bundle connects to the first instance that works and lists its tools. It puts the required routing argument first in each one, and adds bundle-instances and bundle-check. A tool that already has an argument with the routing argument's name is skipped, and the skip is logged.

  • Calling a tool: the bundle removes the routing argument and forwards the call to that instance's own connection, which opens on first use and stays open. A stdio upstream gets only its own settings, plus PATH, HOME and similar, and never sees the other instances' keys.

  • Why instance and not server for Dokploy: Dokploy already uses "server" for its remote build hosts (serverId).

Development

pnpm test        # stub and echo upstreams, an HTTP server, and the real @dokploy/mcp pointed at a dead URL
pnpm typecheck

To change the bundled @dokploy/mcp, bump it in package.json and run pnpm build, which regenerates dist/catalog.json.

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Reduces LLM context window overhead by proxying multiple MCP servers through a few efficient dispatch tools instead of registering hundreds of individual tool schemas. It supports multi-account routing and tool discovery for both CLI-based and persistent MCP server configurations.
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Acts as a proxy/router for multiple downstream MCP servers, exposing only meta-tools to the host to reduce token usage, enabling efficient search and invocation of tools from a fleet of servers.
    4 npm
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Enables AI harnesses to connect to a single MCP endpoint that routes to multiple downstream MCP servers, discovering and executing capabilities on demand while keeping tool schemas out of context.
    4
    13 npm
    Apache 2.0