Skip to content

fix(mcp): reconcile mixed streaming final response and harden visibility logic - #1360

Closed
zhoug9127 wants to merge 2 commits into
smg-project:mainfrom
zhoug9127:feat/mcp-streaming-internal-tool-hiding
Closed

zhoug9127 wants to merge 2 commits into
smg-project:mainfrom
zhoug9127:feat/mcp-streaming-internal-tool-hiding

Conversation

@zhoug9127

Copy link
Copy Markdown
Collaborator

Description

Problem

Streaming MCP interception had a mixed-batch gap: when a streamed turn contained both user function calls and MCP-intercepted calls, final response.completed/persisted output could omit already-executed MCP output items. That creates client state mismatch and replay risk.

In addition, output visibility/index behavior relied on subtle ordering assumptions with limited local guardrails/tests.

Solution

  • Reconcile mixed-mode final response snapshots by removing intercepted MCP function_call placeholders and injecting executed MCP output items before response.completed emission and persistence.
  • Reuse the same reconciliation in stored final response handling for mixed-mode streaming.
  • Add explicit guardrail comments around resolve_output_index_for_forwarding side effects and interception vs redaction context usage.
  • Add warning path when mark_output_hidden is called after an index is already visible.
  • Add targeted tests for:
    • output visibility state transitions,
    • hidden output_item.done handling,
    • mixed-mode final-response reconciliation.
  • Add probe-shape invariant comments in gRPC regular/harmony streaming visibility checks.

Changes

  • model_gateway/src/routers/openai/responses/streaming.rs
    • Added mixed-mode final-response reconciliation helper.
    • Applied reconciliation in send_final_response_event and persisted final response path when interception is disabled but MCP calls executed.
    • Added comments clarifying side-effect ordering and mixed-call passthrough behavior.
    • Added new streaming tests for hidden DONE event and mixed-mode reconciliation.
  • model_gateway/src/routers/openai/mcp/tool_handler.rs
    • Added warning behavior for hide-after-visible attempts.
    • Added visibility transition tests.
  • model_gateway/src/routers/openai/mcp/tool_loop.rs
    • Exported helper used by streaming reconciliation.
  • model_gateway/src/routers/openai/mcp/mod.rs
    • Re-exported helper for streaming module usage.
  • model_gateway/src/routers/grpc/regular/responses/streaming.rs
    • Added probe-shape invariant comment.
  • model_gateway/src/routers/grpc/harmony/streaming.rs
    • Added probe-shape invariant comment.

Test Plan

Reproducible verification run:

  1. cargo test -p smg --lib routers::openai::mcp::tool_handler::tests::
  2. cargo test -p smg --lib routers::openai::responses::streaming::tests::
  3. cargo test -p smg --lib routers::openai::mcp::tool_loop::tests::
  4. cargo test -p smg --test api_tests responses
  5. cargo test -p smg --test api_tests mcp
  6. pre-commit run --all-files

Expected/observed: all commands pass.

Checklist
  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes
  • (Optional) Documentation updated
  • (Optional) Please join us on Slack #sig-smg to discuss, review, and merge PRs

… policy

Signed-off-by: Daisy Zhou <zhoug9127@gmail.com>
…ity logic

Signed-off-by: Daisy Zhou <zhoug9127@gmail.com>
@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d32621d6-3796-4d05-a8aa-972962a4ee4a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zhoug9127 zhoug9127 closed this Apr 23, 2026
@zhoug9127

Copy link
Copy Markdown
Collaborator Author

Superseded by #1361 (same changes opened from upstream branch instead of fork head). Closing this one to avoid duplicate review threads.

@zhoug9127
zhoug9127 deleted the feat/mcp-streaming-internal-tool-hiding branch April 23, 2026 17:22
@github-actions github-actions Bot added documentation Improvements or additions to documentation grpc gRPC client and router changes mcp MCP related changes model-gateway Model gateway crate changes openai OpenAI router changes labels Apr 23, 2026

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request updates the MCP tool interception and redaction logic to support streaming responses and mixed-mode tool execution (where both MCP and user-defined function tools are present). It introduces refined visibility checks for streaming output items and ensures that internal MCP tools are correctly redacted across both non-streaming and streaming paths. My feedback highlights a 'fail-open' security concern in the redaction logic, suggests an optimization for cloning in the dedupe key generation, and points out an overly restrictive filter in the streaming redaction logic that could lead to information leakage.

Comment on lines +185 to +190
output.retain(|item| {
let Ok(json) = to_value(item) else {
return true;
};
!session.should_hide_output_item_json(&json, user_function_names)
});

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 redaction logic here is 'fail-open'. If to_value(item) fails, the item is retained and potentially leaked to the client. For security and privacy features like redaction, it is safer to 'fail-closed' by returning false on error. Additionally, as per repository guidelines, failures in serialization should be logged as warnings to aid in debugging rather than failing silently.

    output.retain(|item| {
        match to_value(item) {
            Ok(json) => !session.should_hide_output_item_json(&json, user_function_names),
            Err(e) => {
                tracing::warn!(error = ?e, "Failed to serialize item for redaction check; hiding item");
                false
            }
        }
    });
References
  1. Instead of silently ignoring potential failures (e.g., from serialization), log them as warnings to aid in debugging. In Rust, prefer using unwrap_or_else to log an error over unwrap_or_default which would fail silently.

}

let server_label = item.get("server_label").and_then(|value| value.as_str())?;
let tools = item.get("tools").cloned().unwrap_or_else(|| json!([]));

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

Avoid unnecessary cloning of the tools array. Since mcp_list_tools_dedupe_key takes a reference to Value, you can pass the reference directly.

Suggested change
let tools = item.get("tools").cloned().unwrap_or_else(|| json!([]));
let tools = item.get("tools").unwrap_or(&Value::Null);

Comment on lines +340 to +347
Some(OutputItemEvent::ADDED) | Some(OutputItemEvent::DONE) => parsed_data
.get("item")
.filter(|item| {
item.get("type")
.and_then(|v| v.as_str())
.is_some_and(is_function_call_type)
})
.is_some_and(|item| session.should_hide_output_item_json(item, user_function_names)),

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 filter is_function_call_type is too restrictive. should_hide_output_item_json is designed to handle multiple item types. By restricting this check to function calls, other internal output items present in the final response (e.g., if an internal mcp_list_tools item is received from an upstream gateway) might be leaked to the client. Ensure redaction logic targets all relevant variants present in the output.

        Some(OutputItemEvent::ADDED) | Some(OutputItemEvent::DONE) => parsed_data
            .get("item")
            .is_some_and(|item| session.should_hide_output_item_json(item, user_function_names)),
References
  1. When redacting items from a response, ensure the redaction logic targets only the item variants that are actually present in the final response output. Intermediate item types that are not part of the final assembled response do not need redaction.

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

Labels

documentation Improvements or additions to documentation grpc gRPC client and router changes mcp MCP related changes model-gateway Model gateway crate changes openai OpenAI router changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant