Please select the area the issue is related to
Gateway
Please select the aspect the issue is related to
Aspect/AI (AI/LLM integration, MCP, AI readiness)
Description
Two defects in mcp-auth each cause an MCP client to discard its entire OAuth session immediately after a successful initialize and stop the server. The user-visible symptom is a connect → re-authenticate → fail loop, even though authentication itself succeeded and initialize returned 200.
Both are instances of the same client behaviour, which is worth stating up front because it makes each defect much more severe than it first looks:
An MCP client treats any 401 on the MCP endpoint as "the access token is invalid". It then invalidates its whole auth session, sends every subsequent request without a token (which the gateway also 401s), and tears the connection down. The first 401 is the cause; every later failure is a consequence.
Verified with VS Code 1.128.1 against the GitHub remote MCP server (https://api.githubcopilot.com/mcp/) proxied by the gateway, with mcp-auth (Keycloak key manager) plus an interceptor-service that injects a per-user upstream credential. Claude Code is unaffected by defect 1 because it does not send a session DELETE.
Defect 1 — mcp-auth does not gate GET or DELETE
The MCP Streamable HTTP transport has clients issue, in addition to JSON-RPC POSTs:
GET <mcp-endpoint> — the standalone asynchronous-notification (SSE) stream
DELETE <mcp-endpoint> — session termination
mcp-auth does not apply authentication to either method, so both pass straight through to the rest of the policy chain with no authenticated context established. (VS Code additionally sends the GET with no Authorization header at all, so there is nothing for a downstream policy to key off even opportunistically.)
The practical consequence: any backend-auth policy that fails closed — for example an interceptor-service that exchanges the caller's token for an upstream credential — has no identity to work with on these two requests and returns 401. That 401 then destroys the client's auth session per the behaviour above, and the MCP session dies right after initialize.
Observed on the gateway (interceptor-service returning 401 because no identity reached it):
POST /github/mcp status=200 (initialize succeeds)
GET /github/mcp status=401 <-- async-notification stream, ungated, no Authorization
POST /github/mcp status=401 (session now poisoned; client sends no token)
POST /github/mcp status=401
DELETE /github/mcp status=401
The interceptor's view of that GET — note the absence of authorization and x-forwarded-authorization:
no subject token in forwarded headers; method=GET headerKeys=
:authority,:method,:path,:scheme,accept,accept-encoding,accept-language,
mcp-session-id,sec-fetch-mode,user-agent,x-forwarded-proto,x-request-id
And the client side, 5.6s later:
[warning] Authentication session for localhost:8080 removed, stopping server
[info] Connection state: Stopped
For comparison, a proxy that forwards the upstream's own response for that GET (GitHub replies 405, as it has no standalone stream) works correctly — VS Code logs the following and proceeds normally to tools/list:
[debug] 405 status connecting to <mcp-endpoint> for async notifications;
they will be disabled: Method Not Allowed
Expected: GET and DELETE on an MCP resource are covered by mcp-auth's methods security configuration like any other MCP request, so that (a) they are authenticated consistently and (b) downstream policies see the same authenticated context they see for a POST.
Workaround: make the backend-auth policy never answer 401 on these paths — return 405 for the notification GET (matching what a real MCP backend without a standalone stream returns) and 204 for the session DELETE (treating a session that cannot be authenticated as already terminated).
Defect 2 — Protected Resource Metadata publishes scopes_supported: [] instead of omitting it
With mcp-auth's requiredScopes empty or unset, the PRM document still contains the key, as an empty array:
{
"resource": "http://localhost:8290/github/mcp",
"authorization_servers": ["http://localhost:8080/realms/mcp"],
"scopes_supported": []
}
scopes_supported is OPTIONAL in RFC 9728 §2, and an empty array is not equivalent to absence. VS Code stores the advertised value as the session's scope set, later recomputes it as undefined, detects a change, and tries to re-acquire a token non-interactively — which fails and takes the session with it:
[info] Scopes changed from [] to undefined, updating
[warning] Error getting token from server metadata: Error: User did not consent to login.
[warning] Authentication session for localhost:8080 removed, stopping server
Note consentRequired is false on the Keycloak client, and Keycloak logs no consent-related event — "User did not consent to login" is the client's wording for "an interactive login was required but the request was made silently", triggered by the scope-set change.
A proxy that omits the field entirely does not exhibit this: the client sees undefined on both sides, detects no change, and never re-authenticates.
Expected: omit scopes_supported from the PRM when there are no scopes to advertise, rather than emitting [].
Workaround: always configure a non-empty requiredScopes (e.g. openid, profile, email), even when scope enforcement is not wanted — noting that mcp-auth does not enforce requiredScopes anyway, so this is purely to avoid publishing [].
Steps to Reproduce
- Deploy an MCP proxy that fronts a remote MCP server, with
mcp-auth and a fail-closed backend-auth policy:
apiVersion: gateway.api-platform.wso2.com/v1
kind: Mcp
metadata:
name: github-mcp-v1.0
spec:
displayName: GitHub MCP
version: v1.0
context: /github
specVersion: "2025-06-18"
upstream:
url: https://api.githubcopilot.com/mcp/
policies:
- name: mcp-auth
version: v1
params:
issuers:
- keycloak-mcp # any OAuth2 key manager
requiredScopes: [] # reproduces defect 2
- name: interceptor-service # any policy that fails closed without an identity
version: v1
params:
endpoint: http://my-interceptor:9100
request:
includeRequestHeaders: true
passthroughOnError: false
tools: []
resources: []
prompts: []
- Defect 1 — confirm
GET/DELETE are not gated. Both reach the backend chain rather than being rejected by mcp-auth, and carry no authenticated context:
curl -i -X GET http://localhost:8290/github/mcp -H 'Accept: text/event-stream' -H 'Mcp-Session-Id: test'
curl -i -X DELETE http://localhost:8290/github/mcp -H 'Mcp-Session-Id: test'
# For comparison, POST *is* gated by mcp-auth:
curl -i -X POST http://localhost:8290/github/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
- Defect 2 — confirm the PRM contains
"scopes_supported": []:
curl -s http://localhost:8290/github/.well-known/oauth-protected-resource | jq .
-
Connect VS Code (1.128.1) to http://localhost:8290/github/mcp and complete the OAuth login. Observe in the MCP output channel (log level Trace): initialize returns a result, notifications/initialized / prompts/list / tools/list are emitted but never reach the gateway, and a few seconds later Authentication session for <auth-server> removed, stopping server. The client then reconnects and loops.
-
Apply either workaround above and the same client completes initialize → tools/list normally.
Severity Level of the Issue
Severity/Major (Important functionality is broken. Should be prioritized. Doesn't need immediate attention)
Environment Details
- Gateway:
1.2.0-beta (mcp-auth v1.1.1) and a locally built 1.2.0-beta-SNAPSHOT (mcp-auth v1.1.2, jwt-auth v1.2.3) — both reproduce
- MCP client: VS Code 1.128.1 (Claude Code v2.1.x reproduces defect 2, and is unaffected by defect 1 as it sends no session
DELETE)
- Authorization server: Keycloak 26.2
- Upstream MCP server: GitHub remote MCP (
https://api.githubcopilot.com/mcp/)
- Host: macOS, Docker Desktop
Related
Please select the area the issue is related to
Gateway
Please select the aspect the issue is related to
Aspect/AI (AI/LLM integration, MCP, AI readiness)
Description
Two defects in
mcp-autheach cause an MCP client to discard its entire OAuth session immediately after a successfulinitializeand stop the server. The user-visible symptom is a connect → re-authenticate → fail loop, even though authentication itself succeeded andinitializereturned200.Both are instances of the same client behaviour, which is worth stating up front because it makes each defect much more severe than it first looks:
Verified with VS Code 1.128.1 against the GitHub remote MCP server (
https://api.githubcopilot.com/mcp/) proxied by the gateway, withmcp-auth(Keycloak key manager) plus aninterceptor-servicethat injects a per-user upstream credential. Claude Code is unaffected by defect 1 because it does not send a sessionDELETE.Defect 1 —
mcp-authdoes not gateGETorDELETEThe MCP Streamable HTTP transport has clients issue, in addition to JSON-RPC
POSTs:GET <mcp-endpoint>— the standalone asynchronous-notification (SSE) streamDELETE <mcp-endpoint>— session terminationmcp-authdoes not apply authentication to either method, so both pass straight through to the rest of the policy chain with no authenticated context established. (VS Code additionally sends theGETwith noAuthorizationheader at all, so there is nothing for a downstream policy to key off even opportunistically.)The practical consequence: any backend-auth policy that fails closed — for example an
interceptor-servicethat exchanges the caller's token for an upstream credential — has no identity to work with on these two requests and returns401. That401then destroys the client's auth session per the behaviour above, and the MCP session dies right afterinitialize.Observed on the gateway (
interceptor-servicereturning401because no identity reached it):The interceptor's view of that
GET— note the absence ofauthorizationandx-forwarded-authorization:And the client side, 5.6s later:
For comparison, a proxy that forwards the upstream's own response for that
GET(GitHub replies405, as it has no standalone stream) works correctly — VS Code logs the following and proceeds normally totools/list:Expected:
GETandDELETEon an MCP resource are covered bymcp-auth'smethodssecurity configuration like any other MCP request, so that (a) they are authenticated consistently and (b) downstream policies see the same authenticated context they see for aPOST.Workaround: make the backend-auth policy never answer
401on these paths — return405for the notificationGET(matching what a real MCP backend without a standalone stream returns) and204for the sessionDELETE(treating a session that cannot be authenticated as already terminated).Defect 2 — Protected Resource Metadata publishes
scopes_supported: []instead of omitting itWith
mcp-auth'srequiredScopesempty or unset, the PRM document still contains the key, as an empty array:{ "resource": "http://localhost:8290/github/mcp", "authorization_servers": ["http://localhost:8080/realms/mcp"], "scopes_supported": [] }scopes_supportedis OPTIONAL in RFC 9728 §2, and an empty array is not equivalent to absence. VS Code stores the advertised value as the session's scope set, later recomputes it asundefined, detects a change, and tries to re-acquire a token non-interactively — which fails and takes the session with it:Note
consentRequiredisfalseon the Keycloak client, and Keycloak logs no consent-related event — "User did not consent to login" is the client's wording for "an interactive login was required but the request was made silently", triggered by the scope-set change.A proxy that omits the field entirely does not exhibit this: the client sees
undefinedon both sides, detects no change, and never re-authenticates.Expected: omit
scopes_supportedfrom the PRM when there are no scopes to advertise, rather than emitting[].Workaround: always configure a non-empty
requiredScopes(e.g.openid,profile,email), even when scope enforcement is not wanted — noting thatmcp-authdoes not enforcerequiredScopesanyway, so this is purely to avoid publishing[].Steps to Reproduce
mcp-authand a fail-closed backend-auth policy:GET/DELETEare not gated. Both reach the backend chain rather than being rejected bymcp-auth, and carry no authenticated context:"scopes_supported": []:Connect VS Code (1.128.1) to
http://localhost:8290/github/mcpand complete the OAuth login. Observe in the MCP output channel (log levelTrace):initializereturns a result,notifications/initialized/prompts/list/tools/listare emitted but never reach the gateway, and a few seconds laterAuthentication session for <auth-server> removed, stopping server. The client then reconnects and loops.Apply either workaround above and the same client completes
initialize→tools/listnormally.Severity Level of the Issue
Severity/Major (Important functionality is broken. Should be prioritized. Doesn't need immediate attention)
Environment Details
1.2.0-beta(mcp-auth v1.1.1) and a locally built1.2.0-beta-SNAPSHOT(mcp-auth v1.1.2, jwt-auth v1.2.3) — both reproduceDELETE)https://api.githubcopilot.com/mcp/)Related
mcp-authstrips theAuthorizationheader injected byset-headers(same integration; that defect is why the backend credential must be injected byinterceptor-serviceat the body phase in the first place)jwt-authtoken-forwarding params inmcp-auth