Skip to content

About

Aggregates several MCP servers into one MCP endpoint, with a built-in admin UI

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

metamcp

A single Go binary that aggregates several MCP servers into one MCP endpoint, with a built-in admin UI.

Inspired by metatool-ai/metamcp, rewritten with only the parts that carry the value: group servers into namespaces, filter and rename their tools, and serve each namespace over Streamable HTTP, SSE or stdio. No database, no accounts, no build step — the UI is plain HTML/CSS/JS embedded in the binary.

License: this project is not open source. Copying, forking or reusing it is not allowed — see LICENSE.

Install

go build -o metamcp ./cmd/metamcp

Configure

listen: :12008
api_key: ${METAMCP_API_KEY}   # optional; ${VAR} reads the environment
ui_password: ""               # optional; set it from the ui so it is stored hashed

servers:
  files:                       # stdio: command is set
    command: npx
    args: [-y, "@modelcontextprotocol/server-filesystem", /tmp]
  github:                      # streamable http: url is set
    url: https://api.githubcopilot.com/mcp/
    headers:
      Authorization: Bearer ${GITHUB_TOKEN}
  legacy:
    type: sse                  # sse needs to be explicit
    url: https://example.com/sse

namespaces:
  dev:
    servers:
      - files                  # everything this server exposes
      - name: github
        allow: [search_issues] # whitelist; omit to allow all
        deny: [delete_repo]    # blacklist, wins over allow
        overrides:
          search_issues:
            name: gh_search
            description: Search GitHub issues

Validate it with metamcp -check -config metamcp.yaml.

Run

metamcp -config metamcp.yaml

Each namespace gets two endpoints:

Transport URL
Streamable HTTP http://host:12008/mcp/dev
SSE http://host:12008/mcp/dev/sse

GET /healthz is unauthenticated and always open.

Admin UI

http://host:12008/ serves an admin UI on the same port:

  • Overview — every server with its live connection status and tool count, every namespace with copyable endpoint URLs. Add, edit, delete and test servers and namespaces.
  • Namespace view — every tool the downstream servers expose, with a checkbox to include it and inline fields to rename or re-describe it. Saving rewrites the config and takes effect immediately: namespaces you did not touch keep their live client sessions.
  • Tool tester — call any tool with JSON arguments and see the result, so you do not need a separate MCP inspector.

Everything the UI does goes through a JSON API under /api/, so it is scriptable too:

Route Does
GET /api/state servers, namespaces, statuses, version, config path
PUT|DELETE /api/servers/{name} add, edit or remove a server (409 if still used)
POST /api/servers/{name}/test connect now and report the status
PUT|DELETE /api/namespaces/{name} add, edit or remove a namespace
GET|POST /api/namespaces/{name}/tools list every downstream tool / save filters and overrides
GET /api/namespaces/{name}/prompts aggregated prompts
GET /api/namespaces/{name}/resources aggregated resources and templates
POST /api/namespaces/{name}/call run a tool
POST /api/namespaces/{name}/prompt get a prompt
POST /api/namespaces/{name}/resource read a resource
PUT /api/settings change listen or api_key (listen needs a restart)

Two things worth knowing:

  • Saving from the UI rewrites the config file, so comments and formatting are lost. The previous version is kept as <config>.bak. ${VAR} references are preserved, never resolved into the file.
  • The UI and the API sit behind the same api_key as the endpoints when one is set. There is no separate login, so bind to localhost unless you set a key.

For stdio-only clients (Claude Desktop) skip the HTTP server entirely:

{
  "mcpServers": {
    "dev": {
      "command": "metamcp",
      "args": ["-config", "/etc/metamcp/metamcp.yaml", "-stdio", "dev"]
    }
  }
}

Auth

Two independent things, because a browser cannot send a header on a plain navigation:

api_key — for MCP clients and scripts. Set it globally or per namespace and every request to that namespace needs it, from any of X-API-Key, Authorization: Bearer <key>, ?api_key= or ?apikey=. It also guards the admin API. Opening the UI once with ?api_key= starts a browser session so the rest of the page works.

ui_password — for the admin UI only. Set it and the UI redirects to a sign in page; the correct password sets an HttpOnly cookie that lasts a week, or until the process restarts. It does nothing for MCP clients. Set it from Settings → UI password so it is written salted and hashed; a plain value written by hand also works.

Neither set means no auth at all, so bind to localhost.

Per-client API keys (SQLite)

The config credentials above are the admin's. For giving different clients different access to the same namespaces, create issued keys — from the UI (API keys page) or the JSON API. They live in a SQLite database (-db flag, default metamcp.db beside the config), because they are created and revoked at runtime, unlike the YAML topology.

  • Each key is named, optional expiry, and a list of grants: a namespace, and optionally a whitelist of canonical tool names (<server>__<tool> — stable across renames). An empty tool list means every tool in that namespace.
  • Deny by default: a key with no grants can reach nothing. Creating the first key closes the endpoints to anonymous clients; deleting all keys reopens them.
  • The secret is shown once at creation and stored only as a hash. Disabled or expired keys stop resolving immediately; keys are replaced, never read back.
  • Scoping is an intersection: a granted tool is further filtered by the namespace's own allow/deny. A browser session never unlocks /mcp/*; MCP clients must present a key (Authorization: Bearer <key>, X-API-Key, or ?api_key=).
  • The admin api_key is never scoped and always sees everything.

sqlite3 ~/.config/metamcp/metamcp.db "SELECT name, prefix, expires_at, disabled FROM api_keys;"

Naming

Tools and prompts are exposed as <server>__<name>, where <server> is the config key with everything outside [a-zA-Z0-9_-] removed. Calls are routed by that prefix, so no state is needed between tools/list and tools/call. Prefix collisions inside a namespace are rejected at startup. Resources keep their own URIs; the owning server is remembered from resources/list and looked up on demand if a client reads without listing.

Behaviour worth knowing

  • Downstream servers are connected lazily on first use and shared by all clients.
  • A dead session is reconnected once, transparently, on the failing request.
  • A downstream server that is broken or slow to fail is logged and skipped: listings degrade instead of failing.
  • A tool blocked by allow/deny returns a tool error, not a protocol error.
  • Listings are not paginated outwards; every downstream page is fetched and merged.

Flags

-config   path to the config file (default metamcp.yaml)
-listen   override the listen address
-stdio    serve one namespace over stdio instead of http
-db       sqlite file for api keys (default: metamcp.db beside the config)
-check    validate the config and exit
-debug    debug logging, including downstream stderr

Docker

docker build -t metamcp .
docker run -p 12008:12008 -v $PWD/metamcp.yaml:/etc/metamcp/metamcp.yaml metamcp

The image ships node and uv so npx/uvx servers work out of the box.

About

Aggregates several MCP servers into one MCP endpoint, with a built-in admin UI

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages