[Bug]: mcp-auth breaks MCP client auth sessions: GET/DELETE are not gated, and PRM publishes empty scopes_supported
@VirajSalaka is already working on this.
Since Jul 28, 2026.
- Dominant language
- Go
- Stars
- 71
- Forks
- 111
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 110
Description
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
401on 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 also401s), and tears the connection down. The first401is 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) streamDELETE <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-authand 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/DELETEare not gated. Both reach the backend chain rather than being rejected bymcp-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/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
- Gateway:
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 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
- #2866 —
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) - #2867 — expose
jwt-authtoken-forwarding params inmcp-auth - #2751 (closed) — same family of problem: an HTTP status/shape on the MCP endpoint that MCP clients cannot recover from
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.