MCP aggregating gateway for ubo-app; communicates with it over its gRPC API
Project description
ubo-app MCP gateway
A standalone subprocess that aggregates every enabled MCP server the user has configured behind a single, token-gated HTTP endpoint. It is the network boundary for MCP in ubo-app: any client — the on-device assistant, Claude Desktop, hermes, OpenClaw, etc. — talks to this one endpoint instead of spawning/managing MCP servers itself.
Why it exists
MCP server management used to live inside the assistant service, so the tools
were reachable only by the assistant. Extracting the gateway into its own
service (ubo_app/services/090-mcp) decouples MCP from the assistant: the
gateway owns the servers, and the assistant becomes just one more client of the
aggregated endpoint.
How it works
┌──────────────── ubo-app core (gRPC :50051) ───────────────┐
│ state.mcp slice (servers, enabled set, metadata) │
└─────────────▲──────────────────────────────┬──────────────┘
│ autorun subscription │ query_secret
│ (enabled servers + metadata) │ (bearer token)
┌─────────────┴──────────────────────────────▼──────────────┐
│ MCP gateway subprocess (this service) │
│ │
│ state → build_mcp_config() → FastMCP proxy │
│ ├── backend: server A (stdio) │
│ ├── backend: server B (sse) │
│ └── … │
│ served over /mcp (Streamable HTTP) + /sse │
│ guarded by Bearer <token> │
└────────────────────────────▲───────────────▲─────────────┘
│ │
assistant client external clients
(Streamable HTTP) (Claude Desktop, …)
- Startup (
main.py) — connects to ubo-app's gRPC store, reads the gateway bearer token from secrets (MCP_GATEWAY_TOKEN_SECRET_ID). If no token is configured it logs and exits rather than serve an open endpoint. - State → config (
gateway.py) — subscribes viaautoruntostate.mcp.enabled_mcp_servers_with_metadataand translates each enabled server's betterproto config (thestdio_mcp_config/sse_mcp_configoneof) into a FastMCP{"mcpServers": {...}}config. Servers with an unrecognized/empty config are skipped with a warning. - Serving (
server.py) — builds a singleFastMCPproxy from that config and exposes its aggregated tools over both transports: Streamable HTTP at/mcpand SSE at/sse. A pure-ASGI_BearerAuthMiddlewarerejects any request withoutAuthorization: Bearer <token>(pure ASGI so it doesn't buffer the streaming responses).
The persistent-socket rebuild (the one non-obvious bit)
FastMCP has no API to remove a backend from a running proxy. So whenever the enabled-server set changes, the whole proxy and its ASGI apps must be rebuilt.
To avoid dropping the port (and breaking connected clients) during a rebuild,
GatewayServer owns a persistent listening socket that is created once and
never closed. The serve loop:
- builds the app for the current config and runs a
uvicorn.Serveron that shared socket; - waits on an
asyncio.Eventthat the autorun callback sets when the desired config differs from the running one; - on change, signals
should_exit, awaits the old server, and loops to rebuild — the listener stays bound the entire time, so the endpoint never disappears.
An empty config still serves a valid (tool-less) gateway rather than crashing.
Configuration
Environment is provided by the service's ubo_handle.py::binary_env_provider:
| Variable | Meaning | Default |
|---|---|---|
MCP_GATEWAY_TOKEN_SECRET_ID |
secret id holding the bearer token | mcp_gateway_token |
MCP_GATEWAY_LISTEN_ADDRESS |
bind address | 0.0.0.0 |
MCP_GATEWAY_LISTEN_PORT |
bind port | 4322 |
UBO_MCP_GATEWAY_LOG_LEVEL |
log level | INFO |
UBO_MCP_GATEWAY_LOG_PATH |
log file path | ./ubo-mcp-gateway.log |
Server definitions themselves live on disk under CONFIG_PATH/mcp_servers/
({name}_{uuid}/config.json), owned by the core MCP service, not this
subprocess. This subprocess never reads the filesystem — it only consumes the
state.mcp slice over gRPC.
Boundaries
- Source of truth is the store. This process is a pure projection of
state.mcp; it dispatches nothing and persists nothing. - Stdio MCP backends are launched by FastMCP as child processes of this
subprocess, so their runtimes (e.g.
uvx,npx) must be onPATHhere. See the runtime-provisioning note below / in the service-level docs.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ubo_app_mcp_gateway-2.0.1.dev260702103102559955.tar.gz.
File metadata
- Download URL: ubo_app_mcp_gateway-2.0.1.dev260702103102559955.tar.gz
- Upload date:
- Size: 9.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3d61f9e24315f98b5a9ffa85ea4a5488b9a107eb1a586692becc3728dc374d69
|
|
| MD5 |
41c9f90947c873c409495c869804696b
|
|
| BLAKE2b-256 |
71a4c7fa6286291a23543564e7c4a993c9bb3d4cbb2dfa722d527ee2f7bc57c7
|
File details
Details for the file ubo_app_mcp_gateway-2.0.1.dev260702103102559955-py3-none-any.whl.
File metadata
- Download URL: ubo_app_mcp_gateway-2.0.1.dev260702103102559955-py3-none-any.whl
- Upload date:
- Size: 10.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
51204751e1f6b27fc4bd32185b105ad7fc6ac061000173fdf7d2479ab9f9fda7
|
|
| MD5 |
eb2f45952445dd3e9646b42205c43152
|
|
| BLAKE2b-256 |
67b8696953811118e6446b88e0c662b0e3afe01390b14e7a0ad9e6106b307858
|