mcp-hub
This server is an MCP gateway that exposes many MCP servers through one endpoint and lets you discover, inspect, call, and manage them.
List all available MCP servers and their current status.
List the tools of a specific server with one-line descriptions.
Get the full JSON input schema of a tool before calling it.
Call tools on any server with arguments matching the tool schema.
Wake a sleeping on-demand server so its first tool call is fast.
Put a server to sleep immediately instead of waiting for its idle timeout.
It also handles auth (OAuth 2.1 or API tokens), hot reload, supervised process restarts, and per-server tool filtering.
Provides access to Audiobookshelf libraries, listening progress, collections, and playlists through the hub.
Provides read-only access to a Calibre-Web library through its OPDS feed.
Enables access to Google Search Console properties, sitemaps, search analytics, and URL inspection.
Enables management of Hetzner Cloud DNS zones and related DNS resources.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-hubsearch paperless for documents tagged 'invoice' from March"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-hub
A dual-era Model Context Protocol (MCP) gateway: it
serves many stdio MCP servers from one container, published over HTTPS, and speaks
both MCP revisions on every endpoint — 2026-07-28 and 2025-11-25. The client
picks, and cannot tell which one it is on from the answers. On the 2026 revision that
includes elicitation — a child server's question reaches the person at the far end
instead of dying at the gateway
(how) — and subscriptions: the hub
serves subscriptions/listen to its clients and subscribes to its children on
whichever revision they speak, so a server that has never heard of it still reaches
a client that speaks nothing else
(how).
Lets MCP clients that cannot spawn a local process — ChatGPT connectors, Claude on the Web and in Code, Mistral Le Chat, Cursor, LibreChat and any other Streamable-HTTP client — reach every server behind it, with a built-in OAuth 2.1 login protected by a single password, plus long-lived API tokens for clients that cannot do OAuth (OpenAI Responses API, xAI API, Gemini API). Per-client recipes: client compatibility.

Want to poke at it first? demo/ is a throwaway hub with three
fake servers — docker compose up -d, then point the
MCP Inspector or
MCPJam at it. Nothing to
configure, nothing to clean up but a volume.
Why
Wrapping each stdio MCP server in its own auth-proxy container costs a full image, an OAuth stack, a hostname and a compose stack per server. mcp-hub replaces N containers with one process:
Config is exactly Claude Code's
mcpServersformat — copy entries 1:1.Path-based routing:
https://host/paperless,https://host/homeassistant, …/hubaggregate: register a single connector and reach every server through 6 meta-tools (list_servers,list_tools,get_tool_schema,call_tool,wake_server,sleep_server) without flooding the model context with N×tools schemas.Per-server tool filtering:
allowTools/denyToolson any server decide which of its tools the hub exposes — exact names orlist_*prefixes. A filtered tool is hidden fromtools/listand refused if a client calls it anyway, before the server is even woken, so a client holding a stale schema cannot reach it.Also without HTTP:
mcp-hub --stdioserves that same aggregate on stdin/stdout for clients that can only spawn a local process (Claude Desktop, Codex, …) — samemcp.json, no TLS, no reverse proxy, no login. Auth exists for the network endpoints; over stdio the trust boundary is the local user.On-demand lifecycle: stdio and docker servers start when used and sleep after 60 idle minutes, answering
initialize/tools/listfrom a persistent snapshot meanwhile — a dozen servers cost only the memory of the ones in use.keepAlive: trueexempts a server,IDLE_TIMEOUT_MINUTES=0the hub.CIMD-first OAuth 2.1: clients identify themselves with a Client ID Metadata Document — the registration-free path the MCP spec now prefers — including
private_key_jwtagainst the keys in their own document (metadata-document clients only). RFC 7591 dynamic registration stays advertised beside it for older clients,mcp-hub-admin clients addissues credentials by hand for anything that can do neither, andCLIENT_REGISTRATIONturns either mechanism off.OAuth outwards, too: a remote server that speaks OAuth gets an
oauthblock instead of a static header. The hub registers itself — with credentials the upstream issued, via RFC 7591, or with its own client metadata document — then obtains and refreshes the token.client_credentialsupstreams need no attention at all; where a person must sign in,mcp-hub-admin upstream loginprints one URL. An upstream that needs re-authorizing shows up as one serverunauthorized, not as a confusing 401 in your client.Supervision: children are pinged and restarted with exponential backoff when they die. A down server answers 503, not silence; a crash-looping server nobody uses is parked instead of restarted forever.
Hot reload: edits to
mcp.jsonstart/stop/restart only the affected servers.Stateless Streamable HTTP: no session state, so claude.ai's reconnect-without-DELETE behaviour cannot leak processes or memory.
Dual-era: every endpoint —
/hub,/<name>/mcpand--stdio— answers MCP2026-07-28and2025-11-25alike; the client picks and cannot tell from the answers which it got. On the 2026 revision that includes elicitation: a server asking the user something returns the question rather than pushing it, so it reaches the person at the far end instead of dying at the gateway. The hub attributes it to the server that asked, strips what could lie about that, drops embedded sampling and roots requests, and seals the resumption state against the call it belongs to.passthrough: "off"withdraws one server's right to ask; details.Change notifications, in both eras: a client opens a
subscriptions/listenstream and hears when a child's tools, prompts or resources change. The hub subscribes to each child the way that child understands —subscriptions/listento a 2026 server,resources/subscribeto a 2025 one — so the era gap is the gateway's problem rather than either end's. The state is the open response, not a session table, so this costs the stateless design nothing. A sleeping server watches nothing and is told to re-read on waking;subscriptions: "off"withdraws one server's right to push; details.Lightweight by design: one Node process, no database (state is one JSON file plus a signing key under
/data), ten runtime dependencies, and multi-arch images — a stated project goal is to run comfortably on a single-board computer like a Raspberry Pi.
Related MCP server: mcphub
Servers to run behind it
The hub is server-agnostic — it serves any stdio MCP server whose entry fits
Claude Code's mcpServers format, which is most of them. These nineteen are
built and maintained alongside it, so their documentation carries the hub entry
you need and their tool filters line up with the hub's own allowTools /
denyTools:
Server | npm | What it reaches |
| Audiobookshelf — libraries, listening progress, collections and playlists | |
| CalDAV — events, tasks and journal entries on any server that speaks it | |
| Calibre-Web — read-only library access through the OPDS feed | |
| CardDAV — contacts, groups and photos on any server that speaks it | |
| FreshRSS — feeds, categories and articles as plain text, not stream ids | |
| Google Search Console — properties, sitemaps, search analytics, URL inspection | |
| Healthchecks — cron and uptime checks, and why one failed | |
| Hetzner Cloud DNS — zones, record sets and BIND import/export | |
| IMAP mailboxes — read, search, organise and draft mail; it cannot send | |
| Linkwarden — bookmarks, collections and the article text it preserved | |
| Mealie — recipes, meal plans, shopping lists and cookbooks | |
| ntfy — publish and update notifications, manage users and topic access | |
| Opengist — gists, revisions, commit history and raw files | |
| OpenStreetMap — geocoding, routing, isochrones and POI search | |
| Rustpad — collaborative pads edited through real OT, not overwrites | |
| SMTP — sends mail, behind a recipient allowlist and a human confirmation | |
| wg-easy v15+ — the full WireGuard client lifecycle | |
| Wiki.js — search, read and edit pages, plus assets, users and groups | |
| Woodpecker CI — repositories, pipelines, logs, secrets and crons |
Each one runs perfectly well on its own over stdio. Put them behind the hub when you want them reachable from a client that cannot spawn a local process, or when you would rather register one connector than nineteen.
Configuration
/config/mcp.json — identical to Claude Code (${VAR} expands from the
container environment; unknown fields are ignored by Claude Code, so the file
stays interchangeable). Install stdio server binaries at a reviewed, exact
version in your image; do not download mutable packages at runtime:
{
"mcpServers": {
"paperless": {
"command": "paperless-mcp",
"args": [],
"env": { "PAPERLESS_API_TOKEN": "${PAPERLESS_API_TOKEN}" }
},
"homeassistant": {
"type": "http",
"url": "http://homeassistant:8123/api/mcp",
"headers": { "Authorization": "Bearer ${HA_TOKEN}" }
},
"private-thing": { "command": "some-mcp", "args": [], "hub": false },
"paperless-readonly": {
"command": "paperless-mcp",
"allowTools": ["search_*", "get_document"],
"denyTools": ["delete_document"]
},
"untrusted": {
"type": "docker",
"image": "ghcr.io/example/untrusted-mcp@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"network": "none",
"memory": "256m"
}
}
}Stdio servers (command/args/env) are spawned as supervised child
processes. Remote servers (type: "http" or "sse" with url and optional
headers) are connected as MCP clients with the configured headers injected
on every request — the same supervision (ping, backoff reconnect, hot reload)
applies. An upstream that speaks OAuth gets an oauth block instead of a
header: the hub registers itself (statically, via RFC 7591 or via a client
metadata document), obtains the token and refreshes it, with one browser visit
started from the admin CLI where the grant needs a person.
"hub": false hides a server from the /hub aggregate; its own path keeps
working. allowTools / denyTools cut finer and apply to every kind of
server: a filtered tool is absent from both tools/list and /hub, and is
refused if called anyway — before the server is woken.
Reserved names: mcp, hub, authorize, token, register,
login, consent, health, livez, revoke, jwks, interaction,
session, userinfo, upstream, .well-known.
All stdio children share the hub's Unix user and can read its mounted files. Only install fully trusted stdio servers. A server with a different trust level belongs in its own container — and it does not have to speak HTTP to get there:
type: "docker"— the hub creates the container and talks stdio across the container boundary over the Docker API. No HTTP listener, no bearer token, no bridge process in the image. The hub itself never gets the Docker socket: a separatemcp-hub-docker-proxycontainer holds it and allows only the container operationsmcp.jsondescribes — nothing privileged, no host mounts, no other images. Credentials can live with the proxy (secretsFrom) so the hub process never holds them — and rotating one is just an edit: the proxy watches the file and recreates the sandbox with the new values.type: "unix"/"tcp"— you start the container, the hub connects to a socket. Costs the hub no privileges at all, and a Unix socket reaches a sandbox running withnetwork_mode: none.
Both carry the newline-delimited JSON-RPC the specification asks custom transports to reuse. See sandboxing and SECURITY.md.
For a custom image, pin every package to an exact version:
FROM ghcr.io/ni-c/mcp-hub:0.10.0 # pin @sha256:<digest> in production
USER root
RUN npm install -g your-mcp-package@1.2.3
USER nodeEnvironment
Without a non-empty PASSWORD or a bcrypt PASSWORD_HASH the HTTP hub still
starts, but its login is disabled: the startup log says so, the sign-in page
answers 503 with the reason, and no client can be approved — so no token can
be issued. A configured hash takes precedence, and a hash that is not a bcrypt
hash disables the login the same way rather than falling back to PASSWORD.
The local --stdio mode uses neither variable.
Variable | Required | Description |
| yes | Public base URL, e.g. |
| one of | bcrypt hash of the login password ( |
| one of | plain-text alternative to |
| no | comma-separated IPs/CIDRs allowed to set |
| no | RFC 8707 tokens bound to |
| no | server name (or |
| no | authenticated MCP JSON body limit, default |
| no | limit per OAuth client, default |
| no | in-flight request limit per OAuth client, default |
| no | open SSE listening streams per OAuth client — one per connected session, default |
| no | Node HTTP header timeout, default |
| no | complete request timeout, default |
| no | listen port (default 80 in the image, 3000 outside) |
| no | default |
| no | default |
| no | additionally mirror all log output into this file, e.g. |
| no | which mechanisms a client may use for a |
| no | bare https origins whose metadata documents are accepted; unset → any |
| no | local development only; relaxes the SSRF guard, warns on every start |
| no | ceiling on stored dynamic registrations, default |
| no | how long a never-approved registration is kept, default |
| no | how long an unused approved registration is kept, default |
| no | idle minutes before an on-demand server sleeps, default |
| no | snapshots of sleeping servers, default |
| no | deadline for one forwarded tool call, default |
| no | let progress notifications extend that deadline, default |
| with docker servers | the policy proxy's socket; a direct daemon socket fails closed |
The full table, including what applies in stdio mode, is in the environment reference.
/data holds the Ed25519 JWT key, registered OAuth clients, approvals and
refresh tokens. Mount it as a volume — recreating it invalidates every
connector authorization.
Every access token is bound to one resource. The OAuth client includes the
resource advertised by the endpoint's RFC 9728 document — no client-side
configuration needed — and the resulting token is valid only there: a token for
/paperless/mcp cannot call /hub, /health or another server. The shorter
/<name> route is canonicalized to /<name>/mcp.
RESOURCE_BOUND_TOKENS=false turns this off and is a migration mode for
deployments from 0.4 and earlier, where tokens were issued without a resource
and reach every path. The hub logs a warning while it is set. Removing it
invalidates those unbound tokens, so every connector authorizes once more.
TRUSTED_PROXIES decides what req.ip is, and therefore what the login rate
limiter counts. List only your own reverse proxy, and make sure it
overwrites X-Forwarded-For rather than appending to it — otherwise a
client can supply its own address and rotate it to sidestep the per-IP limit.
If the variable is unset, every request appears to come from the proxy and
per-IP limiting degrades to a single global counter (the hub logs a warning
at startup). A global cap of 100 failures per 15 minutes applies either way.
Running
Option A — prebuilt image from GHCR (recommended)
Published on every push to main and every vX.Y.Z release tag, for
linux/amd64 and linux/arm64. Browse the versions on the
package page.
docker pull ghcr.io/ni-c/mcp-hub:0.10.0Tags: latest (tip of main), X.Y.Z and X.Y (releases), and
sha-<commit> for a specific build.
Use a version tag instead of latest for controlled updates. For an immutable
deployment, record the resolved digest from docker image inspect and use
ghcr.io/ni-c/mcp-hub:<version>@sha256:<digest> in Compose.
With compose, copy the example and point it at the image instead of building:
services:
mcp-hub:
image: ghcr.io/ni-c/mcp-hub:0.10.0 # replaces `build: .`; pin a digest in production
# ...rest of docker-compose.example.yml unchangedcp docker-compose.example.yml docker-compose.yml # adjust, swap build → image
mkdir -p config && cp mcp.json.example config/mcp.json # adjust
mkdir -p data && sudo chown -R 1000:1000 data # container runs as uid 1000
docker compose up -dOr without compose:
mkdir -p data && sudo chown -R 1000:1000 data # container runs as uid 1000
docker run -d --name mcp-hub \
-p 127.0.0.1:7690:80 \
-e EXTERNAL_URL="https://mcp.example.net" \
-e PASSWORD_HASH="$(htpasswd -bnBC 10 '' 'yourpassword' | tr -d ':\n')" \
-e TRUSTED_PROXIES="192.168.1.0/24" \
-v "$PWD/config:/config:ro" \
-v "$PWD/data:/data" \
ghcr.io/ni-c/mcp-hub:0.10.0Update to a newer image with docker compose pull && docker compose up -d
(or docker pull …, then recreate the container).
Option B — build from source
cp docker-compose.example.yml docker-compose.yml # adjust
mkdir -p config && cp mcp.json.example config/mcp.json # adjust
docker compose up -d --buildOption C — npm (without a container)
CONFIG_PATH=./mcp.json DATA_PATH=./data PASSWORD_HASH='...' \
npx @ni-c/mcp-hubInstalls as @ni-c/mcp-hub
(the unscoped npm name belongs to an unrelated project) and provides the
mcp-hub and mcp-hub-admin binaries. The container remains the recommended
deployment — it provides the isolation, read-only root filesystem and resource
limits that SECURITY.md assumes.
Reverse-proxy requirements: TLS termination, WebSockets/SSE allowed (proxy
buffering off, a request timeout above 310 seconds, a request-body limit at or
below MCP_BODY_LIMIT, and pass X-Forwarded-Proto/Host.
Connect a client: add https://<host>/hub (or https://<host>/<name>/mcp
for one server) as a custom connector — in ChatGPT (developer mode), Claude
Web, Mistral Le Chat, Cursor, LibreChat or any other OAuth-capable MCP client —
and log in once with the password. Claude Code:
claude mcp add -t http name https://<host>/<name>/mcp. API-only clients
(OpenAI Responses API, xAI, Gemini API) use an admin-minted token instead —
see client compatibility.
Each client is confirmed once. Entering the password approves the client that asked; while a login session is still valid, a client you have not seen before gets an explicit Approve / Deny page instead of a code. Approved clients reconnect silently from then on.
List clients or revoke one. The CLI shares /data with the running hub and
both sides re-read the state file before they touch it, so this works against a
live container — a revocation takes effect on the next request:
docker exec mcp-hub node /app/dist/admin.js clients list
docker exec mcp-hub node /app/dist/admin.js clients revoke CLIENT_ID
docker exec mcp-hub node /app/dist/admin.js clients delete CLIENT_ID
docker exec mcp-hub node /app/dist/admin.js clients prune --dry-runRevocation removes the approval and all refresh tokens and immediately rejects
already-issued access tokens. The next connection needs explicit approval.
delete goes further and removes the registration itself, and prune applies
the registration lifecycle rules
on demand — registrations that were never approved expire after a day, unused
ones after 90 days, and a dynamically registered client can also remove its own
registration through RFC 7592.
For clients that cannot do OAuth at all — the OpenAI Responses API, the xAI
API, Gemini's mcp_server tool, plain-header clients — the same CLI mints
long-lived, resource-bound API tokens:
docker exec mcp-hub node /app/dist/admin.js tokens create --resource hub --days 90 --label "openai"
docker exec mcp-hub node /app/dist/admin.js tokens list
docker exec mcp-hub node /app/dist/admin.js tokens revoke TOKEN_IDThe token is printed once and never stored; tokens revoke takes effect
immediately. Per-client recipes:
client compatibility.
Endpoints
Path | Auth | Purpose |
| Bearer | Streamable HTTP endpoint of one server |
| Bearer | aggregate endpoint with the 6 meta-tools |
| none | minimal process liveness ( |
| Bearer | per-server status ( |
| — | OAuth 2.1 · CIMD + DCR; the login and consent pages live under |
| registration token | RFC 7592: a client reads, changes or removes its own registration |
| signed state + hub session | where an upstream returns after |
| none | the hub's own client metadata document, one per |
| none | RFC 8414 metadata |
| none | RFC 9728 metadata (path-scoped) |
Notes & limitations
Change notifications (
listChanged, resource updates) are carried on2026-07-28viasubscriptions/listen, whose state is the open response rather than a session table. A2025-11-25client is offered neither, because that revision needs a channel the stateless transport does not keep — so the capability is withheld instead of announced and dropped. An on-demand server watches nothing while it sleeps; the subscription is re-established on the next wake and the client is told to re-read.Elicitation travels end to end on
2026-07-28: it is a result rather than a push. Sampling and log messages are not forwarded.Access tokens are opaque and last 15 minutes. Revoking a client takes effect on its next request rather than when the token expires — the token is a reference to a stored record, so withdrawing it is a deletion. Refresh tokens rotate; replaying one that was already rotated away is treated as a leak and revokes the whole grant, access tokens included.
Upstream auth is fully decoupled from the hub's own OAuth: an expired upstream token just marks that one server
unauthorized(503 on its path, visible in/health) — clients never see the upstream's 401.One login can approve multiple connectors, but each token is valid only for its requested server or
/hub. Registration remains open as the MCP specification intends; a client only receives codes after confirmation and only at the confirmed redirect target.Failed logins are rate-limited (10/15 min per IP) and logged as
mcp-hub: authentication failure from <ip>for fail2ban.Auth pages deny framing and carry a restrictive CSP. MCP bodies are parsed only after bearer verification and are bounded by size, per-client request rate and per-client concurrency.
Logging to a file for fail2ban
LOG_FILE=/data/mcp-hub.log mirrors every log line into that file, one line
per entry with an ISO-8601 UTC prefix, while leaving the console output alone —
so docker logs keeps working. A jail then reads the file directly:
# /etc/fail2ban/filter.d/mcp-hub-auth.conf
[Definition]
failregex = mcp-hub: authentication failure from <HOST>\s*$
mcp-hub: login rate limit exceeded from <HOST>\s*$
mcp-hub: consent with an invalid CSRF token from <HOST>\s*$
ignoreregex =Only the hub's own lines are mirrored — the stdio children inherit stderr
directly, so their output stays in the container log and the file stays small.
Rotate it with logrotate (copytruncate, since the hub holds the file open).
Why not read the container's own logs instead: the Docker json-file path
contains the container ID and changes on every recreate, and the journald
driver maps all stderr to priority err — since an MCP server must keep
stdout free for the protocol and therefore logs to stderr, every ordinary line
would show up as a system error and drown out host monitoring.
Bans belong in the DOCKER-USER chain (banaction = iptables-allports) when
the hub is published through a container-based reverse proxy: that traffic
arrives via DNAT and FORWARD, and never passes INPUT.
Development
npm install
npm test # vitest: config, OAuth flow, proxy E2E, hub, hot reload
npm run dev # tsx, needs EXTERNAL_URL/PASSWORD/CONFIG_PATH/DATA_PATHAvailable Tools
6 toolscall_toolCall a tool on a serverADestructive
Call a tool on one of the MCP servers. Arguments must match the schema from get_tool_schema.
| Name | Required | Description | Default |
|---|---|---|---|
| tool | Yes | Tool name from list_tools | |
| server | Yes | Server name from list_servers | |
| arguments | No | Tool arguments matching its input schema |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already indicate destructiveHint: true and readOnlyHint: false, so the description need not repeat them. The description adds minimal behavioral context beyond input validation, and does not mention potential side effects or errors. It is not contradictory, but does not enrich the annotation-provided behavioral profile.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is extremely concise, consisting of two short sentences. It conveys the essential purpose and a key constraint without any fluff, making it easy for an agent to parse quickly.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given that there is no output schema, the description appropriately focuses on input requirements. It references get_tool_schema for argument validation, which is helpful context. It does not mention return behavior, but that is acceptable since the tool simply returns the result of the called tool, and the description is sufficient for an agent to invoke it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema provides complete descriptions for all three parameters (tool, server, arguments) with 100% coverage. The description adds no additional semantic meaning beyond restating that arguments must match the schema, which is already evident from the parameter name and description. Thus the baseline score of 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the action ('Call a tool') and the resource ('on one of the MCP servers'), and explicitly ties arguments to get_tool_schema. It is distinct from siblings like list_servers or get_tool_schema, making the purpose unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It implicitly guides usage by stating that arguments must match the schema from get_tool_schema, suggesting a prerequisite. However, it does not explicitly state when to choose this tool over alternatives (e.g., when to use list_tools vs call_tool), leaving some room for inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_tool_schemaGet the full schema of a toolARead-onlyIdempotent
Get the full description and JSON input schema of one tool, needed to construct arguments for call_tool.
| Name | Required | Description | Default |
|---|---|---|---|
| tool | Yes | Tool name from list_tools | |
| server | Yes | Server name from list_servers |
Output Schema
| Name | Required | Description |
|---|---|---|
| name | Yes | |
| server | Yes | |
| annotations | No | |
| description | Yes | The full description, untruncated. |
| inputSchema | Yes | JSON Schema for the arguments of call_tool. |
| outputSchema | No | JSON Schema the structuredContent of a call conforms to; absent if the tool declares none. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly and idempotent traits; the description adds no extra behavioral context (e.g., errors or side effects). It does not contradict annotations, but also does not enhance beyond them.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise—two sentences—and directly states the action, object, and purpose without redundant wording.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple read-only schema retrieval tool, the description covers purpose, parameters, and use case sufficiently. No output schema is needed since the description explicitly says it returns the schema.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Both parameters (tool, server) are described with clear origins: 'Tool name from list_tools' and 'Server name from list_servers', providing complete semantic meaning and matching the 100% schema coverage.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states 'Get the full description and JSON input schema of one tool' and specifies its purpose for constructing arguments to call_tool, effectively distinguishing it from sibling tools like list_tools or call_tool.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit usage context ('needed to construct arguments for call_tool'), indicating when to use it. Does not explicitly mention alternatives or when not to use, but the purpose is clear enough for most scenarios.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_serversList MCP serversARead-onlyIdempotent
List all MCP servers available through this hub, with their status. Call this first to see what is available. Servers marked "hidden" serve their tools only via their own endpoint, but wake_server/sleep_server still manage them.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| servers | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, covering the main behavioral traits. The description adds context about hidden servers and their relationship with wake/sleep, which goes beyond the annotations without contradicting them. It does not introduce new behavioral details but reinforces the non-destructive nature implicitly.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is three sentences, each serving a distinct purpose: stating the function, giving a usage directive, and explaining hidden-server behavior. It is front-loaded with the primary purpose and contains no redundant or extraneous information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's simplicity (no parameters, clear annotations, output schema present), the description fully covers what an agent needs: what the tool does, when to call it, and a notable nuance about hidden servers. The presence of an output schema means the return value does not need to be described in the text.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters, and the schema correctly reflects this (100% coverage with no fields to describe). Per the rubric, the baseline for 0 params is 4, and the description adds no unnecessary parameter details. The description's focus on server status and hidden behavior complements the empty parameter list.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: 'List all MCP servers available through this hub, with their status.' It also includes a directive to call this first, which clarifies its role compared to sibling tools like list_tools. The additional note about hidden servers adds specificity without ambiguity.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit usage guidance with 'Call this first to see what is available.' While it doesn't explicitly contrast with each sibling tool, the context makes it clear that this is the initial discovery step. The mention of hidden servers and wake_server/sleep_server hints at when further actions might be needed, fulfilling the requirement adequately.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_toolsList tools of a serverARead-onlyIdempotent
List the tools of one MCP server with one-line descriptions. Use get_tool_schema before calling a tool for the first time.
| Name | Required | Description | Default |
|---|---|---|---|
| server | Yes | Server name from list_servers |
Output Schema
| Name | Required | Description |
|---|---|---|
| tools | Yes | |
| server | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already establish readOnlyHint, idempotentHint, and destructiveHint false. The description adds value by scoping the operation to one server and disclosing the output level ('one-line descriptions'), which is behavioral context beyond the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two compact sentences, each carrying useful information: what the tool lists and a clear follow-up workflow. No filler or redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple single-parameter read-only list tool with an output schema and safety annotations, the description plus schema provides everything an agent needs to call it correctly. Workflow guidance to get_tool_schema before first use is a valuable addition.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the 'server' parameter is already documented as a server name from list_servers. The description's 'one MCP server' phrasing reinforces that the parameter selects one server but adds little semantic detail beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Description states a specific verb and resource: list the tools of one MCP server, and clarifies the output ('one-line descriptions'). It distinguishes the tool from siblings like list_servers and get_tool_schema without needing to inspect their schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description makes clear this is for listing a single server's tools and advises using get_tool_schema before first-time tool calls, which gives useful workflow context. It does not explicitly spell out when NOT to use it, but the sibling set makes that reasonably inferable.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
sleep_serverPut a server to sleepAIdempotent
Stop an on-demand server immediately instead of waiting for its idle timeout. It restarts automatically on the next tool call.
| Name | Required | Description | Default |
|---|---|---|---|
| server | Yes | Server name from list_servers |
Output Schema
| Name | Required | Description |
|---|---|---|
| name | Yes | |
| status | Yes | What the hub is currently doing with this server. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description adds behavioral details beyond the annotations: it explains the immediate stop, the automatic restart on next call, and implies non-destructive behavior. This supplements the idempotentHint and readOnlyHint annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise, two sentences, front-loaded with the action and key effect, with no unnecessary fluff.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given that an output schema exists, the description does not need to explain return values. It provides sufficient context for the tool's purpose, parameter, and behavioral implications.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The parameter 'server' is described with 'Server name from list_servers', which fully explains its meaning and source, covering 100% of the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the action ('Stop an on-demand server immediately') and its effect ('restarts automatically on the next tool call'), making the tool's purpose unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides usage guidance by contrasting with the idle timeout and mentioning automatic restart, but it does not explicitly compare with the sibling wake_server tool. Still, the core when-to-use context is present.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
wake_serverWake a sleeping serverAIdempotent
Start an on-demand server now so its first tool call is fast. No-op if it is already running.
| Name | Required | Description | Default |
|---|---|---|---|
| server | Yes | Server name from list_servers |
Output Schema
| Name | Required | Description |
|---|---|---|
| name | Yes | |
| status | Yes | What the hub is currently doing with this server. |
| toolCount | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Description adds meaningful behavioral detail beyond annotations, including the no-op behavior and the side effect of waking a server. Annotations already cover idempotency and non-destructiveness, and the description is consistent with them.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short, focused sentences with no filler. The primary purpose is stated first, followed by the important no-op edge case.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple one-parameter tool with an output schema, the description covers the essential context: when to invoke, the effect, and the idempotent behavior. No significant information is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The only parameter 'server' is fully documented in the schema as 'Server name from list_servers', providing 100% coverage. The description adds no additional parameter detail, so baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Clearly states the action ('Start an on-demand server'), the target ('server'), and the benefit ('so its first tool call is fast'). Also distinguishes behavior with the no-op condition and contrasts indirectly with sleep_server.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides clear context for when to use the tool ('start now', 'first tool call fast') and includes the no-op condition. Does not explicitly name alternatives like sleep_server, but the intended usage is inferable.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
6 tool updates
v0.11.2- Changed
call_tool2 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - changed
Input schema / properties / arguments / additionalPropertiesPrevious value: -{}New value: +true
- Changed
get_tool_schema2 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "annotations": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "description": { + "description": "The full description, untruncated.", + "type": "string" + }, + "inputSchema": { + "additionalProperties": true, + "description": "JSON Schema for the arguments of call_tool.", + "properties": {}, + "type": "object" + }, + "name": { + "type": "string" + }, + "outputSchema": { + "additionalProperties": true, + "description": "JSON Schema the structuredContent of a call conforms to; absent if the tool declares none.", + "properties": {}, + "type": "object" + }, + "server": { + "type": "string" + } + }, + "required": [ + "server", + "name", + "description", + "inputSchema" + ], + "type": "object" +}
- Changed
list_servers2 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "servers": { + "items": { + "additionalProperties": false, + "properties": { + "description": { + "description": "The child's own title, empty if it has never connected.", + "type": "string" + }, + "hidden": { + "const": true, + "description": "Present only for a server whose tools are served by its own endpoint alone.", + "type": "boolean" + }, + "name": { + "description": "Name to pass to the other tools.", + "type": "string" + }, + "status": { + "description": "What the hub is currently doing with this server.", + "enum": [ + "starting", + "up", + "down", + "stopped", + "sleeping", + "unauthorized" + ], + "type": "string" + }, + "toolCount": { + "description": "Tools this server offers through the hub, after its filter.", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + } + }, + "required": [ + "name", + "description", + "status", + "toolCount" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "servers" + ], + "type": "object" +}
- Changed
list_tools2 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "server": { + "type": "string" + }, + "tools": { + "items": { + "additionalProperties": false, + "properties": { + "annotations": { + "additionalProperties": true, + "description": "The child's own annotations, verbatim; absent if it declared none.", + "properties": {}, + "type": "object" + }, + "description": { + "description": "First line only, capped at 120 characters.", + "type": "string" + }, + "hasOutputSchema": { + "const": true, + "description": "Present when the tool declares an output schema, which get_tool_schema returns.", + "type": "boolean" + }, + "name": { + "type": "string" + } + }, + "required": [ + "name", + "description" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "server", + "tools" + ], + "type": "object" +}
- Changed
sleep_server2 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "status": { + "description": "What the hub is currently doing with this server.", + "enum": [ + "starting", + "up", + "down", + "stopped", + "sleeping", + "unauthorized" + ], + "type": "string" + } + }, + "required": [ + "name", + "status" + ], + "type": "object" +}
- Changed
wake_server2 fields changed- changed
Input schema / $schemaPrevious value: -"http://json-schema.org/draft-07/schema#"New value: +"https://json-schema.org/draft/2020-12/schema" - changed
Output schema / (root)Previous value: -nullNew value: +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "status": { + "description": "What the hub is currently doing with this server.", + "enum": [ + "starting", + "up", + "down", + "stopped", + "sleeping", + "unauthorized" + ], + "type": "string" + }, + "toolCount": { + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + } + }, + "required": [ + "name", + "status", + "toolCount" + ], + "type": "object" +}
6 tool updates
v0.9.0- First observed
call_tool - First observed
get_tool_schema - First observed
list_servers - First observed
list_tools - First observed
sleep_server - First observed
wake_server
TDQS
Scored across 6 tools
Each tool has a clearly distinct role: discovery (list_servers, list_tools), schema inspection, execution, and lifecycle management. No two tools overlap in purpose.
All tool names follow the consistent verb_noun pattern with snake_case, making the API predictable and easy to navigate.
Six tools is well-scoped for a hub server: discovery, schema access, execution, and on-demand lifecycle management. Each tool earns its place.
The tool set covers the full hub workflow: listing servers, listing and inspecting tools, calling tools, and managing server lifecycle. No obvious gaps for the stated purpose.
Maintenance
Related MCP Connectors
Self-hosted federated MCP gateway: one OAuth 2.1 MCP server in front of N apps, user-level scopes.
MCP Hub: AI service discovery, per-user OAuth, and multi-service workflow orchestration
Governed MCP gateway: one endpoint for your tools, with credential custody and audit log.
MCP server for Argo RPG Platform — connects AI assistants to campaign data via OAuth2
Related MCP Servers
- AlicenseAqualityCmaintenanceA hub server that connects to and manages other MCP (Model Context Protocol) servers.722 npm62MIT
- AlicenseNot gradedqualityAmaintenanceA unified hub for centrally managing and dynamically orchestrating multiple MCP servers/APIs into separate endpoints with flexible routing strategies.547 npm2,426Apache 2.0
- FlicenseNot gradedqualityNot gradedmaintenanceMCP Hub aggregates and proxies multiple Model Context Protocol servers into a unified Streamable HTTP interface. It allows users to combine diverse stdio, SSE, and HTTP-based servers while providing tool namespacing, health monitoring, and secure authentication.700 npm-
- AlicenseNot gradedqualityBmaintenanceA self-hosted MCP hub that unifies multiple MCP server configurations behind a single local endpoint, with a web dashboard for management, live tool-call logging, and per-person access tokens.4MIT