Enterprise MCP gateway for Spring Boot. Put one governed endpoint in front of every MCP server your agents use — with authentication, tool-level authorization, quotas, audit and metrics enforced centrally instead of reimplemented in each server.
An organisation that adopts MCP ends up with MCP servers everywhere — one for the issue tracker, one for the warehouse, one per internal platform team. Every one of them then has to answer the same questions independently:
- Who is calling, and are they allowed to call this tool?
- How many calls per minute is that agent entitled to?
- What did it actually do, and can we prove it six months from now?
Answering those N times is how you get N different answers. Put the gateway at the boundary and you answer them once.
Claude / Cursor / your agent
│
│ one MCP endpoint
▼
┌──────────────────────┐
│ MCP Gateway │ identity → authorization → quota
│ │ → routing → audit + metrics
└──────────┬───────────┘
┌────────┼────────┐
▼ ▼ ▼
GitHub Database Internal
MCP MCP MCP
Authorization-aware discovery. Most gateways deny a call at execution time but still hand every caller the full tool catalog — which leaks the name, description and argument schema of every tool behind the gateway. Here tools/list runs through the same policy engine as tools/call, so a caller is only ever shown tools it could actually invoke. An analyst and an admin connecting to the same endpoint see different catalogs. (how it is proved)
Spring-native. Not a sidecar, not another runtime to operate. It is a Spring Boot starter that binds mcp.gateway.* and integrates with the Spring Security context you already have, so roles come from your existing identity provider. Runs on Boot 4.1 and Boot 3.x, and if you would rather not embed it at all, the same thing ships as a container image.
Survives its upstreams. Sessions open lazily and are guarded by a circuit breaker, so one unreachable MCP server does not fail the gateway's startup or cost every caller a request timeout. Catalogs refresh on a schedule, so a server deployed after the gateway joins by itself.
It costs about +0.3 ms at the median over calling the upstream directly, with policy, quotas and audit all switched on — measured, with the harness and the caveats.
Needs nothing but Docker. Brings up a gateway in front of two MCP servers with deny-by-default policy and quotas on:
git clone https://github.com/ashishgituser/springboot-mcp-gateway
cd springboot-mcp-gateway
docker compose up --buildPoint any MCP client at http://localhost:8080/mcp. What you should see:
tools/list |
returns github__searchCode, github__createPullRequest, database__query — and not database__delete, which the policy denies |
calling database__delete |
refused by the gateway; the database server never sees it |
/actuator/health |
reports github and database as separate upstreams |
| gateway logs | one structured JSON audit line per call, with the decision and the reason |
The policy lives in deploy/compose/gateway.yml — edit it and restart to watch the catalog change.
<dependency>
<groupId>io.github.ashishgituser</groupId>
<artifactId>mcp-gateway-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>mcp:
gateway:
servers:
- id: github
endpoint: http://github-mcp:8080/mcp
- id: database
endpoint: http://database-mcp:8080/mcpThat is a working gateway: both servers' tools and prompts are merged into one catalog, namespaced <serverId>__<name> so names never collide, their resources are aggregated too, and the lot is served on /mcp. Everything below is optional hardening on top.
For an unreleased commit, JitPack builds any tag or commit on demand (com.github.ashishgituser.springboot-mcp-gateway:mcp-gateway-spring-boot-starter:main-SNAPSHOT).
Container images are published to GHCR for every push to main and every tag:
docker run -p 8080:8080 \
-v ./gateway.yml:/app/config/application.yml:ro \
ghcr.io/ashishgituser/mcp-gateway-server:latestEvery property also binds from the environment, so simple deployments need no config file at all. The image runs as a non-root user, shuts down gracefully, and exposes Actuator liveness/readiness probes.
Authentication is delegated to mcp-security — add org.springaicommunity:mcp-server-security-spring-boot and configure OAuth2 or API keys per its own docs. The gateway adds authorization on top.
mcp:
gateway:
policy:
enabled: true # default false - until then every call is forwarded
default-effect: DENY # what happens when no rule matches
filter-tool-list: true # default true - hide denied tools from tools/list
rules:
# First match wins, so narrow exceptions go above the broad rule they override.
- effect: DENY
tools: ["database__delete*"]
- effect: ALLOW
roles: ["admin"]
tools: ["*"]
- effect: ALLOW
principals: ["ci-bot"]
tools: ["github__createPullRequest"]principals, roles and tools are each optional — an omitted constraint matches anything. Roles populate when Spring Security authenticates the caller; without it, callers resolve to the servlet principal with no roles, so role rules never match.
tools entries are globs (*, ?) matched against every MCP capability, not just tools: the namespaced tool name, the namespaced prompt name, and the resource URI. That is deliberate — if rules only covered tools, an agent denied docs__internalReview could read the same content as a resource instead. It also means URI patterns work directly:
- effect: DENY
tools: ["file:///etc/*", "*__delete*"]Set filter-tool-list: false if you publish your tool inventory elsewhere and want the full catalog visible with enforcement only at call time.
mcp:
gateway:
rate-limit:
enabled: true # default false
capacity: 100 # max tokens a bucket holds
refill-tokens: 100
refill-period: 1m
scope: PRINCIPAL # PRINCIPAL | TOOL | PRINCIPAL_AND_TOOL
store: MEMORY # MEMORY | REDISstore: MEMORY (the default) keeps buckets in the gateway process, which means a deployment of N replicas hands out N times the configured quota. store: REDIS shares them — add spring-boot-starter-data-redis, point spring.data.redis.* at your Redis, and every replica draws from the same bucket. Refill and consume run as a single Lua script so two replicas cannot both spend the last token, and elapsed time comes from Redis's clock rather than each caller's.
No configuration needed for any of it:
- Metrics —
mcp.gateway.tool.calltimer tagged withtool.nameandoutcome(allowed,denied,rate_limited,not_found,error), plusmcp.gateway.tool.listfor discovery. Recorded through Micrometer'sObservationRegistry, so addingspring-boot-starter-actuatorexports them and adding a tracing bridge produces spans from the same observations. - Health — an
upstreamsindicator reportingUP/DOWNper configured server. - Audit — one structured JSON line per call on the
io.github.ashishgituser.mcpgateway.auditlogger, so it can be routed to its own sink independently of application logs.
mcp:
gateway:
audit:
enabled: true
include-arguments: false # default false - see below
redact: ["*password*", "*token*", "*secret*"]Argument capture is off by default: arguments are where credentials and personal data live, and audit logs usually travel further than the gateway does. Turn it on and matching keys are masked (not dropped, so the log still shows the argument was there).
mcp:
gateway:
refresh-interval: 60s # 0 to freeze the catalog at whatever startup found
request-timeout: 20s
circuit-breaker:
failure-threshold: 3
open-duration: 30s
servers:
- id: github
endpoint: http://github-mcp:8080/mcp
request-timeout: 10sConsecutive transport failures take an upstream out of rotation for open-duration; one probe then decides whether it is back. Protocol errors don't count — the server answered, so it is up.
| Supported | Notes | |
|---|---|---|
| Spring Boot | 4.1.x, and 3.x | Boot 3 loses only the Actuator health indicator — details, verified in CI |
| Java | 17, 21 | both tested in CI |
| MCP SDK | 2.0.0 | |
| Client → gateway transport | Streamable HTTP | SSE and stdio not yet supported |
| Gateway → upstream transport | Streamable HTTP | |
| MCP primitives proxied | Tools, prompts, resources | resource subscriptions and completions are not |
| Rate limiting | In-memory or Redis | mcp.gateway.rate-limit.store |
| Module | Purpose |
|---|---|
mcp-gateway-core |
Routing, policy, capability visibility, rate-limit SPI, audit model, upstream resilience. No Spring dependency. |
mcp-gateway-autoconfigure |
@AutoConfiguration and mcp.gateway.* binding. |
mcp-gateway-spring-boot-starter |
The dependency you add to your app. |
mcp-gateway-server |
Standalone container-ready distribution. |
mcp-gateway-sample |
Example application, and where the end-to-end tests live. |
mcp-gateway-demo-upstream |
Configurable MCP server backing the quickstart. |
mvn -B verify runs three layers, on Java 17 and 21 in CI:
- Unit tests — router, policy engine, capability visibility, rate limiter, argument redaction, circuit breaker.
- Architecture tests (ArchUnit) —
mcp-gateway-coremay not reference Spring or Servlet classes; its policy/rate-limit/observability packages may not depend back on the router; autoconfiguration is constructor-wired, never field-injected. - End-to-end tests — real MCP servers booted in-process, driven through the gateway's real HTTP endpoint by a real MCP client. They assert what the claims above depend on: that a denied tool is absent from
tools/list, that a denied call never reaches the upstream (proved with upstream invocation counters, not response shape), that quota rejection happens before the upstream, that a denied prompt and a denied resource URI are refused the same way a denied tool is (so no primitive is a way around a rule), and that a gateway boots and serves while one of its upstreams is dead, then folds it back in when it returns.
- Multi-server routing and tool aggregation
- Auth & policy enforcement (on mcp-security)
- Observability: metrics, health, audit logging
- Rate limiting (on Bucket4j)
- Authorization-aware tool discovery
- Upstream circuit breaking and periodic catalog refresh
- Audit argument capture with redaction
- Container image and docker compose quickstart
- Redis-backed distributed rate limiter
- Proxy MCP prompts and resources, not just tools
- Published latency benchmarks
- Spring Boot 3.x compatibility
- Architecture and request lifecycle
- Security model and threat coverage
- Benchmarks
- Running on Spring Boot 3.x
- Releasing
- Changelog
See CONTRIBUTING.md.