Release 1.36.0 - Pooled mcpgw Connections, Calling Tools Found by Search, and an Opt-In OBO Token Cache
November 2026
Upgrading from 1.35.0
This section covers everything you need to know to upgrade from 1.35.0 to 1.36.0.
Breaking Changes
NGINX_DISABLE_API_AUTH_REQUEST=true takes effect again. The flag had done nothing since the nginx templates drifted from the text the generator searched for, so nginx kept auth_request on /api/ whatever the setting (#1964). It now removes auth_request from the /api/ location, and the registry then accepts only the browser session cookie there: Bearer tokens, M2M tokens and static API keys get 401. If you set the flag to true in extra_env/registry.env, .env, registry_extra_env or extraEnv, remove it unless you want cookie-only /api/. Podman's default changes from true to false to match every other surface, so a Podman install that never set the flag keeps the behavior it had in practice (#1985).
The Helm charts reserve more environment variable names. The auth-server chart now reserves the OBO cache, secret-store and OpenBao variables, the registry chart reserves the three OBO cache variables, and the mcpgw chart reserves the four EGRESS_HTTP_POOL_* variables (#1796, #1798). A release that sets any of them through extraEnv fails to render. Move the value to the chart's own setting, listed under New Environment Variables below.
Other upgrade notes
The registry and auth-server egress pools now use the configured keep-alive expiry. url_guard passed its pool limits to httpx next to a custom transport, which httpx ignores, so both pools ran on the httpx default of 5 seconds instead of the configured EGRESS_HTTP_POOL_KEEPALIVE_EXPIRY_SECONDS (30 by default). Idle connections now stay open for 30 seconds. Keep the value below the shortest idle timeout of any load balancer between the gateway and its upstreams, or set it to 0 to turn keep-alive reuse off. The Helm charts used to replace a pool setting of 0 with the default, and now keep it (#1796).
mcpgw hides the reason behind a blocked outbound request. When mcpgw's egress guard refuses a destination, the tool now returns "Outbound request blocked by the mcpgw egress guard. See the mcpgw logs for details." It used to return the error text, which included internal hostnames and resolved addresses. mcpgw logs the reason once at WARNING (#1982).
Per-server PRM scopes change on providers other than Entra. The Protected Resource Metadata served for obo_exchange servers, and for virtual servers backed by them, now advertises the same scopes as the gateway-wide document: openid email profile offline_access, or MCP_ADVERTISED_SCOPES when set. It used to advertise the Entra-style <resource>/user_impersonation, which Keycloak refused with invalid_scope, so no IDE could sign in to a Keycloak obo_exchange server. Entra is unchanged (#1972).
Keycloak OBO checks that the exchanged token belongs to the same user. The auth server refuses an exchanged token whose sub differs from the user's token, or a user token with no sub, and it checks the JWKS URL before sending the token request. On Entra, the audit log records unsupported_grant_type and access_denied as exchange_failed again, as it did before 1.35.0. On Docker Compose, Keycloak 25 needs admin-fine-grained-authz added to KC_FEATURES before any OBO exchange succeeds. See the Keycloak section of docs/design/egress-auth-design.md (#1957, #1983).
The registry returns text skill assets as text again. Since 1.35.0 every skill asset came back base64-encoded, and mcpgw's get_skill_content returned only metadata for templates, schemas and config files unless the caller passed include_binary=True. An asset that decodes as strict UTF-8 with no NUL bytes now comes back in content; anything else stays base64. Every resource response includes size_bytes, and mcpgw adds content_type for text resources (#1963, #1973).
The first ECS apply redeploys three services. The registry and auth-server task definitions gain the OBO cache settings, and the mcpgw task definition gains the four pool settings, so the first terraform apply after upgrading redeploys all three even with the cache off.
New Environment Variables
| Variable | Default | Description |
|---|---|---|
EGRESS_OBO_CACHE_ENABLED
| false
| Cache exchanged OBO tokens in the per-user SecretStore, so repeated calls by one user to one audience reuse a valid token. Read by the registry and the auth server. |
EGRESS_OBO_CACHE_MAX_TTL_SECONDS
| 300
| Longest time a cached token is reused, applied below the token's own expiry. This bounds how long a revocation takes to apply on cache hits. |
EGRESS_OBO_CACHE_EXPIRY_SKEW_SECONDS
| 30
| Do not reuse a cached token this close to its expiry. Must be less than the maximum TTL. |
AUTH_SERVER_OPENBAO_TOKEN
| empty | Docker Compose only. The OpenBao token the auth server uses for the cache. Leave empty unless the cache is on. Compose has no namespace scoping, so this token can read every stored egress credential. |
mcpgw now reads the existing EGRESS_HTTP_POOL_MAX_CONNECTIONS, EGRESS_HTTP_POOL_MAX_KEEPALIVE, EGRESS_HTTP_POOL_KEEPALIVE_EXPIRY_SECONDS and EGRESS_HTTP_POOL_CONNECT_RETRIES. Set them to the same values on the registry, the auth server and mcpgw.
New Helm values:
| Value | Default | Description |
|---|---|---|
registry.egressAuth.oboCacheEnabled, auth-server.egressAuth.oboCacheEnabled
| "false"
| Turn the OBO cache on. Set both. |
*.egressAuth.oboCacheMaxTtlSeconds, *.egressAuth.oboCacheExpirySkewSeconds
| "300", "30"
| Cache reuse window, in both charts. |
auth-server.egressAuth.credentialEncryptionKey
| empty | Encrypts cached OBO tokens. Set it to the registry's egressAuth.credentialEncryptionKey.
|
auth-server.egressAuth.secretStoreBackend, .secretsManager.*, .openbao.*
| openbao, OpenBao role auth-server-obo-cache
| Secret-store settings for the cache. They must match the registry chart. The stack's openbao-init job creates the scoped role.
|
mcpgw.app.httpPoolMaxConnections, .httpPoolMaxKeepalive, .httpPoolKeepaliveExpirySeconds, .httpPoolConnectRetries
| "100", "20", "30", "1"
| mcpgw's pool settings. Keep them in step with the registry and auth-server charts. |
New Terraform variables: egress_obo_cache_enabled (false), egress_obo_cache_max_ttl_seconds (300) and egress_obo_cache_expiry_skew_seconds (30).
Upgrade Instructions
Docker Compose
cd mcp-gateway-registry
git pull origin main
git checkout 1.36.0
# Remove NGINX_DISABLE_API_AUTH_REQUEST=true from extra_env/registry.env
# (or .env on Podman) unless you want cookie-only /api/.
# Review new env vars in .env.example, then rebuild and restart:
./build_and_run.shKubernetes / Helm (EKS)
cd mcp-gateway-registry
git pull origin main
git checkout 1.36.0
# REQUIRED: the auth-server, mcpgw, registry and stack charts changed
cd charts/mcp-gateway-registry-stack
helm dependency build
helm dependency update
# Move any newly reserved names out of extraEnv (see Breaking Changes),
# then upgrade:
helm upgrade mcp-gateway . -f your-values.yamlTerraform / ECS
cd mcp-gateway-registry
git pull origin main
git checkout 1.36.0
# Three new optional variables. The first apply redeploys the registry,
# auth-server and mcpgw services.
cd terraform/aws-ecs
terraform plan
terraform applyMajor Features
mcpgw reuses its connections
mcpgw opened and closed a new HTTP client for every registry API call and every Keycloak M2M token request, so each tool call paid for a new TCP connection. It now keeps one pooled client for the life of the process, shared by all seven registry calls, the client_credentials token request and the JWKS fetch.
- mcpgw cannot import the registry's URL guard, so it has its own copy. It resolves, checks and pins every request to an address before it takes a pooled connection. It allows only the hosts in
REGISTRY_BASE_URLandKEYCLOAK_INTERNAL_URL, and refuses cloud metadata, link-local, multicast and reserved addresses. Tests keep the copy in step withurl_guard. - When a name resolves to several addresses, mcpgw tries the one that last connected first. Before, every request tried an unreachable first answer, such as
::1forlocalhostwith an IPv4-only listener, and dropped the warm connection. - mcpgw runs one token refresh at a time, so ten concurrent calls on an empty cache make one token request.
- The same change fixes the registry and auth-server pools, which had ignored their configured keep-alive (see Other upgrade notes).
See docs/egress-http-client-pooling.md.
Agents can call tools they find with search_registry
An agent that found a server with search_registry used to be told only to add it to its MCP client, which usually means restarting the session. Agents now have two more ways to call a tool they found, and mcpgw still has no tool that calls other tools on the agent's behalf.
- Coding agents that can run shell commands get curl steps in mcpgw's instructions and in the
search_registrydescription. The token comes fromREGISTRY_TOKEN, and the agent is told to ask the user to export it rather than look for one in files. - Applications built on LangGraph, Strands or the Claude Agent SDK can use
agents/discovered_tool_client.py, which searches the registry and calls the tool through the gateway while the application keeps the token. It sends the token only to the registry's own origin and refuses anendpoint_urlon any other scheme, host or port, since a registrant controls that value. - Both paths call the gateway as the user, so the gateway checks the user's own grants on every call.
See docs/ai-registry-tools.md.
Opt-in cache for exchanged OBO tokens
By default the auth server runs a fresh on-behalf-of exchange against the IdP for every request to an obo_exchange server. With EGRESS_OBO_CACHE_ENABLED=true, it stores each exchanged token in the per-user SecretStore and reuses it for the same user and audience until it nears expiry or reaches EGRESS_OBO_CACHE_MAX_TTL_SECONDS. It works with Entra and Keycloak.
- Each entry belongs to one user ID, IdP, audience and set of scopes. Entries sit in a reserved namespace that the connections API and the credential vend cannot read.
- The auth server uses the cache only when
/validateverified the request's bearer token with the IdP. Requests with a session cookie, a gateway-minted token or no stable user ID run the stateless exchange. - Concurrent requests across replicas wait for one exchange instead of each running their own, and any cache error falls back to the stateless exchange.
- The auth server gets only the access it needs: a scoped IAM policy on ECS, and a scoped OpenBao role and policy on Helm, limited to the cache namespace.
- A cache hit skips the IdP's re-check of conditional access and revocation for the length of the reuse window. Leave the cache off where that matters.
See docs/egress-obo-cache.md.
What's New
Security
- Keycloak OBO refuses an exchanged token issued for a different user, and checks the realm JWKS URL before it sends the gateway's client secret (#1957)
- A new test shows that a JWKS URL pointing at a private address or a cloud metadata address stops the exchange before any token request (#1983)
- mcpgw's blocked-egress errors no longer include internal hostnames or addresses (#1982)
- The discovered-tool helper sends the registry token only to the registry's own origin (#1976)
Egress and OBO
- Opt-in cache for exchanged OBO tokens on every deployment surface (#1798)
- Keycloak-specific OBO error mappings apply only on Keycloak, and Entra keeps its own (#1957)
- The design doc records that Keycloak 25 needs
admin-fine-grained-authzfor token exchange (#1983)
mcpgw
- One pooled client for registry and token traffic, with its own egress guard (#1796)
- The instructions and
search_registrydescription explain how to call a tool found by search (#1976) get_skill_contentreturnscontent_typefor text resources (#1973)
Skills
- Semantic search returns each skill's real
health_status,last_checked_timeandskill_md_raw_url(#1966) - Text assets come back as text, with
size_byteson every resource (#1963)
Deployment
- Podman defaults
NGINX_DISABLE_API_AUTH_REQUESTtofalse(#1985) - The pool settings reach mcpgw on Docker Compose, Helm and Terraform (#1796)
- The telemetry collector's DocumentDB final snapshot name stays the same across plans (#1954)
Documentation
- The parameter reference lists the Helm secret-generation values added in 1.35.0 (#1969)
- The writing skill and
scripts/prose-scan.pycatch more phrasing that reads badly aloud (#1975)
Bug Fixes
- Setting
NGINX_DISABLE_API_AUTH_REQUEST=truechanges the nginx config again (#1964) - IDE sign-in to Keycloak
obo_exchangeservers no longer fails withinvalid_scope(#1972) - Skills in semantic search no longer report
health_status: unknown(#1966) - Inline (AgentCore) skills are no longer marked unhealthy when the registry's own route answers a
HEADrequest with 405 (#1966) - Text skill assets are no longer returned only as base64 (#1963)
- The registry and auth-server egress pools use the configured keep-alive expiry instead of 5 seconds (#1796)
- The Helm charts keep a pool setting of
0instead of replacing it with the default (#1796) terraform planon the telemetry collector no longer reports a change to the DocumentDB final snapshot name every run (#1954)
Closed Issues
| Issue | Title | Closed By |
|---|---|---|
| #1971 | fix(oauth): per-server PRM advertises Entra-only user_impersonation scope on Keycloak, so IDE login to obo_exchange servers fails with invalid_scope | PR #1972 |
| #1962 | NGINX_DISABLE_API_AUTH_REQUEST has no effect: its /api/ block no longer matches the nginx templates | PR #1964 |
| #1952 | Skill health_status is not returned by semantic search / search_registry (always "unknown"), so clients can't skip unhealthy skills
| PR #1966 |
| #1947 | terraform/telemetry-collector: timestamp() leaves final_snapshot_identifier unknown during plans | PR #1954 |
| #1785 | Reuse long-lived HTTPX client for mcpgw to Registry calls | PR #1796 |
| #1666 | Egress OBO: optional caching of exchanged tokens in the configured SecretStore | PR #1798 |
| #1662 | Keycloak startupProbe disabled by default causes CrashLoopBackOff on fresh installs (bug) | manual |
Pull Requests Included
| PR | Title |
|---|---|
| #1985 | fix(podman): default NGINX_DISABLE_API_AUTH_REQUEST to false |
| #1983 | fix(egress): follow-ups to #1957 (subject-token comments, JWKS IP test, Keycloak 25 prerequisite) |
| #1982 | fix(mcpgw): follow-ups to #1796 (refresh-lock test, generic egress error, doc cleanup) |
| #1976 | feat(mcpgw): let agents call tools they find with search_registry, without an invoke tool |
| #1975 | docs(writing): add read-aloud tells and scan for the ones a regex can catch |
| #1973 | fix(skills): return content_type for text resources and log asset shape |
| #1972 | fix(oauth): advertise IdP-valid scopes in per-server PRM on non-Entra providers (#1971) |
| #1969 | docs: add the Helm secret-generation values from #1784 to the parameter reference |
| #1966 | fix(skills): return real health status in semantic search (#1952) |
| #1964 | fix(nginx): make NGINX_DISABLE_API_AUTH_REQUEST drop /api/ auth_request again |
| #1963 | fix(skills): return UTF-8 text assets in content again |
| #1957 | refactor(egress): Keycloak OBO follow-ups from the #1678 review |
| #1955 | chore(deps): bump zod from 3.25.76 to 4.6.6 in /cli in the npm_and_yarn group across 1 directory |
| #1954 | fix(terraform): keep telemetry final snapshot name stable across plans |
| #1798 | feat(egress): optional OBO exchanged-token cache (issue #1666) |
| #1796 | feat(mcpgw): pool the registry and M2M token egress on one long-lived client |
Security Dependency Updates
| Package | Previous | Updated | Scope |
|---|---|---|---|
| zod | 3.25.76 | 4.6.6 | cli/ (#1955)
|
Contributors
Thank you to all contributors for this release:
- Amit Arora (@aarora79)
- omrishiv (@omrishiv)
- Kang-minjong (@kang0225)
- zerone0x (@zerone0x)
- Gonçalo Faustino (@go-faustino)
Support
Full Changelog: 1.35.0...1.36.0