Skip to main content

A lean, FastMCP-based gateway that turns a registry of upstream MCP servers into one governed, namespaced MCP endpoint mounted on FastAPI.

Project description

fast-gateway

Python Built on FastMCP FastAPI Checked with mypy Lint: ruff License: MIT

A lean Python package that mounts on FastAPI and turns a registry of upstream MCP servers into one governed, namespaced MCP endpoint. The core stays thin; everything cross-cutting — auth, policy, human-in-the-loop, redaction, audit, cost limits — is a hook function you pass at mount time, or a plugin that bundles several together.

many upstream MCP servers  ──►  fast-gateway  ──►  one governed /mcp endpoint
   github, slack, jira…          (namespaced + filtered + policy-checked)

[!NOTE] Status: 0.0.3, under active development. APIs may change. Implemented and tested: the server registry, proxy builder, full hook pipeline, groups with allow/deny, group-scoped endpoints, the plugin system, the search_tools / describe_tool meta-tools, the bundled reference hooks (audit / deny / confirm), the local fast-gateway CLI + config/policy files, the browser human-in-the-loop plugin, and a Docker image — see the roadmap.

Why

Point an LLM at a dozen MCP servers directly and you get a dozen connections, a dozen auth schemes, no namespacing, no central policy, and no way to hide a dangerous tool. fast-gateway puts one endpoint in front of them all:

  • One connection, many servers — register upstreams in a store; the gateway proxies each under its own namespace (github_*, slack_*, …).
  • Governed — filter which tools are exposed, gate calls behind policy or human approval, redact results, audit everything — all as hooks.
  • Reuse, don't rebuildFastMCP already does proxying, transport bridging, composition/namespacing, and protocol middleware. This package builds only what it lacks: the registry, groups, the builder, the hook runner, and the plugin seam.

Features

  • Server registry (Store) — persistent CRUD over upstream MCP servers; ships with a zero-setup SqliteStore, swappable for Postgres / Redis / in-memory via one protocol.
  • Namespaced proxying — each enabled server is mounted under its name as a prefix; reload() rebuilds the mounts from the registry.
  • Five hook seamspre_mcp_connect, pre_list_tools, pre_tool_call, confirmation, post_tool_call. Auth, policy, HIL, redaction, audit, and cost limits are all plain async functions.
  • Access control — per-server and per-group allow/deny glob lists, enforced on both list_tools (hides) and call_tool (blocks).
  • Groups & group-scoped endpoints — expose a curated subset of servers/tools at /mcp/g/{group}, served by the same shared MCP app (no per-group duplication).
  • Plugins — bundle hooks, FastMCP middleware, an admin router, ASGI mounts, and meta-tools into one named extension with setup / teardown.
  • Optional policy engine — an agt extra wires Microsoft's agent-governance-toolkit (agent-os) in as a policy plugin.
  • Local CLI & Dockerfast-gateway serve / add / list / group / connect, a TOML config + policy file, a browser human-in-the-loop approval page, and a Docker image for self-hosting.
  • Typed throughoutmypy --strict, py.typed, full type hints.

Architecture

FastAPI app
 ├── /admin       → APIRouter (CRUD: servers, groups, reload)         [we build]
 ├── /mcp         → FastMCP.http_app()  (the gateway MCP server)      [FastMCP]
 │                    ├── mount(proxy_github, namespace="github")
 │                    ├── mount(proxy_slack,  namespace="slack")      ← namespacing
 │                    └── HookMiddleware + search meta-tools
 └── /mcp/g/{grp} → same MCP app, scoped to one group's servers/tools [we build]

The gateway is a parent FastMCP server that proxies each registered upstream and mounts it under a namespace, exposed as an ASGI app you mount onto your own FastAPI app alongside an admin router for CRUD. A HookMiddleware and an AccessPolicy wrap every list_tools / call_tool request.

Two ways to run it

The same gateway runs in two distinct modes — pick the one that fits:

A. Embed (library) B. Standalone (CLI/Docker)
Who App developers Anyone running it locally / self-hosted
How create_gateway(...) + gateway.install(your_fastapi_app) fast-gateway serve --config gateway.json
Governance You write hooks / plugins in Python JSON policy + bundled reference hooks
Human-in-the-loop Your own confirmation hook Built-in browser approval plugin
Where it lives Inside your service, your routes, your auth A daemon you point your agent at
Start here Quickstart Run it locally

Mode A mounts the gateway into your own API, controlling every seam in Python — the path to production. Mode B is a complete, ready-to-use local daemon: no Python, config-driven, with the browser HIL and OAuth login already wired in — use it as-is to put your MCP servers behind one governed endpoint. It is also the most direct way to learn fast-gateway: its entire wiring is one small function, factory.build_app, that composes create_gateway with the bundled reference hooks and the HIL plugin. Experiment locally with policy, groups, and approvals, then read factory.build_app to see exactly how to assemble your own gateway when you move to Mode A.

Getting started

Prerequisites

  • Python 3.11+
  • uv (recommended) or pip

Install

uv add fast-gateway        # or: pip install fast-gateway

Quickstart

Mode A — embed in your FastAPI app. Mount the gateway into your own service and wire governance in Python. For the no-code standalone daemon, see Run it locally (Mode B).

import os
from fastapi import FastAPI
from fast_gateway import ConnectContext, ConnectSettings, Hooks, SqliteStore, create_gateway


async def inject_auth(ctx: ConnectContext) -> ConnectSettings | None:
    # Auth is just a hook: return headers merged over the server's static headers.
    if ctx.server.name == "github":
        return ConnectSettings(headers={"Authorization": f"Bearer {os.environ['GH_TOKEN']}"})
    return None


gateway = create_gateway(
    store=SqliteStore("gateway.db"),
    hooks=Hooks(pre_mcp_connect=[inject_auth]),
)

# The MCP server manages sessions via lifespan, so wire it on the host app:
app = FastAPI(lifespan=gateway.lifespan)
gateway.install(app)            # mounts /admin (CRUD) and /mcp (MCP endpoint)

Register an upstream server through the admin API, then reload:

curl -X POST http://127.0.0.1:8000/admin/servers \
  -H 'content-type: application/json' \
  -d '{"name": "math", "url": "http://127.0.0.1:9000/mcp/", "transport": "http"}'

curl -X POST http://127.0.0.1:8000/admin/reload

Its tools now appear at the gateway endpoint under the math_ namespace.

Run the bundled example

make run          # uv run uvicorn examples.basic_app:app --reload
# Admin + OpenAPI docs: http://127.0.0.1:8000/docs
# MCP endpoint:         http://127.0.0.1:8000/mcp/

Run it locally (CLI & Docker)

Mode B — standalone daemon. This section is the no-code path: a configured daemon you point your agent at. For embedding the gateway in your own FastAPI app, see Quickstart (Mode A) instead.

Don't want to write Python? Install the CLI extra and run the gateway as a local daemon, then point your coding agent (Claude Code, etc.) at the one endpoint.

uv tool install --from fast-gateway 'fast-gateway[cli]'   # or: pipx install 'fast-gateway[cli]'
# or, from a checkout: uv tool install --from . 'fast-gateway[cli]'
fast-gateway serve
# Created default config at /Users/you/.fast-gateway/gateway.json   ← first run only
# MCP endpoint  : http://127.0.0.1:8000/mcp/
# Admin API     : http://127.0.0.1:8000/admin
# OpenAPI docs  : http://127.0.0.1:8000/docs
# OAuth status  :   - datadog: ready                               ← per OAuth upstream

fast-gateway auto-creates ~/.fast-gateway/gateway.json (with sane policy + HIL defaults) on first run; subsequent runs reuse it. Override with --config <path>, $FAST_GATEWAY_CONFIG, or drop a ./gateway.json next to your shell — the CLI checks all three in that order.

Register your upstream MCP servers once, from another shell — they persist in the SQLite registry and reload live:

fast-gateway add deepwiki https://mcp.deepwiki.com/mcp
fast-gateway add context7 https://mcp.context7.com/mcp --deny "*_delete_*"
fast-gateway list
fast-gateway group create readonly
fast-gateway group members readonly --server deepwiki --server context7

Then connect your agent once — and never reconfigure it again:

fast-gateway connect          # prints the `claude mcp add …` command + a .mcp.json block
# claude mcp add --transport http gateway http://127.0.0.1:8000/mcp/

Because the agent points at one stable endpoint, adding more servers later needs no agent changes — the new tools flow through the same /mcp and appear the next time the agent lists tools (on (re)connect). See fast-gateway --help for remove, enable, disable, reload, and group commands.

[!NOTE] Upstreams are HTTP/SSE only. To put a local stdio server (e.g. npx …) behind the gateway, bridge it to HTTP first — fastmcp run your_server.py --transport http or a tool like mcp-proxy — then fast-gateway add the resulting URL.

OAuth-protected upstreams

Many hosted MCP servers (Datadog, etc.) require an OAuth login. Register the server with --oauth, then run the browser login once — tokens are cached on disk and refreshed automatically thereafter, so the gateway connects unattended after that.

fast-gateway add datadog https://<your-dd-mcp-endpoint> --oauth --scope read
fast-gateway login datadog          # opens the browser, completes OAuth, caches tokens
fast-gateway logout datadog         # clear the cached tokens for a server

Run login up front to avoid a browser popup during a daemon reload. Tokens live under ~/.fast-gateway/oauth (override with oauth_token_dir in the config or $FAST_GATEWAY_OAUTH_DIR); the cache is shared between the CLI and the daemon. The CLI auto-discovers the same config the daemon uses, so a customised oauth_token_dir is picked up automatically — no need to pass --config to login/logout unless you want to point at a non-default file. For headless hosts, run login on a machine with a browser, or use the upstream's API-key header fallback via --header. After a fresh fast-gateway add datadog … --oauth, the CLI prints Next: fast-gateway login datadog as a reminder, and fast-gateway serve reports per-server token status at startup so you can spot a missing login before any agent traffic hits it.

[!WARNING] Security: the /mcp endpoint is unauthenticated. The daemon holds upstream OAuth refresh tokens, so do not expose the mapped port to untrusted networks. Set admin_token in your config to protect the admin API. Inbound MCP authentication and encryption-at-rest for the token cache are planned follow-ups — see the roadmap.

[!NOTE] OAuth is a Mode-B-only feature (OAuthPlugin), wired automatically by build_app (the CLI / fast-gateway serve path). It is not registered in a Mode-A embedded mount (create_gateway called directly). OAuth requires a human at a terminal to complete the browser authorization-code flow, so it is not appropriate for a headless library embedding. In Mode A, inject auth tokens via a pre_mcp_connect hook in ConnectSettings instead — see the Quickstart example.

Human-in-the-loop, in the browser

The config's policy object drives governance with plain globs: deny hard-blocks tools, confirm routes them through human approval, and audit logs every call.

// gateway.json
{
  "policy": {
    "confirm": ["*_delete_*", "*_write_*"],   // these need a human "yes"
    "audit": true
  },
  "hil": { "enabled": true, "auto_open_browser": true }
}

When an agent calls a confirm-matched tool, the gateway opens a browser approval page showing the tool name, its arguments, and the reason, and blocks the call until you click Approve or Deny (a timeout denies — fail-safe). The approval page lives at /admin/hil; in a headless/Docker run the approval URL is logged for you to open manually.

Docker

cp examples/gateway.json gateway.json     # edit to taste
docker compose up --build                 # serves on http://localhost:8000
fast-gateway add deepwiki https://mcp.deepwiki.com/mcp   # CLI talks to the mapped port

The image ships the gateway + CLI; the registry DB persists on the gateway-data volume. (stdio upstreams need their runtimes — bridge them to HTTP as noted above.)

Hooks

A hook is an async function, grouped in a Hooks container and passed at mount time. Each binds to the layer where it belongs:

Hook Binds to Runs
pre_mcp_connect proxy client factory before opening an upstream session
pre_list_tools HookMiddleware.on_list_tools on catalog requests
pre_tool_call HookMiddleware.on_call_tool (pre) before forwarding a call
confirmation on_call_tool (when REQUIRE_CONFIRMATION) human-in-the-loop approval
post_tool_call HookMiddleware.on_call_tool (post) after the upstream result

Hooks chain in registration order. A pre_tool_call hook may continue, mutate args, deny, or return REQUIRE_CONFIRMATION — which triggers the confirmation hooks.

[!IMPORTANT] Confirmation is fail-safe: if any confirmation hook rejects, or none is registered, the call is denied. Policy, guardrails, audit, and cost limits are all just hooks — nothing special in the core.

from fast_gateway import Hooks, ToolCallResult, ToolDecision


async def block_deletes(ctx) -> ToolCallResult | None:
    if ctx.message.name.endswith("_delete_all"):
        return ToolCallResult(decision=ToolDecision.REQUIRE_CONFIRMATION, reason="destructive")
    return None


async def approve(ctx) -> bool:
    return await ask_a_human(ctx.tool_name, ctx.arguments)  # your HIL channel


hooks = Hooks(pre_tool_call=[block_deletes], confirmation=[approve])

Access control

Every server record carries allow / deny glob lists; groups carry their own on top. deny wins over allow; an empty allow means "allow all". The policy is enforced in two places: hidden from list_tools and blocked at call_tool.

// POST /admin/servers
{ "name": "fs", "url": "...", "deny": ["delete_*", "*_admin"] }

Groups & group-scoped endpoints

Create a group, set its membership, and a curated view is served at /mcp/g/{group} — showing only that group's member servers with the group's allow/deny applied on top of each server's own rules. One shared parent server backs every group view; there is no per-group proxy duplication.

/mcp                 → all enabled servers, every permitted tool
/mcp/g/analytics     → only the 'analytics' group's servers & tools

Plugins

A plugin is a named bundle of extensions applied at create_gateway time. Where a single hook is one function, a plugin can contribute hooks and FastMCP middleware (for around-the-call control like circuit breaking or retry), an admin APIRouter, ASGI sub-app mounts, meta-tool registration, and async setup / teardown bound to the gateway lifespan.

from fast_gateway import create_gateway, SqliteStore

gateway = create_gateway(
    store=SqliteStore("gateway.db"),
    plugins=[MyAuditPlugin(), MyRateLimitPlugin()],
)

A plugin implements the Plugin protocol: a name, a contributions(context) method returning PluginContributions, and setup / teardown coroutines. The GatewayContext it receives exposes the store, the parent mcp, and a reload callable. These authoring types are top-level exports:

from fast_gateway import Plugin, PluginContributions, GatewayContext

Optional: agent-os plugin (experimental)

[!WARNING] The agt integration is experimental. Its upstream dependency is not yet on PyPI, so it installs only inside a uv project (see the install note below).

The agt extra wires Microsoft's agent-governance-toolkit (agent-os) in as AgtAgentOsPlugin. Its core capability is the policy engine: it evaluates policy for every tool call — scoped to the active group — and denies calls the policy rejects. Additional agent-os capabilities are opt-in toggles on AgtAgentOsSettings (which reuses agent-os's own config types — DetectionConfig, IntentCategory, EgressRule):

Toggle Seam Effect
enable_prompt_injection pre_tool_call deny calls whose arguments look like prompt injection
enable_semantic_policy (+ semantic_deny) pre_tool_call deny calls whose classified intent is dangerous
enable_response_scan post_tool_call block responses flagged unsafe (credential/PII/threat)
enable_credential_redaction post_tool_call redact secrets/PII out of responses
enable_egress_policy (+ egress_rules) pre_mcp_connect refuse upstreams whose URL is outside the allowlist
uv add "fast-gateway[agt]"   # from within a uv project — honors the git source
from fast_gateway import create_gateway, SqliteStore
from fast_gateway.plugins.agentos import AgtAgentOsPlugin, AgtAgentOsSettings

gateway = create_gateway(
    store=SqliteStore("gateway.db"),
    plugins=[
        AgtAgentOsPlugin(
            AgtAgentOsSettings(
                policy_dir="./policies",
                fail_closed=True,
                enable_prompt_injection=True,
                enable_response_scan=True,
                enable_credential_redaction=True,
            )
        )
    ],
)

[!NOTE] The agt extra is sourced from the agent-governance-toolkit GitHub monorepo (via uv [tool.uv.sources]) until agent-os-kernel 4.x is published to PyPI. Because of that git source, install it from within a uv project (uv add "fast-gateway[agt]"), which honors the source; a plain pip install "fast-gateway[agt]" cannot resolve the dependency and will fail until it lands on PyPI. Upstream, agent-os-kernel is being renamed/consolidated to agent-governance-toolkit-core. The gateway and the plugin system work fully without the extra — only this one integration needs it.

Admin API

Method Path Purpose
GET / POST /admin/servers list / register servers
GET / PATCH / DELETE /admin/servers/{id} read / update / remove
GET /admin/servers/{id}/tools live tool introspection
POST /admin/servers/{id}/test connect + handshake check
GET / POST /admin/groups list / create groups
GET / PATCH / DELETE /admin/groups/{id} read / update / remove
PUT /admin/groups/{id}/servers set membership
POST /admin/reload rebuild mounts from the store

CRUD writes to the Store; POST /admin/reload (or await gateway.reload()) rebuilds the proxy mounts. There is no live hot-swap in v1 — simple and lean.

[!WARNING] The /admin API is unauthenticated by default and mutates the registry — registering upstreams, rewriting allow/deny lists, injecting connection headers, and triggering reload. The host app must protect it. Pass FastAPI dependencies via Gateway.install(app, admin_dependencies=[Depends(require_admin)]) to guard the admin router, and/or place it behind reverse-proxy or network-level auth.

Store

The gateway's only persistence dependency is the Store protocol. SqliteStore (single file, zero setup) ships as the default; Postgres / Redis / in-memory are drop-in via store= with no core changes.

class Store(Protocol):
    async def initialize(self) -> None: ...
    async def list_servers(self) -> list[ServerRecord]: ...
    async def create_server(self, data: ServerCreate) -> ServerRecord: ...
    # … plus get/patch/delete for servers and groups

Development

make install     # uv sync (venv + deps incl. dev group)
make check       # lint + format-check + typecheck + tests  (CI gate; run before done)
make test        # pytest
make format      # ruff format + safe lint fixes
make build       # sdist + wheel

Tooling: uv (env + packaging), ruff (lint + format), mypy --strict (types, the gate), pytest + pytest-asyncio. Run make help for all targets.

[!TIP] On Windows, make is not built in — use it from WSL/Git Bash, install GNU Make (scoop install make), or run the underlying uv run ... commands directly.

Roadmap

Phase Deliverable Status
0 Package skeleton, Store protocol + SqliteStore, create_gateway() done
1 Server CRUD + builder (registry → proxy mount) + reload() + pre_mcp_connect done
2 HookMiddleware: pre_tool_call / post_tool_call / pre_list_tools done
3 Groups + per-server/group allow-deny + group-scoped /mcp/g/{group} endpoints done
Plugin system + agent-os policy integration done
4 search_tools / describe_tool meta-tools + catalog cache done
5 Reference hooks (audit, deny, confirmation), docs, packaging done
6 Local CLI (fast-gateway), JSON config + policy, browser HIL, upstream OAuth, Docker done

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fast_gateway-0.0.3.tar.gz (50.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fast_gateway-0.0.3-py3-none-any.whl (62.2 kB view details)

Uploaded Python 3

File details

Details for the file fast_gateway-0.0.3.tar.gz.

File metadata

  • Download URL: fast_gateway-0.0.3.tar.gz
  • Upload date:
  • Size: 50.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for fast_gateway-0.0.3.tar.gz
Algorithm Hash digest
SHA256 919b0a0505ae16ed9309fd165eb5b3e8be4c92695fa5ca2bf394d91e0a1e42ce
MD5 b3c3604c0ba82a5f846baad18ee177e3
BLAKE2b-256 2c68aea5b2ab74820d3ece7c13d0300c6c6a906feb67762cd69955f3dd3fa9d0

See more details on using hashes here.

File details

Details for the file fast_gateway-0.0.3-py3-none-any.whl.

File metadata

  • Download URL: fast_gateway-0.0.3-py3-none-any.whl
  • Upload date:
  • Size: 62.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for fast_gateway-0.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 e8487e954d5c9af799bdfb5473fa0bea93d073371e81bbe6e94671b7d6677aaf
MD5 faca8af04aa6f10054bbcca87dd15b07
BLAKE2b-256 1341e12066f025e15db7fbfdbeac7353841027ce8bd50df82884b272b29ce147

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page