Repository navigation
Conversation
The legacy GET /mcp stream took its deadline from request.tokenPayload only, so with resolveAuthorizationContext it outlived the resolved expiresAt, and a context that had already expired was still served a stream. Read the deadline from the resolved authorization context, which also carries the built-in token's exp, and answer an expired context with 401 before a session is created, as subscriptions/listen already does. Fixes #336 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LmhsFjk8W96X2PvmShmDm9
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Base:
feat/mcp-2026-07-28(#170), notmain.resolveAuthorizationContextexists only on that branch, so the bug cannot be fixed on main.Problem
With
resolveAuthorizationContext, the legacyGET /mcpSSE stream ignores the resolvedexpiresAt. Its deadline comes only fromrequest.tokenPayload?.exp, so:expiresAt300ms ahead, the stream is still open after 1sexpiresAtthat has already passed, the GET still answers 200 and opens a stream (and creates a session)Fix
In the legacy GET handler, before the session is looked up or created:
authContextFrom(request). It is memoized per request, so the resolver does not run again, and on the built-in token path it already carriesexpasexpiresAt.expiresAthas already passed, answer401with a JSON body, plusWWW-Authenticatewhen the plugin runs OAuth. This is the same responsesubscriptions/listengives an expired context. The check runs before the hijack, so the reply goes through Fastify normally.expiresAtfor the stream deadline in place oftokenPayload.exp. ThesseMaxConnectionMsinterplay and the capped-timer logic stay as they were.Tests
test/sse-resolved-auth-expiry.test.ts:expiresAt300ms ahead closes the stream (it stayed open past 1.5s before)Mcp-Session-Id, and nosessionStore.createcall (it got 200 before)test/sse-token-expiry.test.tsstill passes, so the JWTexppath is unchanged. Lint, typecheck and the full suite pass on this branch (1150 tests).Fixes #336
🤖 Generated with Claude Code
https://claude.ai/code/session_01LmhsFjk8W96X2PvmShmDm9