Skip to content

fix(auth): parse fractional expires_in and stop treating expiry 0 as no expiry - #3770

Closed
Ben-Home wants to merge 1 commit into
modelcontextprotocol:mainfrom
Ben-Home:fix/token-expiry-robustness
Closed

Ben-Home wants to merge 1 commit into
modelcontextprotocol:mainfrom
Ben-Home:fix/token-expiry-robustness

Conversation

@Ben-Home

@Ben-Home Ben-Home commented Oct 26, 2026 •

Copy link
Copy Markdown

Scope correction (this PR stays closed)

I opened this as two defects with user-visible impact and I was wrong about the impact. Checking the path end to end after the fact:

  • OAuthToken.expires_in is int | None, and pydantic rejects both "3599.5" and 3599.5 with a ValidationError before calculate_token_expiry is ever called. float() there therefore does not repair any reachable flow; it only makes a defensive helper match its own annotation and docstring.
  • token_expiry_time is written only from calculate_token_expiry (or None), so a literal 0.0 is not reachable through SDK code. The falsy-zero change is hardening for state a caller supplies, not a bug fix.

The expiry boundary is the real defect in this area and a maintainer already has it: #3761.

So this is a two-line hardening patch on defensive paths, not a bug fix, and the repository's contribution policy is clear that a change still has to justify the contract it adds. Leaving it closed unless a maintainer wants the hardening. The evidence for the claim above, measured on current main:

OAuthToken(access_token='a', expires_in='3599.5')  -> ValidationError (expires_in)
OAuthToken(access_token='a', expires_in=3599.5)    -> ValidationError (expires_in)
calculate_token_expiry('3599.5')                   -> ValueError: invalid literal for int()

What the diff does, described accurately

src/mcp/shared/auth_utils.py: calculate_token_expiry declares int | str | None and documents string handling, but calls int(expires_in), which raises on a fractional string and truncates a float. Using float() makes the helper agree with its own annotation. No reachable caller benefits today.

src/mcp/client/auth/oauth2.py: is_token_valid() reads not self.token_expiry_time, which conflates None ("no expiry declared") with any falsy value, including 0.0. Comparing against None explicitly states the intended meaning. No SDK path currently produces 0.0.

tests/shared/test_auth_utils.py and tests/client/test_auth.py carry the matching tests for both, including the None, integer, numeric-string, fractional-string and float inputs the annotation already claimed to support.

Verification

uv run pytest tests/client tests/shared -q
1804 passed, 1 xfailed

uv run ruff check src/mcp/shared/auth_utils.py src/mcp/client/auth/oauth2.py \
  tests/shared/test_auth_utils.py tests/client/test_auth.py
All checks passed!

…no expiry

calculate_token_expiry declares int|str but calls int(), so a server emitting a
fractional expires_in as a string raises ValueError during token setup, and a float
input is truncated instead of kept. Parse with float() so both work.

is_token_valid tested token_expiry_time for truthiness, so an expiry of 0.0 was read
as 'no expiry' and the token was presented forever. Compare against None explicitly.

Tests cover None, int, numeric string, fractional string and float inputs, plus the
zero-expiry predicate.
@github-actions github-actions Bot added the missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md) label Oct 26, 2026
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

This PR has been closed automatically. This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue, and this PR doesn't link an open issue yet.

  • If you're already assigned to an issue for this, add Fixes #<n> to the description and the PR will reopen on its own.
  • If there's no issue yet, please open one instead: what you ran into, why it matters for your use case, and a minimal reproduction. That context is super important to us and is what we use to decide what to prioritise.
  • If there's an issue but you're not assigned, add Fixes #<n> anyway so they're linked, then engage on the issue itself by confirming the repro or describing the approach you'd take. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. If you are assigned, this PR reopens automatically.

You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way.

CONTRIBUTING.md has the full reasoning, but in short:

  • We're a small team with very little capacity to review community PRs right now.
  • Many recent PRs are AI-generated with little human review, and reviewing one carefully still costs a maintainer as much time as it ever did. A well-described issue is usually more useful to us than the code.

Maintainers: reopen, remove missing-issue-link, or add bypass-issue-check to override.

@github-actions github-actions Bot closed this Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant