MCP Manager
by Feanus
README.md
# MCP Manager
MCP Manager exposes multiple MCP servers through one Streamable HTTP gateway.
The gateway provides a single client endpoint, namespaces backend tools, and
routes each call to either a registered local server host or a remote MCP
server.
The default profile enables the Arxiv and calculator servers.
## Architecture
```text
+--> server-host --> stdio MCP server
MCP client --> MCP gateway ---+--> server-host --> stdio MCP server
+--> remote Streamable HTTP MCP server
```
- Clients connect to the gateway at `http://<gateway>:8080/mcp`.
- The gateway loads a profile, discovers tools, exposes them with each
manifest's configured namespace and separator, and routes calls to healthy
backends. The default separator is `__`; Atlas manifests use `_`.
- A `server-host` process owns one stdio MCP child, exposes it over Streamable
HTTP, and registers it with the gateway. The gateway never starts stdio
processes itself.
- Remote Streamable HTTP backends are contacted directly and do not need a
`server-host` process.
- Catalog manifests describe how servers run; profiles decide which servers
and tools a deployment exposes.
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the runtime and security
boundaries.
## Repository components
| Location | Purpose |
| --- | --- |
| `catalog/servers/` | One server manifest per MCP backend: transport, command or URL, environment, lifecycle, concurrency, credentials, and tool namespace. |
| `profiles/` | Deployment profiles that select servers and apply tool and credential policies. |
| `src/mcp_manager/gateway/` | The client-facing MCP gateway, tool registry, and registration API. |
| `src/mcp_manager/host/` | The HTTP host used to run and supervise one stdio MCP server. |
| `src/mcp_manager/runtime/` | Backend connections, routing, concurrency, and subprocess supervision. |
| `src/mcp_manager/config/` | Typed models and YAML configuration loading. |
| `services/` | Local adapters for MCP servers that need repository-specific behavior. |
| `scripts/` | Convenience launchers for the gateway and server hosts. |
| `examples/` | Curl and Python clients for discovery and tool calls. |
Backend-specific profiles, credentials, and examples are documented in
[profiles/README.md](profiles/README.md).
## Deploy the gateway and MCP servers
Install Python 3.11 or newer and
[`uv`](https://docs.astral.sh/uv/), then install the project:
```bash
uv sync --extra dev
```
Node.js 20 or newer is also required for profiles that use Node-based servers.
Package runners such as `uvx` and `npx` may download a pinned backend the first
time it starts.
### Launch with the convenience scripts
The launchers bind on all interfaces, derive the advertised address from the
first value returned by `hostname -i`, and share `gateway.json` in the
repository root. Override `MCP_MANAGER_ADVERTISE_HOST` when the first address
is not reachable by peers.
Start the default Arxiv-and-calculator gateway on the current machine:
```bash
MCP_MANAGER_PROFILE=profiles/default.yaml scripts/run_gateway.sh
```
For a local deployment, start one host per selected stdio server in separate
terminals. Hosts on the same IP require distinct ports:
```bash
MCP_MANAGER_ADVERTISE_HOST=127.0.0.1 \
MCP_MANAGER_SERVER_PORT=9001 scripts/run_mcp_servers.sh arxiv
MCP_MANAGER_ADVERTISE_HOST=127.0.0.1 \
MCP_MANAGER_SERVER_PORT=9002 scripts/run_mcp_servers.sh calculator
```
For the Brain cluster, allocate one CPU machine per server. Each allocated
machine has its own IP, so every host may use the default port `9001`:
```bash
scripts/allocate_cpu.sh arxiv
scripts/allocate_cpu.sh calculator
```
The allocator reads `spec.resources.cpu` and `spec.resources.memoryMiB` from
the selected stdio server's catalog manifest. Override either value for a
particular launch when needed:
```bash
MCP_MANAGER_CPU=16 MCP_MANAGER_MEMORY_MIB=32000 \
scripts/allocate_cpu.sh arxiv
```
The lookup uses the validated `mcp-manager server-resources` command rather
than parsing YAML in shell. Environment overrides take precedence over the
manifest values.
The gateway and hosts automatically append the cluster route
`100.96.0.0/12` to `NO_PROXY` and `no_proxy`. Internal registration and MCP
connections also disable HTTPX environment proxies directly. Override the
route with `MCP_MANAGER_CLUSTER_NO_PROXY` if the deployment network changes.
The discovery file contains the gateway URL and a new generation ID on each
gateway startup. Hosts reread it before every registration attempt, retain the
last valid record during filesystem errors, and reconnect after a gateway URL
change. A host keeps its stdio child alive while the gateway is unavailable.
Useful overrides are:
| Variable | Default | Purpose |
| --- | --- | --- |
| `MCP_MANAGER_PROFILE` | `profiles/all.yaml` | Gateway profile to expose. |
| `MCP_MANAGER_GATEWAY_PORT` | `8080` | Gateway listen and advertised port. |
| `MCP_MANAGER_SERVER_PORT` | `9001` | One server-host listen and advertised port. |
| `MCP_MANAGER_CPU` | catalog `spec.resources.cpu` | Per-launch CPU allocation override. |
| `MCP_MANAGER_MEMORY_MIB` | catalog `spec.resources.memoryMiB` | Per-launch memory allocation override. |
| `MCP_MANAGER_GATEWAY_DISCOVERY_FILE` | `<repo>/gateway.json` | Shared discovery record. |
| `MCP_MANAGER_ADVERTISE_HOST` | first `hostname -i` address | Peer-reachable hostname or IP. |
| `MCP_MANAGER_CLUSTER_NO_PROXY` | `100.96.0.0/12` | Private cluster route. |
The gateway can start before its hosts. `/health` reports process liveness;
`/ready` remains HTTP 503 until every backend selected by the profile has a
ready instance. A standalone host's `/health` checks its local MCP child, while
its `/ready` additionally requires successful gateway registration.
### Verify and call the deployment
Set the addresses reported by `hostname -i` or `brainctl get process`:
```bash
GATEWAY_IP=100.104.35.33
ARXIV_IP=100.100.90.20
```
Check the gateway and direct Arxiv host:
```bash
curl --noproxy '*' "http://$GATEWAY_IP:8080/health"
curl --noproxy '*' "http://$GATEWAY_IP:8080/ready"
curl --noproxy '*' "http://$ARXIV_IP:9001/health"
curl --noproxy '*' "http://$ARXIV_IP:9001/ready"
uv run python examples/list_mcp_tools.py \
--url "http://$ARXIV_IP:9001/mcp"
```
List only the gateway's namespaced Arxiv tools:
```bash
curl --noproxy '*' --fail --silent --show-error \
-X POST "http://$GATEWAY_IP:8080/mcp" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {"_meta": {"servers": ["arxiv"]}}
}' | python -m json.tool
```
Search Arxiv through the gateway:
```bash
curl --noproxy '*' --fail --silent --show-error \
-X POST "http://$GATEWAY_IP:8080/mcp" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "arxiv_search_papers",
"arguments": {
"query": "\"attention mechanism\"",
"max_results": 5,
"sort_by": "relevance"
}
}
}' | python -m json.tool
```
Call the calculator or run the 1000-request concurrency test:
```bash
curl --noproxy '*' --fail --silent --show-error \
-X POST "http://$GATEWAY_IP:8080/mcp" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "calculator_calculate",
"arguments": {"expression": "88 * 2345"}
}
}' | python -m json.tool
uv run python examples/load_test_calculator.py \
--url "http://$GATEWAY_IP:8080/mcp" \
--requests 1000 --concurrency 32 --expression '88 * 2345'
```
The calculator expression returns `206360`. The load tester reports success
and failure counts, throughput, end-to-end p50/p95/p99 latency, and a sample
MCP response. It exits nonzero if any request fails.
### Use request-scoped credentials
Credentialed backends receive API keys from the MCP client on each request.
The real key is never stored in a catalog manifest or profile. It belongs at:
```text
params._meta.credentials.<server_name>.<credential_field>
```
For example, start a gateway that exposes the hosted GitHub MCP server:
```bash
MCP_MANAGER_PROFILE=profiles/github.yaml scripts/run_gateway.sh
```
Keep the token in the client process environment and list the GitHub tools:
```bash
export GITHUB_TOKEN='<your-github-token>'
uv run python examples/list_mcp_tools.py \
--url "http://$GATEWAY_IP:8080/mcp" \
--server github \
--credential-env github=GITHUB_TOKEN
```
`--credential-env` is a client-side convenience: it reads `GITHUB_TOKEN` and
adds it to `params._meta.credentials.github.apiKey`. The gateway does not read
that environment variable. The equivalent `tools/call` request is:
```bash
curl --noproxy '*' --fail --silent --show-error \
-X POST "http://$GATEWAY_IP:8080/mcp" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
--data-binary @- <<JSON
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"_meta": {
"credentials": {
"github": {"apiKey": "${GITHUB_TOKEN}"}
}
},
"name": "github__get_me",
"arguments": {}
}
}
JSON
```
A single-field server can receive up to 64 interchangeable keys in one call:
```json
{
"_meta": {
"credentials": {
"github": {"apiKey": ["<github-token-1>", "<github-token-2>"]}
}
}
}
```
For a rate-limited server, the first applicable policy atomically chooses the
offered key with the earliest next admission time. Any later applicable
policies admit that same key. This lets an immediately available key run
instead of waiting behind a recently used key. When no rate-limit policy is
configured, all keys are equally available and calls rotate across them. A
retry performs selection again. Repeating `--credential-env` for the same
server and field builds the equivalent pool in the example client.
Send the declared fields on both credential-dependent `tools/list` and
`tools/call` requests. A `tools/list` request may include credentials for
multiple servers so their tools can be discovered together. Each `tools/call`
invokes one tool and may include credentials only for that tool's routed server.
Discovery uses the first offered key and does not consume rate-limit admission.
Multi-field backends use the same structure—for example,
`credentials.oxylabs.username` and `credentials.oxylabs.password`. See
[profiles/README.md](profiles/README.md#request-scoped-credentials) for every
included server's field names and more examples.
If a shared filesystem is unavailable, use explicit `--gateway-url` and
`--advertise-url` values instead of the discovery-file options. Remote HTTP
entries such as GitHub and Hugging Face do not need a `server-host` process.
The registration API is unauthenticated, so expose it only on a trusted private
network or place an authenticating proxy in front of it.
## Modify the configuration
Configuration is split into two layers:
1. A file in `catalog/servers/` defines a backend and how to connect to it.
2. A file in `profiles/` selects catalog entries and controls which tools and
credential uses are allowed for one deployment.
For example, this profile exposes every Arxiv tool but only the calculator's
`calculate` tool:
```yaml
apiVersion: mcp.manager/v1alpha1
kind: MCPProfile
metadata:
name: example
spec:
servers:
- serverRef: arxiv
tools:
allow: [] # empty means all discovered tools
deny: []
- serverRef: calculator
tools:
allow: [calculate]
deny: []
```
To change a deployment:
1. Copy or edit a profile under `profiles/`.
2. Add or remove `serverRef` entries and adjust `tools.allow`/`tools.deny`.
3. Start a `server-host` for every selected stdio entry. Add a manifest under
`catalog/servers/` first if the backend is new.
4. Validate the result, then restart the gateway. Restart a host as well when
its catalog manifest changed.
```bash
uv run mcp-manager --profile profiles/example.yaml list-servers
uv run mcp-manager --profile profiles/example.yaml doctor
```
Server manifests can define stdio commands, remote HTTP URLs, non-secret fixed
environment values, request-scoped credential slots, lifecycle policy, and
concurrency limits. See [docs/ADDING_SERVERS.md](docs/ADDING_SERVERS.md) for the
complete schema patterns.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues