MCP Gateway
MCP Gateway
A FastAPI app that reads MCP server definitions from a dedicated folder
(mcp-servers/), connects to each source server (local stdio process or remote
http/sse endpoint), applies a per-server tool allow/block policy, and
re-exposes each one as a streamable-HTTP MCP endpoint at /mcp/<name>.
Docs: make docs (local MkDocs at http://127.0.0.1:8001), make docs-build,
or make docs-up / docker compose up --build docs (Nginx at
http://localhost:8001).
mcp-servers/github.yaml -> http://localhost:8000/mcp/github
mcp-servers/docs.yaml -> http://localhost:8000/mcp/docs
mcp-servers/postgres.yaml -> http://localhost:8000/mcp/postgresQuick start
uv sync
cp .env.example .env # fill in any tokens your YAML files reference
uv run uvicorn aisafenet.app.main:create_app --factory --reloadThen enable a definition in mcp-servers/ (enabled: true) and restart. Check
what is mounted:
curl localhost:8000/api/v1/health
curl localhost:8000/serversPoint an MCP client at the new URL with a Bearer API key. Local seed key
(dev only): aisk_dev_local_00000000000000000001.
{
"mcpServers": {
"github-via-gateway": {
"url": "http://localhost:8000/mcp/github",
"headers": {
"Authorization": "Bearer aisk_dev_local_00000000000000000001"
}
}
}
}Python / FastMCP:
from fastmcp import Client
async with Client(
"http://localhost:8000/mcp/postgres",
auth="aisk_dev_local_00000000000000000001",
) as client:
tools = await client.list_tools()Each successful connect opens a row in {SAFE_DB_SCHEMA}.sessions bound to the
API key and MCP mcp-session-id. Gateway logs include session, mcp_session,
and key fields. DB connection knobs: GATEWAY_DATABASE__* and SAFE_DB_SCHEMA
(see .env.example).
Sessions expire after GATEWAY_SESSION__IDLE_TTL_SECONDS with no requests
(default 24 hours; 0 disables). Clients that send HTTP DELETE with
mcp-session-id (Streamable HTTP session terminate) also close the row
immediately.
Request history UI
Build the React console, then open http://localhost:8000/ui/ and authenticate
with the local development user:
username: admin
password: changemeThe UI exchanges these credentials at POST /api/login for a server-side
session carried by an HttpOnly, SameSite=Lax cookie. POST /api/logout
revokes that session. Change the seeded credentials before non-local use.
make ui-install
make ui-build
make devThe console shows request history across API keys owned by the signed-in user,
including original and protected SQL, session metadata, PII transformations,
guard decisions, status, and latency. MCP clients still authenticate with
Bearer API keys.
For frontend development, run make ui-dev; Vite proxies /api to port 8000.
Docker Compose also runs the console as a separate Nginx service at
http://localhost:5173 (FRONTEND_PORT overrides the port). Nginx proxies
/api to the gateway service. The gateway-embedded build remains available at
http://localhost:8000/ui/.
Defining a server
One YAML file per source server; see mcp-servers/README.md for the full field reference. A stdio example:
name: github
enabled: true
source:
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_PERSONAL_ACCESS_TOKEN: ${GITHUB_TOKEN}
tools:
allow: [] # empty = all tools
block: [delete_*]The policy is enforced in both directions: blocked tools are removed from
tools/list, and a direct tools/call for one is rejected.
Local LLM safety guard
The optional guard layers deterministic SQL/PII checks with a local model classification. It inspects both tool arguments and results, uses validated JSON verdicts, caches repeat decisions, redacts payloads at INFO level, and fails closed by default.
On Apple Silicon, install and run Ollama on the host so it can use Metal. Docker Desktop cannot pass the Mac GPU into an Ollama container:
brew install ollama
ollama serve
ollama pull qwen3:4b
ollama pull qwen3.6:35b-a3bEnable the guard in settings.toml ([guard] enabled = true) or via .env:
GATEWAY_GUARD__ENABLED=true
GATEWAY_LLM__BASE_URL=http://localhost:11434/v1When the gateway itself runs in Compose, its base URL defaults to
http://host.docker.internal:11434/v1. Linux hosts with an NVIDIA GPU can
instead run docker compose --profile llm up; that profile is intentionally
not suitable for macOS.
Per-server behavior can override global settings:
guard:
enabled: true
inspect_results: trueThe connector uses the OpenAI-compatible API rather than an Ollama-specific
SDK. To use vLLM, llama.cpp, or LM Studio later, change
GATEWAY_LLM__BASE_URL. Guard classifications send
reasoning_effort: "none" to avoid thinking-token latency; red-team agent
calls retain each model's default reasoning behavior.
Tracing tool calls
Every tool call gets a short trace id (see aisafenet/core/tracing.py)
that's attached to every log line produced while handling it — the tool-policy
check, the guard's call/result review, and the underlying LLM request. Log
lines carry it as [trace=<id>]:
2026-07-31 14:50:01,123 INFO aisafenet [trace=9f3a1c2b0e77]: tool_call start server='postgres' tool='query'
2026-07-31 14:50:01,124 INFO aisafenet [trace=9f3a1c2b0e77]: Tool call on server 'postgres': 'query' argument_keys=['sql']
2026-07-31 14:50:01,210 INFO aisafenet [trace=9f3a1c2b0e77]: Guard verdict kind=call model=guard decision=allow confidence=0.95 latency_ms=85.4
2026-07-31 14:50:01,255 INFO aisafenet [trace=9f3a1c2b0e77]: tool_call done latency_ms=132.1Set GATEWAY_LOG_LEVEL=DEBUG to also see the nested spans (guard.review_call,
llm.complete, llm.http_post, tool.execute, guard.review_result) with
their own latencies and the full guard-model system prompt, user payload, and
JSON answer. Debug logs can contain SQL, tool results, PII, or secrets; use them
only in controlled development environments. This is log-based, not
OpenTelemetry — there's no external tracing backend to run.
Call reports
Every guarded tool call comes back with a record of what the gateway did to it.
The structured form sits in the result's _meta under the aisafenet key, and a
one-line summary is appended as a final text block so the calling model sees it
too:
aisafenet: dropped columns credit_card; hashed in query email; guard call=allow; guard result=allowThe executed_sql field holds the statement actually sent downstream with the
session data_key replaced by __DATA_KEY__, so reports are safe to log or
persist. See aisafenet/guards/reporting.py for the full model. The report
is assembled outside the guard, so its contents never enter the guard prompt.
Red-team agent
With the gateway and Ollama running, execute the included bounded PII exfiltration scenario:
uv run python -m app.agents --scenario exfiltrate-piiThe agent obtains tool schemas from /mcp/postgres, never connects directly to
Postgres, and writes a JSONL audit transcript under runs/. Scenario files set
hard step and wall-clock limits. Use GATEWAY_LLM__AGENT_MODEL to select the
tool-calling model. Transcripts may contain sensitive test data; runs/ is
gitignored and should be handled as restricted audit output.
Configuration
Non-secret defaults live in settings.toml. Secrets and deploy overrides use
.env / GATEWAY_* environment variables (nested fields use __).
See docs/configuration.md for the full list.
Source | Use for |
| Models, guard, TTLs, dirs, mount prefix |
| DB credentials, tokens, per-machine overrides |
Definitions are validated at startup: an invalid file, or a ${VAR} with no
value and no ${VAR:-fallback} default, aborts the boot with the offending file
name instead of silently serving a broken endpoint.
Docker
docker compose up --build gatewayThe mcp-servers/ folder is mounted read-only, and the image includes Node.js so
npx-based stdio servers work.
Postgres MCP (aisafenet)
mcp-servers/postgres.yaml is enabled by default and proxies the official
@modelcontextprotocol/server-postgres server to your Docker Postgres instance.
Start the database first, then the gateway:
docker compose up -d postgres
uv run uvicorn aisafenet.app.main:create_app --factory --reload
# or: docker compose up gatewayConnect a client to http://localhost:8000/mcp/postgres.
Open scripts/mcp_test_client.ipynb with the
project's .venv kernel to list tools/resources and run the included read-only
Postgres query. Change MCP_URL, tool_name, or arguments in the notebook to
test another endpoint or tool.
To use the mcp/postgres Docker image directly (outside the gateway), build it
with docker compose build postgres-mcp and run:
docker run -i --rm --add-host=host.docker.internal:host-gateway \
mcp/postgres postgresql://aisafe:aisafe@host.docker.internal:5432/aisafenetLayout
Path | Role |
Thin Uvicorn application-factory entry point | |
Startup sequence for logging, dependency wiring, and app assembly | |
Dependency-injector providers and dependency lifetimes | |
Validated, environment-driven Pydantic settings | |
Loads configs, mounts MCP apps, and registers gateway routes | |
YAML schema ( | |
Folder scan, validation, | |
Builds a FastMCP proxy + transport per definition | |
Tool policy and LLM guard enforcement | |
Per-call report of applied transforms and guard verdicts | |
OpenAI-compatible local-model adapter | |
Python guard script pool and dispatcher | |
Bounded red-team agent and scenarios | |
Gateway error hierarchy |
Tests
uv sync --all-groups
uv run pytest
uv run ruff check .