github IBM/mcp-context-forge v1.1.1
v1.1.1 - Release 2026-11-10 - Token Exchange Delegation, CEL Tool Validation, A2A v1 Routes, and Security Hardening

one hour ago

[1.1.1] - 2026-11-10 - Token Exchange Delegation, CEL Tool Validation, A2A v1 Routes, and Security Hardening

Overview

Release 1.1.1 delivers token exchange delegation and Admin UI support, CEL tool validation policies, A2A /v1 task and agent card routes, and upstream progress relay to modern clients. It also hardens ownership, OAuth discovery, credential masking, and token scoping, and it completes the removal of the repository-owned Rust runtime. This release consolidates 72 PRs across:

  • Protocol & Transport - Modern clients receive relayed upstream progress, and federated pagination results are deduplicated.
  • Security & Auth - Added token exchange delegation, CEL tool validation policies, bearer-token precedence over auth cookies, create-time ownership derivation, issuer-scoped OAuth endpoint validation, credential masking, and removal of the emSign/eMudhra CA roots.
  • API & Platform - Added /v1 A2A task, push notification config, and agent card routes, A2A 1.0 agent cards, mcp_server_id and virtual_server aliases, and server_id on tool preview.
  • Operations & Tooling - Added an Admin UI deprecation notice, dataplane publishing on control-plane changes, prek-based pre-commit hooks, and removal of the Rust build, CI, and benchmark infrastructure.
  • Testing - Stabilised sandbox worker startup and the OTEL and A2A test suites, and split main.py startup helpers for direct test invocation.

Added

Protocol & Transport

  • Progress for modern requests in JSON mode (#6647, #6648, #7270) - With JSON_RESPONSE_ENABLED=true, a 2026-07-28 request that carries _meta.progressToken and accepts text/event-stream may get a text/event-stream response once progress is sent or after 15 s without a response; a request that finishes earlier without progress gets JSON. On that stream, errors arrive as JSON-RPC errors with HTTP 200, and closing the stream cancels the request. Legacy clients see no change. The gateway now relays upstream progress to modern clients on resources/read and prompts/get (#6648), completion/complete, and direct-proxy tools/call. The gateway no longer forwards the client progressToken upstream on resources/read, prompts/get, and completion/complete, including legacy and /rpc completion. A retried upstream resource read sends no progress callback, so progress values never restart.

  • Server instructions in the handshake (#4693, #7254) - A virtual server now sends instructions in its handshake answer for both initialize (2025-11-25) and server/discover (2026-07-28). When a virtual server uses tools from exactly one upstream server, the gateway sends that server's instructions, stored on Gateway.upstream_instructions at connect time and at each refresh. An operator can set Server.instructions through the REST API (ServerCreate, ServerUpdate) and the Admin UI. null selects the single-upstream rule, and an empty string sends no instructions. A server with tools from several upstream servers and no operator text sends the gateway default text. Two idempotent Alembic migrations add the columns. Merging instructions from several upstream servers remains open under #4693.

API & Platform

  • Product-terminology request aliases for gateway_id and the virtual-server server body key (#6544, #7179) - Tool, resource, and prompt create/update bodies, and the gateway/MCP-server handshake test request, now also accept mcp_server_id as an alias of gateway_id ("MCP server" is the product term for what the code calls a "gateway"). The tool, resource, and prompt list endpoints and GET /v1/search accept mcp_server_id as a query alias of gateway_id. POST /servers and POST /v1/virtual-servers also accept a top-level virtual_server key as an alias of server. Sending two spellings with different values returns 422. Existing field names and body keys keep working, OpenAPI property names are unchanged, and responses keep the existing key names. In OpenAPI, the Body_create_server_* request components are replaced by ServerCreateRequest. Output-side aliasing, RBAC permission strings, audit resource_type values, and config names remain tracked under #6544.
  • A2A task and push notification config routes (#6702, #7372) - Clients can now read and manage A2A task data through /v1/a2a/{agent_id}/tasks. GET lists tasks with pageSize, pageToken, and nextPageToken pagination, GET .../{task_id} returns one task, and POST .../{task_id}/cancel cancels it. POST, GET, and DELETE on .../{task_id}/pushNotificationConfigs manage push notification configs. Reads require a2a.read, cancel requires a2a.invoke, and config writes require a2a.update. Routes apply the same team and visibility scoping as the internal handlers, and return 404 for tasks the caller cannot see. Responses never include auth_token. The /_internal/a2a/* routes are unchanged.
  • Public A2A agent card lookup (#6700, #7182) - Added GET /v1/a2a/{agent_id}/card, gated on a2a.read. The route resolves a standalone A2A agent's card first, then falls back to a virtual server exposing an A2A interface. Previously the card was reachable only from the internal Rust-sidecar route.
  • server_id on tool preview (#7249) - ToolPreviewRequest accepts an optional server_id. The gateway verifies that the caller can access the virtual server, then considers only the tools attached to it. Preview without server_id keeps the global behavior.
  • Post-authorization validation of stored OAuth connections (#7314, #7343) - POST /v1/mcp-servers/test-handshake accepts validation_mode=post_authorization. The mode checks that the caller's stored OAuth token permits a live tools/list request on an enabled authorization_code gateway. It requires gateways.read and gateways.update, rejects caller-supplied credential overrides, and reads the stored token without refresh or persistence. The response reports separate token, negotiation, and listing outcomes within a shared ten-second deadline.
  • Dataplane publishing on control-plane changes (#5578, #5580, #6994, #6979) - Changes to virtual servers, tools, resources, prompts, gateways, users, and teams now notify the dataplane publisher. The publisher wakes on these events and publishes to Redis sooner. It uses a lock so that several instances can run. Published routes exclude gateways owned by inactive users.
  • OAuth completion signal to the parent app (#7185) - After the OAuth callback page fetches tools for a gateway, it posts an oauth_callback message with status, gatewayId, and gatewayName to the opener window and closes itself.

Security & Auth

  • Token exchange Admin UI (#7236, #7265) - Platform administrators can create and edit token-exchange gateways in the Admin UI. A token-exchange gateway can also hold an optional discovery client-credentials credential. Registration, Fetch Tools, refresh, and health checks use it. Tool calls keep the per-user RFC 8693 exchange.
  • Token exchange delegation (#7238, #7266) - A token-exchange gateway can send an RFC 8693 actor_token with each exchange. Set oauth_config.actor_token_source to client_credentials or client_assertion. The default none keeps impersonation, and the request body does not change. actor_token_type overrides the default token type. The optional oauth_config.actor object names a separate actor client through client_id, client_secret, token_url, and token_endpoint_auth_method. Delegation fails closed: when the actor token is unavailable, the call fails and no impersonation request is sent. Deploy the release to every worker before you enable delegation, because an older worker ignores actor_token_source and impersonates.
  • Tool validation policy (#5046, #7444) - A CEL expression, tool_validation_policy, on a gateway or a tool decides which discovered tools are stored enabled. A tool inherits its gateway's policy, and a gateway without one inherits the global TOOL_DESCRIPTION_FORBIDDEN_PATTERNS list. Setting a policy requires the new validation_policy.manage permission, held by platform administrators and, for records of shared teams, team administrators, and writes an audit entry. Expressions run in a forked sandbox (CEL_EXECUTION, CEL_TIMEOUT_SECONDS, CEL_WORKERS).
    • Upgrade note: a discovered tool the global list refuses is now stored disabled, with the reason in validation_refusal, instead of being skipped, and skipped_tools no longer lists it. POST /tools returns 400 instead of 422 for a refused description. PUT /tools/{id} saves a refused description and disables the tool instead of returning 422. A refused tool is never listed or invocable, as before.
  • Upstream OAuth flows follow the MCP 2026-07-28 authorization (#6639, #7333) - The OAuth callback validates the iss parameter against the authorization server issuer before it redeems the code (RFC 9207). A present iss must match exactly. An absent iss is refused only when the server advertises iss support. A gateway without a configured issuer skips the check. On a provider error callback with a mismatching iss, the gateway replaces the provider error text. Dynamic client registration requests send application_type, set by the new DCR_APPLICATION_TYPE setting (auto by default: native for a loopback redirect URI, web otherwise). When a gateway's OAuth issuer changes, the gateway deactivates registered clients bound to the old issuer and logs a warning if a static client ID carries over unchanged.
  • Webhook HMAC replay protection (#7329, internal issue 1149, PSIRT ADV0369535) - The webhook_notification plugin accepts hmac_include_timestamp: true. The signature then covers {timestamp}.{delivery_id}.{body}, and the plugin sends X-Timestamp and X-Delivery-Id headers. Each retry is signed again with a fresh timestamp. The default stays false, so existing receivers keep verifying HMAC(body). The default flips to true in a later minor release.

Operations & Tooling

  • Admin UI deprecation notice (#7297, #7301) - After login, the Admin UI shows a dismissible banner below the header. The banner announces that Admin UI support ends on 27 January 2027 and links to the new UI repository and its getting-started guide. The banner is on by default. Set MCPGATEWAY_UI_DEPRECATION_NOTICE_ENABLED=false and restart the gateway to hide it. The date is informational. The change adds no automatic expiry, redirect, or shutdown.
  • Multi-architecture builds skip the Admin UI on s390x and ppc64le (#7294, #7295) - The Containerfile and the multiplatform workflow skip the HTMX Admin UI and Node.js frontend build steps for linux/s390x and linux/ppc64le.
  • Faster pre-commit with prek (#7475) - make pre-commit runs the hooks through prek on all files. The new make pre-commit-fast runs them on files changed since PRE_COMMIT_BASE_REF (default origin/main), and pull request CI uses it. PREK_VERSION pins the prek release.
  • Web UI SSO client seed (#7050) - Added a seed for the Web UI SSO client.

Changed

  • A2A agent cards follow the 1.0 schema (breaking for card consumers) (#7027, #7209) - Cards for agents with a stored protocol version of 1.x now use the A2A 1.0 AgentCard shape, with supportedInterfaces, securitySchemes, and securityRequirements. protocol_version defaults to "1.0", so most existing agents change shape. These fields leave 1.x cards: url, protocolVersion, supportsAuthenticatedExtendedCard, and capabilities.stateTransitionHistory. A caller that sends A2A-Version: 0.3 to the sidecar keeps the legacy shape for a 1.x agent. A caller that sends no version follows the stored version. Agents with a stored version of 0.x keep the legacy shape. Server cards now aggregate streaming, pushNotifications, and the default input and output modes from enabled associated agents instead of hardcoded values. Server cards skip malformed capability entries. Cards never expose credential values. Agents with authheaders auth, query_param auth without a stored parameter name, or an incomplete OAuth configuration omit the security scheme, because 1.0 requires a name or flow URLs that the stored configuration cannot supply. Scheme names are basicAuth, bearerAuth, apiKeyAuth, and oauth2Auth. OAuth schemes read the stored scopes list and always emit the scope map.
  • Token exchange audit events carry purpose and mode (#7236, #7238) - Each token-exchange audit event has a purpose key (invocation or discovery) and a mode key (impersonation or delegation). The log message changes from token-exchange succeeded user=... to token-exchange succeeded purpose=... mode=... user=..., and the failure message changes the same way. Update log-text alert rules that match the old prefix. Structured consumers of extra["token_exchange"] are not affected.
  • Plugin manager cache TTL removed (#7256, #7428) - Removed the unused plugin_manager_cache_ttl setting and the DEFAULT_CACHE_TTL constant. Plugin managers now live for the worker lifetime and change only through explicit invalidation (invalidate_all, invalidate_team, reload_tenant). Multi-worker deployments rely on Redis pub/sub invalidation. Multi-worker deployments without Redis need a worker restart after binding changes.
  • main.py startup helpers extracted (#7229, #7230, #7231, #7232, #7233) - Extracted _register_middleware, _include_optional_routers, _register_ui_routes, _wait_for_redis_at_startup, and _enable_plugin_subsystem_if_configured so that tests can call each helper directly. Behavior is unchanged.

Removed

  • Repository-owned Rust runtime and workspace (#5817, #7258, #7354, #7359, #7369) - Remove crates/mcp_runtime, the root Cargo workspace, the Rust test suites and CI workflow, the Rust-vs-Python plugin benchmark targets, and Rust build infrastructure. The separate external Rust dataplane remains outside this removal.
  • devpi Makefile targets (#7280) - Removed the devpi Makefile targets and references.

Breaking Changes

  • REST tool URL template values are percent-encoded (#7387) - Values now fill {param} as one encoded path segment or one query value. Before, the gateway inserted the raw value into the URL.
    • / reaches the backend as %2F. Tools that relied on a raw / in one parameter must use two parameters.
    • In a query placeholder, & and = stay inside the value, and + arrives as a literal +, not a space.
    • A space in a path placeholder used to fail URL validation and now goes out as %20.
    • A value that already holds %XX (for example 42%2F43) used to pass and now returns an error.
    • A value with a . or .. segment returns an error.
  • LLM gateway models authorization (#7388) - GET /llm/gateway/models and GET /v1/llm/gateway/models now require the llm.read RBAC permission. Previously, authentication alone passed this route. It now matches GET /llmchat/gateway/models and GET /v1/models. All built-in roles hold llm.read. Principals without llm.read (no roles, custom roles, or a custom DEFAULT_USER_ROLE) now receive 403.
  • Credential updates require auth_type (#6704, #7172) - PUT /a2a/{agent_id} and PUT /gateways/{gateway_id} return 422 when a request sets auth_token, auth_password, or auth_header_value without auth_type. Before, the A2A route returned 200 and ignored the credential, and the gateway route returned 200 and wiped the stored credential. Empty strings count as unset. The Admin UI always sends auth_type. Partial-body REST clients must add auth_type.
  • Gateway test endpoints require gateways.update and ownership for stored credentials (#7390) - POST /v1/mcp-servers/test and POST /admin/gateways/test return 403 when the target MCP server has a stored basic, bearer, custom-header, or OAuth credential. An OAuth authorization-code token is the caller's own and is not affected. To pass, the caller needs gateways.update in the MCP server's team and must pass the ownership check of PUT /gateways/{gateway_id}. The owner, a team owner of a team MCP server, and a platform admin pass that check. For an MCP server without a team, gateways.update must come from a global role. POST /v1/mcp-servers/test-handshake applies the same rule when the request adds a path or headers. A bare handshake still needs only gateways.read. Tests against MCP servers without stored credentials also need only gateways.read. To test new headers with only gateways.read, use credential_mode: candidate_only.
  • Resource reads on token-exchange gateways fail closed (#7236) - resources/read on a token-exchange gateway now raises ResourceError when the request has no user JWT or the RFC 8693 exchange fails. Before, the read went upstream without an Authorization header. MCP clients and GET /resources/{id} callers now receive an error instead of an unauthenticated upstream response. Unauthenticated callers on deployments that set MCP_REQUIRE_AUTH to false and callers whose authorization server rejects the exchange are affected. Send an authenticated user JWT and verify the gateway's token-exchange configuration before upgrading.

Security

  • X-Upstream-Authorization no longer overrides gateway-managed credentials (#7341) - The header is dropped, with a warning, for gateways with auth_type oauth, bearer, basic, or authheaders, and for REST tools and A2A agents with the same auth types. Previously any authenticated caller could replace the stored per-user upstream credential.
  • Per-gateway identity propagation on tool and resource calls (#7329, internal issue 1149, PSIRT ADV0369535, CWE-200) - MCP and REST tool calls and resource reads ignored the gateway identity_propagation override and sent user identity headers when the global setting was on. A gateway set to enabled: false now receives no identity headers or _meta. Deployments with global propagation on and a gateway override of off no longer disclose user identity to that gateway.
  • Gateway connectivity test credential gate (#7390) - Gateway connectivity tests send stored MCP server credentials only for callers with gateways.update who pass the MCP server ownership check.
  • Create ownership derived from the authenticated caller (#7296) - POST /servers, /resources, /prompts, /tools, their bulk variants, and the Admin UI tool paths ignore the payload owner_email and team_id. The owner is the caller. A nested team_id that differs from the top-level team_id, lies outside the token's teams, or lacks the create permission returns 403, and a public-only token cannot create team or private records. Admin bulk tool import assigns items to the token team or the caller's personal team. POST /admin/grpc returns 403 for a team_id outside the caller's teams. Update routes check ownership and the target team before name or URI conflicts, so a 409 no longer confirms that a record exists in another team. Resources and prompts that gateway discovery creates keep the gateway's team and owner. Requests inside the caller's own team keep their status codes and bodies.
  • Discovered OAuth endpoints are validated against the issuer origin (#7268) - The gateway checks token_endpoint and authorization_endpoint from an authorization server metadata document before it stores them in oauth_config. Each endpoint must share the origin of the configured issuer and pass the outbound URL policy.
  • ssrf_allow_localhost applies to OAuth HTTP guards (#7165, #7196) - OAuth endpoints on loopback hosts no longer fail an extra HTTPS-only check when ssrf_allow_localhost=true. External HTTP URLs stay rejected, and localhost HTTP stays disabled by default.
  • jq_guard denies more built-ins (#7413) - The tool response filter gate now denies halt, halt_error, builtins, while, until, recurse, recurse_down, foreach, and repeat.
  • emSign and eMudhra CA roots revoked (#7300) - The container image removes the emSign Root CA G1/C1 and emSign ECC Root CA G3/C3 anchors from the OS trust store and from the Python certifi bundle.
  • oauthlib 4.0.1 (#7471) - Raised the oauthlib floor to >=4.0.1 for GHSA-3jp2-3mrg-2vxc.

Fixed

  • OIDC JWKS fetches honor SKIP_SSL_VERIFY (#4026, #7217) - JWKS retrieval for SSO and external-IdP token verification now uses the shared TLS verification setting, like OIDC discovery, token exchange, and user info. Before, an identity provider with a self-signed certificate failed at JWKS retrieval even with SKIP_SSL_VERIFY=true. With the flag set, the gateway logs a warning on each JWKS fetch, because unverified JWKS keys let a network attacker forge tokens. Use the flag in development only. Per-provider CA bundles remain open under #4026.
  • External-IdP tokens on /mcp (#7435, #7450) - Trusted external-IdP bearer tokens now authenticate on /mcp, matching /rpc. An oauth_enabled virtual server now also accepts trusted-provider tokens whose issuer is outside its authorization_servers. The gateway checks the audience against the provider api_audience, not the server resource. No per-server opt-out exists.
  • REST URL template reuses a placeholder (#7387) - A REST tool URL template that uses the same placeholder twice, such as /{id}/x/{id}, now fills both occurrences. Before, it failed with "Required URL parameter 'id' not found in arguments".
  • Actionable error when an upstream MCP server redirects (#7163, #7416) - Health checks, gateway registration, Test Connection, and the handshake test now report Upstream redirected to <scheme://host/path>; update the gateway URL to the redirect target. for an upstream 301, 302, 303, 307, or 308 response. The message drops the query string, fragment, and userinfo of the Location header. Before, the gateway showed a generic error. The gateway still does not follow the redirect, to keep SSRF and credential-forwarding protection. Register the redirect target URL directly, for example https://host/mcp/ instead of https://host/mcp.
  • Observability sessions (#5073) - Reuse one observability session per HTTP trace lifecycle, instead of four to six, while preserving independent span and trace completion. Recover trace completion after span persistence failures and redact credentials from HTTP trace URLs and exception stack traces.
  • A2A and gateway credential masking (#6704, #7172) - A2AAgentRead and GatewayRead mask stored credentials on every a2a.read and gateways.read path. Regression tests cover basic, bearer, and authheaders auth types.
  • A2A agent credential rotation (#7172) - PUT /a2a/{agent_id} now persists a new basic, bearer, or custom-header credential. The update path ignored these values before.
  • Admin UI A2A edit keeps stored credentials (#7172) - The Admin UI form resubmits the mask for an unchanged credential. update_agent no longer stores the mask in place of the real credential, for basic, bearer, and multi-header auth.
  • Masked password with a new username (#7172) - An update that changes auth_username and keeps the masked auth_password now pairs the new username with the stored password, for A2A agents and gateways.
  • Gateway description-only update keeps credentials (#7172) - PUT /gateways/{gateway_id} with only unrelated fields, such as description, no longer wipes auth_value.
  • Instana tracer context keyword (#7208, #7174) - The OpenTelemetry request middleware retries the span start without the context keyword when the tracer rejects it with TypeError, so Instana tracers older than 3.12 no longer abort inbound requests that carry a traceparent header.
  • Plugin manager rebuilds (#6532, #7245) - Tools of a team without tool-specific plugin bindings share one cached plugin manager, so external plugins such as OPA initialise once per team instead of once per tool. Tools with their own bindings keep their own manager. The invalidation listener now rebuilds managers after each Redis resubscribe.
  • LLM Chat loads MCP tools with the MCP SDK client (#7090, #7332) - langchain-mcp-adapters has no release compatible with MCP SDK 2.x and is removed from the llmchat extra; LLM Chat lists and calls tools through mcp.client.Client.
  • Bearer token precedence over auth cookies (#7389) - When a request carries both a Bearer token in the configured auth header (AUTH_HEADER_NAME, default Authorization) and a jwt_token or access_token cookie, every auth layer (token scoping, require_auth, require_admin_auth, /admin/*, LLM chat connect) now authenticates the Bearer principal, and token scopes apply to that credential. Cookie-only and Authorization: Basic requests keep cookie authentication. LLM chat connect also accepts the access_token cookie.
  • Duplicate items in federated pagination (#6936, #7253) - An upstream MCP server that repeats an item across pagination responses no longer blocks gateway registration. The gateway deduplicates by tool and prompt name, resource uri, and template uri_template, and keeps the first object and the encounter order.
  • Admin UI edit forms return 422 for missing fields (#6937, #6980) - The Admin UI resource, prompt, gateway, and A2A edit forms return HTTP 422 when a required field is missing. A missing or blank prompt template is rejected. Partial updates of federated resources keep working.
  • Sandbox workers start before the pool serves requests (#7361) - SandboxPool.start() now warms every configured worker. Before, it warmed one, and a lazily spawned worker could exceed the one-second call timeout under parallel load.

Documentation

  • Rust runtime removal (#5823, #7441) - Update deployment and contributor guidance, architecture diagrams, and the deprecation registry. ADR-058 records verified removals and remaining internal dispatch cleanup.
  • Developer role permissions (#6935, #7252) - The role summaries, RBAC guide, multi-tenancy guide, and provisioning tutorials now state that the built-in developer role grants resource CRUD and gateway management. Authorization behavior is unchanged.
  • Repeated pagination cursor contract (#6938, #7251) - The get_list_paginated() docstring now states that a repeated cursor logs a warning and stops pagination. It does not raise ValueError.

Testing

  • A2A request context tests (#7331) - The tests use JWT-shaped bearer tokens.
  • OTEL span flush in tests (#7330) - Tests set OTEL_BSP_MAX_EXPORT_BATCH_SIZE=1 to flush every span immediately.

Chores

PR Description
#7370 tolerate a dequeued merge-queue ref in SARIF upload
#7092 suppress audited Makefile false positives inline
#7384 suppress false-positive detect-secrets findings in Python files
#7477 prune stale baseline entries and keep keys sorted
#7235 gate the MCP conformance matrix with cf-integration 0.6.1
#7379 default tests to rstest on Python 3.14
#7495 update python, npm and container image dependencies
#7290 update secrets baseline, README and SECURITY
#7302 routine Python dependency updates
#7342 resolve Dependabot security vulnerabilities
#7356 audit found secrets in .secrets.baseline
#7358 update CPEX plugin versions
#7429 remove unused pytype
#7492 match the renamed pre-commit workflow steps in the CI decision

Don't miss a new mcp-context-forge release

NewReleases is sending notifications on new releases.