modelcontextprotocol / modelcontextprotocol/typescript-sdk
Client transport: HTTP 429 responses are not retried with Retry-After
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 13.4k
- Forks
- 2.2k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 4
Description
Summary
StreamableHTTPClientTransport does not handle HTTP 429 Too Many Requests responses. When a server responds with 429 + Retry-After, the client throws a generic SdkError instead of waiting the indicated interval and retrying. Clients that don't add their own retry logic therefore fail-fast on any rate limit.
Current behaviour
In packages/client/src/client/streamableHttp.ts (main, commit-of-today):
- Explicit status handling exists for 401/403 (OAuth re-auth), 405 (no GET stream), 202 (accepted), SSE disconnect (exponential backoff +
retry:field). - 429 falls through to the generic
!response.okpath around line 633 and surfaces as a plainSdkErrorwith status/statusText only. - Neither
Retry-Afternor the IETF draft-standardRateLimit-*headers (RFC 9331) are read.
Grep for 429 in the file returns no hits.
Impact
- Servers that implement reasonable rate limits (e.g.
express-rate-limit) always return 429 withRetry-Afteron excess traffic. Every client that speaks to such a server crashes on overload instead of backing off. - The spec (
docs/specification/.../basic/transports.mdxacross 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, draft) does not say anything about rate-limit headers, so today each server/client pair has to reinvent this. The missing client behaviour is the sharp end of that gap. - Observed in the wild with a server plugin (MiYo Kado MCP Gateway, https://github.com/MMoMM-org/miyo-kado): server emits
RateLimit-Limit,RateLimit-Remaining,RateLimit-Reset, andRetry-Afteron 429; clients built on this SDK still fail rather than retry. Kado ships a client-side reference retry loop intest/live/mcp-live.test.ts(probeRetryAfter()+callTool()) as a workaround.
Reproduction
Minimal repro against any MCP server behind a rate limiter:
- Call
tools/callrepeatedly above the configured rate (e.g. 200 req/min). - Server returns 429 with
Retry-After: <seconds>and standardRateLimit-*headers. - Client code awaiting
transport.send(...)receives a thrownSdkErrorimmediately. No retry, no waiting.
Expected: the transport waits at least Retry-After seconds (or falls back to a bounded exponential backoff when the header is missing) and retries the same request once or twice before surfacing the error.
Suggested direction
Similar in spirit to #1370 (incorporate OAuth retry logic into the SDK):
- On 429, parse
Retry-After(HTTP-date or delta-seconds per RFC 7231 §7.1.3). If absent, fall back to exponential backoff. Optionally also honourRateLimit-Reset(RFC 9331) as the wait hint. - Wait, then retry the original request up to a small, configurable number of attempts.
- Surface a clear error only after retries are exhausted, ideally including the parsed
Retry-Aftervalue so callers can decide what to do next. - Allow opting out via a constructor option for clients that want to handle rate limiting themselves.
Happy to prepare a PR if there is interest and directional agreement.
Environment
- Repo:
modelcontextprotocol/typescript-sdk, branchmain, filepackages/client/src/client/streamableHttp.ts - Spec versions checked: 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, draft — none mention rate limiting
- Related: #1370 (OAuth retry logic from conformance helpers)
Contributor guide
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.
Research direction
Start in packages/client/src/client/streamableHttp.ts around the generic !response.ok path near line 633, and compare the existing 401/403 and SSE retry handling. Review #1370 and test/live/mcp-live.test.ts for related retry behavior. Done means 429 responses honor the documented wait hints, retry within a bounded policy, and produce a clear error after exhaustion.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100