github agentic-community/mcp-gateway-registry 1.37.0
1.37.0 - MCP 2026-07-28 Support, Session Cookie Hardening and UI Branding

34 minutes ago

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 for tools/call and no other method. A custom scope that relied on the old behavior for a method it does not list, such as resources/read or prompts/get, gets 403 for that method until you add the method to its methods list. The unrestricted seed scopes and the example scope files already list resources/read, prompts/list and prompts/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 with name:. It used to succeed with a yielded no subject warning, on every deployment. Search the auth-server logs for that line before you upgrade. (#2014)

  • The auth server refuses to start on an edited session block in auth_server/oauth2_providers.yml. Every session cookie setting now comes from the environment, which the registry and the auth server share. If you set secure, max_age_seconds, cookie_name, samesite or httponly in the yml, or a domain other than the shipped ${SESSION_COOKIE_DOMAIN} placeholder, move the value to SESSION_COOKIE_SECURE, SESSION_MAX_AGE_SECONDS, SESSION_COOKIE_NAME or SESSION_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.sessionCookieSecure and global.sessionCookieDomain once for the registry and the auth server, and it refuses a render where a subchart value such as registry.app.sessionCookieDomain or auth-server.app.sessionCookieSecure differs from what the globals render. Move those values to the globals. (#2025)

  • /branding is a reserved path. The registry now serves UI branding files at /branding/, so registering a server or agent whose path starts with /branding fails. 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-scanner 4.8.7). The root project no longer depends on langchain-mcp-adapters or strands-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-Authenticate header (it carried two), and a 403 carries WWW-Authenticate: Bearer resource_metadata="...".

  • Refused MCP requests. The body of a 403 for an MCP request is now a JSON-RPC error with code -32003 that names the refused method, the tool for tools/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 Origin header is the gateway's own origin, taken from AUTH_SERVER_EXTERNAL_URL or REGISTRY_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 resolved instead of unhealthy: url blocked by SSRF guard.

  • HTTP+SSE. Registering a server whose only transport is sse logs 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_SECONDS later (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 sub gets no per-user egress until it carries sub or 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_DOMAIN with 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. EgressTokenResponse no longer carries authorize_url. (#2014)

  • Okta custom authorization server. With OKTA_AUTH_SERVER_ID set, 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_CIDRS on the auth-server. Docker Compose, Terraform and Helm now pass SSRF_ALLOWED_CIDRS to 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_CIDRS to global.ssrfAllowedCidrs. The stack chart has a new global.ssrfAllowedCidrs key that the registry, auth-server and mcpgw all read, and it wins over each subchart's app.ssrfAllowedCidrs. If your values set only registry.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 to global.ssrfAllowedCidrs. (#2024)

  • Extra env can no longer set SSRF_ALLOWED_CIDRS. Terraform refuses the name in registry_extra_env, auth_server_extra_env and mcpgw_extra_env. The Helm charts now compare extraEnv names with reserved names after trimming and upper-casing them, so ssrf_allowed_cidrs fails the render too. Set ssrf_allowed_cidrs in Terraform or global.ssrfAllowedCidrs in Helm instead. (#2024)

  • SESSION_COOKIE_SECURE=false now takes effect on HTTPS. The login cookie used to stay Secure because the yml's secure: true won over the environment. With false, the cookie now has no Secure attribute. Both services warn about it at startup. Check .env, extraEnv, your Terraform variables and your copy of infra/config.yaml, which shipped cookieSecure: false before 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_DOMAIN change, 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_status reads the session from the Cookie header 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)

  • /validate ignores X-User-Pool-Id, X-Client-Id and X-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 requires SESSION_COOKIE_SECURE=true and an empty SESSION_COOKIE_DOMAIN; both services refuse to start otherwise, and terraform plan and cdk synth fail. On the Helm stack chart, global.sessionCookieHostPrefix renders an empty cookie domain, so turning it on ends cross-subdomain cookie sharing. Delete the stored sessions in oauth_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: in font-src and img-src. See the "Content-Security-Policy allowances" section in frontend RUM. UI_BRANDING_DIR joins the reserved names for extraEnv and extra_env/registry.env. (#2000)

  • Request log lines lose their query string. The httpx request 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 as id_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.sh

Kubernetes / 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.yaml

Terraform / 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 apply

Major 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, execution and _meta, route resources/templates/list, and add resultType, cacheScope and ttlMs to lists for 2026-07-28 clients.
  • Results forwarded from handshake-era backends to 2026-07-28 clients gain resultType: "complete", and forwarded resources/read results also gain cacheScope and ttlMs.
  • The gateway's pre-consent lists carry resultType, cacheScope and ttlMs for 2026-07-28 clients.
  • Virtual servers answer an unknown tool, resource or prompt with -32602 for 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_scopes or origin_refused) in error.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/discover first 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 the path exactly as search_registry returns 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/discover and notifications/* are allowed for any scope that grants access to the server; resources/read, prompts/list and prompts/get join the default method set.
  • A per-user filtered tools/list is marked cacheScope: "private" on direct routes.
  • An MCP, virtual-server, agent or proxy request signed in with the browser session cookie must carry an Origin header naming the gateway's own origin. A POST or DELETE without Origin, or with null, a malformed value or another site's origin, gets 403 before the gateway checks scopes or mints a token. A GET without Origin passes, because browsers leave it out of same-origin GETs. The origin parser is shared with the CORS allowlist in registry/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 sub instead of the reusable Cognito username, and the Okta uid instead of the editable login. A login with no account id keys on name:<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)
  • /validate no longer takes Cognito pool settings from the X-User-Pool-Id, X-Client-Id and X-Region request 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 httpx request lines and uvicorn access-log lines, which also keeps OAuth callback code and state values out of the auth server's access log. See logging. (#2029, closes #2027)

Egress credentials

  • An admin can let an oauth_user or pat server 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-route tools/list returns the connect tool (for oauth_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, or list_tools_before_consent in POST /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 lint now 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 uv now use Homebrew, pipx install uv==0.13.5 or a pinned container image, in place of a remote install script piped to sh. (#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.py and tests/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, lets mcp_2026_gateway_checks.py check the fields on a resources/read the virtual router forwards, and the -32602 not-found code. (#2001)

Dependencies

  • GitHub Actions updates, including astral-sh/setup-uv 10.3.3 and bridgecrewio/checkov-action 12.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 $defs broke 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_CIDRS range 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 in 240.240.0.0/16. A listed range now admits the reserved IPv4 addresses inside it. Reserved IPv6, 255.255.255.255 and 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 /validate since 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.sh split a --filter value 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:


Support


Full Changelog: 1.36.0...1.37.0

Don't miss a new mcp-gateway-registry release

NewReleases is sending notifications on new releases.