Release 1.37.0 - MCP 2026-07-28 Support, Session Cookie Hardening and UI Branding
Unreleased. This page grows as pull requests merge into the 1.37.0 milestone.
Upgrading from 1.36.0
This section covers everything you need to know to upgrade from 1.36.0 to 1.37.0.
Breaking Changes
- A wildcard tool grant no longer grants every MCP method. A scope with
tools: ["*"]now grants every tool fortools/calland no other method. A custom scope that relied on the old behavior for a method it does not list, such asresources/readorprompts/get, gets 403 for that method until you add the method to itsmethodslist. The unrestricted seed scopes and the example scope files already listresources/read,prompts/listandprompts/get. See scopes. (#1993)
To find affected groups, see what is being refused, and fix them, follow Check your scopes in the upgrade FAQ.
-
An OIDC login that returns no account id now fails. A Keycloak, Cognito, Entra, Okta, Auth0 or PingFederate login, or any login whose token response includes an
id_token, gets 401 when it yields no subject, a subject that is not a string, or one starting withname:. It used to succeed with ayielded no subjectwarning, on every deployment. Search the auth-server logs for that line before you upgrade. (#2014) -
The auth server refuses to start on an edited
sessionblock inauth_server/oauth2_providers.yml. Every session cookie setting now comes from the environment, which the registry and the auth server share. If you setsecure,max_age_seconds,cookie_name,samesiteorhttponlyin the yml, or adomainother than the shipped${SESSION_COOKIE_DOMAIN}placeholder, move the value toSESSION_COOKIE_SECURE,SESSION_MAX_AGE_SECONDS,SESSION_COOKIE_NAMEorSESSION_COOKIE_DOMAIN. The startup error names each key and the variable to set. (#2025) -
Helm: session cookie overrides move to the stack globals. The stack chart sets
global.sessionMaxAgeSeconds,global.sessionCookieSecureandglobal.sessionCookieDomainonce for the registry and the auth server, and it refuses a render where a subchart value such asregistry.app.sessionCookieDomainorauth-server.app.sessionCookieSecurediffers from what the globals render. Move those values to the globals. (#2025) -
/brandingis a reserved path. The registry now serves UI branding files at/branding/, so registering a server or agent whose path starts with/brandingfails. Check existing registrations before you upgrade. (#2000)
Other upgrade notes
-
What changes when I upgrade to a release with MCP 2026-07-28 support? collects everything below with troubleshooting steps.
-
MCP SDK 2.x. The registry's tool discovery, health probe, security scanner and mcpgw now run on MCP SDK 2.x (
mcp>=2.3.0,<3, fastmcp 4.1.6 in mcpgw,cisco-ai-mcp-scanner4.8.7). The root project no longer depends onlangchain-mcp-adaptersorstrands-agents; nothing in it imported them. -
Larger tool calls. The gateway now authorizes MCP request bodies up to 1 MB. Tool calls above about 10 KB used to fail with a 500.
-
Authentication headers. A 401 now carries one
WWW-Authenticateheader (it carried two), and a 403 carriesWWW-Authenticate: Bearer resource_metadata="...". -
Refused MCP requests. The body of a 403 for an MCP request is now a JSON-RPC error with code
-32003that names the refused method, the tool fortools/call, and the server. It used to be{"error": "Access forbidden"}. (#2011, closes #2006) -
Browser-session MCP requests. An MCP request signed in with the registry's browser session cookie gets 403 unless its
Originheader is the gateway's own origin, taken fromAUTH_SERVER_EXTERNAL_URLorREGISTRY_URL. Clients that send a bearer token are not affected. (#2011, closes #1996) -
Health status. A backend whose host does not resolve now reads
unhealthy: host could not be resolvedinstead ofunhealthy: url blocked by SSRF guard. -
HTTP+SSE. Registering a server whose only transport is
sselogs a deprecation warning. Nothing is rejected. -
Per-user egress: sign in again. Every user signed in at upgrade time has no vault key until they sign in again, at most
SESSION_MAX_AGE_SECONDSlater (8 hours by default). Until then, connecting an account and submitting a PAT return 403 and Connected Accounts is empty. Signing out and back in fixes it. (#2014) -
Per-user egress: reconnect once. The vault now keys every user on the identity provider's immutable account id. Users who signed in through Cognito's access-token path (this includes the reference ECS deployment), clients that present raw Okta access tokens, and users of GitHub, Google or custom logins that map no subject reconnect each egress server once. The old entries stay in the secret store, unread. A bearer token without
subgets no per-user egress until it carriessubor is replaced by a registry-generated token. See egress credential vault. (#2014) -
Per-user egress: consents finish in the browser that started them. The callback host must receive both the registry session cookie and the new consent cookie, so it has to be the host the UI runs on, or share
SESSION_COOKIE_DOMAINwith it. A consent in progress at upgrade time fails once. A discovery identity designated under a Cognito username, an Okta login or a bare fallback username no longer matches; delete it and designate it again.EgressTokenResponseno longer carriesauthorize_url. (#2014) -
Okta custom authorization server. With
OKTA_AUTH_SERVER_IDset, the browser login now uses that server as well as token validation. Before, the login went to the org server and failed on an issuer mismatch. See Okta setup. (#2014) -
Discovery documents. The auth server now keeps the Keycloak and PingFederate discovery documents until it restarts. Restart it after you change the identity provider's endpoints. (#2019)
-
SSRF_ALLOWED_CIDRSon the auth-server. Docker Compose, Terraform and Helm now passSSRF_ALLOWED_CIDRSto the auth-server and mcpgw as well as the registry. The auth-server's generic proxy therefore reaches private addresses inside a listed range, where it used to refuse them with a 502 even after the registry accepted the server at registration. The auth-server still ignores the setting for OAuth token requests and for calls that carry an injected egress credential. (#2024) -
Helm: move
SSRF_ALLOWED_CIDRStoglobal.ssrfAllowedCidrs. The stack chart has a newglobal.ssrfAllowedCidrskey that the registry, auth-server and mcpgw all read, and it wins over each subchart'sapp.ssrfAllowedCidrs. If your values set onlyregistry.app.ssrfAllowedCidrs, the registry renders as before, but the auth-server and mcpgw get an empty value and the generic proxy keeps refusing those targets. Move the value toglobal.ssrfAllowedCidrs. (#2024) -
Extra env can no longer set
SSRF_ALLOWED_CIDRS. Terraform refuses the name inregistry_extra_env,auth_server_extra_envandmcpgw_extra_env. The Helm charts now compareextraEnvnames with reserved names after trimming and upper-casing them, sossrf_allowed_cidrsfails the render too. Setssrf_allowed_cidrsin Terraform orglobal.ssrfAllowedCidrsin Helm instead. (#2024) -
SESSION_COOKIE_SECURE=falsenow takes effect on HTTPS. The login cookie used to staySecurebecause the yml'ssecure: truewon over the environment. Withfalse, the cookie now has noSecureattribute. Both services warn about it at startup. Check.env,extraEnv, your Terraform variables and your copy ofinfra/config.yaml, which shippedcookieSecure: falsebefore this release. (#2025) -
One sign-out for a browser holding two session cookies. The registry and the auth server now read the session cookie by its exact name and treat two copies as no session. A browser that kept an old copy, for example after a
SESSION_COOKIE_DOMAINchange, is signed out once and the refused copies are expired. A cookie left on a domain the deployment no longer names has to be cleared in the browser. (#2025) -
A stale cookie beside a bearer token. A client that sends a bearer token together with a session cookie that no longer resolves gets 403 from CSRF-protected registry routes until it drops the cookie. (#2025)
-
Health WebSocket scripts.
/api/health/ws/health_statusreads the session from theCookieheader only and refuses the handshake with HTTP 403 otherwise. A script that connected with?mcp_gateway_session=...must send the cookie in a header instead. (#2025) -
/validateignoresX-User-Pool-Id,X-Client-IdandX-Region. nginx now sends them empty. Clients that still send them keep working. (#2025) -
SESSION_COOKIE_HOST_PREFIX. Turning it on renames the cookie to__Host-<SESSION_COOKIE_NAME>, so every user signs in again once. It requiresSESSION_COOKIE_SECURE=trueand an emptySESSION_COOKIE_DOMAIN; both services refuse to start otherwise, andterraform planandcdk synthfail. On the Helm stack chart,global.sessionCookieHostPrefixrenders an empty cookie domain, so turning it on ends cross-subdomain cookie sharing. Delete the stored sessions inoauth_sessions_<namespace>when you turn it on, so the old ones end at once. (#2025) -
Content-Security-Policy in front of the UI. The new pre-paint script needs a hash, and the audit grid needs
data:infont-srcandimg-src. See the "Content-Security-Policy allowances" section in frontend RUM.UI_BRANDING_DIRjoins the reserved names forextraEnvandextra_env/registry.env. (#2000) -
Request log lines lose their query string. The
httpxrequest line and the uvicorn access log in the registry and the auth server keep the method, URL or path, and status, and drop the query string. The registry's logout used to write the user's ID token to both logs asid_token_hint. (#2029, closes #2027)
New Environment Variables
| Variable | Default | Description |
|---|---|---|
SESSION_COOKIE_HOST_PREFIX
| false
| Names the session cookie __Host-<SESSION_COOKIE_NAME>, which no other host can set. Requires SESSION_COOKIE_SECURE=true and no SESSION_COOKIE_DOMAIN. Read by the registry and the auth server. (#2025)
|
SESSION_MAX_AGE_SECONDS
| 28800
| Session lifetime, cookie Max-Age and the oldest signature either service accepts. Existing setting, now passed to both containers on every surface. (#2025)
|
MCP_BRANDING_DIR
| ./branding
| Docker Compose only: the host folder mounted read-only at /app/branding in the registry. See UI branding. (#2000)
|
UI_BRANDING_DIR
| /app/branding
| Folder the registry serves branding files from. Set by the Helm chart when registry.branding.enabled is true. (#2000)
|
New Terraform variables: session_max_age_seconds (default 28800), session_cookie_host_prefix (default false) and ui_branding_archive_b64 (empty by default). New Helm values: global.sessionMaxAgeSeconds, global.sessionCookieSecure, global.sessionCookieDomain, global.sessionCookieHostPrefix and global.ssrfAllowedCidrs on the stack chart, and registry.branding.enabled, existingConfigMap and mountPath.
Two new optional API fields: expected_issuer on egress and discovery OAuth configurations (see egress credential vault), and list_tools_before_consent on a server's egress configuration (see listing a server's tools before the user connects).
Upgrade Instructions
Deploy the 1.37.0 images for the registry, auth-server and mcpgw. Before you do, check the Breaking Changes above: a custom scope that relies on tools: ["*"], an edited yml session block, a Helm subchart session override, or a registration under /branding.
Docker Compose
cd mcp-gateway-registry
git pull origin main
git checkout 1.37.0
# Review .env.example for the new variables, and check the Breaking Changes above, then:
./build_and_run.shKubernetes / Helm (EKS)
cd mcp-gateway-registry
git pull origin main
git checkout 1.37.0
# REQUIRED: the subcharts changed, so rebuild the packaged dependencies
cd charts/mcp-gateway-registry-stack
helm dependency build
helm dependency update
# Move any subchart session or SSRF overrides to the globals (see Breaking Changes), then:
helm upgrade mcp-gateway . -f your-values.yamlTerraform / ECS
cd mcp-gateway-registry
git pull origin main
git checkout 1.37.0
# New optional variables: session_max_age_seconds, session_cookie_host_prefix,
# ui_branding_archive_b64. The apply redeploys the registry, auth-server and mcpgw services.
cd terraform/aws-ecs
terraform plan
terraform applyMajor Features
MCP 2026-07-28 clients and servers
The gateway serves clients of MCP 2026-07-28 and clients of earlier revisions on the same URLs. A 2026-07-28 client sends no initialize and no session id, and carries its protocol version in params._meta. Direct routes forward both kinds of request unchanged. Virtual servers answer server/discover, bridge a 2026-07-28 client to handshake-era backends, and talk 2026-07-28 to backends that support it. MCP 2026-07-28 support goes through every changelog entry. (#1993, closes #1715)
Headers checked against the body
nginx rejects a request whose Mcp-Method, Mcp-Name or MCP-Protocol-Version header disagrees with its body (HTTP 400, -32020), before authorization. The gateway authorizes the body, and a backend may act on the headers, so the two must name the same operation. (#1993)
Consent and sign-in for 2026-07-28 clients
With EGRESS_CONSENT_USE_ELICITATION=true, a 2026-07-28 client that declares URL elicitation gets the egress consent prompt as an input_required result and retries on its own after the user connects. An optional expected_issuer on a server's egress OAuth configuration makes the callback refuse a sign-in reply whose RFC 9207 iss names a different issuer. (#1993)
Session cookie hardening
The registry and the auth server now read the browser session cookie the same way: one parser that matches the exact name, across every Cookie header, and treats two copies as no session. Before, the auth server matched any name ending in mcp_gateway_session and took the first copy while the registry took the last, so another host under a shared parent domain could make one request look like two sessions. Every cookie attribute and the session lifetime now come from the shared settings, so the login, the logout and the signature check agree. A new opt-in, SESSION_COOKIE_HOST_PREFIX, names the cookie __Host-<name>, which browsers let no other host set or overwrite. See cookie security design. (#2025)
UI themes and runtime branding
Light and dark mode both render correctly across the UI, with every color taken from a documented set of CSS variables. A deployment can change colors, logos, favicon, named themes and fonts by supplying a folder of files at deploy time, without rebuilding the image: a bind mount on Docker Compose, a ConfigMap on Helm (registry.branding.*), or ui_branding_archive_b64 on Terraform. scripts/generate-ui-theme.py builds a theme from one brand color and reports WCAG contrast. The audit log table is now an AG Grid grid with search across every column. See UI branding. (#2000)
What's New
Protocol
- Virtual servers keep each tool's
title,annotations,outputSchema,icons,executionand_meta, routeresources/templates/list, and addresultType,cacheScopeandttlMsto lists for 2026-07-28 clients. - Results forwarded from handshake-era backends to 2026-07-28 clients gain
resultType: "complete", and forwardedresources/readresults also gaincacheScopeandttlMs. - The gateway's pre-consent lists carry
resultType,cacheScopeandttlMsfor 2026-07-28 clients. - Virtual servers answer an unknown tool, resource or prompt with
-32602for 2026-07-28 clients. - A 403 for an MCP request carries a JSON-RPC error that names the refused method, tool and server, with the reason (
not_permitted,no_scopesororigin_refused) inerror.data. It names no scopes, groups or token claims. (#2011, closes #2006) - The 2026-07-28 rules live in one place per language:
registry/common/mcp_protocol.py, and one block of constants with one shaping function in the Lua router. A test fails if the two copies disagree. - mcpgw serves both protocol revisions.
Registry, health and discovery
- Tool discovery and the security scanner reach HTTPS servers with the registry's CA trust on MCP SDK 2.x.
- The health check asks
server/discoverfirst and remembers handshake-era servers for an hour. - Metrics read the client name and protocol version of 2026-07-28 requests from
_meta. - mcpgw has a new
get_custom_entity(path)tool that returns a custom entity record with its attributes, taking thepathexactly assearch_registryreturns it. Before, an agent that reached the registry only through mcpgw could find a record but never read its attributes. (#1907, closes #1808)
Security
server/discoverandnotifications/*are allowed for any scope that grants access to the server;resources/read,prompts/listandprompts/getjoin the default method set.- A per-user filtered
tools/listis markedcacheScope: "private"on direct routes. - An MCP, virtual-server, agent or proxy request signed in with the browser session cookie must carry an
Originheader naming the gateway's own origin. A POST or DELETE withoutOrigin, or withnull, a malformed value or another site's origin, gets 403 before the gateway checks scopes or mints a token. A GET withoutOriginpasses, because browsers leave it out of same-origin GETs. The origin parser is shared with the CORS allowlist inregistry/common/request_origin.py. (#2011, closes #1996) - An egress consent stores a token only when the browser completing it is signed in as the user who started it and presents that consent's own cookie, whose SHA-256 the encrypted state carries. The callback uses up the single-use state before any other check and clears the cookie on every outcome. Before, a consent link opened in a browser with no registry session could store that browser's provider account in the first user's vault. (#2014)
- The per-user egress vault keys every user on the identity provider's immutable account id: the Cognito
subinstead of the reusable Cognito username, and the Oktauidinstead of the editable login. A login with no account id keys onname:<provider>:<username>. Before, a new Cognito user given a deleted user's username, or an Okta user given another's login, could read the earlier user's provider tokens. (#2014) - A token signed with a key the auth server has not cached makes it fetch the identity provider's keys again, at most once a minute per provider. A key rotation now takes effect within a minute, where before it waited out the one-hour cache. (#2019)
- A session cookie sent more than once now counts as no session in both services, which log one warning with the count and never the value. Logout with a duplicated cookie ends the session of every validly signed copy, up to eight. CSRF is skipped only for a request with no session cookie at all. (#2025)
/validateno longer takes Cognito pool settings from theX-User-Pool-Id,X-Client-IdandX-Regionrequest headers, and the health WebSocket no longer reads a session from the URL query string, where access logs record it. (#2025)- The registry's logout no longer writes the user's ID token to the registry and auth-server logs. Both services drop the query string from
httpxrequest lines and uvicorn access-log lines, which also keeps OAuth callbackcodeandstatevalues out of the auth server's access log. See logging. (#2029, closes #2027)
Egress credentials
- An admin can let an
oauth_userorpatserver list its stored tools to users who have not connected their account yet, for clients that only call tools an admin enabled ahead of time. With the setting on, a direct-routetools/listreturns the connect tool (foroauth_user) and then the stored tools the caller's scope allows. Calling one returns the connect URL or the PAT message, and nothing reaches the backend until the user connects. Turn it on with the checkbox in the server's Egress Auth settings,registry_management.py egress-configure --list-tools-before-consent, orlist_tools_before_consentinPOST /api/servers/{path}/egress-auth. It is off by default. See listing a server's tools before the user connects. (#2015, closes #1986)
Frontend
- Light mode and dark mode both render correctly; several components used to be readable in only one of them.
npm run lintnow fails on raw color classes, so new UI code uses the theme tokens. (#2000) - After you register a server, agent or custom record, the dashboard lists and the sidebar counts update without a page reload. (#2026)
Documentation
- A new page, MCP 2026-07-28 support, goes through every entry in the spec changelog.
- Install steps for
uvnow use Homebrew,pipx install uv==0.13.5or a pinned container image, in place of a remote install script piped tosh. (#1999, closes #1994) - ADOPTERS.md lists organizations that run the gateway in production, and explains how to add one. (#2028)
Testing
- Two new scripts,
tests/e2e/mcp_client_matrix.pyandtests/e2e/mcp_2026_gateway_checks.py, check a running gateway with old and new MCP clients. - A handshake-only test backend,
cli/examples/mcp-everything-legacy.json, letsmcp_2026_gateway_checks.pycheck the fields on aresources/readthe virtual router forwards, and the -32602 not-found code. (#2001)
Dependencies
- GitHub Actions updates, including
astral-sh/setup-uv10.3.3 andbridgecrewio/checkov-action12.3139.0. (#1979, #2008) - Weekly lockfile refreshes with a 7-day supply-chain quarantine. (#1984, #2010)
Bug Fixes
- MCP request bodies above about 10 KB failed with a 500. (#1993)
- nginx resolved request-time upstreams through public DNS, so local container names failed. (#1993)
- Virtual server tool changes on backends already in use did not reach the router. (#1993)
- Tools found by health-loop discovery reached virtual mappings and the tool catalog with empty schemas. (#1993)
- Schemas with a
["object", "null"]root or$defsbroke during virtual-server normalization. (#1993) - The auth server fetched the identity provider's signing keys (JWKS) on every token validation, adding 110 to 260 ms per request behind the egress forward proxy. Keycloak and PingFederate fetched their discovery document on every request too. (#2019, closes #2012)
- An
SSRF_ALLOWED_CIDRSrange could not admit a reserved address, so a registry in an Istio mesh could not health-check, discover tools on, or scan a server behind an auto-allocated ServiceEntry address in240.240.0.0/16. A listed range now admits the reserved IPv4 addresses inside it. Reserved IPv6,255.255.255.255and the cloud metadata addresses stay blocked. (#2024, closes #2018) - The auth server's MCP proxy returned a 502 and dropped the whole response when an upstream cut a chunked SSE stream without its terminating chunk, even after every event had arrived. It now delivers the complete events when one of them carries the JSON-RPC response, and still returns 502 when none does. (#1909, #2023, closes #1908)
- Entra bearer tokens that need Graph group recovery got 503 from
/validatesince 1.36.0, because the auth server sent Graph a(token, expires_in)pair as the bearer token. (#2013, closes #2009) terraform/aws-ecs/scripts/view-cloudwatch-logs.shsplit a--filtervalue on spaces and expanded globs in it. (#1998, closes #1981)- On the register page, the path stopped following the name after the first character. (#2000)
- New assets and the sidebar counts did not appear on the dashboard until a page reload. (#2026)
Closed Issues
| Issue | Title | Closed By |
|---|---|---|
| #2027 | Logout writes the OIDC ID token to registry and auth-server logs | PR #2029 |
| #2018 | url_guard: an explicit SSRF_ALLOWED_CIDRS entry cannot admit a reserved range | PR #2024 |
| #2012 | Entra auth provider re-fetches JWKS on every request: get_auth_provider() discards the per-instance cache | PR #2019 |
| #2009 | Entra bearer group recovery returns 503: _lookup_bearer_groups passes obo_exchange tuple as Graph token (1.36.0) | PR #2013 |
| #2006 | Name the refused method and tool in the gateway's MCP 403 response | PR #2011 |
| #1997 | Centralize MCP 2026-07-28 result shaping and protocol constants (after #1993) | PR #2001 |
| #1996 | Validate Origin on cookie-authenticated MCP requests | PR #2011 |
| #1994 | macos-setup skill: UV_FAIL remediation pipes a remote script into sh | PR #1999 |
| #1986 | Pre-consent tools/list on direct routes lists only the connect tool, so Gemini Enterprise users who haven't connected can't use the server
| PR #2015 |
| #1981 | terraform/aws-ecs/scripts/view-cloudwatch-logs.sh: --filter value is split on spaces | PR #1998 |
| #1908 | mcp_proxy buffers filtered SSE responses, so a truncated chunked stream discards the whole response as a 502 | PR #2023 |
| #1813 | Bug: Editing "Group Mappings" on an existing group has no effect (silently reverted) | manual |
| #1808 | feat(mcpgw): add get_custom_entity so discovered custom records can be retrieved through MCP | PR #1907 |
| #1715 | MCP 20260728 | PR #1993 |
Pull Requests Included
| PR | Title |
|---|---|
| #2029 | fix(logging): drop query strings from httpx and access-log request lines |
| #2028 | |
| #2026 | |
| #2025 | |
| #2024 | fix(egress): allow some reserved CIDRS in SSRF_ALLOWED_CIDRS |
| #2023 | fix(auth-server): keep a truncated SSE stream's bytes and require a JSON-RPC response |
| #2019 | fix(auth): share JWKS and discovery caches across provider instances |
| #2015 | feat(egress): list stored tools before consent on direct routes |
| #2014 | |
| #2013 | fix(entra): send Graph the OBO access token, not the (token, expires_in) pair |
| #2011 | Check Origin on cookie MCP requests and name what a 403 refused |
| #2010 | |
| #2008 | |
| #2001 | refactor(mcp): shape 2026-07-28 results in one place; add the missing cache fields and not-found code |
| #2000 | |
| #1999 | docs: install uv from a package manager, not a remote script piped to sh |
| #1998 | fix(terraform): pass --filter-pattern as a single argument in view-cloudwatch-logs.sh |
| #1993 | MCP 2026-07-28 support: serve old and new clients on every route |
| #1984 | |
| #1979 | |
| #1909 | |
| #1907 | feat(mcpgw): add get_custom_entity to fetch a custom record by path |
Contributors
Thank you to all contributors for this release:
- Amit Arora (@aarora79)
- omrishiv (@omrishiv)
- Bilal Malik (@pinktownscavenger)
- Lucas Anjos (@anjosluc)
- Rızgar Ozan (@RizgarOzan)
Support
Full Changelog: 1.36.0...1.37.0