Skip to content

fix(sse): close GET /mcp streams when the opening token expires - #279

Merged
mcollina merged 3 commits into
mainfrom
fix/254-close-streams-at-token-exp
Oct 15, 2026
Merged

mcollina merged 3 commits into
mainfrom
fix/254-close-streams-at-token-exp

Conversation

@mcollina

@mcollina mcollina commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Problem

With authorization enabled, the bearer token is only checked when the legacy SSE GET /mcp stream opens. After that the stream keeps delivering notifications even once the token has expired.

Fix

  • If the validated token payload (request.tokenPayload) has an exp, the stream is ended at exp by an unref'd timer, which is cleared when the stream closes. The client reconnects with a fresh token and resumes with Last-Event-ID.
  • The configurable maximum lifetime already exists as sseMaxConnectionMs (SEP-1699), so no new option is added. The stream now closes at min(exp, sseMaxConnectionMs).
  • setTimeout fires immediately on delays above 2^31-1 ms (~24.8 days), so long deadlines are waited out in capped steps that reschedule until the deadline.
  • README and the sseMaxConnectionMs JSDoc describe the behaviour.

subscriptions/listen streams (#170) are not covered here.

Tests

test/sse-token-expiry.test.ts:

  • a stream opened with a JWT whose exp is about 2s away closes within 1s after exp, even with a longer sseMaxConnectionMs
  • opening a stream adds no ref'd timers (checked with process.getActiveResourcesInfo(); this test fails if .unref() is removed)
  • a token expiring in 30 days (mock timers) keeps the stream open past the 24.8-day timer limit and closes it once time passes exp
  • sseMaxConnectionMs still applies without authorization

Fixes #254

🤖 Generated with Claude Code

https://claude.ai/code/session_01UF4S3w7iA83V9DVsasLJzM

Comment thread src/routes/mcp.ts Outdated
closeReason = 'Closing SSE stream because the token that opened it expired; it may reconnect with a fresh token'
}

// setTimeout fires immediately on delays past 2^31-1 ms (~24.8 days)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long-lived tokens never expire. When exp is more than 24.8 days away, this skips setTimeout() entirely. With no shorter sseMaxConnectionMs, the SSE stream can continue after the token expires. we should schedule a capped timer and reschedule until exp, or reject long delays.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bc55055: the close timer now waits in unref'd steps capped at 2^31-1 ms and reschedules until the deadline (the earlier of exp and sseMaxConnectionMs), and the pending timer is cleared on close. Added a mock-timers test with a token that expires in 30 days: the stream stays open after time moves past the 24.8-day limit and closes once it moves past exp.

@mcollina
mcollina force-pushed the fix/254-close-streams-at-token-exp branch from a0559fd to bc55055 Compare October 14, 2026 10:10
mcollina and others added 3 commits October 14, 2026 13:13
Access is checked only when a long-lived SSE stream opens, so a stream
kept receiving notifications after the bearer token that opened it had
expired. When authorization is enabled and the token carries an `exp`,
schedule an unref'd timer that ends the stream at exp, cleared when the
stream closes. The client reconnects with a fresh token and resumes
with Last-Event-ID.

The existing sseMaxConnectionMs option (SEP-1699) already provides a
configurable maximum stream lifetime; the stream now closes at
min(exp, sseMaxConnectionMs). Delays beyond setTimeout's 2^31-1 ms limit
are skipped instead of firing immediately.

Fixes #254

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UF4S3w7iA83V9DVsasLJzM
The close timer and AbortSignal.timeout are both unref'd, so in CI the
event loop could drain before the stream ended and node:test cancelled
the suite. Wait with a ref'd timer instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UF4S3w7iA83V9DVsasLJzM
A token whose exp was more than 2^31-1 ms (~24.8 days) away skipped the
close timer entirely, so the stream outlived the token. Wait in capped,
unref'd steps until the deadline instead, and clear the pending timer on
close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UF4S3w7iA83V9DVsasLJzM
@mcollina
mcollina force-pushed the fix/254-close-streams-at-token-exp branch from bc55055 to c71bfab Compare October 14, 2026 11:16
@mcollina
mcollina merged commit f0e86e7 into main Oct 15, 2026
4 checks passed
@mcollina
mcollina deleted the fix/254-close-streams-at-token-exp branch October 15, 2026 06:47
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.

Notification streams outlive the token that opened them

2 participants