Skip to content

mcp_transcoder: add sse server response support.#45374

Open
guoyilin42 wants to merge 1 commit into
envoyproxy:mainfrom
guoyilin42:sse
Open

mcp_transcoder: add sse server response support.#45374
guoyilin42 wants to merge 1 commit into
envoyproxy:mainfrom
guoyilin42:sse

Conversation

@guoyilin42
Copy link
Copy Markdown
Contributor

Commit Message: mcp_transcoder: add sse server response support.
Additional Description:
This PR introduces Server-Sent Events (SSE) server response support to the MCP JSON REST bridge filter. When the upstream server responds with a text/event-stream content type, the filter will now parse the stream and wrap each extracted complete event payload into a distinct text content item within the JSON-RPC content array on the fly.

Key Changes:

  • Added SseResponseExtractor: Introduced sse_response_extractor.cc and its header to process incoming chunks of an SSE stream and return completed event payloads.
  • Updated McpJsonRestBridgeFilter: Modified the encoding path to check for the text/event-stream content type. It now delegates chunk processing to the new SSE extractor and formats the results into the JSON-RPC streaming envelope.
  • Testing: Added unit tests in sse_response_extractor_test.cc to verify the handling of single/multiple events, multiline data, incomplete chunked events, and CRLF line endings. Also added ToolsCallSseStreamingTranscoding to integration tests.

Risk Level: Low
Testing: Unit and Integration tests.
Docs Changes: N/A
Release Notes: N/A
Platform Specific Features: N/A

Signed-off-by: Yilin Guo <guoyilin@google.com>
@repokitteh-read-only
Copy link
Copy Markdown

CC @envoyproxy/api-shepherds: Your approval is needed for changes made to (api/envoy/|docs/root/api-docs/).
envoyproxy/api-shepherds assignee is @wbpcode
CC @envoyproxy/api-watchers: FYI only for changes made to (api/envoy/|docs/root/api-docs/).

🐱

Caused by: #45374 was opened by guoyilin42.

see: more, trace.

@guoyilin42
Copy link
Copy Markdown
Contributor Author

/gemini review

cc @paulhong01

#44146

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for Server-Sent Events (SSE) responses in the MCP JSON-RPC REST bridge filter. It adds an SseResponseExtractor to parse incoming text/event-stream chunks on the fly and wrap completed event payloads into distinct text items within the JSON-RPC content array. The review feedback highlights two critical robustness and security concerns: first, nlohmann::json::dump() could throw an exception and crash Envoy if it encounters invalid UTF-8 in the untrusted upstream payload, which should be mitigated by using a safe error-handling replacement strategy; second, the unbounded buffering of incomplete SSE events poses a memory exhaustion (DoS) risk, necessitating a maximum buffer size limit.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

json content_item;
content_item[McpConstants::TYPE_FIELD] = McpConstants::TEXT_FIELD;
content_item[McpConstants::TEXT_FIELD] = event_payload;
std::string serialized_item = content_item.dump();
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.

high

By default, nlohmann::json::dump() throws an exception if it encounters invalid UTF-8 characters. Since event_payload is parsed from the upstream server's response (which is untrusted external data), receiving malformed UTF-8 could cause an uncaught exception and crash the Envoy process. To prevent this, use json::error_handler_t::replace to safely replace invalid UTF-8 sequences with the replacement character.

Suggested change
std::string serialized_item = content_item.dump();
std::string serialized_item = content_item.dump(-1, ' ', false, json::error_handler_t::replace);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Waiting reviewer's opinion.

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.

Can we use JsonEscaper::escapeString?

std::vector<std::string> SseResponseExtractor::processChunk(absl::string_view chunk,
bool end_stream) {
std::vector<std::string> event_payloads;
buffer_.append(chunk.data(), chunk.size());
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.

medium

The buffer_ member variable accumulates incoming chunks of the SSE stream until a complete event (terminated by a double newline) is found. If a misbehaving or malicious upstream server sends an infinite stream of data without any double newlines, buffer_ will grow indefinitely, potentially leading to memory exhaustion (OOM) and a Denial of Service (DoS). Consider enforcing a maximum limit on the size of buffer_ and closing the connection or returning an error if the limit is exceeded.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Waiting reviewer's opinion.

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.

I think it is better to apply the max_response_body_size limitation.

std::vector<std::string> SseResponseExtractor::processChunk(absl::string_view chunk,
bool end_stream) {
std::vector<std::string> event_payloads;
buffer_.append(chunk.data(), chunk.size());
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.

I think it is better to apply the max_response_body_size limitation.


while (!buffer_view.empty()) {
// Safely handles chunk boundaries and all line-ending formats
auto result = Http::Sse::SseParser::findEventEnd(buffer_view, end_stream);
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.

minor nit: Since this type is not very obvious, consider avoid using auto here.

json content_item;
content_item[McpConstants::TYPE_FIELD] = McpConstants::TEXT_FIELD;
content_item[McpConstants::TEXT_FIELD] = event_payload;
std::string serialized_item = content_item.dump();
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.

Can we use JsonEscaper::escapeString?

// When enabled, the response body is streamed directly to the client without buffering. Each
// chunk is JSON escaped as it arrives and wrapped with a pre-built JSON-RPC prefix and suffix.
//
// For Server-Sent Events (SSE) responses (i.e. ``text/event-stream`` content type), Envoy parses
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.

Are we going to support SSE case when text_content_streaming_enabled: False in the follow-up change?


// Processes an incoming chunk of an SSE stream and returns any completed event payloads.
// Set end_stream to true if this is the final chunk of the response.
std::vector<std::string> processChunk(absl::string_view chunk, bool end_stream = false);
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.

nit: Default it to false doesn't seem to be needed since the current logic is passing end_stream from encodeData

std::string escaped_chunk = JsonEscaper::escapeString(chunk, JsonEscaper::extraSpace(chunk));

std::string output_to_add;
if (is_sse_response_) {
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.

nit/optional: consider wrapping this logic into a utility function since encodeData has contained more logic and long.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants