mcpmu is an MCP server aggregator that manages multiple MCP servers and exposes their tools through a unified interface.
┌─────────────────────────────────────────────────────────────┐
│ Claude Code / Codex │
│ (MCP Client) │
└─────────────────────────────┬───────────────────────────────┘
│ spawns via stdin/stdout
▼
┌─────────────────────────────────────────────────────────────┐
│ mcpmu │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ stdio Server │ │
│ │ (MCP JSON-RPC protocol) │ │
│ └──────────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┴───────────────────────────┐ │
│ │ Tool Aggregator │ │
│ │ (collects tools from managed servers) │ │
│ │ (routes tool calls to correct server) │ │
│ └──────────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┴───────────────────────────┐ │
│ │ Supervisor │ │
│ │ (manages server process lifecycle) │ │
│ └──────────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┴───────────────────────────┐ │
│ │ Config │ │
│ │ (~/.config/mcpmu/config.json) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────┬───────────────────────────────┘
│ spawns & manages
▼
┌─────────────────────────────────────────────────────────────┐
│ Managed MCP Servers │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ filesystem │ │ github │ │ sqlite │ ... │
│ │ server │ │ server │ │ server │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
Claude Code/Codex spawns mcpmu as a subprocess. No daemons, no manual startup.
// ~/.claude/mcp_servers.json
{
"mcpmu": {
"command": "mcpmu",
"args": ["serve", "--stdio", "--config", "~/.config/mcpmu/config.json", "--namespace", "default"]
}
}The mcpmu config is designed so server entries remain compatible with the common mcpServers object shape used by MCP clients. In practice that means the server config uses the familiar field names:
commandargscwdenv
This keeps manual editing easy (copy/paste a server definition from a client config into mcpmu, then add the namespace assignments/permissions as needed).
The stdio server exposes a single toolset per process, selected by namespace at startup. Configure multiple MCP entries that run the same binary with different --namespace values.
If multiple namespaces exist and none is selected (and no default is set), mcpmu fails initialize with an actionable error rather than accidentally exposing all tools.
// Work context
{
"mcpmu-work": {
"command": "mcpmu",
"args": ["serve", "--stdio", "--config", "~/.config/mcpmu/config.json", "--namespace", "work"]
}
}
// Personal context
{
"mcpmu-personal": {
"command": "mcpmu",
"args": ["serve", "--stdio", "--config", "~/.config/mcpmu/config.json", "--namespace", "personal"]
}
}Namespaces are the preferred mechanism for selecting toolsets, but separate config files are still supported when you want fully isolated settings.
{
"mcpmu-project-x": {
"command": "mcpmu",
"args": ["serve", "--stdio", "--config", "~/.config/mcpmu/project-x.json", "--namespace", "default"]
}
}Serve mode uses a two-phase tools/list flow so clients are not blocked behind slow upstream discovery.
- On
initialize,mcpmuadvertisestools.listChanged: true. - On the first
tools/list,mcpmustarts or probes all selected servers concurrently. - It waits up to an 8 second grace period and returns the tools that are already ready.
- Any remaining discovery continues in the background with the normal per-server timeout.
- If background discovery makes progress,
mcpmusendsnotifications/tools/list_changedso the client can refresh with anothertools/list. - Config reloads that may change the visible tool set also send
notifications/tools/list_changed.
This keeps tools/list responsive for clients with tight request timeouts while still converging to the full aggregated tool set.
Serve mode passes through resources/* and prompts/* MCP methods from upstream servers (enabled by default, disable with --resources=false or --prompts=false).
- Resources: URIs are passed through unmodified from upstream servers. A reverse map (URI → server name) is built during
resources/listand used to routeresources/readcalls to the correct upstream server. All MCP resource fields are preserved, includingannotations,title, andsize.resources/templates/listis also supported (returns an empty list if no upstream servers provide templates). - Prompts: Names are qualified as
serverName.promptName(same as tools). Descriptions are prefixed with[serverName]. Onprompts/get, the prefix is stripped before forwarding upstream. - No caching: Resources and prompts are fetched on demand from upstream servers, not cached or discovered at startup.
- No permissions: Unlike tools, resources and prompts have no permission layer — they are read-only and user-initiated.
Tool access follows a four-tier resolution. The server-level global deny applies even without a namespace:
- Server global deny (
server deny-tool) — hard block, no override. Applies even without a namespace. - Explicit tool permission (
permission set) — highest namespace-level priority - Per-server default (
permission set-server-default) — overrides namespace default for one server - Namespace default (
namespace set-deny-default) — fallback for all servers - Allow — if nothing else applies
A namespace-level explicit allow cannot override a server global deny. To re-enable a globally denied tool, remove it from deniedTools via server allow-tool.
This enables defense-in-depth: globally deny dangerous tools at the server level, then use namespace permissions for fine-grained control over the rest.
Tools from managed servers are exposed with serverId.toolName format:
filesystem.read_file
filesystem.write_file
github.create_issue
github.list_repos
mcpmu.servers_list # Manager tools
mcpmu.servers_start
mcpmu.servers_stop
mcpmu.namespaces_list
serverId is a stable internal identifier (auto-generated short [a-z0-9], no .), not the human display name.
The TUI includes a registry browser for discovering and installing servers from the official MCP registry (registry.modelcontextprotocol.io). Press a on the server list to open an add-method selector:
- Manual — opens the blank add-server form
- Official Registry — opens a searchable browser with debounced live search, detail view with install preview, and pre-populates the add-server form with the selected server's command/args/env
The registry client (internal/registry/) handles API calls, type mapping, and install spec generation (package selection, runtime hints, env var placeholders).
mcpmu embeds a SKILL.md file in the binary (cmd/mcpmu/skill_data/SKILL.md via //go:embed). The mcpmu skill install command writes this to agent-specific skill directories (~/.claude/skills/mcpmu/, ~/.codex/skills/mcpmu/, ~/.agents/skills/mcpmu/). A checked-in mirror at .claude/skills/mcpmu/SKILL.md is kept in sync by a test assertion.
mcpmu web starts a browser-based management UI on 127.0.0.1:8080 (configurable via --addr). The web UI and TUI are mutually exclusive managers — a manager.lock file prevents concurrent instances.
Stack: Go net/http (stdlib, Go 1.22+ route patterns) + htmx + Go html/template, all embedded via go:embed. Single binary, no build step.
Architecture (internal/web/):
server.go— HTTP server setup, route registration, template parsing, config mutation helperpages.go— Full-page handlers (server list, server detail, namespace list, namespace detail)forms.go— Form page handlers and CRUD operations (add/edit/delete servers and namespaces)actions.go— Live action handlers (start/stop, OAuth login/logout, denied tools, permissions, server defaults, set default namespace)handlers.go— JSON API endpoints (/api/servers,/api/namespaces,/api/config/export|import)registry.go— Registry browser page, htmx fragment, and JSON API (/api/registry/search)fragments.go— htmx fragment handlers (server table, status pill, registry results)sse.go— Server-Sent Events for live log streamingstatus.go—StatusTrackersubscribes to event bus, maintains last-known status per servermiddleware.go— Request logging, panic recovery
Data flow: Browser requests go through middleware to handlers, which read/write config (same internal/config package as TUI), interact with the supervisor for start/stop, and subscribe to the event bus for live status and logs.
Config mutations: The mutateConfig helper reloads config from disk, applies the mutation, and atomically saves — safe for the single-manager design.
- Non-blocking initialize: Return immediately; optionally start
eagerservers in background (otherwise start on-demand) - Lazy server start: Servers start on first tool call (configurable)
- Progressive tool discovery:
tools/listreturns ready tools within a grace window, then notifies clients when stragglers finish - Graceful degradation: If one server fails, others still work
- Strict output discipline: stdout = MCP protocol only, stderr = logs
- Transport-agnostic core: Easy to add HTTP later if needed