Per-agent routing
By default, every enabled server in mcphub.yaml reaches every non-disabled agent. That is the right default for one person with one toolbox — but in a multi-agent setup you often want different agents to see different subsets: a review agent that only needs code search, a deploy agent that should never see the database server, a minimal agent that gets nothing but the gateway's meta-tools.
Per-agent routing does exactly that with two optional lists on an agent entry:
agents:
codex:
type: codex
path: ~/.codex/config.toml
mode: gateway
servers: [codemap, vecgrep] # which servers it may reach
tools: [codemap__codemap_find, vecgrep__vecgrep_search] # which exact tools it may callservers— which downstream servers this agent may reach.tools— which exactserver__toolnames it may call. Gateway mode only: a direct agent talks to each server itself, so mcphub has no proxy in the path to filter individual tools.mcphub doctorrejectstoolson a direct-mode agent.
Curation, not a security boundary
Routing controls what the gateway advertises and honors for a well-behaved agent — it keeps context lean and agents on-task. It is not a hard isolation layer: anything that can run mcphub mcp serve without --agent (or talk to the downstream servers directly) sees everything. Don't use routing to protect secrets from a hostile process; use it to stop your review agent burning tokens on deployment tools.
Omitted vs empty: the three states
Each list is a pointer in the config — absent and empty mean different things:
In mcphub.yaml | Meaning |
|---|---|
| key omitted entirely | All. No restriction — the pre-routing behavior. |
servers: [] / tools: [] | None. A deliberately minimal agent. |
servers: [a, b] / tools: [a__x] | Only these. |
So an agent with no servers and no tools keys behaves exactly as before, and adding routing to one agent never changes what the others see.
agents:
claude:
type: claude
path: ~/.claude.json
mode: gateway
# no servers/tools keys -> sees every enabled server
minimal:
type: opencode
path: ~/.config/opencode/opencode.json
mode: gateway
servers: [] # sees NO downstream servers, only the mcphub meta-toolsA listed server that is disabled (or missing from the enabled set) is dropped silently — routing selects within the enabled servers, it does not enable anything. A servers entry naming a server that doesn't exist in the config at all is a validation error.
How it works in gateway mode
When a gateway-mode agent has any routing config (either list present, even empty), mcphub sync writes its harness entry with an extra flag:
mcphub mcp serve --agent codexinstead of the plain mcphub mcp serve that unscoped agents get. When that gateway starts, it looks up the named agent's servers/tools allowlists and:
- Advertises only the subset. In
expose: all, only in-scopeserver__toolnames are mounted. The agent never sees out-of-scope tools, so they cost zero context. - Scopes the meta-tools too. In
expose: lazy,mcphub_list_servers,mcphub_search_tools,mcphub_describe_tool, andmcphub_resolve_toolonly surface in-scope servers and tools. - Refuses out-of-scope calls.
mcphub_call_toolon a tool outside the allowlists fails withtool server__x is out of scope for this agent, andmcphub_get_resultscope-checks the stored server/tool before paging a spooled result.
A bare mcphub mcp serve (no --agent) is unscoped and serves everything, as is --agent <name> for an agent with no routing keys. An --agent naming an agent that doesn't exist in the config is an error — a stale flag in a harness file fails fast instead of silently serving everything or nothing.
Restart to apply
Routing is enforced at gateway startup. After editing servers/tools, run mcphub sync (the --agent flag in the harness entry may need to appear or disappear) and restart the agent so it relaunches its gateway.
How it works in direct mode
A direct-mode agent gets no proxy, so routing happens at sync time: mcphub sync writes only the listed enabled servers into the agent's config, verbatim. servers: [] writes none. tools is not allowed — with no gateway in the path there is nothing to enforce a per-tool list.
agents:
opencode:
type: opencode
path: ~/.config/opencode/opencode.json
mode: direct
servers: [vecgrep] # only vecgrep is written into opencode.jsonAll the usual sync guarantees still apply: dry-run by default, a timestamped .bak before any write, and pruning scoped to entries mcphub owns.
tools rules
The tools list is validated by mcphub doctor (and on every config load):
- Entries are exact namespaced names:
server__tool. No wildcards — to allow a whole server, put it inserversand omittoolsrestrictions for it. - Each entry's server prefix must name a server that exists in the config.
- If the agent also has a
serverslist, everytoolsentry must reference a server on that list — a tool is only callable when both lists allow it. toolson amode: directagent is rejected.
agents:
reviewer:
type: claude
path: ~/.claude.json
mode: gateway
servers: [codemap]
tools: [codemap__codemap_find] # OK: codemap is in servers
# tools: [vecgrep__search] # error: vecgrep not in this agent's servers list
# tools: [codemap__*] # error: wildcards are not supportedInspecting routing
mcphub status --server codemap # which agents route to codemap + proxied-call count
mcphub doctor --server codemap # single-server registration/routing/usage summary
mcphub sync # dry run: see the exact --agent args and server setsmcphub sync (dry run, the default) is the fastest way to confirm what each agent will actually receive before you --write anything. Once calls flow, mcphub stats tells you whether a scoped-down agent is actually using what you left in scope.
Routing vs the other knobs
Routing composes with mcphub's other context-budget features rather than replacing them:
expose: lazyshrinks how tools are advertised (meta-tools + on-demand discovery); routing shrinks which tools exist for an agent at all. A scoped lazy gateway discovers only in-scope tools.enabled: falseon a server removes it for everyone; a routing list removes it for one agent.disabled: trueon an agent skips the agent during sync entirely;servers: []still syncs it, just with an empty (gateway) or absent (direct) server set.