{"id":51131762,"url":"https://github.com/ranfysvalle02/m-gate","last_synced_at":"2026-06-25T13:01:37.307Z","repository":{"id":363618409,"uuid":"1264032459","full_name":"ranfysvalle02/m-gate","owner":"ranfysvalle02","description":"MongoDB powered MCP Gateway. A multi-tenant MCP (Model Context Protocol) gateway with auth, RBAC, guardrails, a WASM code sandbox, queryable encryption, and observability.","archived":false,"fork":false,"pushed_at":"2026-06-18T04:47:41.000Z","size":49324,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-18T05:07:28.729Z","etag":null,"topics":["agents","api","context","context-api","function-calling","gateway","llm","mcp","mcp-client","mcp-gateway","mcp-server","model-context-protocol","mongodb","mongodb-atlas","python","security","tokens","tools"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ranfysvalle02.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-09T13:50:19.000Z","updated_at":"2026-06-18T04:47:44.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ranfysvalle02/m-gate","commit_stats":null,"previous_names":["ranfysvalle02/m-gate"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ranfysvalle02/m-gate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ranfysvalle02%2Fm-gate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ranfysvalle02%2Fm-gate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ranfysvalle02%2Fm-gate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ranfysvalle02%2Fm-gate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ranfysvalle02","download_url":"https://codeload.github.com/ranfysvalle02/m-gate/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ranfysvalle02%2Fm-gate/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34775946,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-25T02:00:05.521Z","response_time":101,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agents","api","context","context-api","function-calling","gateway","llm","mcp","mcp-client","mcp-gateway","mcp-server","model-context-protocol","mongodb","mongodb-atlas","python","security","tokens","tools"],"created_at":"2026-06-25T13:01:36.320Z","updated_at":"2026-06-25T13:01:37.248Z","avatar_url":"https://github.com/ranfysvalle02.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mdb-mcp-gateway\n\n![](m-gate.png)\n\n\u003e Go from **no MCP** to **MCP-gateway magic** in minutes: paste a Python function\n\u003e and the gateway turns it into a real, sandboxed MCP tool behind one endpoint —\n\u003e a hosted, effectively \"serverless\" virtual MCP. MCP is just **JSON-RPC over the\n\u003e document model**, so it all lives on **MongoDB**; **hybrid search on MongoDB\n\u003e Atlas** is the power-up that keeps routing sharp once your catalog grows.\n\n---\n\n## MCP, demystified: it's just JSON-RPC over the document model\n\nStrip away the launch-day acronyms and the Model Context Protocol is the oldest\npattern in computing: a client sends a request, a server returns a response.\nThree verbs do the work — `initialize` (handshake), `tools/list` (the menu), and\n`tools/call` (run one) — and every message is a tiny **JSON-RPC 2.0** envelope:\n\n```json\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": \"call-1\",\n  \"method\": \"tools/call\",\n  \"params\": { \"server\": \"weather\", \"name\": \"get_current_weather\",\n              \"arguments\": { \"city\": \"Montreal\", \"unit\": \"celsius\" } }\n}\n```\n\nThat shape — nested, self-describing, free to differ from its neighbors — is a\n**document**. So the whole protocol (a tool call, a tool definition, an\nembedding, a scope, an audit record) stores natively in MongoDB with no ORM, no\nmigration, and no impedance mismatch: the request on the wire *is* the document\nat rest. That is the quiet reason this gateway is one engine instead of four.\n\n## Build a tool in minutes: virtual MCPs via `code_exec`\n\nThe fastest way from \"no MCP\" to \"MCP magic\" isn't wiring up a server — it's\n**writing a function**. In the admin console you paste a Python function; the\ngateway lints it, encrypts it at rest, runs it sandboxed in WebAssembly, and\nexposes it as a real MCP tool behind one endpoint. No Dockerfile, no deploy, no\ninfra to babysit — a \"virtual\" MCP server that is hosted and effectively\nserverless.\n\n```python\ndef word_count(text: str) -\u003e dict:\n    \"\"\"Count the words and characters in the given text.\"\"\"\n    words = [w for w in text.split() if w]\n    return {\"words\": len(words), \"characters\": len(text)}\n```\n\nSave it and an agent can immediately discover and call `word_count`. The runtime\n([`services/code_tools.py`](services/code_tools.py) + the wasm sandbox) gives\nauthored functions a small, safe `context` with everything a useful tool needs —\nand nothing else:\n\n- `context.db` — tenant-scoped MongoDB, host-relayed (no credentials in the\n  sandbox), gated by the tool's `action_type` (`read` / `write` / `destructive`).\n- `context.env` — per-server encrypted secrets, injected at runtime.\n- `context.tools` — call sibling tools in your tenant to compose workflows.\n- `context.http` — opt-in, SSRF-screened outbound HTTPS (the jail has no sockets).\n- **Pinned pip** — declare `package==x.y.z` requirements and they install\n  wheels-only under a deny-by-default, per-tenant allowlist.\n\nThe sandbox is network-isolated and fuel/memory/wall-clock bounded, and every\ntool you author can be **exported as a runnable FastMCP project** — so nothing\nyou build here is locked in. See [CONTEXT.md](CONTEXT.md) for the full author\nguide.\n\n## One gateway endpoint\n\nWhatever you build or connect, agents reach it through a single front door:\nJSON-RPC at `POST /rpc` and a mounted MCP app at `/mcp`. Discovery is\nidentity-scoped and routes by meaning; invocation is authorized, quota-metered,\nand audited. Point Cursor, Claude Desktop, or VS Code at the one URL and every\ntool — virtual or connected — shows up.\n\n## Advanced: connect existing MCP servers (GitHub, MongoDB, …)\n\nWhen you already run an MCP server — or want to front an official one such as the\nGitHub or MongoDB MCP — register it as a network/stdio downstream\n(`streamable_http`, `sse`, or `stdio`) and the gateway proxies it with the same\nauth, egress allowlisting, resiliency, and audit as everything else. This is the\n\"advanced\" path; most tools start life as a `code_exec` virtual MCP above.\n\n## Advanced power-up: hybrid search routing, in one place\n\nOnce your catalog grows past a handful of tools, handing the agent the whole menu\nevery turn burns tokens and degrades tool choice. The upgrade is to treat tool\nselection as **retrieval** — and the retrieval that holds up is hybrid:\n\nSemantic (vector) routing — embed the catalog, hand back the tools a task needs —\nis the well-trodden first step, and it works: it slashes the per-turn token bill.\nBut vector-only retrieval has a blind spot: it fumbles the **exact tokens** agents\nconstantly use (a tool name, an error code, an order ID), because cosine\nsimilarity rewards meaning, not spelling.\n\nThe fix is **hybrid search** — fusing semantic\n(vector) and lexical (full-text/BM25) retrieval into one ranked result with\nReciprocal Rank Fusion, so an agent finds the right tool whether it asks in\nkeywords *or* in intent. The interesting part isn't that we do hybrid search;\nit's **how little infrastructure it takes**, because MongoDB Atlas does all of it\nin one `$rankFusion` query over one collection — no separate vector DB, search\nengine, or sync pipeline to keep in lockstep.\n\n### Why hybrid search is genuinely hard to operate\n\nNeither retrieval method is sufficient alone:\n\n- **Lexical (BM25)** nails exact tokens — tool names, error codes, SKUs — but is\n  blind to intent. Ask it for *\"dangerous storm warnings\"* and it happily ranks\n  an unrelated `list_customer_orders` first because of common words like \"for\"\n  and \"my\". (That's a real result from this repo — see below.)\n- **Vector (semantic)** understands intent but can miss the exact identifier a\n  user typed verbatim.\n\nThe fix the industry settled on is **Reciprocal Rank Fusion (RRF)**: run both\nretrievers, then merge by *rank position* (`1 / (60 + rank)`) so you never have\nto normalize an unbounded BM25 score against a 0–1 cosine score. The pattern is\nsimple. **Operating it is not** — at least not the traditional way:\n\n```mermaid\nflowchart LR\n    A[Agent / Gateway] --\u003e|keyword query| ES[(Search engine\u003cbr/\u003eElasticsearch / Solr)]\n    A --\u003e|embed + vector query| VDB[(Vector DB\u003cbr/\u003ePinecone / Milvus)]\n    ES --\u003e|ranked list A| F[Fusion + merge service\u003cbr/\u003eclient-side RRF]\n    VDB --\u003e|ranked list B| F\n    F --\u003e A\n    SYNC[[CDC / sync pipeline]] -. keep docs + _id consistent .-\u003e ES\n    SYNC -. keep docs + _id consistent .-\u003e VDB\n    classDef pain fill:#fde,stroke:#c33;\n    class ES,VDB,F,SYNC pain;\n```\n\nThat's **four moving parts** to support one feature: a search engine, a vector\nDB, a fusion/merge service, and a sync pipeline keeping the two stores\nconsistent by shared `_id`. Each has its own scaling, backup, and access model —\nand they drift out of sync the moment one write lands in one store but not the\nother. This is the \"architectural sprawl\" trap.\n\n### How MongoDB Atlas collapses it to one query\n\nThe same documents carry both a `$search` (Atlas Search / BM25) index and a\n`$vectorSearch` index. A single `$rankFusion` aggregation stage runs both arms\nand fuses them with RRF — natively, server-side, in one round trip:\n\n```mermaid\nflowchart LR\n    A[Agent / Gateway] --\u003e|\"one $rankFusion aggregate\"| DB\n    subgraph DB [MongoDB Atlas · tool_catalog · one collection]\n        VEC[Vector Search index]\n        TXT[Atlas Search index]\n    end\n    DB --\u003e|RRF-fused, ranked result| A\n    classDef good fill:#dfe,stroke:#393;\n    class VEC,TXT good;\n```\n\nNo second store. No client-side merge. No sync pipeline. No `_id` reconciliation.\nThe catalog, both indexes, and the fusion math live on **one engine**. That is\nthe entire pitch of this project distilled to one stage.\n\nThe pipeline this repo actually runs (`services/hybrid_search.py`):\n\n```js\ndb.tool_catalog.aggregate([\n  { $rankFusion: {\n      input: { pipelines: {\n        vectorPipeline:   [ { $vectorSearch: { index: \"hybrid-vector-search\", path: \"embedding\",\n                                                queryVector: embed(query), numCandidates: 100, limit: 20 } } ],\n        fullTextPipeline: [ { $search: { index: \"hybrid-full-text-search\",\n                                         text: { query: query, path: [\"name\",\"description\",\"server\"] } } },\n                            { $limit: 20 } ]\n      } },\n      combination: { weights: { vectorPipeline: 0.5, fullTextPipeline: 0.5 } },\n      scoreDetails: true\n  } },\n  { $project: { name: 1, description: 1, score: { $meta: \"score\" },\n                scoreDetails: { $meta: \"scoreDetails\" } } },\n  { $sort: { score: -1 } }, { $limit: 5 }\n])\n```\n\n### See it for yourself: one query, three modes\n\nThe gateway exposes a `mode` so you can run the **same query** as `vector`,\n`text`, or `hybrid` and watch the arms disagree. Real output from this repo for\n`\"look up a purchase by its id\"`:\n\n| mode | top 3 results |\n| --- | --- |\n| `vector` | `find_order`, `update_order_status`, `list_customer_orders` |\n| `text` | `find_order`, **`severe_weather_alerts`** ← lexical noise, `list_customer_orders` |\n| `hybrid` | `find_order`, `list_customer_orders`, `severe_weather_alerts` |\n\nLexical-only drags an irrelevant *weather* tool into the order results on common\nwords; the semantic arm corrects it, so hybrid keeps both real order tools above\nthe noise. The `scoreDetails` are the receipts that *both* arms ran and how the\nfusion was computed:\n\n```json\n{\n  \"value\": 0.01639,\n  \"description\": \"value output by reciprocal rank fusion algorithm, computed as sum of (weight * (1 / (60 + rank))) across input pipelines from which this document is output, from:\",\n  \"details\": [\n    { \"inputPipelineName\": \"fullTextPipeline\", \"rank\": 1, \"weight\": 0.5, \"value\": \"...\" },\n    { \"inputPipelineName\": \"vectorPipeline\",   \"rank\": 2, \"weight\": 0.5, \"value\": \"...\" }\n  ]\n}\n```\n\n\u003e **Versions / notes.** `$rankFusion` is native to MongoDB 8.1+. This repo runs\n\u003e `mongodb/mongodb-atlas-local:8.3.2-20260618T112243Z` (a pinned patch-level\n\u003e build, not the floating `:8.3` tag) in Compose — with a matching `crypt_shared`\n\u003e 8.3.2 in the image — and in the integration tier. If you need score-based (not\n\u003e rank-based) fusion with normalization, MongoDB also offers `$scoreFusion`. The\n\u003e gateway degrades to the semantic arm if the fusion stage is unavailable, so\n\u003e search never hard-fails.\n\n---\n\n## Quick Start (Implemented)\n\n\u003e In a hurry? **[QUICKSTART.md](QUICKSTART.md)** is the 5-minute path: `docker compose\n\u003e up`, generate a token in the admin console, and connect Cursor — security on by default.\n\n\u003e Deploying for real? See **[ARCHITECTURE.md](ARCHITECTURE.md)** for a complete\n\u003e as-built system map, then **[DEPLOYMENT.md](DEPLOYMENT.md)** for Docker Compose,\n\u003e single-container, Kubernetes, and Helm paths, plus an embeddings setup and\n\u003e production hardening checklist. For going live, also read\n\u003e **[PRODUCTION.md](PRODUCTION.md)** (operations \u0026 hardening),\n\u003e **[SECURITY.md](SECURITY.md)** (security model \u0026 vulnerability reporting), and\n\u003e **[NETWORK-SECURITY.md](NETWORK-SECURITY.md)** (trust boundaries \u0026 what's handled\n\u003e at the perimeter), **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** (failure-mode\n\u003e runbook), **[docs/API.md](docs/API.md)** (REST + JSON-RPC reference),\n\u003e **[READONLY.md](READONLY.md)** (safely showcase: read-only tenants, viewers \u0026 tool\n\u003e curation), and\n\u003e **[docs/QUERYABLE-ENCRYPTION.md](docs/QUERYABLE-ENCRYPTION.md)** (QE setup and operations).\n\u003e For function-author ergonomics and runtime capabilities, see\n\u003e **[CONTEXT.md](CONTEXT.md)**.\n\n### Demo in 60s -\u003e production in 5 steps\n\n1. **Start the stack**\n   ```bash\n   docker compose up --build\n   ```\n2. **Verify gateway health**\n   ```bash\n   curl http://localhost:8000/health\n   ```\n3. **Open the Admin Studio**\n   - `http://localhost:8000/ui` (`demo@demo.com` / `demo`)\n4. **Call seeded tools**\n   - Code servers: `weather`, `orders`, `utilities`, `analytics`\n   - Public server: `deepwiki` (requires outbound HTTPS to `mcp.deepwiki.com`)\n5. **Graduate to production posture**\n   - Use the hardening path in this README, then follow\n     [DEPLOYMENT.md](DEPLOYMENT.md), [PRODUCTION.md](PRODUCTION.md),\n     [SECURITY.md](SECURITY.md), and [NETWORK-SECURITY.md](NETWORK-SECURITY.md).\n\nImportant first-run note:\n\n- Bootstrap now performs an **embedding preflight** and fails loudly if your\n  embedding provider is unreachable.\n- With default Compose settings this means Ollama must be running on your host\n  and `nomic-embed-text` must be pulled before bootstrap:\n  ```bash\n  ollama pull nomic-embed-text\n  ```\n\nThis repository now includes a working end-to-end MCP Gateway with:\n\n- FastAPI + FastMCP gateway mounted at `http://localhost:8000/mcp`\n- MongoDB Atlas Local (`mongod` + `mongot`) via Docker Compose\n- **Semantic `tools/list` discovery**: a task query (`X-MCP-Query` header) returns a curated, ranked shortlist instead of the full catalog — the \"route by meaning\" front door\n- **Identity-bound scope on both discovery and invocation**: scope filtering in search/list and explicit authorization checks in `tools/call`, including required `server:\u003cname\u003e` scopes\n- Hybrid tool search (`$rankFusion`: vector + full-text) over `tool_catalog`\n- **GA-safe hybrid fallback**: application-side RRF keeps hybrid retrieval working when `$rankFusion` preview features are unavailable\n- **Always-included tools**: flag a tool `metadata.always_included` in Admin Studio to pin it to the top of every search result regardless of relevance — still scope-filtered, and counted against the caller's `limit` (toggle with `HYBRID_PIN_ALWAYS_INCLUDED`)\n- **Resiliency**: a hard downstream deadline (`DOWNSTREAM_TIMEOUT_MS`, default 2000ms) with protocol-safe JSON-RPC error frames\n- **Active-active-safe registry watching**: each gateway replica persists its own change-stream resume token (`routing_registry::\u003cinstance_id\u003e`) so pods do not overwrite each other's stream position\n- **Downstream auth, kept minimal**: per-server `metadata.auth.scheme` is gateway-minted workload JWT (default) or `none` (the downstream/tenant owns its own auth); credentials rotate/reconnect through the existing warm-client cache path\n- **Inbound MCP-client auth**: username/password via `POST /auth/token` (OAuth2 password grant) plus optional HTTP Basic on the MCP surface; full OAuth is bring-your-own-IdP via `AUTH_MODE=jwks` with RFC 9728 resource-metadata discovery\n- **Queryable Encryption for downstream secrets**: `routing_registry.env` / `command` / `args` / `metadata` can be encrypted at rest with DEKs in `encryption.__keyVault`, backed by LocalStack AWS KMS (default Compose) or a local 96-byte master key\n- **Embedding resiliency**: retries + circuit breaker + lexical fallback when embedding providers are unavailable\n- **Pluggable, admin-configurable embeddings**: Ollama, OpenAI, Azure OpenAI, Voyage AI, and Google Gemini — switchable at runtime from the admin panel, with vector width auto-detected per provider (see [Embeddings](#embeddings))\n- **Layered guardrails**: regex floor + optional semantic injection classifier over a versioned `guardrail_signatures` vector corpus, plus optional Presidio NER redaction\n- **Semantic cache model provenance**: cache entries are stamped with `embedding_model` / `embedding_dim` / `embedding_version`, with version-aware lookups and migration tooling\n- Default Ollama embeddings (`nomic-embed-text`) through `http://host.docker.internal:11434`\n- Demo defaults: code-powered `weather`, `orders`, `utilities`, and `analytics` servers (wasm sandbox execution) plus a prewired public `deepwiki` server\n- **Tenant-scoped virtual DB bridge** for code tools: `context.db[...]` queries\n  relay through the host process (no sandbox network access or DB credentials),\n  gated by each tool's `action_type` (`read` / `write` / `destructive`)\n- **Opt-in outbound HTTP bridge** for code tools: `context.http.get(url, auth=\"ENV_KEY\")`\n  relays through the host (the wasm jail has no sockets), screened by a deny-by-default,\n  SSRF-proof, IP-pinned egress allowlist (`tenant ∩ EGRESS_GLOBAL_ALLOWLIST`), with\n  secrets injected host-side. Off by default (`SANDBOX_HTTP_BRIDGE_ENABLED`)\n- **Per-server encrypted runtime env**: code tools read `context.env[\"KEY\"]`; values are managed from Admin Studio Secrets and never returned after write\n- **Explore Database authoring assistant** in Admin Studio: browse tenant\n  collections, sample documents, run read-only queries, and insert/copy\n  generated `context.db[...]` snippets directly into function source\n- **Tenant soft-delete + retention**: `DELETE /admin/tenants/{id}` is a reversible soft-delete by default (`POST /admin/tenants/{id}/restore` undoes it within the retention window); a background reaper drops the physical DB before removing the control doc, and `?hard=true` keeps the immediate hard delete\n- **Streaming usage/billing export**: `GET /admin/tenants/{id}/usage/export` (CSV) and `GET /admin/telemetry/export` (JSONL) stream over a cursor with `from`/`to` range filters — no load-all ceiling\n- **Self-service beta registration**: opt-in public sign-up (`POST /auth/register` + `/ui/register`) provisions an isolated tenant per registrant, capped to an `unconfirmed`, code-tools-only tier (1 server / 1 tool, small quota, no external transports) with per-IP throttling and a global beta cap; a platform-admin confirms accounts to lift the caps — see [AUTH.md](AUTH.md)\n- **Admin analytics dashboard**: scalable `$group` aggregation endpoints (`/admin/analytics/*`) power scope-aware Dashboard/Telemetry charts (usage trend, top tools/servers, success-vs-error + latency, quota utilization) over a self-hosted Chart.js — no full-collection scans\n- **Sandbox quota preflight**: code tools whose worst-case sandbox cost can't fit the remaining `sandbox_seconds` quota are rejected before execution (one shared check enforced identically on `/rpc` and `/mcp`), with a `gateway_quota_preflight_blocks_total` metric\n- **Sandbox pool max-age + health sweep**: idle warm workers are proactively retired by age and ping-health, complementing the reactive `max_jobs` recycle\n- **Observability**: request IDs, JSON logs, Prometheus `/metrics`, prebuilt Prometheus alert rules, a provisioned Grafana dashboard (`http://localhost:3000`), OpenTelemetry tracing (`ENABLE_TRACING=true`) with spans around RPC handling and downstream hops, and health split (`/health/live`, `/health/ready`)\n- **Delivery artifacts**: k8s manifests, Helm chart, CI workflow (lint + format + types + 82% coverage gate), pre-commit, Ruff, and MyPy configuration\n\n### Prerequisites\n\n- Docker / Docker Compose\n- Ollama running on your host machine\n- Pulled embedding model:\n\n```bash\nollama pull nomic-embed-text\n```\n\nOptional for ML NER redaction (`GUARDRAIL_PII_NER_ENABLED=true`):\n\n```bash\npip install -e \".[guardrails-ml]\"\npython -m spacy download en_core_web_sm\n```\n\n### Run\n\n```bash\ndocker compose up --build\n```\n\nBy default, Compose now starts `localstack` + `kms-init` and runs the gateway\nwith Queryable Encryption enabled for `routing_registry` secret-bearing fields.\nThe KMS key ARN is written to a shared volume and loaded through\n`AWS_KMS_KEY_ARN_FILE=/kms-config/kms_key_id`.\nCompose also runs `secrets-init` once and writes stable file-backed secrets for\n`EMBEDDING_SECRET_FILE` and `ADMIN_SESSION_SECRET_FILE` into the\n`gateway_secrets` volume (instead of relying on fallback secrets).\nThe demo stack enables `CODE_TOOL_EXECUTION_ENABLED=true` and\n`SANDBOX_DB_BRIDGE_ENABLED=true`, so seeded code tools (including the click-tracker\nanalytics demo) run immediately inside the wasm sandbox with `context.db`.\nQueryable Encryption is still demonstrated through encrypted routing fields and\nauthored function source/secrets at rest — without the old non-functional\n`secure-stdio` fixture server.\n\nTo use a local master key instead of LocalStack KMS:\n\n```bash\n# Generate a 96-byte QE local key (base64) and save it as a file.\npython - \u003c\u003c'PY'\nimport base64, os\nprint(base64.b64encode(os.urandom(96)).decode())\nPY\n```\n\nSet `KMS_PROVIDER=local` and `QE_LOCAL_MASTER_KEY_FILE=/kms-config/local-master-key.b64`\nfor `bootstrap` and `gateway` (see `docker-compose.yml` comments).\n\n### Docker Compose hardening map (the \"near-perfect\" path)\n\nUse this progression to move from demo convenience toward production posture:\n\n1. **Keep file-backed secrets enabled** (default): `EMBEDDING_SECRET_FILE` and\n   `ADMIN_SESSION_SECRET_FILE` come from `gateway_secrets`.\n2. **Use cloud embedding providers over HTTPS** for real workloads:\n   - set `EMBEDDING_PROVIDER=openai|azure_openai|voyage|gemini`\n   - mount `EMBEDDING_API_KEY_FILE` (never hardcode the key in compose).\n3. **Harden auth for production**: auth is always on (`AUTH_MODE=hs256` by\n   default); for production move to `jwks`, set issuer/audience, and disable\n   wildcard CORS.\n4. **Pin explicit origins and proxy trust**:\n   - `CORS_ALLOW_ORIGINS=https://your-app.example.com`\n   - `FORWARDED_ALLOW_IPS=\u003cyour-ingress-cidr\u003e`\n5. **Graduate to production deployment docs**:\n   [`DEPLOYMENT.md`](DEPLOYMENT.md) + [`PRODUCTION.md`](PRODUCTION.md) +\n   [`SECURITY.md`](SECURITY.md) for full hardening, key rotation, and network policy.\n\nThe bootstrap service will:\n\n1. Wait for MongoDB\n2. Create Search + Vector Search indexes\n3. Seed `routing_registry`, `session_context`, and a ready-to-use demo user\n   (`agent@demo.com`, role Demo/`tool:invoke`) — skipped when `ENVIRONMENT=production`\n4. Sync downstream tools into `tool_catalog` with embeddings\n\n### Verify\n\n\u003e **Auth is always on.** Health/observability endpoints are open, but the data\n\u003e plane (`/rpc`, `/mcp`) requires a `Authorization: Bearer \u003ctoken\u003e`. The fastest\n\u003e way to get one: open the admin console **Users** tab and click **Generate\n\u003e token** on `agent@demo.com` — it hands you a ready-to-paste token plus Cursor\n\u003e `mcp.json` / curl snippets. Add `-H \"Authorization: Bearer $TOKEN\"` to the\n\u003e `/rpc` examples below.\n\n- Health:\n\n```bash\ncurl http://localhost:8000/health\n```\n\n- Dashboards and alerts:\n\n```bash\nopen http://localhost:3000   # Grafana (admin/admin)\nopen http://localhost:9090   # Prometheus + alert rules\n```\n\n- JSON-RPC hybrid search (default `mode` is `hybrid`):\n\n```bash\ncurl -X POST http://localhost:8000/rpc \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"jsonrpc\":\"2.0\",\n    \"id\":\"search-1\",\n    \"method\":\"tools/search\",\n    \"params\":{\"query\":\"weather in montreal\",\"limit\":5}\n  }'\n```\n\n- Compare retrieval modes on the same query (`mode`: `hybrid` | `vector` |\n  `text`). Run all three to see the vector and lexical arms disagree, then watch\n  `$rankFusion` reconcile them:\n\n```bash\nfor MODE in vector text hybrid; do\n  echo \"== $MODE ==\"\n  curl -s -X POST http://localhost:8000/rpc \\\n    -H \"Content-Type: application/json\" \\\n    -d \"{\\\"jsonrpc\\\":\\\"2.0\\\",\\\"id\\\":\\\"m\\\",\\\"method\\\":\\\"tools/search\\\",\n         \\\"params\\\":{\\\"query\\\":\\\"look up a purchase by its id\\\",\\\"limit\\\":3,\\\"mode\\\":\\\"$MODE\\\"}}\"\ndone\n```\n\n- Semantic `tools/list` (route by meaning). With no query you get the full\n  catalog; with an `X-MCP-Query` header you get a curated shortlist:\n\n```bash\ncurl -X POST http://localhost:8000/rpc \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-MCP-Query: I need to check the weather forecast\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":\"list-1\",\"method\":\"tools/list\",\"params\":{}}'\n```\n\n- Initialize handshake + capabilities:\n\n```bash\ncurl -X POST http://localhost:8000/rpc \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":\"init-1\",\"method\":\"initialize\",\"params\":{}}'\n```\n\n- Paginated `tools/list`:\n\n```bash\ncurl -X POST http://localhost:8000/rpc \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":\"list-paged\",\"method\":\"tools/list\",\"params\":{\"limit\":2,\"cursor\":\"0\"}}'\n```\n\n- Identity-bound scope. Caller groups/scopes come from the verified token claims\n  (`groups`/`scopes`); the catalog returned is filtered to what that identity is\n  allowed to see. Mint a token whose scopes exclude `orders:write` and\n  `update_order_status` is filtered out:\n\n```bash\n# token scoped to orders,readonly: update_order_status (orders:write) is filtered out\ncurl -X POST http://localhost:8000/rpc \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":\"list-2\",\"method\":\"tools/list\",\"params\":{}}'\n```\n\n- JSON-RPC tool call through gateway proxy:\n\n```bash\ncurl -X POST http://localhost:8000/rpc \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"jsonrpc\":\"2.0\",\n    \"id\":\"call-1\",\n    \"method\":\"tools/call\",\n    \"params\":{\n      \"server\":\"weather\",\n      \"name\":\"get_current_weather\",\n      \"arguments\":{\"city\":\"Montreal\",\"unit\":\"celsius\"}\n    }\n  }'\n```\n\n- Cache migration status (admin route):\n\n```bash\ncurl -X POST http://localhost:8000/admin/cache/migrate \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"mode\":\"status\"}'\n```\n\n- Cache migration via CLI (status / purge / reembed):\n\n```bash\npython -m scripts.migrate_cache --mode status\npython -m scripts.migrate_cache --mode purge\npython -m scripts.migrate_cache --mode reembed --batch-size 200\n```\n\n### Web admin console\n\nThe gateway serves a branded, dark-mode admin UI at `http://localhost:8000/ui` —\nsign in, mint tokens, wire up servers, and connect an MCP client without ever\ntouching a terminal.\n\n![Admin console dashboard with the one-click Connect Now hero](docs/images/dashboard-connect-now.png)\n\nThe **Connect Now** hero is the zero-to-connected fast path: one click mints a\ntool-ready demo account and hands back a bearer token plus a ready-to-paste Cursor\n`mcp.json` (the one-time password is shown once):\n\n![Demo credential modal showing the bearer token, one-time password, and Cursor mcp.json](docs/images/demo-credential.png)\n\nAuthor Python functions that run sandboxed in WebAssembly and surface as real MCP\ntools, or connect servers you already run — all from the **MCP Servers** studio:\n\n![MCP Servers studio listing virtual code servers and connected servers](docs/images/servers-studio.png)\n\n- The **Dashboard** and **Telemetry** tabs render live analytics from scalable\n  `$group` aggregation endpoints (`/admin/analytics/*`) with a self-hosted\n  Chart.js: usage trends, top tools/servers, request-vs-error + latency lines, and\n  quota-utilization bars. Scope follows your role — platform-admins see\n  cross-tenant rollups (and can filter to one tenant), tenant-admins see their own.\n- The **Usage \u0026 Quota** tab shows current usage against quota with utilization\n  meters, a platform-admin quota editor, and a recent-events table with CSV export.\n- Tabs are deep-linkable via the URL hash (e.g. `…/ui#telemetry`).\n- In `docker-compose.yml`, demo credentials are preconfigured:\n  - `ADMIN_EMAIL=demo@demo.com`\n  - `ADMIN_PASSWORD=demo`\n- Login is required for the admin surface (`/ui` and `/admin/*`) in **all** `AUTH_MODE`s.\n- Browser sessions are cookie-based (HttpOnly); mutating admin API calls require a CSRF header.\n\n**Self-service beta sign-up \u0026 confirmation tiers.** With\n`SELF_REGISTRATION_ENABLED=true`, the login screen gains a **Create an account**\nlink to a public sign-up page at `/ui/register`. New accounts start\n**`unconfirmed`** — a tightly-capped, code-tools-only tier (1 server / 1 tool,\nsmall quota, no external transports) that's safe to open to the public — and a\nplatform-admin promotes them to **`confirmed`** from the **Tenants** tab\n(confirmation pill + Confirm/Unconfirm buttons + an \"Unconfirmed queue\" filter).\nSee [AUTH.md](AUTH.md) §2.3 and §3.3 for the full security model.\n\nYou can still use the admin CLI under strict admin auth:\n\n```bash\nADMIN_EMAIL=demo@demo.com ADMIN_PASSWORD=demo \\\npython -m scripts.admin --base-url http://localhost:8000 server list --tenant-id local-dev\n```\n\nDisable the UI with:\n\n```bash\nADMIN_UI_ENABLED=false\n```\n\n### Embeddings\n\nEmbeddings power vector and hybrid search, the semantic cache, and the semantic\nguardrail classifier. The provider is **pluggable** and can be configured two\nways, with the control DB taking precedence over the environment:\n\n1. **Environment** (boot-time default).\n2. **Admin panel** at `/ui` → **Embeddings** (runtime, persisted, recommended).\n\nProvider, model, and key are editable at runtime (control DB wins over env). The\nscreenshot below shows the panel running on **Voyage AI** — MongoDB's first-party\nembedding stack, the recommended drop-in (see [VOYAGE-AI.md](VOYAGE-AI.md)):\n\n![Embeddings panel running on Voyage AI, model voyage-3, with the key encrypted at rest](docs/images/embeddings.png)\n\nSupported providers:\n\n| Provider | `EMBEDDING_PROVIDER` | Auth | Default model |\n| --- | --- | --- | --- |\n| Ollama (local) | `ollama` | none | `nomic-embed-text` |\n| OpenAI | `openai` | `EMBEDDING_API_KEY` | `text-embedding-3-small` |\n| Azure OpenAI | `azure_openai` | `EMBEDDING_API_KEY` + endpoint/deployment | (deployment) |\n| Voyage AI | `voyage` | `EMBEDDING_API_KEY` | `voyage-3` |\n| Google Gemini | `gemini` | `EMBEDDING_API_KEY` | `text-embedding-004` |\n\nKey behaviors:\n\n- **Dimensions are auto-detected** by embedding a short probe string when a\n  config is applied — you never hand-configure vector widths, and the stored\n  width is always exactly what the provider returns (so Atlas vector indexes\n  can't drift out of sync with the data).\n- **API keys are encrypted at rest** in the control DB (Fernet, keyed by\n  `EMBEDDING_SECRET`, falling back to `ADMIN_SESSION_SECRET` / `JWT_SECRET`) and\n  are always masked in API responses. Prefer a file mount via\n  `EMBEDDING_API_KEY_FILE` / `EMBEDDING_SECRET_FILE` in production.\n- **Changing the provider/model/dimensions auto-reprovisions** everything that\n  depends on the embedding space: it re-embeds every tenant's `tool_catalog`,\n  drops and recreates the `hybrid-vector-search` indexes with the new\n  `numDimensions`, refreshes the semantic cache, and re-embeds the control-plane\n  guardrail signature corpus. Progress is tracked in `control_db.embedding_status`\n  and surfaced live in the panel.\n- Configuration is **global** (gateway-wide), so all tenants stay on a single,\n  consistent embedding space.\n\nAdmin endpoints (platform-admin only):\n\n```text\nGET    /admin/embedding          # current config (key masked) + reprovision status\nPUT    /admin/embedding          # validate, persist, reload, and reprovision\nPOST   /admin/embedding/test     # dry-run: reachability + detected dimensions\nGET    /admin/embedding/status   # reprovision progress\n```\n\nExample: switch to OpenAI from the CLI-style API (the gateway detects the width):\n\n```bash\ncurl -X PUT http://localhost:8000/admin/embedding \\\n  -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n  -d '{\"provider\":\"openai\",\"model\":\"text-embedding-3-small\",\"api_key\":\"sk-...\"}'\n```\n\n\u003e Switching providers is a heavy operation (re-embedding + index rebuilds). It runs\n\u003e in the background and search degrades gracefully (lexical fallback) while indexes\n\u003e rebuild.\n\n### Standalone gateway container\n\nFor single-container deployment, point the gateway at MongoDB and enable startup bootstrap:\n\n```bash\ndocker build -t mdb-mcp-gateway .\ndocker run --rm -p 8000:8000 \\\n  -e MONGODB_URI=\"mongodb://\u003chost\u003e:27017/?replicaSet=rs0\" \\\n  -e MONGODB_DB_NAME=\"mcp_gateway\" \\\n  -e AUTO_BOOTSTRAP=true \\\n  -e ADMIN_UI_ENABLED=true \\\n  -e ADMIN_EMAIL=\"admin@example.com\" \\\n  -e ADMIN_PASSWORD=\"change-me-now\" \\\n  -e ADMIN_SESSION_SECRET=\"a-very-long-session-secret\" \\\n  mdb-mcp-gateway\n```\n\nImportant: the gateway requires an **Atlas-capable MongoDB deployment** (Atlas Local or Atlas\ncluster) with Search + Vector Search support and replica-set semantics (for the registry watcher's\nchange streams). A plain standalone `mongod` is not sufficient.\n\n### Run tests\n\nThe suite has two tiers.\n\n**Unit tier (fully offline)** — an in-memory async MongoDB fake and a\ndeterministic embedding stub (`tests/fakes.py`) stand in for Atlas and Ollama,\nand downstream HTTP is mocked with `respx`. No external services required:\n\n```bash\npip install -r requirements-dev.txt\npytest -q -m \"not integration and not load\"\n```\n\nTo reproduce the CI quality gate locally:\n\n```bash\nruff check . \u0026\u0026 ruff format --check . \u0026\u0026 mypy .\npytest -q -m \"not integration and not load\" --cov --cov-report=term-missing --cov-fail-under=82\n```\n\n**Integration tier (18 tests, real stack)** — runs the actual `$rankFusion` /\n`$vectorSearch` / `$search` pipelines, the semantic cache, catalog sync, index\nDDL, and a concurrency benchmark against a real MongoDB Atlas Local engine and a\nreal embedding provider.\n\nThe tier **owns its own engine**: it starts a pinned\n`mongodb/mongodb-atlas-local` container via [testcontainers](https://testcontainers.com/),\nverifies it is genuinely search-capable (not a plain `mongod`), bootstraps an\n**isolated throwaway database**, and tears everything down afterwards — so it\nnever touches a shared cluster or leaves residue. All you need is Docker running\nand the embedding model pulled:\n\n```bash\nollama pull nomic-embed-text          # embedding model (host Ollama)\npytest -q -m \"integration or load\"    # testcontainers starts Atlas Local for you\n```\n\nTo run against an existing Atlas Local instead of provisioning one (e.g. a CI\nservice container or `docker compose up -d mongodb`), point the tier at it — it\nstill verifies the engine and uses an isolated DB:\n\n```bash\nINTEGRATION_MONGODB_URI=mongodb://localhost:27017/?directConnection=true \\\n    pytest -q -m \"integration or load\"\n```\n\nThe pinned image tag is overridable via `INTEGRATION_ATLAS_IMAGE`, and Ollama\nvia `OLLAMA_BASE_URL`. If Docker is unavailable and no URI override is given, the\nwhole tier skips cleanly — a no-op on a bare laptop, a hard gate in CI.\n\n### Local JWKS token flow (offline)\n\nThis is the **`AUTH_MODE=jwks`** path, for exercising asymmetric RS256 verification\noffline. (The default setup is `hs256` — there, mint tokens from the admin console's\n**Credentials → Get config** button or `POST /auth/token`, not the script below.)\n\nThis repo ships a local dev RSA keypair + JWKS so the jwks path needs no external IdP:\n\n```bash\npython -m scripts.mint_token --groups orders readonly --roles tool:invoke\n```\n\nThen set:\n\n```bash\nAUTH_MODE=jwks\nJWKS_LOCAL_PATH=./config/dev-jwks.json\nJWT_ISSUER=http://localhost:8000\nJWT_AUDIENCE=mdb-mcp-gateway\n```\n\n**Key rotation.** The JWKS is cached for `JWKS_CACHE_TTL_SECONDS`, but a token whose\n`kid` is not in the cached set triggers an immediate out-of-band refresh rather than\nwaiting out the TTL — so a rotated-in signing key is honored on the next request. To\nkeep a flood of bogus `kid`s from hammering the IdP, that refresh is throttled to once\nper `JWKS_MIN_REFRESH_SECONDS`.\n\n### Tenancy: provisioning and isolation\n\nA tenant's data lives in its own physical database (`tenant_db_name()` derives a\ncollision-safe name from the verified `tenant_id` claim). Tenant-scoped RPC methods\n(`tools/call`, `tools/list`, `tools/search`) call `ensure_tenant_ready()` before\ntouching any tenant collection:\n\n- **Unknown tenant + `AUTO_PROVISION_TENANTS=true` (default):** the tenant's database\n  and indexes are created on first use (cached per process; provisioning is\n  idempotent and does not block on Atlas index build).\n- **Unknown tenant + `AUTO_PROVISION_TENANTS=false`:** the request returns a clear\n  JSON-RPC error (`INVALID_REQUEST`, `data.reason = \"tenant_not_provisioned\"`) instead\n  of silently running against a missing database and returning empty results. Use this\n  mode where tenant ids originate from untrusted callers and provisioning should be an\n  explicit operator step (`POST /admin/tenants` or `scripts/admin.py`).\n\n### Rate limiting\n\nThe per-(tenant, client-ip) limiter counts requests per fixed sub-window but\nestimates the rate over a rolling window by weighting the previous window by how much\nof it still overlaps \"now\". This removes the fixed-window failure mode where a caller\nspends a full quota at the end of one window and again at the start of the next (a 2x\nboundary burst). Tune it with `RATE_LIMIT_WINDOW_SECONDS` and `RATE_LIMIT_MAX_REQUESTS`.\n\n### Active-active watcher resume state\n\n`services/registry_watcher.py` stores resume tokens per gateway instance in\n`control_db.watcher_state` using `_id = routing_registry::\u003cinstance_id\u003e` where\n`instance_id` comes from `GATEWAY_INSTANCE_ID` (or host name fallback). This keeps\nreplicas from clobbering each other's stream position. Resume-token docs are TTL'd by\n`WATCHER_RESUME_TTL_SECONDS`, so stale pod IDs self-clean.\n\n### Downstream auth brokering\n\nThe gateway brokers only a **workload identity** to third-party downstream servers,\nselected per server via `metadata.auth.scheme`:\n\n- `jwt` (default): gateway-minted short-lived RS256 workload identity (`iss`, `aud`,\n  `sub=tenant:\u003cid\u003e:gateway`, `tenant_id`, `iat`, `exp`, `jti`)\n- `none`: no injected transport credential — the **downstream service or the tenant**\n  presents its own authentication (vendor API key, basic auth, OAuth, mTLS, ...)\n\nThird-party credentials (API keys, passwords, OAuth client secrets) are intentionally\n**not** brokered per-server by the gateway; they belong to the downstream/tenant. When a\ndownstream needs its own credential, set `scheme=none` and terminate that auth downstream\n(or in front of it). For a code (`transport=code`) server's own logic, per-server secrets\nremain available via `context.env` (`PUT /admin/servers/{server}/env`).\n\nThe broker (`services/credential_broker.py`) caches credentials per `(tenant, server)`,\nand the warm client pool reconnects only when a cached credential is near expiry. `jwt`\nkeeps TTL + refresh-skew rotation; `none` uses a long-lived cache entry.\n\nSecurity defaults:\n\n- Credential material is never logged.\n- The `jwt` bearer is refused on plaintext `http://` downstream endpoints unless\n  `DOWNSTREAM_ALLOW_INSECURE_CREDENTIALS=true`.\n- In production, the bundled dev JWT signing key remains rejected; configure your own\n  `DOWNSTREAM_JWT_PRIVATE_KEY(_FILE)`.\n\nQuick metadata snippets:\n\n- Gateway workload identity (default)\n  ```json\n  { \"auth\": { \"scheme\": \"jwt\", \"audience\": \"downstream-service\" } }\n  ```\n- Downstream owns its own auth\n  ```json\n  { \"auth\": { \"scheme\": \"none\" } }\n  ```\n\n### Inbound MCP-client auth (username/password + OAuth)\n\nMCP clients connecting to the gateway's own surface (`/rpc`, `/mcp`) can authenticate with\na username/password in addition to the bearer/JWT flows (`AUTH_MODE=hs256|jwks`):\n\n- `POST /auth/token` — OAuth2 password grant. Exchanges username + password (the same\n  managed users + bootstrap admin used by the console login) for a short-lived bearer:\n  ```bash\n  curl -X POST http://localhost:8000/auth/token \\\n    -H \"Content-Type: application/x-www-form-urlencoded\" \\\n    -d 'grant_type=password\u0026username=$EMAIL\u0026password=$PASSWORD'\n  # -\u003e {\"access_token\":\"...\",\"token_type\":\"bearer\",\"expires_in\":28800}\n  # then call /rpc or /mcp with: Authorization: Bearer \u003caccess_token\u003e\n  ```\n- Optional HTTP Basic directly on `/rpc`/`/mcp` via `MCP_BASIC_AUTH_ENABLED=true`.\n- **OAuth is bring-your-own-IdP**: run the gateway as a resource server with\n  `AUTH_MODE=jwks`; spec-compliant MCP clients discover the issuer via\n  `GET /.well-known/oauth-protected-resource` (RFC 9728), advertised when `AUTH_MODE=jwks`\n  or `OAUTH_METADATA_ENABLED=true`.\n\nAuthorization: reaching `/rpc` requires `admin`, `tool:invoke`, or `tool:read`;\n**calling** a tool additionally requires `admin` or `tool:invoke` (`tool:read` is\ndiscover-only).\n\nSee [`AUTH.md`](AUTH.md) for the complete authentication \u0026 authorization reference\n(inbound pipeline, RBAC, downstream credential brokering, settings, and recipes).\n\n### Safely showcase: read-only tenants, viewers, and curated tools\n\nFor demos, audits, or sharing the platform without risk, the gateway adds\nadmin-controlled, least-privilege guardrails. **[`READONLY.md`](READONLY.md) is the\nfull walkthrough with screenshots**; the auth model lives in\n[`AUTH.md`](AUTH.md) §3.2:\n\n- **Read-only tenant** — freeze a tenant (`POST /admin/tenants/{id}/read-only`):\n  it stays `active` and discoverable, but `tools/call` and tenant config edits\n  return `403` (`tenant_read_only`). Platform-admin always bypasses.\n- **`viewer` role** — a read-only **console** login: browse the UI + tool source,\n  every mutation `403`. Mint via **Credentials → Access level 🔍 Explore → Create\n  credential**.\n- **`tool:read` role** — a discover-only **MCP** token: `tools/list` /\n  `tools/search` work, `tools/call` is refused (`invoke_not_permitted`). Minted by\n  the same **Explore** access level.\n- **Per-tenant tool curation** — an `allowlist` (`server/name` / `server/*`) and a\n  `max_tools` cap (`GET`/`PUT /admin/tenants/{id}/tool-policy`), plus per-server\n  enable/disable and a per-tool kill-switch (`disabled_tools`) that blocks a tool\n  for everyone, including admins (`tool_disabled`). Curation filters both\n  discovery and invocation, so a showcase only ever surfaces the curated set.\n- **Per-tenant code-package (pip) policy** — what a tenant's code tools may install\n  is the global operator ceiling (`SANDBOX_ALLOWED_REQUIREMENTS`) **intersected**\n  with a tenant allowlist (`GET`/`PUT /admin/tenants/{id}/code-requirements`, or\n  **Code packages** in the console). Empty tenant allowlist ⇒ stdlib-only. The\n  intersection is enforced consistently while authoring, on save, in the test-run,\n  and at runtime, and the Functions Studio shows an allow/deny chip per requirement.\n\nNet effect: hand teammates a viewer login on a frozen, curated tenant and they can\nexplore everything and run nothing, while platform-admins retain full control.\n\n![Read-only admin console with the sticky amber \"Read-only access\" banner](docs/images/readonly-console.png)\n\n\u003e Step-by-step with screenshots: **[`READONLY.md`](READONLY.md)**.\n\n### From the blog post to this repo\n\n`blog.md` is the narrative; this table maps each idea to where it actually lives\nin the code, so the post and the implementation stay honest with each other.\n\n| Blog concept | Where it lives in this repo |\n| --- | --- |\n| Route by meaning (curated `tools/list`) | `tools/list` + `X-MCP-Query` in `gateway/routers/rpc.py` → `HybridSearchService.search_tools` |\n| Identity-bound scope (`scopes` + server namespace filters on `$vectorSearch`) | `build_rank_fusion_pipeline(allowed_scopes=...)` in `services/hybrid_search.py`; `scopes` + `server` filter fields in `database/indexes.py` |\n| Verified claim → search filter | `gateway/middleware/auth.py` (verified token claims `groups`/`scopes`) threaded into the RPC router |\n| Hybrid search (lexical + vector fusion) | `$rankFusion` (RRF) is the **core** retrieval in `services/hybrid_search.py`; `mode=hybrid\\|vector\\|text` lets you compare the arms |\n| One control plane on MongoDB | `tool_catalog`, `routing_registry`, `semantic_cache`, `audit_telemetry` on one engine |\n| Resiliency: deadline + protocol-safe failure | `DOWNSTREAM_TIMEOUT_MS` + `DownstreamTimeout`/`DownstreamError` → `UPSTREAM_TIMEOUT`/`INTERNAL_ERROR` frames |\n| Catalog freshness off the hot path | `scripts/bootstrap.py` + `services/registry_watcher.py` (Change Streams), embeddings computed on sync |\n| Close the loop with the audit trail | `services/telemetry_logger.py` → time-series `audit_telemetry` |\n\n\u003e Okta JWT verification (Section 2 of the post) is documented as the production\n\u003e wiring; locally the gateway verifies HS256 JWT claims (`AUTH_MODE=hs256`, the\n\u003e default) — mint one from the admin console's **Credentials → Get config** button — so the\n\u003e scope-to-retrieval mapping is fully exercisable without an external IdP.\n\n# Architectural Blueprint: Building a High-Throughput, Reactive MCP Gateway with FastAPI and MongoDB Async\n\nMost Model Context Protocol (MCP) implementations look great in a weekend demo but crumble under real enterprise demands. When you have hundreds of LLM agents running concurrent tasks, spinning up individual, static connections to a dozen isolated MCP servers creates a chaotic web of infrastructure.\n\nTo solve this, we need an intelligent, enterprise-grade **MCP Gateway**.\n\nBy pairing **FastAPI** (the king of async Python web frameworks) with **FastMCP** and the native **PyMongo Async API**, we can build a gateway that acts as a single, hardened entry point. It treats MongoDB not just as a cold storage database, but as the live, reactive brain of the entire orchestration layer.\n\nHere is the comprehensive production architecture, design layout, and execution roadmap to build it.\n\n---\n\n## The High-Level Architecture\n\nThe gateway sits as an asynchronous reverse proxy between your upstream LLM orchestrators (like LangChain, LlamaIndex, or custom UIs) and your downstream, internal MCP tools.\n\n### Core Architectural Pillars\n\n1. **Asynchronous Edge to Core:** The entire stack relies on non-blocking I/O. FastAPI runs the ASGI event loop, FastMCP manages the asynchronous JSON-RPC protocol states, and PyMongo Async talks directly to MongoDB over native async sockets without thread-pool overhead.\n2. **Reactive Configuration:** Zero restarts for tool discovery. We use MongoDB Change Streams to live-update the gateway's memory space the millisecond an internal microservice or tool mapping updates in the database.\n3. **Decoupled Security \u0026 Observation:** Authentication, Role-Based Access Control (RBAC), PII scrubbing, and cost metrics are treated as global middleware layers rather than hardcoded logic inside the tool executions.\n\n---\n\n## Project Directory Layout\n\nAn enterprise-grade Python application needs strict separation of concerns. This layout isolates database management, network routing, and business logic to remain highly maintainable as your system grows.\n\n```text\nmcp-gateway/\n├── config/                 # Deployment, env vars, and Pydantic global settings\n├── database/               # Native PyMongo Async initialization and pool managers\n├── gateway/                # FastAPI routing layers, SSE endpoints, WebSocket protocols\n│   ├── middleware/         # Security, Rate-limiter, Guardrails, Tenant-isolation\n│   └── routers/            # Dynamic JSON-RPC handlers mapping clients to downstream servers\n├── services/               # Core business orchestration\n│   ├── cache_manager.py    # MongoDB Vector Search semantic cache interface\n│   ├── registry_watcher.py # MongoDB Change Stream listener for live tool mounting\n│   └── telemetry_logger.py # High-throughput writing to Mongo Time-Series collection\n├── models/                 # Pydantic schemas validating MCP and JSON-RPC 2.0 specs\n├── tests/                  # Integration and stress testing matrix\n├── Dockerfile\n└── README.md\n\n```\n\n---\n\n## Architectural Breakdown \u0026 Core Logic\n\n### 1. The Reactive Storage Layer (MongoDB Blueprint)\n\nTo optimize database read/write profiles, we partition our data models into highly specific MongoDB collection architectures:\n\n| Collection Name | MongoDB Feature Used | Purpose |\n| --- | --- | --- |\n| `routing_registry` | **Change Streams \u0026 Partitions** | Holds metadata and URLs of active downstream MCP servers. |\n| `session_context` | **TTL Indexes** | Keeps ephemeral token states and session contexts alive, auto-purging them after inactivity. |\n| `semantic_cache` | **MongoDB Vector Search** | Caches expensive LLM responses based on embedding similarity of tool arguments. |\n| `audit_telemetry` | **Time-Series Collections** | Highly compressed, high-frequency logging of every single tool execution token cost and response latency. |\n\n### 2. High-Level Gateway Logic (Pseudocode)\n\nHere is the high-level logic running inside the gateway's pipeline, illustrating how a request transitions from an incoming API call to an optimized, audited execution.\n\n#### A. The Middleware Pipeline Loop\n\nEvery incoming request must pass through a strict validation chain before a tool is ever evaluated or invoked.\n\n```python\n# Conceptual pseudocode for the FastAPI global request pipeline\n\nASYNC FUNCTION mcp_request_pipeline(client_request):\n    # 1. Tenant \u0026 Authenticity Check\n    tenant_id = extract_and_verify_jwt(client_request.headers)\n    IF NOT tenant_id:\n        RETURN ErrorResponse(STATUS=401, MESSAGE=\"Unauthorized Access\")\n        \n    # 2. Rate Limiting via Context Verification\n    allowed = CHECK_RATE_LIMIT_IN_MONGO(tenant_id, client_request.client_ip)\n    IF NOT allowed:\n        RETURN ErrorResponse(STATUS=429, MESSAGE=\"Rate Limit Exceeded\")\n        \n    # 3. RBAC Filtering\n    user_roles = FETCH_ROLES_FROM_SESSION_CONTEXT(tenant_id, client_request.user_id)\n    has_permission = EVALUATE_ABAC_MATRIX(user_roles, client_request.target_tool)\n    IF NOT has_permission:\n        RETURN ErrorResponse(STATUS=403, MESSAGE=\"Insufficient Permissions for Tool\")\n\n    # 4. Content Guardrails (Inbound)\n    sanitized_args = RUN_PII_AND_PROMPT_INJECTION_SHIELD(client_request.arguments)\n    client_request.arguments = sanitized_args\n\n    # Proceed to Execution Router\n    RETURN AWAIT execute_mcp_routing_layer(client_request, tenant_id)\n\n```\n\n#### B. The Smart Routing \u0026 Caching Layer\n\nOnce validated, the gateway uses semantic optimization to determine if it can bypass downstream compute entirely before running the tool through the FastMCP engine.\n\n```python\n# Conceptual pseudocode for semantic caching and routing execution\n\nASYNC FUNCTION execute_mcp_routing_layer(request, tenant_id):\n    # 1. Look for a semantic shortcut using MongoDB Vector Search\n    cached_payload = AWAIT check_vector_index_for_similar_execution(\n        tool_name=request.target_tool,\n        args=request.arguments,\n        threshold=0.95\n    )\n    IF cached_payload IS NOT NONE:\n        # Async log hit to telemetry and return immediately\n        START_BACKGROUND_TASK(log_telemetry, request, status=\"CACHE_HIT\")\n        RETURN cached_payload\n\n    # 2. Fetch live client connection from memory-mapped FastMCP Registry\n    mcp_client = InMemoryFastMCPRegistry.get_client(request.target_server)\n    IF mcp_client IS NONE:\n        RETURN ErrorResponse(STATUS=503, MESSAGE=\"Target MCP Server Offline\")\n\n    # 3. Execute downstream network call asynchronously\n    TRY:\n        raw_response = AWAIT mcp_client.call_tool(request.target_tool, request.arguments)\n        \n        # 4. Outbound Content Guardrails\n        validated_response = AUDIT_OUTPUT_FOR_DATA_EXFILTRATION(raw_response)\n\n        # 5. Populate cache and telemetry concurrently\n        START_BACKGROUND_TASK(save_to_semantic_cache, request.target_tool, request.arguments, validated_response)\n        START_BACKGROUND_TASK(log_telemetry, request, status=\"LIVE_EXECUTION_SUCCESS\")\n\n        RETURN validated_response\n\n    EXCEPT DownstreamTimeoutException:\n        START_BACKGROUND_TASK(log_telemetry, request, status=\"TIMEOUT_FAILURE\")\n        # Protocol-safe JSON-RPC error frame (HTTP 200), not a transport-level 5xx,\n        # so MCP clients can parse the failure instead of choking on it.\n        RETURN JsonRpcErrorFrame(CODE=-32004, MESSAGE=\"UPSTREAM_TIMEOUT\")\n\n```\n\n#### C. The Dynamic Self-Healing Engine\n\nTo achieve zero-downtime reconfiguration, the gateway runs a background loop listening for operational events directly out of MongoDB's replication log.\n\n```python\n# Conceptual pseudocode for live cluster hot-reloading\n\nASYNC FUNCTION watch_mongodb_cluster_changes():\n    # Connect directly to the change stream of the configuration collection\n    ASYNC WITH db.routing_registry.watch() AS change_stream:\n        ASYNC FOR change IN change_stream:\n            server_id = change.document_key._id\n            \n            IF change.operation_type IN [\"insert\", \"update\", \"replace\"]:\n                server_doc = AWAIT db.routing_registry.find_one({\"_id\": server_id})\n                \n                IF server_doc.is_enabled:\n                    # Dynamically construct a new FastMCP client connection string \n                    # and hot-swap it inside the active gateway pool\n                    AWAIT InMemoryFastMCPRegistry.mount_or_update(\n                        name=server_doc.name, \n                        url=server_doc.connection_url\n                    )\n                ELSE:\n                    AWAIT InMemoryFastMCPRegistry.unmount(server_doc.name)\n                    \n            ELIF change.operation_type == \"delete\":\n                AWAIT InMemoryFastMCPRegistry.unmount_by_id(server_id)\n\n```\n\n---\n\n## Full Execution \u0026 Rollout Roadmap\n\nBuilding this requires an organized, multi-phased implementation strategy to move securely from foundational scaffolding to an optimized, production-hardened platform.\n\n### Phase 1: Core Scaffolding \u0026 Async Foundation\n\n* **Objective:** Establish the asynchronous backbone of the web framework and connection pooling.\n* **Tasks:**\n* Initialize the FastAPI shell, integrating Pydantic settings for system-wide configuration.\n* Set up the global `AsyncMongoClient` layer to manage connection pools without blocking.\n* Build the standard JSON-RPC 2.0 base request/response schemas to align with core MCP specifications.\n\n\n\n### Phase 2: Reactive Routing \u0026 Dynamic Service Discovery\n\n* **Objective:** Connect external clients to multiple downstream endpoints through runtime lookups.\n* **Tasks:**\n* Implement the background execution loop utilizing PyMongo Change Streams to track additions or removals in `routing_registry`.\n* Build the FastMCP client instantiation wrapper that accepts incoming payloads and maps them to dynamic server pools.\n* Create the core FastAPI SSE (Server-Sent Events) and WebSocket transport hooks to handle bidirectional streaming safely.\n\n\n\n### Phase 3: Enterprise Security, Guardrails \u0026 Tenancy\n\n* **Objective:** Secure the perimeter against data leaks, unauthorized access, and request failures.\n* **Tasks:**\n* Embed authentication hooks into FastAPI dependencies to read incoming bearer tokens against user session tables.\n* Implement standard circuit breakers and backoff loops so a failure in an isolated internal microservice doesn't bring down the main gateway.\n* Write data validation interceptors inside the request loop to scrub outputs for sensitive records (e.g., matching PII regex structural patterns) before return delivery.\n\n\n\n### Phase 4: Intelligence, Optimization \u0026 Scale\n\n* **Objective:** Maximize performance, drive down token costs, and establish deep system observation.\n* **Tasks:**\n* Configure a MongoDB Atlas Vector Search index over the `semantic_cache` collection.\n* Wire up a local embedding workflow to analyze incoming argument patterns and intercept repetitive downstream calls.\n* Turn on the native MongoDB Time-Series collection engine for `audit_telemetry` to capture structural logs cleanly.\n* Package the entire architecture into multi-stage Docker builds optimized for Kubernetes or cloud auto-scaling deployment.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Franfysvalle02%2Fm-gate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Franfysvalle02%2Fm-gate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Franfysvalle02%2Fm-gate/lists"}