modelcontextprotocol / modelcontextprotocol/typescript-sdk

Client transport: HTTP 429 responses are not retried with Retry-After

Open
#1,892 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement needs decision P3
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.ok path around line 633 and surfaces as a plain SdkError with status/statusText only.
  • Neither Retry-After nor the IETF draft-standard RateLimit-* 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 with Retry-After on excess traffic. Every client that speaks to such a server crashes on overload instead of backing off.
  • The spec (docs/specification/.../basic/transports.mdx across 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, and Retry-After on 429; clients built on this SDK still fail rather than retry. Kado ships a client-side reference retry loop in test/live/mcp-live.test.ts (probeRetryAfter() + callTool()) as a workaround.

Reproduction

Minimal repro against any MCP server behind a rate limiter:

  1. Call tools/call repeatedly above the configured rate (e.g. 200 req/min).
  2. Server returns 429 with Retry-After: <seconds> and standard RateLimit-* headers.
  3. Client code awaiting transport.send(...) receives a thrown SdkError immediately. 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):

  1. 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 honour RateLimit-Reset (RFC 9331) as the wait hint.
  2. Wait, then retry the original request up to a small, configurable number of attempts.
  3. Surface a clear error only after retries are exhausted, ideally including the parsed Retry-After value so callers can decide what to do next.
  4. 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, branch main, file packages/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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.