MCPGateway
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@MCPGatewayShow the audit trail for the last 24 hours"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
MCPGateway
A multi-tenant control plane for the Model Context Protocol. One URL for every agent, a deny-by-default policy engine on every tool call, and a replayable audit trail tied to a real identity.
Table of contents
Related MCP server: MCP Multi-Server Gateway
Why this exists
MCP made it trivial to give an agent tools. It made nothing else trivial.
The moment you run more than one MCP server in production you inherit a pile of problems the protocol deliberately does not solve:
Problem | What actually happens without a gateway |
N servers, N configs | Every agent, IDE and teammate re-configures every server by hand. |
Credentials everywhere | Provider API keys sit in plaintext in |
All-or-nothing access | An MCP server exposes 40 tools; the agent that needs 1 of them gets all 40. |
No identity | The upstream sees one shared token. "Who deleted the repo?" has no answer. |
Silent rug pulls | A provider changes a tool's schema or description overnight; agents keep calling it. |
No blast radius control | One slow upstream stalls every agent. One buggy agent exhausts a shared quota. |
MCPGateway sits between agents and upstream MCP servers and solves exactly those six problems, with nothing in the hot path that isn't necessary.
What it does
Aggregates any number of upstream MCP servers into one namespaced catalog and exposes it behind a single connection URL per agent.
Authenticates every connection to a named
Principal(a person, an agent, or a service) inside aTenant— never a shared secret.Authorises every
tools/listandtools/callagainst a versioned, deny-by-default policy that can constrain individual argument values, not just tool names.Vaults upstream credentials with envelope encryption; the agent never sees them, and rotation is a single mutation.
Detects schema drift by fingerprinting every upstream tool schema and emitting a domain event the moment a provider changes one behind your back.
Wraps every upstream call in a rate limiter, bulkhead, circuit breaker, retry with full-jitter backoff, and a timeout — scoped per tenant and per provider.
Records every decision and call into a replayable audit trail, and exports Prometheus metrics for all of it.
Architecture
System context
flowchart LR
subgraph Agents["AI agents & clients"]
A1["Claude Desktop"]
A2["Custom agent<br/>(LangGraph / SDK)"]
A3["IDE assistant"]
end
subgraph GW["MCPGateway"]
direction TB
DP["Data plane<br/>MCP over Streamable HTTP<br/>/mcp/:token"]
CP["Control plane<br/>GraphQL /graphql"]
POL["Policy engine<br/>deny by default"]
CAT["Catalog<br/>+ drift detection"]
VLT["Credential vault<br/>envelope encryption"]
RES["Resilience layer"]
AUD["Audit + metrics"]
end
subgraph Upstreams["Upstream MCP servers"]
U1["github-mcp"]
U2["slack-mcp"]
U3["payments-mcp"]
end
A1 & A2 & A3 -->|"one URL per identity"| DP
DP --> POL --> CAT --> RES --> U1 & U2 & U3
RES --> VLT
DP --> AUD
CP -.->|"tenants, providers,<br/>policies, tokens, audit"| GW
AUD --> PROM["Prometheus / Grafana"]Hexagonal layering
Dependencies point strictly inward. The domain has no idea HTTP, GraphQL or Postgres exist, which is why every rule in it is testable in microseconds.
flowchart TB
subgraph Driving["Driving adapters (inbound)"]
GQL["api/resolvers.ts<br/>GraphQL"]
MCPR["api/mcp-router.ts<br/>MCP JSON-RPC"]
CLI["cli.ts"]
end
subgraph App["Application layer — use cases"]
GS["GatewayService<br/>list + call + intercept"]
CS["CatalogService<br/>aggregate + drift"]
SS["SessionService<br/>tokens + sessions"]
AS["AdminService<br/>control plane"]
end
subgraph Domain["Domain — pure, no I/O"]
POL["policy.ts<br/>evaluatePolicy"]
TEN["tenant.ts"]
PRV["provider.ts"]
SES["session.ts<br/>state machine"]
EVT["events.ts"]
AUD["audit.ts"]
end
subgraph Ports["Ports (interfaces)"]
P["ports.ts<br/>Repositories, CredentialStore,<br/>EventBus, McpClientFactory"]
end
subgraph Driven["Driven adapters (outbound)"]
MEM["adapters/memory<br/>in-memory repos"]
MCPC["mcp/client.ts + transports<br/>http | stdio | in-memory"]
VAULT["app/vault.ts<br/>AES-GCM envelope"]
BUS["app/event-bus.ts"]
end
Driving --> App
App --> Domain
App --> Ports
Ports -.implemented by.-> DrivenTool-call flow
Every step below is a real code path, in order, with the failure mode it guards.
sequenceDiagram
autonumber
participant A as Agent
participant H as HTTP /mcp/:token
participant S as SessionService
participant G as GatewayService
participant P as Policy engine
participant R as ResiliencePolicy
participant U as Upstream MCP server
participant X as Audit + metrics
A->>H: POST tools/call {name, arguments}
H->>S: authenticate(token)
alt token unknown / revoked / expired / principal disabled
S--xA: 401 unauthorized
end
S-->>H: Caller {tenant, principal}
H->>G: callTool(ctx, request)
G->>G: resolve qualified name -> provider + tool
G->>P: evaluatePolicy(principal, provider, tool, arguments)
alt deny (no matching allow, explicit deny, or argument violation)
P-->>G: Deny {ruleId, reason, violations}
G->>X: record outcome=denied
G--xA: -32010 policy_denied + violations
end
P-->>G: Allow {ruleId}
G->>G: run CallInterceptors (PromptShield hook)
G->>R: execute(scope = tenant/provider)
R->>R: rate limit -> bulkhead -> circuit breaker -> retry -> timeout
R->>U: tools/call with vaulted credential injected
alt upstream fails
U--xR: error
R->>R: retry w/ full jitter, then trip breaker
R-->>G: UpstreamUnavailableError
G->>X: record outcome=error
G--xA: -32020 upstream_unavailable
end
U-->>R: result
R-->>G: result
G->>X: record outcome=success, duration, result digest
G-->>A: tools/call resultSession lifecycle
stateDiagram-v2
[*] --> pending: open(caller, transport)
pending --> ready: initialize handshake
pending --> failed: handshake error
ready --> closing: close(reason)
ready --> failed: transport error
closing --> closed
failed --> closed
closed --> [*]
note right of pending
Illegal transitions are rejected by
a single TRANSITIONS table, not by
scattered if-statements.
end noteComponent reference
Component | Path | Responsibility | Key design note |
| Typed success/failure without exceptions |
| |
| 14 domain errors carrying | One error taxonomy maps cleanly onto both HTTP status and JSON-RPC codes | |
Branded IDs |
| A | |
| Canonical-JSON fingerprint of a tool schema | Key-order independent, so drift detection has no false positives | |
Config | Zod-validated, nested, parsed once at boot | A bad env var fails at startup, never mid-request | |
Metrics | Dependency-free Prometheus counters/gauges/histograms | No client library in the dependency tree; ~200 lines and fully tested | |
| Per-scope quota with lazy refill |
| |
| closed → open → half-open | Injected | |
| Concurrency cap + bounded queue | Also provides | |
| Composes all five primitives in the correct order | Rate limit before bulkhead: reject cheaply before consuming a slot | |
MCP protocol | Zod-validated JSON-RPC 2.0 + MCP | Batch of length 0 is rejected per JSON-RPC §6 | |
Transports |
| All three satisfy one | |
| Generic, context-parameterised MCP server | Same router serves the gateway and the test fixtures | |
Policy engine | Deny-by-default evaluation with argument constraints | Explicit deny always wins; | |
| Aggregate, namespace, diff and refresh upstream tools | Returns a structured | |
| The hot path: filter, authorise, proxy, record |
| |
| AES-256-GCM envelope encryption + | Master key wraps per-record DEKs; comparison is constant-time | |
| Pooled upstream clients + per-scope resilience | Evicts a client on error so a poisoned connection is never reused | |
GraphQL API | 12 queries + 14 mutations + 1 subscription | GraphQL over REST: the control plane is a graph, and one round trip beats six |
Quick start
# Requires Node >= 22 and pnpm
pnpm install
# 1. Watch the whole system work, end to end, with three fake upstream servers.
pnpm demo
# 2. Run the real server (in-memory storage, GraphiQL enabled)
cp .env.example .env
pnpm dev
# GraphQL http://localhost:8080/graphql
# MCP http://localhost:8080/mcp/<connection-token>
# Metrics http://localhost:8080/metrics
# 3. Everything else
pnpm typecheck # tsc over src + test + benchmarks
pnpm test # 213 tests
pnpm test:cov # + coverage gate (85 / 80 / 85 / 85)
pnpm bench # latency + complexity table
pnpm build # dist/With Docker:
export VAULT_MASTER_KEY="$(openssl rand -base64 32)"
export ADMIN_API_KEY="$(openssl rand -hex 32)"
docker compose up -d # gateway + postgres
docker compose --profile monitoring up -d # + prometheus, grafana, alertmanager, otelLive demo output
pnpm demo boots three in-process MCP servers, registers them for tenant acme,
publishes a policy, and then exercises the whole system. This is verbatim output,
not a mock-up:
1. Aggregated catalog: three upstream MCP servers behind one endpoint
github__create_issue <- github/create_issue digest=c89537e73a42
github__list_repos <- github/list_repos digest=e04ab9142728
slack__post_message <- slack/post_message digest=4c58eebbbfd6
payments__refund <- payments/refund digest=860462b4e7dc
2. Per-identity tool visibility (deny by default)
alice sees 3: github__create_issue, github__list_repos, slack__post_message
releasebot sees 1: github__create_issue
3. Allowed call, proxied to the real upstream server
result: [{"type":"text","text":"issue opened in acme/api: Flaky integration test"}]
4. Argument-level policy: the agent may only touch approved repos
allowed -> [{"type":"text","text":"issue opened in acme/web: Release 1.4.0 checklist"}]
denied -> policy_denied: policy denied: argument constraints violated
violations=["repo must be one of [\"acme/api\",\"acme/web\"]"]
5. Explicit deny always wins, even for a privileged user
denied -> policy denied: refunds require a finance-approved principal
6. Upstream schema drift detection
refreshed 3 providers, drift events: 1
DRIFT github__create_issue: c89537e7 -> 3fea86ad
7. Replayable audit trail tied to real identity
principal tool outcome duration
alice payments__refund denied 0ms
releasebot github__create_issue success 0ms
releasebot github__create_issue denied 0ms
alice github__create_issue success 0ms
8. Prometheus metrics (excerpt)
mcpgateway_tool_calls_total{outcome="success",provider="github",tool="github__create_issue"} 2
mcpgateway_tool_calls_total{outcome="denied",provider="github",tool="github__create_issue"} 1
mcpgateway_tool_calls_total{outcome="denied",provider="payments",tool="payments__refund"} 1
mcpgateway_policy_decisions_total{effect="allow",tool="github__create_issue"} 2
mcpgateway_policy_decisions_total{effect="deny",tool="github__create_issue"} 1
mcpgateway_policy_decisions_total{effect="deny",tool="payments__refund"} 1Step 4 is the point of the whole project: releasebot is allowed to call
github__create_issue, but only with repo in an approved set. Tool-level
allow-lists cannot express that. This one can.
API
Control plane (GraphQL)
Guarded by x-api-key: $ADMIN_API_KEY. GraphiQL is served at /graphql when
GRAPHIQL_ENABLED=true (never in production).
Query | Purpose |
| Tenants with nested principals and providers |
| Identities in a tenant |
| Registered upstream MCP servers |
| Status, tool count, last refresh, last error |
| The aggregated, namespaced tool catalog |
| The active policy document with its version |
| Dry-run a decision before shipping a policy change |
| Live sessions and issued tokens |
| Paginated, filterable audit trail |
| Live circuit-breaker / bulkhead state per scope |
Mutation | Purpose |
| Tenant lifecycle |
| Identity lifecycle |
| Upstream lifecycle (secret stored on registration) |
| Re-wrap an upstream secret without touching the provider record |
| Force re-discovery; returns per-provider diffs including drift |
| Publish a new version of the access policy |
| Mint and kill the single per-agent MCP URL |
| Terminate a live agent session |
events(tenantId) streams the DomainEvent union — tool calls, policy decisions,
schema drift, credential rotations, session transitions — filtered to one tenant.
Dry-run a policy change before it reaches production:
query {
evaluatePolicy(
tenantId: "acme"
principalId: "releasebot"
providerId: "github"
toolName: "create_issue"
arguments: { repo: "acme/secret-infra", title: "..." }
) {
effect # "deny"
ruleId # "ci-can-open-issues"
reason # "argument constraints violated"
violations # ["repo must be one of [\"acme/api\",\"acme/web\"]"]
}
}Data plane (MCP over Streamable HTTP)
POST /mcp/:connectionToken speaks JSON-RPC 2.0. The token is the identity — no
extra header, so it drops straight into any MCP client config:
{
"mcpServers": {
"everything": {
"url": "https://gateway.example.com/mcp/mcp_live_a1b2c3d4"
}
}
}Method | Behaviour |
| Negotiates protocol version, returns capabilities |
| Returns only the tools this principal is allowed to call |
| Policy check → interceptors → resilient proxy → audit |
| Proxied and aggregated |
| Liveness |
batch | Supported; empty arrays rejected per JSON-RPC §6 |
GET /mcp/:token with accept: text/event-stream opens the server→client SSE stream.
JSON-RPC error codes: -32010 policy denied, -32020 upstream unavailable,
-32021 upstream timeout, -32030 rate limited — alongside the standard
-32600/-32601/-32602/-32603.
Policy model
A policy is a versioned, ordered list of rules. Evaluation is deny-by-default:
If any
denyrule matches → deny. Explicit deny always wins.Otherwise, if an
allowrule matches → check its argument constraints.Otherwise → deny (no rule matched).
new PolicyBuilder(TenantId("acme"), version)
.allow({
id: "eng-full-access",
subjects: ["group:engineering"],
providers: ["github", "slack"],
tools: ["*"],
})
.allow({
id: "ci-can-open-issues",
subjects: ["group:ci"],
providers: ["github"],
tools: ["create_issue"],
arguments: [
{ path: "repo", rule: { kind: "one_of", values: ["acme/api", "acme/web"] } },
{ path: "title", rule: { kind: "max_length", value: 120 } },
],
})
.deny({
id: "no-refunds",
subjects: ["*"],
providers: ["payments"],
tools: ["refund"],
description: "refunds require a finance-approved principal",
})
.build();Subjects match a principal id (alice), a group (group:engineering), a kind
(kind:agent) or *. Argument constraints address nested values by dotted path
(config.target.region) and support one_of, not_one_of, matches (regex),
max_length, max_items, range and required.
Resilience
Every upstream call passes through five primitives, composed in this order and scoped
to tenant/provider so one noisy tenant cannot affect another:
flowchart LR
C["callTool"] --> RL["Rate limiter<br/>token bucket"]
RL -->|"reject cheaply"| E1["rate_limited (-32030)"]
RL --> BH["Bulkhead<br/>concurrency + queue"]
BH -->|"shed load"| E2["bulkhead_rejected"]
BH --> CB["Circuit breaker<br/>closed/open/half-open"]
CB -->|"fail fast"| E3["circuit_open"]
CB --> RT["Retry<br/>full-jitter backoff"]
RT --> TO["Timeout"]
TO --> U["Upstream"]Ordering is deliberate: rate limiting is O(1) and rejects before a bulkhead slot is consumed; the breaker sits outside retry so a dead upstream is not retried 3× per request while it is already known to be down; the timeout is innermost so it bounds a single attempt rather than the whole retry sequence.
Only errors marked retryable are retried — a policy_denied or validation_failed
is never retried, because it will never succeed.
Observability
Structured logging (pino) with correlation IDs threaded from the inbound request
through every service call. redactArguments strips tool arguments and secret-shaped
fields before anything is written.
Metrics at GET /metrics:
Metric | Type | Labels |
| counter |
|
| histogram |
|
| counter |
|
| counter |
|
| histogram |
|
| counter |
|
| counter |
|
| counter |
|
| gauge |
|
| gauge |
|
| counter |
|
Dashboards and alerts ship in monitoring/: a Grafana overview dashboard (RED + governance + resilience rows), 12 Prometheus alert rules, an Alertmanager route into Grafana OnCall with severity-based escalation and inhibition, and an OpenTelemetry collector config that deletes tool arguments and auth headers before any trace leaves the process.
Data model and partitioning
See db/migrations/0001_init.sql.
Every table is keyed by tenant_id first — that single column is the shard key, so a
future horizontal split moves whole tenants and never splits a tenant's rows.
tool_call_audit is the only unbounded table, so it is:
RANGE partitioned by month on
occurred_at— retention becomesDROP TABLEof a whole partition (O(1)) instead of a multi-million-rowDELETE, and every time-bounded audit query prunes to a single partition;HASH sub-partitioned 4 ways by
tenant_idwithin each month, so one high-volume tenant cannot make another tenant's index scans hot.
create_audit_partition() and drop_audit_partitions_older_than() are shipped as
functions; the current month plus three ahead are pre-created so ingestion never hits a
missing partition.
Domain events go through a transactional outbox (event_outbox) written in the same
transaction as the state change — no dual-write, no lost events.
Connection tokens are stored hashed; a database leak yields no working credentials.
Performance and complexity
pnpm bench — measured on Node v24.10.0, linux/x64:
Operation | Complexity | Iterations | p50 | p95 | p99 | ops/sec |
| O(R × (S+P+T+A)) | 50,000 | 0.0007 ms | 0.0012 ms | 0.0031 ms | 941,355 |
| O(R × (S+P+T+A)) | 20,000 | 0.0260 ms | 0.0492 ms | 0.1991 ms | 30,536 |
| O(n log n) on keys | 20,000 | 0.0052 ms | 0.0078 ms | 0.0204 ms | 144,680 |
| O(1) amortised | 200,000 | 0.0002 ms | 0.0003 ms | 0.0005 ms | 3,914,955 |
| O(1) | 20,000 | 0.0003 ms | 0.0026 ms | 0.0031 ms | 1,862,237 |
| O(T × R) | 5,000 | 0.0051 ms | 0.0087 ms | 0.0412 ms | 125,513 |
| O(R + upstream) | 3,000 | 0.0450 ms | 0.1473 ms | 0.3507 ms | 14,615 |
| O(n) over tenant slice | 2,000 | 0.4546 ms | 0.7978 ms | 1.1650 ms | 1,931 |
| O(series) | 2,000 | 0.0168 ms | 0.0273 ms | 0.1132 ms | 46,864 |
Where R = rules, S/P/T = subject/provider/tool patterns per rule, A = argument constraints, T = tools in catalog, n = records.
Reading the numbers. Policy evaluation adds ≈0.7 µs to a call that will spend
tens of milliseconds in the upstream — governance is free at this scale. Catalog
resolution is a Map lookup, deliberately: the hot path must not scan. The 201-rule
case is the honest worst case and is the one place a future optimisation (indexing
rules by provider before scanning) would pay off; it is not needed yet, and the
benchmark exists so a regression is visible the day it lands.
audit.query is the slowest operation by design — it is an operator-facing scan, not a
hot path, and in Postgres it is served by the partitioned index rather than this
in-memory adapter.
Test results
pnpm test:cov — 213 tests across 14 files, all passing.
Suite | Tests | What it pins down |
8 |
| |
5 | Error taxonomy, | |
6 | Branding, | |
10 | Counter/gauge/histogram maths, label escaping, exposition format | |
6 | Env mapping, defaults, rejection of malformed values | |
30 | Timeout, retry jitter bounds, breaker transitions, bucket refill, bulkhead queueing, full policy composition — all on a | |
16 | Deny-by-default, explicit-deny precedence, wildcards, every argument constraint, nested paths | |
10 | Legal and illegal state transitions | |
18 | Envelope encrypt/decrypt, rotation, tamper detection, constant-time compare, event bus fan-out and handler isolation | |
22 | JSON-RPC framing, batch rules, version negotiation, router dispatch and error-code mapping | |
17 | Streamable HTTP incl. SSE multi-event, 202/204, bad content-type, network failure; stdio against a real child process; in-memory isolation | |
28 | Catalog aggregation and drift diffs, gateway allow/deny/upstream-failure paths, session lifecycle, admin mutations | |
21 | Health, metrics, admin auth, MCP routing, error mapping | |
16 | JSON scalar over every literal kind, all queries, all mutations, subscription tenant-filtering |
Coverage (thresholds: statements 85 / branches 80 / functions 85 / lines 85):
Module | Stmts | Branch | Funcs |
| 99.53% | 91.63% | 97.11% |
| 98.97% | 96.52% | 100% |
| 96.55% | 88.18% | 91.66% |
| 94.07% | 84.50% | 94.73% |
| 91.71% | 84.12% | 82.22% |
| 82.77% | 77.41% | 82.45% |
All files | 92.47% | 86.38% | 91.46% |
The domain and resilience layers — where a bug is silent and expensive — are near 100%. The API layer is lower because its uncovered branches are transport plumbing already exercised end-to-end by the HTTP suite.
Six real source bugs were found by these tests, not by review:
evictIdle()compared a stale token count against the burst size, so a bucket that had been drained once was never reclaimed — a slow memory leak per idle scope.rpcCodeForwas not exported, so callers silently fell back to-32603.Zod parse failures mapped to
-32603(internal error) instead of-32602(invalid params) — the client could not tell "you sent bad arguments" from "we broke".An unknown MCP method threw an anonymous
Errorwith a stapled property thatrpcCodeForignored, somethod not foundreported as an internal error.JsonRpcPayloadSchemaaccepted an empty batch[], which JSON-RPC 2.0 §6 forbids.InMemoryTransportthrew a plainErroron a closed transport, so callers could not treat transports uniformly — it now throwsUpstreamUnavailableErrorlike the others.
Configuration
Every value is parsed and validated once at startup by src/shared/config.ts; see .env.example for the full annotated list.
Group | Variables |
HTTP |
|
Storage |
|
Secrets |
|
Resilience |
|
Catalog |
|
Audit |
|
Observability |
|
VAULT_MASTER_KEYandADMIN_API_KEYhave development defaults sopnpm demoruns with zero setup.docker-compose.ymlrefuses to start without real values.
Operations
Runbook — first three things to check.
Alert | Likely cause | First action |
| A provider silently changed a tool definition (the MCP rug-pull vector) |
|
| An upstream is down or slow |
|
| A policy change broke a legitimate workflow, or an agent is misbehaving |
|
| A tenant is over quota |
|
| Upstream latency, not gateway latency | Compare |
Incident containment. revokeConnection(token) kills one agent instantly;
setPrincipalDisabled kills every session for an identity; setProviderDisabled
removes an upstream from every catalog without deleting its configuration.
Container. Multi-stage build → distroless, non-root uid 10001, read-only root
filesystem, all capabilities dropped, no-new-privileges, and a HEALTHCHECK that
runs mcpgateway healthcheck (no shell or curl needed in the image).
Project layout
MCPGateway/
├── src/
│ ├── shared/ Result, errors, branded ids, clock, metrics, logger, config
│ ├── resilience/ timeout, retry, circuit breaker, rate limiter, bulkhead, policy
│ ├── mcp/ JSON-RPC + MCP protocol, client, generic server router
│ │ └── transports/ http (streamable + SSE) | stdio | in-memory
│ ├── domain/ tenant, provider, session, policy, events, audit (pure, no I/O)
│ ├── app/ ports + CatalogService, GatewayService, SessionService,
│ │ AdminService, vault, upstream registry, event bus
│ ├── adapters/ memory storage, MCP client factory, fixture MCP server
│ ├── api/ GraphQL SDL + resolvers, MCP router, HTTP app
│ ├── demo/ the runnable end-to-end scenario
│ ├── container.ts composition root
│ ├── server-entry.ts
│ └── cli.ts
├── test/ 213 tests mirroring the src layout
├── benchmarks/ criterion-style latency + complexity harness
├── db/migrations/ partitioned schema
├── monitoring/ prometheus, alerts, alertmanager, otel, grafana
├── postman/ runnable collection with assertions
└── .github/workflows/ ci.ymlDesign decisions
Why GraphQL for the control plane and JSON-RPC for the data plane? They are different problems. The control plane is a graph — "show me this tenant, its principals, their providers, and the last 20 audit records" is one query instead of six REST round trips, and the subscription gives operators a live event feed for free. The data plane is not negotiable: MCP is JSON-RPC 2.0, so the gateway speaks it exactly.
Why a hand-written metrics implementation? It is ~200 lines, fully tested, has zero dependencies, and emits the exposition format Prometheus expects. Pulling in a client library to count integers is a dependency I would have to defend at audit time.
Why branded IDs? Passing a ProviderId where a TenantId belongs is the kind of
bug that reaches production and leaks data across tenants. Branding makes it a compile
error. Same reasoning behind the TRANSITIONS table for sessions and the discriminated
DomainEvent union — the type checker enforces exhaustiveness so a new event type
cannot be silently ignored.
Why interfaces for everything, in-memory for now? The ports in
src/app/ports.ts are the contract; adapters/memory is one
implementation and a Postgres one is another. That is why the entire test suite runs in
~2 seconds with no containers, and why the schema in db/migrations/ can land without
touching a single line of domain code.
Why a CallInterceptor seam? Prompt-injection scanning, PII redaction and
exfiltration detection are a separate concern with a separate release cadence. They
belong behind an interface, not inlined into GatewayService.
Roadmap
Postgres adapter behind the existing ports (schema is already written)
OAuth 2.1 device flow for upstream providers that require user consent
SAML / SCIM for enterprise identity sync
Policy simulation against replayed historical traffic
PromptShield integration through the
CallInterceptorseam
License
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Flicense-qualityDmaintenanceA centralized gateway and router that integrates multiple MCP servers into a single endpoint with built-in policy enforcement and secret management. It features a Web GUI for managing tool access, audit logs, and multi-environment configurations across various sub-servers.Last updated
- Flicense-qualityCmaintenanceAggregate, route, and orchestrate multiple MCP backend servers behind a single MCP endpoint.Last updated
- Alicense-qualityBmaintenanceCentralized MCP control plane that proxies multiple upstream MCP servers with tool namespacing, filtering, policy enforcement, audit logging, and health checks.Last updated7MIT
- Alicense-qualityDmaintenanceA unified MCP gateway that aggregates multiple MCP servers and API plugins behind a single endpoint with authentication, rate limiting, audit logging, REST API bridge, and web dashboard.Last updated8MIT
Related MCP Connectors
Control plane for autonomous software labor. Agents claim objectives over MCP with audit trail.
Self-hosted federated MCP gateway: one OAuth 2.1 MCP server in front of N apps, user-level scopes.
A paid remote MCP for CLI tool MCP, built to return verdicts, receipts, usage logs, and audit-ready
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ABHIJEET-MUNESHWAR/MCPGateway'
If you have feedback or need assistance with the MCP directory API, please join our Discord server