Skip to content

fix(sse): end the legacy SSE stream at the resolved context's expiresAt - #357

Open
mcollina wants to merge 1 commit into
feat/mcp-2026-07-28from
fix/336-sse-resolved-auth-expiry
Open

mcollina wants to merge 1 commit into
feat/mcp-2026-07-28from
fix/336-sse-resolved-auth-expiry

Conversation

@mcollina

Copy link
Copy Markdown
Member

Base: feat/mcp-2026-07-28 (#170), not main. resolveAuthorizationContext exists only on that branch, so the bug cannot be fixed on main.

Problem

With resolveAuthorizationContext, the legacy GET /mcp SSE stream ignores the resolved expiresAt. Its deadline comes only from request.tokenPayload?.exp, so:

  • if the resolver returns an expiresAt 300ms ahead, the stream is still open after 1s
  • if the resolver returns an expiresAt that 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:

  • Read the identity with authContextFrom(request). It is memoized per request, so the resolver does not run again, and on the built-in token path it already carries exp as expiresAt.
  • If expiresAt has already passed, answer 401 with a JSON body, plus WWW-Authenticate when the plugin runs OAuth. This is the same response subscriptions/listen gives an expired context. The check runs before the hijack, so the reply goes through Fastify normally.
  • Otherwise, use expiresAt for the stream deadline in place of tokenPayload.exp. The sseMaxConnectionMs interplay and the capped-timer logic stay as they were.

Tests

test/sse-resolved-auth-expiry.test.ts:

  • a resolver expiresAt 300ms ahead closes the stream (it stayed open past 1.5s before)
  • an expired resolver context gets 401, no Mcp-Session-Id, and no sessionStore.create call (it got 200 before)

test/sse-token-expiry.test.ts still passes, so the JWT exp path 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

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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant