Skip to content

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

Closed
zhoug9127 wants to merge 11 commits into
mainfrom
feat/mcp-streaming-internal-tool-hiding
Closed

zhoug9127 wants to merge 11 commits into
mainfrom
feat/mcp-streaming-internal-tool-hiding

Conversation

@zhoug9127

@zhoug9127 zhoug9127 commented Apr 23, 2026 •

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

Summary by CodeRabbit

  • Bug Fixes
    • Internal non‑builtin tools and related metadata are consistently redacted from both assembled responses and streaming output.
  • New Features
    • Builtin-routed tool results remain visible while internal items are hidden.
    • User-defined functions now take precedence over intercepted tool calls to avoid accidental exposure.
    • Streaming omits hidden tool events and avoids emitting empty or misleading intermediate events.
  • Documentation
    • Clarified runtime visibility rules for internal vs builtin-routed tools.

… 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

Warning

Rate limit exceeded

@zhoug9127 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 58 minutes and 21 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 58 minutes and 21 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 80643441-7963-4d14-b7a7-a35153a2c6ff

📥 Commits

Reviewing files that changed from the base of the PR and between 5f30737 and 0dec1dc.

📒 Files selected for processing (9)
  • crates/mcp/src/core/session.rs
  • model_gateway/src/routers/common/mcp_utils.rs
  • model_gateway/src/routers/grpc/common/responses/streaming.rs
  • model_gateway/src/routers/grpc/common/responses/utils.rs
  • model_gateway/src/routers/grpc/harmony/responses/non_streaming.rs
  • model_gateway/src/routers/grpc/harmony/responses/streaming.rs
  • model_gateway/src/routers/openai/mcp/tool_handler.rs
  • model_gateway/src/routers/openai/mcp/tool_loop.rs
  • model_gateway/src/routers/openai/responses/streaming.rs
📝 Walkthrough

Walkthrough

Centralizes MCP visibility and interception: adds McpToolSession predicates, threads user-function name context through routers/handlers, applies session-driven visibility filtering across streaming and non‑streaming paths, refactors mcp_list_tools emission/deduplication, and introduces per-output visibility state in OpenAI MCP streaming.

Changes

Cohort / File(s) Summary
Documentation
crates/mcp/src/core/config.rs, docs/reference/mcp-internal-servers.md
Clarify internal: true semantics: internal non‑builtin metadata/tool-trace items are redacted from assembled and streaming output; builtin‑routed tool results remain visible.
Session-level Helpers
crates/mcp/src/core/session.rs
Add should_intercept_function_call and should_emit_streaming_mcp_list_tools to centralize interception and streaming-list visibility decisions.
gRPC common responses (exports)
model_gateway/src/routers/grpc/common/responses/mod.rs
Expose additional visibility helpers (emit_visible_mcp_list_tools_sequence, retain_client_visible_*) via re-exports.
gRPC common responses (streaming/utils)
model_gateway/src/routers/grpc/common/responses/streaming.rs, .../utils.rs
Add is_client_visible_streaming_output_item and retain/filter helpers for output/tools; implement session-aware emit of visible mcp_list_tools and tool_choice normalization/validation.
gRPC Harmony & Regular routers
model_gateway/src/routers/grpc/harmony/.../responses/{streaming,non_streaming}.rs, .../streaming.rs, model_gateway/src/routers/grpc/regular/.../responses/{streaming,non_streaming}.rs
Switch interception to session.policy (should_intercept_function_call), thread user_function_names, apply visibility retention, consolidate mcp_list_tools emission, and gate streaming events per-tool based on visibility probes.
OpenAI streaming context & routing
model_gateway/src/routers/openai/context.rs, .../responses/history.rs, .../responses/streaming.rs, .../responses/non_streaming.rs
Add user_function_names to streaming context; replace list-tools label extraction with dedupe-key extraction; thread user_function_names through streaming processors and implement mixed-mode reconciliation / hidden-event suppression.
OpenAI MCP module & tool-loop
model_gateway/src/routers/openai/mcp/mod.rs, .../tool_loop.rs, .../tool_handler.rs
Introduce dedupe-key generation/extraction, thread dedupe keys through ToolLoopState/context, add visibility-aware output-index assignment and deferred mapping, prune intercepted placeholders from mixed/final responses, and update signatures to accept user_function_names.
Common util
model_gateway/src/routers/common/mcp_utils.rs
collect_user_function_names now includes names in Namespace tools as well as top-level Function tools.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Router as Router (gRPC/OpenAI)
    participant Session as McpToolSession
    participant Handler as Tool Handler
    participant Emitter as Event Emitter

    Client->>Router: Send request with tool calls
    Router->>Router: collect_user_function_names
    Router->>Session: should_intercept_function_call(tool_name, user_function_names)?
    alt intercept (MCP)
        Session-->>Router: true
        Router->>Session: should_hide_output_item_json?(item)
        alt hidden
            Session-->>Router: true
            Router->>Emitter: skip emission / redact
        else visible
            Session-->>Router: false
            Router->>Handler: resolve visibility, allocate indices
            Handler->>Emitter: emit output_item_added / progress events
            Emitter->>Client: stream events
        end
    else not intercepted (user function)
        Session-->>Router: false
        Router->>Handler: route to user function
    end

    Router->>Session: should_emit_streaming_mcp_list_tools?(server_label)
    alt emit
        Session-->>Router: true
        Router->>Emitter: emit_visible_mcp_list_tools_sequence
        Emitter->>Client: mcp_list_tools events
    else skip
        Session-->>Router: false
    end

    Router->>Router: apply retain_client_visible_* to final response
    Router->>Client: send filtered final response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

Possibly related PRs

Suggested labels

tests

Suggested reviewers

  • CatherineSue
  • key4ng
  • slin1237

Poem

🐇 I nudge the session’s hidden trail,
Quietly strip the secret trail.
Streams now sing what users may see,
Internal crumbs tucked silently.
Hooray — the rabbit hops, content and free. 🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main changes: fixing a mixed-mode streaming final response gap and hardening visibility logic in MCP handling.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-streaming-internal-tool-hiding

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.

@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 implements comprehensive visibility filtering and redaction for MCP (Model Context Protocol) tools across both gRPC and OpenAI-compatible endpoints, covering streaming and non-streaming paths. It introduces logic to distinguish between internal MCP tools, which are hidden from clients, and user-defined or builtin tools. Key changes include the addition of visibility tracking in the StreamingToolHandler, utility functions for filtering response outputs, and logic to handle mixed-mode turns where both MCP and user-defined tools are present. The review feedback identifies critical edge cases where executed MCP results could be lost or duplicated in final responses, specifically when reaching tool limits or transitioning between MCP and user-driven iterations.

Comment thread model_gateway/src/routers/openai/mcp/tool_loop.rs
Comment thread model_gateway/src/routers/grpc/regular/responses/non_streaming.rs
Comment thread model_gateway/src/routers/openai/mcp/tool_loop.rs

@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: 8e1386d5b4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/openai/mcp/tool_loop.rs
@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
Comment thread model_gateway/src/routers/openai/mcp/tool_loop.rs Outdated

@claude claude 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.

Thorough review of all 17 changed files across the MCP visibility/interception overhaul. The changes are consistent across all four response paths (OpenAI streaming/non-streaming, gRPC regular, gRPC harmony) and the new should_intercept_function_call / should_emit_streaming_mcp_list_tools / OutputVisibilityState abstractions are well-designed.

Summary: 0 🔴 Important, 1 🟡 Nit, 0 🟣 Pre-existing

Key things verified:

  • Mixed MCP + user-function partitioning logic is correct across all paths (the old has_exposed_tool → new should_intercept_function_call gives user-defined functions proper precedence on name collisions)
  • Streaming visibility probe items are constructed with the fields should_hide_output_item_json actually inspects
  • mcp_list_tools dedup key change from label-only to label+tools is backwards-compatible with stored conversation chains (extraction runs against stored output items at load time)
  • restore_original_tools now receives the session for proper redaction in passthrough/mixed mode
  • OutputVisibilityState state machine correctly prevents hidden→visible transitions and logs a warning on the reverse
  • Test coverage for the new visibility state machine, mixed-mode reconciliation, and hidden-event dropping

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
model_gateway/src/routers/grpc/harmony/responses/non_streaming.rs (1)

291-315: ⚠️ Potential issue | 🟠 Major

Filter response.output before returning mixed MCP/function results.

This mixed path builds executed MCP calls as FunctionToolCall output items, then only filters response.tools. Internal MCP call names/results can still leak through response.output; the max-tool path already applies the missing output filter.

🛡️ Proposed fix
                     if mcp_tracking.total_calls() > 0 {
                         inject_mcp_metadata(
                             &mut response,
                             &mcp_tracking,
                             &session,
                             &user_function_names,
                         );
                     }
+                    retain_client_visible_output_items(
+                        &mut response.output,
+                        &session,
+                        &user_function_names,
+                    );
                     retain_client_visible_response_tools(
                         &mut response,
                         &session,
                         &user_function_names,
                     );

Based on learnings, mixed function+MCP paths can place internal MCP FunctionToolCall items directly in response.output, so pre-existing output items must be filtered before returning client-visible responses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/grpc/harmony/responses/non_streaming.rs` around
lines 291 - 315, The mixed MCP+function result path must also filter
response.output to avoid leaking internal MCP FunctionToolCall items: after
building the response (after build_tool_response and after inject_mcp_metadata
if present) iterate and retain only client-visible output items, removing any
FunctionToolCall entries whose tool name is not in user_function_names or is an
internal MCP tool (use the same predicate/visibility logic as
retain_client_visible_response_tools); implement this by applying that predicate
to response.output (e.g., response.output.retain(|item| is_client_visible(item,
&session, &user_function_names))) before returning the response.
model_gateway/src/routers/grpc/regular/responses/non_streaming.rs (1)

275-302: ⚠️ Potential issue | 🔴 Critical

Don’t skip MCP output reconciliation on the user-function-only exit.

If a prior iteration executed MCP calls and this iteration returns only user function calls, this early return drops state.mcp_call_items from the final response and persisted output. Inject the accumulated MCP items before returning.

🐛 Proposed fix
                 })?;
+                if state.total_calls > 0 {
+                    let tool_items = std::mem::take(&mut state.mcp_call_items);
+                    session.inject_client_visible_mcp_output_items(
+                        &mut responses_response.output,
+                        tool_items,
+                        &user_function_names,
+                    );
+                }
                 retain_client_visible_response_tools(
                     &mut responses_response,
                     &session,

Based on learnings, inject_client_visible_mcp_output_items must apply the visibility policy to both injected MCP items and pre-existing output in mixed function+MCP early-exit paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/grpc/regular/responses/non_streaming.rs` around
lines 275 - 302, Early-return when mcp_tool_calls.is_empty() drops previously
accumulated MCP items (state.mcp_call_items); before returning the converted
ResponsesResponse from conversions::chat_to_responses and after calling
retain_client_visible_response_tools, call
inject_client_visible_mcp_output_items to inject and reconcile
state.mcp_call_items into the responses_response (ensuring the visibility policy
is applied to both injected MCP items and any pre-existing output items) so the
final returned/persisted response includes MCP outputs from prior iterations.
model_gateway/src/routers/grpc/regular/responses/streaming.rs (1)

591-610: ⚠️ Potential issue | 🔴 Critical

Move tool-call visibility before streaming chunk emission.

convert_and_accumulate_stream emits chat tool-call chunks through emitter.process_chunk before this partition runs, so hidden MCP calls can already leak as function_call events, and user function calls can be emitted again later. The converter needs the session/user-function context and must buffer or suppress tool-call deltas until visibility is known.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/grpc/regular/responses/streaming.rs` around lines
591 - 610, The converter currently emits tool-call chunks inside
convert_and_accumulate_stream (via emitter.process_chunk) before visibility is
determined, causing MCP/function calls to leak; change the flow so tool-call
deltas are buffered and not emitted until visibility is known: have
convert_and_accumulate_stream either (A) accept session and user_function_names
and suppress/buffer any tool-call deltas (do not call emitter.process_chunk for
those) while accumulating, or (B) split into a first pass that accumulates the
raw streamed events (without emitting) then call
extract_all_tool_calls_from_chat and session.should_intercept_function_call to
partition into mcp_tool_calls vs function_tool_calls, and only afterwards emit
non-tool chunks and the appropriate visible tool-call events via
emitter.process_chunk and tx. Ensure references: convert_and_accumulate_stream,
extract_all_tool_calls_from_chat, session.should_intercept_function_call, and
emitter.process_chunk are updated so tool-call emission is conditional on the
partition result.
model_gateway/src/routers/openai/mcp/tool_loop.rs (1)

1105-1153: ⚠️ Potential issue | 🟠 Major

Drop original MCP function_call items in incomplete responses.

This path prefixes incomplete/executed MCP items but leaves the original intercepted MCP function_call placeholders in output_array. That can duplicate visible MCP calls and leak hidden/internal placeholders when max_tool_calls is exceeded.

Proposed fix
     // Convert only MCP-intercepted function_call items in output to mcp_call format.
     if let Some(output_array) = obj.get_mut("output").and_then(|v| v.as_array_mut()) {
         // Find any function_call items and convert them to mcp_call (incomplete)
         let mut incomplete_items = Vec::new();
-        for item in output_array.iter() {
+        let mut retained_items = Vec::with_capacity(output_array.len());
+        for item in output_array.drain(..) {
             let item_type = item.get("type").and_then(|t| t.as_str());
             if item_type.is_some_and(is_function_call_type) {
                 let tool_name = item.get("name").and_then(|v| v.as_str()).unwrap_or("");
                 if !session.should_intercept_function_call(tool_name, &user_function_names) {
                     // Non-intercepted calls (user tools, unknown names, collisions) must remain
                     // function_call output items.
-                    continue;
+                    retained_items.push(item);
+                    continue;
                 }
                 let args = item
                     .get("arguments")
                     .and_then(|v| v.as_str())
                     .unwrap_or("{}");
@@
                 );
                 incomplete_items.push(mcp_call_item);
+                continue;
             }
+
+            retained_items.push(item);
         }
 
         // Add mcp_list_tools and executed mcp_call items at the beginning
         if state.total_calls > 0 || !incomplete_items.is_empty() {
             let mut prefix = visible_mcp_list_tools_items(session, &list_tools_bindings);
@@
             prefix.extend(
                 incomplete_items.into_iter().filter(|item| {
                     !session.should_hide_output_item_json(item, &user_function_names)
                 }),
             );
-            output_array.splice(0..0, prefix);
+            prefix.extend(retained_items);
+            *output_array = prefix;
+        } else {
+            *output_array = retained_items;
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/openai/mcp/tool_loop.rs` around lines 1105 - 1153,
The current code collects incomplete MCP mcp_call items but leaves the original
intercepted `function_call` entries in `output_array`, causing duplicates/leaks;
change the logic in the block that iterates `output_array` so that when you
detect an intercepted function call (using `is_function_call_type` +
`session.should_intercept_function_call`) you record that entry for removal
(e.g., collect its index or mark it) instead of only pushing a new
`mcp_call_item` into `incomplete_items`, then after building `prefix` remove or
replace those original entries from `output_array` (or reconstruct
`output_array` without them) before splicing in `prefix`; keep the same helpers
(`build_mcp_call_item`, `visible_mcp_list_tools_items`, `state.mcp_call_items`,
`session.should_hide_output_item_json`) so hidden/internal placeholders are not
left behind.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/reference/mcp-internal-servers.md`:
- Around line 19-23: The doc incorrectly states that `internal: true` applies
only to servers declared under `servers:`; update the wording to clarify that
the session's internal-server set also includes statically registered internal
servers (e.g., those in `static_servers`) so streaming and final redaction
covers both declared (`config.servers`) and static registrations; reference
`McpOrchestrator::internal_server_names()` behavior when explaining that
internal server lists are derived from both `static_servers` and
`config.servers`.

In `@model_gateway/src/routers/openai/mcp/tool_loop.rs`:
- Around line 786-795: retained_output_items currently only hides items via
session.should_hide_output_item_json but still returns MCP-intercepted
function_call placeholders; update retained_output_items to also filter out any
output objects that represent function calls intended for MCP interception by
excluding items where item is an object with "type" == "function_call" and the
function "name" is an MCP placeholder (e.g. starts_with "mcp_" or is in a
session-tracked intercepted set). Use the existing session reference (e.g. call
a new helper like session.is_intercepted_mcp_function(name) if needed) alongside
session.should_hide_output_item_json and user_function_names so the function
returns only visible user/non-MCP calls.

---

Outside diff comments:
In `@model_gateway/src/routers/grpc/harmony/responses/non_streaming.rs`:
- Around line 291-315: The mixed MCP+function result path must also filter
response.output to avoid leaking internal MCP FunctionToolCall items: after
building the response (after build_tool_response and after inject_mcp_metadata
if present) iterate and retain only client-visible output items, removing any
FunctionToolCall entries whose tool name is not in user_function_names or is an
internal MCP tool (use the same predicate/visibility logic as
retain_client_visible_response_tools); implement this by applying that predicate
to response.output (e.g., response.output.retain(|item| is_client_visible(item,
&session, &user_function_names))) before returning the response.

In `@model_gateway/src/routers/grpc/regular/responses/non_streaming.rs`:
- Around line 275-302: Early-return when mcp_tool_calls.is_empty() drops
previously accumulated MCP items (state.mcp_call_items); before returning the
converted ResponsesResponse from conversions::chat_to_responses and after
calling retain_client_visible_response_tools, call
inject_client_visible_mcp_output_items to inject and reconcile
state.mcp_call_items into the responses_response (ensuring the visibility policy
is applied to both injected MCP items and any pre-existing output items) so the
final returned/persisted response includes MCP outputs from prior iterations.

In `@model_gateway/src/routers/grpc/regular/responses/streaming.rs`:
- Around line 591-610: The converter currently emits tool-call chunks inside
convert_and_accumulate_stream (via emitter.process_chunk) before visibility is
determined, causing MCP/function calls to leak; change the flow so tool-call
deltas are buffered and not emitted until visibility is known: have
convert_and_accumulate_stream either (A) accept session and user_function_names
and suppress/buffer any tool-call deltas (do not call emitter.process_chunk for
those) while accumulating, or (B) split into a first pass that accumulates the
raw streamed events (without emitting) then call
extract_all_tool_calls_from_chat and session.should_intercept_function_call to
partition into mcp_tool_calls vs function_tool_calls, and only afterwards emit
non-tool chunks and the appropriate visible tool-call events via
emitter.process_chunk and tx. Ensure references: convert_and_accumulate_stream,
extract_all_tool_calls_from_chat, session.should_intercept_function_call, and
emitter.process_chunk are updated so tool-call emission is conditional on the
partition result.

In `@model_gateway/src/routers/openai/mcp/tool_loop.rs`:
- Around line 1105-1153: The current code collects incomplete MCP mcp_call items
but leaves the original intercepted `function_call` entries in `output_array`,
causing duplicates/leaks; change the logic in the block that iterates
`output_array` so that when you detect an intercepted function call (using
`is_function_call_type` + `session.should_intercept_function_call`) you record
that entry for removal (e.g., collect its index or mark it) instead of only
pushing a new `mcp_call_item` into `incomplete_items`, then after building
`prefix` remove or replace those original entries from `output_array` (or
reconstruct `output_array` without them) before splicing in `prefix`; keep the
same helpers (`build_mcp_call_item`, `visible_mcp_list_tools_items`,
`state.mcp_call_items`, `session.should_hide_output_item_json`) so
hidden/internal placeholders are not left behind.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 909d32c3-50fe-466b-9cc6-833a6490c92b

📥 Commits

Reviewing files that changed from the base of the PR and between e8f8473 and 8e1386d.

📒 Files selected for processing (17)
  • crates/mcp/src/core/config.rs
  • crates/mcp/src/core/session.rs
  • docs/reference/mcp-internal-servers.md
  • model_gateway/src/routers/grpc/common/responses/mod.rs
  • model_gateway/src/routers/grpc/common/responses/streaming.rs
  • model_gateway/src/routers/grpc/common/responses/utils.rs
  • model_gateway/src/routers/grpc/harmony/responses/non_streaming.rs
  • model_gateway/src/routers/grpc/harmony/responses/streaming.rs
  • model_gateway/src/routers/grpc/harmony/streaming.rs
  • model_gateway/src/routers/grpc/regular/responses/non_streaming.rs
  • model_gateway/src/routers/grpc/regular/responses/streaming.rs
  • model_gateway/src/routers/openai/context.rs
  • model_gateway/src/routers/openai/mcp/mod.rs
  • model_gateway/src/routers/openai/mcp/tool_handler.rs
  • model_gateway/src/routers/openai/mcp/tool_loop.rs
  • model_gateway/src/routers/openai/responses/history.rs
  • model_gateway/src/routers/openai/responses/streaming.rs

Comment thread docs/reference/mcp-internal-servers.md Outdated
Comment thread model_gateway/src/routers/openai/mcp/tool_loop.rs
Signed-off-by: Daisy Zhou <zhoug9127@gmail.com>
@mergify

mergify Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Hi @zhoug9127, this PR has merge conflicts that must be resolved before it can be merged. Please rebase your branch:

git fetch origin main
git rebase origin/main
# resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts that need to be resolved label Apr 23, 2026
Comment thread model_gateway/src/routers/openai/mcp/tool_loop.rs
Comment thread model_gateway/src/routers/openai/mcp/tool_loop.rs Outdated

@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: 24fb73d626

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/openai/responses/streaming.rs
Signed-off-by: Daisy Zhou <zhoug9127@gmail.com>
@mergify mergify Bot removed the needs-rebase PR has merge conflicts that need to be resolved label Apr 23, 2026
Comment thread model_gateway/src/routers/openai/responses/streaming.rs
Comment thread model_gateway/src/routers/openai/mcp/tool_loop.rs
@mergify mergify Bot added needs-rebase PR has merge conflicts that need to be resolved and removed needs-rebase PR has merge conflicts that need to be resolved labels Apr 24, 2026

@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: 1c685e10a2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/openai/responses/streaming.rs Outdated
@zhoug9127
zhoug9127 force-pushed the feat/mcp-streaming-internal-tool-hiding branch from 1c685e1 to 87ff9b5 Compare April 24, 2026 03:50
Comment thread model_gateway/src/routers/openai/responses/streaming.rs
Comment thread model_gateway/src/routers/grpc/regular/responses/non_streaming.rs

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_gateway/src/routers/grpc/common/responses/utils.rs`:
- Around line 399-410: emit_visible_mcp_list_tools_sequence currently iterates
session.mcp_servers() and may emit duplicate mcp_list_tools for bindings sharing
the same binding.label; change the function to deduplicate by binding.label
(e.g., track seen labels in a HashSet) so that for each unique label you call
session.list_tools_for_server(binding.server_key) and
emitter.emit_mcp_list_tools_sequence(&binding.label, &tools_for_server, tx) only
once, still honoring
session.should_emit_streaming_mcp_list_tools(&binding.label) before emitting;
keep all referenced symbols (emit_visible_mcp_list_tools_sequence,
session.mcp_servers, session.should_emit_streaming_mcp_list_tools,
session.list_tools_for_server, emitter.emit_mcp_list_tools_sequence,
binding.label, binding.server_key) unchanged.

In `@model_gateway/src/routers/grpc/harmony/responses/non_streaming.rs`:
- Around line 322-326: The branch that calls
retain_client_visible_response_tools(&mut response, &session,
&user_function_names) does not filter already-populated response.output and
therefore leaks FunctionToolCall entries added by build_tool_response; update
the same visibility filtering to also remove or replace hidden/internal
FunctionToolCall items from response.output (not just response.tools) — locate
the build_tool_response usage and ensure the logic in
retain_client_visible_response_tools (or immediately after it) applies the
visibility predicate to response.output entries (matching on FunctionToolCall)
so internal tool names/outputs are dropped or redacted for the given session and
user_function_names.

In `@model_gateway/src/routers/openai/mcp/tool_handler.rs`:
- Around line 211-227: process_event currently calls ensure_output_index()
directly when handling response.output_item.done which bypasses visibility-aware
logic; change that branch to call
resolve_output_index_for_forwarding(upstream_index) (or check
visibility_state(upstream_index) before calling ensure_output_index()) so hidden
items do not consume a mapped index. Update the response.output_item.done
handling in process_event to use resolve_output_index_for_forwarding (or guard
with OutputVisibilityState::Visible) and preserve the existing behavior of
find_call_mut and assigned_output_index mutation when a mapping is returned.

In `@model_gateway/src/routers/openai/responses/streaming.rs`:
- Around line 1584-1637: The test currently doesn't exercise
reconcile_mixed_mode_final_response's retain-by-call_id branch because the
placeholder items lack a "call_id" and state.conversation_history is empty;
update the test to add a matching "call_id" on the MCP placeholder item (e.g.,
"call_id": "cid1") and populate state.conversation_history with a function_call
entry whose call_id is the same "cid1" so the retain closure has to check and
remove the executed placeholder; keep the rest of the setup (user_function_names
and assertions) the same so the final assertions still verify that the executed
MCP entry is removed and the user function_call remains.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 823728a6-851d-4232-b509-d428129781f5

📥 Commits

Reviewing files that changed from the base of the PR and between c91f932 and 87ff9b5.

📒 Files selected for processing (11)
  • model_gateway/src/routers/grpc/common/responses/utils.rs
  • model_gateway/src/routers/grpc/harmony/responses/non_streaming.rs
  • model_gateway/src/routers/grpc/harmony/responses/streaming.rs
  • model_gateway/src/routers/grpc/harmony/streaming.rs
  • model_gateway/src/routers/grpc/regular/responses/non_streaming.rs
  • model_gateway/src/routers/grpc/regular/responses/streaming.rs
  • model_gateway/src/routers/openai/mcp/mod.rs
  • model_gateway/src/routers/openai/mcp/tool_handler.rs
  • model_gateway/src/routers/openai/mcp/tool_loop.rs
  • model_gateway/src/routers/openai/responses/non_streaming.rs
  • model_gateway/src/routers/openai/responses/streaming.rs

Comment thread model_gateway/src/routers/grpc/common/responses/utils.rs
Comment thread model_gateway/src/routers/grpc/harmony/responses/non_streaming.rs
Comment thread model_gateway/src/routers/openai/mcp/tool_handler.rs
Comment thread model_gateway/src/routers/openai/responses/streaming.rs

@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: 87ff9b5cf6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/openai/mcp/tool_handler.rs
Comment thread model_gateway/src/routers/openai/mcp/tool_loop.rs
@zhoug9127
zhoug9127 force-pushed the feat/mcp-streaming-internal-tool-hiding branch 2 times, most recently from ad9fcad to 1e14be5 Compare April 24, 2026 04:11
Comment thread model_gateway/src/routers/openai/responses/streaming.rs
Comment thread model_gateway/src/routers/grpc/harmony/responses/non_streaming.rs Outdated
Comment thread model_gateway/src/routers/grpc/harmony/responses/non_streaming.rs

@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: 1e14be5c4b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/openai/responses/streaming.rs
Comment thread model_gateway/src/routers/grpc/common/responses/utils.rs Outdated
…onciliation

Signed-off-by: Daisy Zhou <zhoug9127@gmail.com>
@zhoug9127
zhoug9127 force-pushed the feat/mcp-streaming-internal-tool-hiding branch from 1e14be5 to 5f30737 Compare April 24, 2026 06:07

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
model_gateway/src/routers/openai/mcp/tool_handler.rs (1)

280-284: ⚠️ Potential issue | 🟠 Major

output_item.done still commits hidden items to a visible index.

handle_streaming_with_tool_interception() calls process_event() before the hide/redaction gate in model_gateway/src/routers/openai/responses/streaming.rs, so this resolve_output_index_for_forwarding() call still allocates and marks Unknown -> Visible for a hidden DONE-only item. The event is then dropped later, but next_index has already advanced, so the next visible item can start at 1/2/... again.

Please defer the Unknown -> Visible transition out of this branch until the forwarding path has decided the item is client-visible, or only reuse an existing mapping here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/src/routers/openai/mcp/tool_handler.rs` around lines 280 - 284,
OutputItemEvent::DONE currently calls resolve_output_index_for_forwarding which
creates a new Unknown->Visible mapping even for items that will later be hidden;
inside handle_streaming_with_tool_interception (which calls process_event before
the hide/redaction gate) this advances next_index incorrectly. Change the DONE
branch in tool_handler.rs to avoid creating a new mapping: either call a
non-creating lookup (e.g., get_existing_output_index_for_forwarding) or check if
a mapping exists before calling resolve_output_index_for_forwarding, and only
perform the Unknown->Visible transition when the forwarding path decides the
item is client-visible (defer the transition until after forward/visibility
decision). Ensure you reference OutputItemEvent::DONE and
resolve_output_index_for_forwarding when locating the code to modify.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_gateway/src/routers/grpc/common/responses/utils.rs`:
- Around line 267-324: has_custom_tool currently only checks top-level
ResponseTool::Custom, missing Custom tools nested inside
ResponseTool::Namespace; update has_custom_tool(tools: &[ResponseTool],
custom_name: &str) to also traverse ResponseTool::Namespace variants
(recursively or by iterating the namespace's inner tools) and return true if any
nested ResponseTool::Custom has the matching name (leave callers like
is_tool_choice_compatible and has_allowed_tool_reference unchanged so
ResponsesToolChoice::Custom and AllowedTools see namespaced customs).

---

Duplicate comments:
In `@model_gateway/src/routers/openai/mcp/tool_handler.rs`:
- Around line 280-284: OutputItemEvent::DONE currently calls
resolve_output_index_for_forwarding which creates a new Unknown->Visible mapping
even for items that will later be hidden; inside
handle_streaming_with_tool_interception (which calls process_event before the
hide/redaction gate) this advances next_index incorrectly. Change the DONE
branch in tool_handler.rs to avoid creating a new mapping: either call a
non-creating lookup (e.g., get_existing_output_index_for_forwarding) or check if
a mapping exists before calling resolve_output_index_for_forwarding, and only
perform the Unknown->Visible transition when the forwarding path decides the
item is client-visible (defer the transition until after forward/visibility
decision). Ensure you reference OutputItemEvent::DONE and
resolve_output_index_for_forwarding when locating the code to modify.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 191e939c-79aa-4605-b717-f9bf85d07b66

📥 Commits

Reviewing files that changed from the base of the PR and between 87ff9b5 and 5f30737.

📒 Files selected for processing (12)
  • model_gateway/src/routers/common/mcp_utils.rs
  • model_gateway/src/routers/grpc/common/responses/utils.rs
  • model_gateway/src/routers/grpc/harmony/responses/non_streaming.rs
  • model_gateway/src/routers/grpc/harmony/responses/streaming.rs
  • model_gateway/src/routers/grpc/harmony/streaming.rs
  • model_gateway/src/routers/grpc/regular/responses/non_streaming.rs
  • model_gateway/src/routers/grpc/regular/responses/streaming.rs
  • model_gateway/src/routers/openai/mcp/mod.rs
  • model_gateway/src/routers/openai/mcp/tool_handler.rs
  • model_gateway/src/routers/openai/mcp/tool_loop.rs
  • model_gateway/src/routers/openai/responses/non_streaming.rs
  • model_gateway/src/routers/openai/responses/streaming.rs

Comment thread model_gateway/src/routers/grpc/common/responses/utils.rs

@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: 5f30737939

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/openai/responses/streaming.rs Outdated
Comment thread model_gateway/src/routers/openai/responses/streaming.rs Outdated

@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: e00e7c45a0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread model_gateway/src/routers/openai/responses/streaming.rs Outdated
Signed-off-by: Daisy Zhou <zhoug9127@gmail.com>

@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: 3f3c990ecf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +463 to +467
let classification_session = ctx.session.or(redaction_session);

let is_mcp_call_arguments_event = matches!(
resolved_event_type,
FunctionCallEvent::ARGUMENTS_DELTA | FunctionCallEvent::ARGUMENTS_DONE

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 Keep passthrough argument events in function_call form

When mixed-mode streaming switches to passthrough (ctx.session is None), this still falls back to redaction_session for MCP argument classification, so later client-owned MCP calls are treated as intercepted. In that state, forward_streaming_event drops response.function_call_arguments.delta and remaps *.done to mcp_call_arguments.*, while the corresponding output items remain function_call; this can remove arguments the client needs and produce inconsistent event types/IDs for the same call. Classification for argument remapping should stay tied to active interception, and use redaction-only logic only for hide/drop decisions.

Useful? React with 👍 / 👎.

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

@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: 0dec1dc95e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +417 to +420
let mut seen_labels = HashSet::new();
for binding in session.mcp_servers() {
if !seen_labels.insert(binding.label.clone()) {
continue;

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 mcp_list_tools per server binding, not per label

The new seen_labels gate drops later bindings that reuse an earlier server_label, so streaming now emits only the first server’s mcp_list_tools block for that label. McpToolSession allows multiple bindings to share a label (for example different server_key entries), and those tools can still be exposed/called, so this dedupe makes the streamed metadata incomplete and can desync clients that rely on mcp_list_tools to discover callable tools. Deduping should use a binding-level key (e.g. label+server_key/tools) rather than label alone.

Useful? React with 👍 / 👎.

@zhoug9127 zhoug9127 closed this Apr 27, 2026
@lightseek-bot
lightseek-bot deleted the feat/mcp-streaming-internal-tool-hiding branch April 30, 2026 05:07
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