Skip to content

🦣 fix: Response Size Limits for Streamable HTTP MCP Responses#13219

Merged
danny-avila merged 6 commits into
devfrom
fix/mcp-size-limits-http
May 21, 2026
Merged

🦣 fix: Response Size Limits for Streamable HTTP MCP Responses#13219
danny-avila merged 6 commits into
devfrom
fix/mcp-size-limits-http

Conversation

@danny-avila

Copy link
Copy Markdown
Owner
  • Added environment variables for maximum response and line sizes in streamable HTTP responses.
  • Introduced functions to handle response size validation and error logging.
  • Updated MCP connection logic to enforce these limits, ensuring safe handling of large responses.

- Added environment variables for maximum response and line sizes in streamable HTTP responses.
- Introduced functions to handle response size validation and error logging.
- Updated MCP connection logic to enforce these limits, ensuring safe handling of large responses.
Copilot AI review requested due to automatic review settings May 21, 2026 03:38
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

Copilot AI left a comment

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.

Pull request overview

This PR adds defensive limits and truncation around potentially large streaming MCP responses and tool execution errors to reduce memory pressure and improve safety when upstream systems return unexpectedly large payloads.

Changes:

  • Added configurable size limits (via env vars) for streamable HTTP MCP SSE responses (total bytes + per-line bytes).
  • Implemented an SSE response guard in the MCP undici fetch wrapper to validate response size and log diagnostics on rejection.
  • Truncated tool error messages/stacks in agent tool execution handling to avoid propagating extremely large errors to logs and callers.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
packages/api/src/mcp/connection.ts Adds SSE response size/line guard and applies it to responses returned from the custom undici fetch/redirect loop.
packages/api/src/agents/handlers.ts Introduces safe truncation for tool error messages/stacks and updates logging + returned errorMessage.
.env.example Documents new env vars for streamable HTTP MCP SSE response limits.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/api/src/mcp/connection.ts Outdated
Comment on lines +166 to +170
const chunks: ArrayBuffer[] = [];
const reader = (response.body as unknown as ReadableStream<unknown>).getReader();
let totalBytes = 0;
let currentLineBytes = 0;
let chunkCount = 0;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 768c9aa. I replaced the buffering guard with an incremental TransformStream so SSE POST responses can be consumed as chunks arrive instead of waiting for EOF. The guard now emits a bounded JSON-RPC SSE error for the request id when an SSE cap trips. Covered by the new valid-streaming SSE test in MCPConnectionSSRF.test.ts.

Comment on lines 838 to 840
if (!isMethodPreservingRedirect || redirects >= MAX_REDIRECTS) {
return response;
return guardMCPStreamableHTTPResponse(response, responseContext);
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 768c9aa. Added focused MCPConnectionSSRF coverage for oversized JSON response bytes, oversized SSE line handling, and a valid SSE POST response proving the guarded response streams before EOF.

Comment on lines +1236 to 1243
const { message, logContext } = getSafeToolError(toolError);
logger.error(`[ON_TOOL_EXECUTE] Tool ${tc.name} error`, logContext);
return {
toolCallId: tc.id,
status: 'error' as const,
content: '',
errorMessage: error.message,
errorMessage: message,
};

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 768c9aa. Added handlers.spec coverage for oversized thrown tool errors, asserting the returned errorMessage is truncated and the logger context reports messageTruncated: true.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df6125b375

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +156 to +158
const contentType = response.headers.get('content-type') ?? '';
if (!contentType.toLowerCase().includes('text/event-stream')) {
return response;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Enforce non-GET byte caps for JSON responses

The new guard returns early for any non-SSE response (content-type not containing text/event-stream), so MCP_STREAMABLE_HTTP_MAX_RESPONSE_BYTES is never applied to application/json POST responses. In Streamable HTTP, POST requests can validly return JSON, which means a buggy or malicious MCP server can still send an unbounded JSON body and bypass the configured response-size protection.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 768c9aa. The guard now applies MCP_STREAMABLE_HTTP_MAX_RESPONSE_BYTES to non-GET responses regardless of content type, so application/json POST responses are capped too. Added a JSON POST byte-limit regression test.

Comment thread packages/api/src/mcp/connection.ts Outdated
Comment on lines +193 to +196
while (true) {
const { value, done } = await reader.read();
if (done) {
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve streaming semantics for SSE POST responses

This loop reads the entire SSE body to EOF before returning a Response, which turns a streamed POST response into a buffered one. Streamable HTTP servers may emit notifications/progress messages before the final JSON-RPC response, so delaying delivery until close breaks incremental processing and can make long-running tool calls appear stalled.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 768c9aa. The SSE path no longer buffers to EOF. It wraps the response body in a TransformStream, passes valid chunks through incrementally, and emits a bounded JSON-RPC error event if a limit trips. Added a test that reads the first SSE chunk before the server closes.

Comment thread packages/api/src/agents/handlers.ts Outdated
Comment on lines +182 to +183
const rawMessage = error instanceof Error ? error.message : String(error);
const message = truncateMiddle(rawMessage, MAX_TOOL_ERROR_MESSAGE_CHARS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard error-string coercion in tool failure handler

When a tool throws a non-Error, getSafeToolError coerces it via String(error). That coercion can itself throw (e.g., custom toString/Symbol.toPrimitive), which would escape this catch block and trigger the outer fatal path instead of returning a per-tool error result. In that case, one malformed thrown object can fail the whole tool-execution batch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 768c9aa. Added stringifyThrownValue() so non-Error thrown values with throwing toString/Symbol coercion still produce a per-tool error result instead of escaping to the fatal batch path. Covered in handlers.spec.ts.

Comment thread packages/api/src/mcp/connection.ts Outdated
Comment on lines +86 to +87
const parsed = Number.parseInt(raw, 10);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : defaultValue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Reject partially numeric env values for byte limits

getNonNegativeIntegerEnv uses parseInt, which accepts numeric prefixes from malformed values (for example "1e6" becomes 1, "16MB" becomes 16) instead of treating them as invalid. That silently applies much smaller limits than intended and can cause unexpected MCP response blocking in production when operators use common human-readable env formats.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 768c9aa. getNonNegativeIntegerEnv now accepts only all-digit strings and safe integers, so values like 1e6 or 16MB fall back to the default instead of being partially parsed.

@github-actions

Copy link
Copy Markdown
Contributor

GitNexus: 🚀 deployed

The LibreChat-pr-13219 index is now live on the MCP server.
Deploy run

@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 81e0c617ad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/mcp/connection.ts Outdated

if (!isMethodPreservingRedirect || redirects >= MAX_REDIRECTS) {
return response;
return guardMCPStreamableHTTPResponse(response, responseContext);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scope response-size guard to streamable-http transport

Apply the new size guard only for streamable HTTP requests, not every non-GET request path in createFetchFunction. This helper is also used by the legacy SSE transport (constructTransport passes createFetchFunction in both sse and streamable-http branches), so the new MCP_STREAMABLE_HTTP_* limits now affect SSE POST responses even though the config/docs describe them as streamable-http-only; that can unexpectedly reject existing SSE servers with larger POST payloads.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 45d0700. The response-size guard is now opt-in from createFetchFunction and is enabled only when constructing the streamable-http transport. Legacy SSE transport fetches keep their previous behavior; added a regression test that proves the streamable HTTP caps are not applied unless the transport opts in.

Comment thread packages/api/src/mcp/connection.ts Outdated
Comment on lines +261 to +263
if (isEventStream && context.requestIds.length > 0) {
controller.enqueue(buildSSEErrorEvents(context.requestIds, message));
controller.terminate();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit fallback SSE errors only for unresolved request IDs

The oversized-stream fallback currently emits an error event for every ID parsed from the original POST body, regardless of whether some IDs already received a response earlier in the same stream. For batched or partially completed streams, this can send duplicate responses for the same JSON-RPC ID; the SDK protocol handler drops the first response handler and treats later duplicates as unknown-response errors, creating spurious transport errors exactly when the guard is supposed to fail gracefully.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 45d0700. I removed the synthetic SSE fallback event entirely, so the guard no longer emits JSON-RPC errors for request ids that may already have completed. The guarded stream now fails with a bounded diagnostic error instead.

Comment thread packages/api/src/agents/handlers.ts Outdated
message: string;
logContext: Record<string, unknown>;
} {
const rawMessage = error instanceof Error ? error.message : stringifyThrownValue(error);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve message field when non-Error objects are thrown

Handle thrown plain objects with a message property before falling back to String(error). The new logic now converts non-Error throws via String(error), so a common throw shape like { message: 'timeout' } becomes "[object Object]" in errorMessage, which is a regression from the previous behavior (toolError as Error would surface .message) and makes tool failures much harder for users and agents to diagnose.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 45d0700. getSafeToolError now preserves a message property from thrown plain objects before falling back to safe String coercion, with handlers.spec coverage for { message: ... } throws.

Comment thread packages/api/src/mcp/connection.ts Outdated
Comment on lines +262 to +263
controller.enqueue(buildSSEErrorEvents(context.requestIds, message));
controller.terminate();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prevent fallback SSE error from appending to partial line

When an oversized SSE line spans multiple chunks, earlier chunks from that same line are already forwarded, and then this fallback appends a synthetic data: {...} error event mid-line. Because SSE parsers buffer incomplete lines across chunks, the injected fallback is merged into malformed line content instead of becoming a valid event, so the intended bounded JSON-RPC error may never be parsed by the client.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 45d0700. The guard no longer appends a synthetic SSE event after partial oversized-line bytes. It throws/errors the guarded stream with a bounded message, preventing eventsource-parser from continuing to accumulate the oversized line or receiving malformed injected data.

@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@github-actions

Copy link
Copy Markdown
Contributor

GitNexus: 🚀 deployed

The LibreChat-pr-13219 index is now live on the MCP server.
Deploy run

@github-actions

Copy link
Copy Markdown
Contributor

GitNexus: 🚀 deployed

The LibreChat-pr-13219 index is now live on the MCP server.
Deploy run

@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila danny-avila changed the title fix: implement response size limits for streamable HTTP MCP responses 🦣 fix: Response Size Limits for Streamable HTTP MCP Responses May 21, 2026
@danny-avila danny-avila merged commit 1ed84ee into dev May 21, 2026
11 of 12 checks passed
@danny-avila danny-avila deleted the fix/mcp-size-limits-http branch May 21, 2026 05:29
@github-actions

Copy link
Copy Markdown
Contributor

GitNexus: 🚀 deployed

The LibreChat-pr-13219 index is now live on the MCP server.
Deploy run

fuuuzzy pushed a commit to fuuuzzy/LibreChat that referenced this pull request May 22, 2026
…avila#13219)

* fix: implement response size limits for streamable HTTP MCP responses

- Added environment variables for maximum response and line sizes in streamable HTTP responses.
- Introduced functions to handle response size validation and error logging.
- Updated MCP connection logic to enforce these limits, ensuring safe handling of large responses.

* fix: address MCP response guard review findings

* fix: satisfy logger spy typings

* fix: clarify blocked MCP response errors

* fix: harden MCP guard review edge cases

* test: cover MCP oversized SSE call failures
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.

2 participants