⏱️ fix: Separate MCP GET SSE Stream Timeout from POST and Suppress SDK-Internal Recovery Errors (#11936)

* fix: Separate MCP GET SSE body timeout from POST and suppress SDK-internal stream recovery

- Add a dedicated GET Agent with a configurable `sseReadTimeout` (default 5 min,
  matching the Python MCP SDK) so idle SSE streams time out independently of POST
  requests, preventing the reconnect-loop log flood described in Discussion #11230.
- Suppress "SSE stream disconnected" and "Failed to reconnect SSE stream" errors
  in setupTransportErrorHandlers — these are SDK-internal recovery events, not
  transport failures. "Maximum reconnection attempts exceeded" still escalates.
- Add optional `sseReadTimeout` to BaseOptionsSchema for per-server configuration.
- Add 6 tests: agent timeout separation, custom sseReadTimeout, SSE disconnect
  suppression (3 unit), and a real-server integration test proving the GET stream
  recovers without a full transport rebuild.

* fix: Refactor MCP connection timeouts and error handling

- Updated the `DEFAULT_SSE_READ_TIMEOUT` to use a constant for better readability.
- Introduced internal error message constants for SSE stream disconnection and reconnection failures to improve maintainability.
- Enhanced type safety in tests by ensuring the options symbol is defined before usage.
- Updated the `sseReadTimeout` in `BaseOptionsSchema` to enforce positive values, ensuring valid configurations.

* chore: Update SSE read timeout documentation format in BaseOptionsSchema

- Changed the default timeout value comment in BaseOptionsSchema to use an underscore for better readability, aligning with common formatting practices.
This commit is contained in:
Danny Avila 2026-02-24 21:05:58 -05:00 committed by GitHub
parent 44dbbd5328
commit 9a8a5d66d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 254 additions and 10 deletions

View file

@ -19,6 +19,8 @@ const BaseOptionsSchema = z.object({
startup: z.boolean().optional(),
iconPath: z.string().optional(),
timeout: z.number().optional(),
/** Timeout (ms) for the long-lived SSE GET stream body before undici aborts it. Default: 300_000 (5 min). */
sseReadTimeout: z.number().positive().optional(),
initTimeout: z.number().optional(),
/** Controls visibility in chat dropdown menu (MCPSelect) */
chatMenu: z.boolean().optional(),
@ -212,6 +214,7 @@ const omitServerManagedFields = <T extends z.ZodObject<z.ZodRawShape>>(schema: T
schema.omit({
startup: true,
timeout: true,
sseReadTimeout: true,
initTimeout: true,
chatMenu: true,
serverInstructions: true,