Skip to content

APP-2527 Phase 2: MCP data pipeline#12828

Open
cephalonaut wants to merge 3 commits into
matthew/app-2527-phase1from
matthew/app-2527-phase2
Open

APP-2527 Phase 2: MCP data pipeline#12828
cephalonaut wants to merge 3 commits into
matthew/app-2527-phase1from
matthew/app-2527-phase2

Conversation

@cephalonaut

Copy link
Copy Markdown
Contributor

What

Thread the structured serde_json::Value MCP tool call request through to RequestedCommandView and normalize CallMCPToolResult into a renderable form. No visible UI change — the old Text render path stays active.

  • McpRequest struct: holds name: String and args: serde_json::Value for tree rendering
  • New fields on RequestedCommandView: mcp_request: Option<McpRequest> and mcp_tree_state: JsonTreeState (path-keyed expansion state for both request and response trees)
  • New action variants: ToggleJsonNode { path } and ToggleJsonString { path } on RequestedCommandViewAction, handled by delegating to mcp_tree_state
  • McpRenderable enum and mcp_result_to_renderable(): normalizes CallMCPToolResult — prefers structured_content, falls back to parsing text content as JSON, then wraps plain text as a JSON String value; Error and Cancelled map directly
  • update_mcp_request(): sets mcp_request on the view from stream updates
  • block.rs: extends handle_mcp_tool_stream_update to accept and propagate the coerced display_input and name, calling update_mcp_request on the view

Why

Phase 2 of APP-2527. Establishes the data pipeline so Phase 3 can wire render_json_tree into the MCP tool call detail body without any additional data-layer work.

Agent Mode

  • This PR was created by Warp Agent Mode

Testing

Added 5 unit tests for mcp_result_to_renderable in json_tree_tests.rs:

  • Success with structured_contentTree(value)
  • Success with JSON text content → Tree(parsed_value)
  • Success with non-JSON text → Tree(String(text))
  • ErrorError(msg)
  • CancelledCancelled

All 31 tests in the json_tree_tests suite pass. No existing tests regressed.


Conversation: https://staging.warp.dev/conversation/ebf3f6a3-c6f6-4fff-9aca-5e4582527167
Run: https://oz.staging.warp.dev/runs/019ede47-c45b-78a5-ac6e-7fd91b3c6404
This PR was generated with Oz.

@cla-bot cla-bot Bot added the cla-signed label Jun 19, 2026

@cephalonaut cephalonaut left a comment

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.

Phase 2 Review — APP-2527

Verdict: MINOR — all blocking Phase 2 requirements are met; four minor items must be resolved before Phase 3 launches.


✅ Requirements satisfied

  • McpRequest { name: String, args: serde_json::Value } struct defined and used.
  • RequestedCommandView carries mcp_request: Option<McpRequest> and mcp_tree_state: JsonTreeState.
  • ToggleJsonNode { path: Vec<PathSegment> } and ToggleJsonString { path: Vec<PathSegment> } action variants are present and fully handled in handle_action with the correct toggle / toggle_string + ctx.notify() calls.
  • JsonTreeState and PathSegment are imported from Phase 1 (crate::ui_components::json_tree), not redefined.
  • handle_mcp_tool_stream_update signature extended correctly; the command_text string is built identically to before.
  • mcp_result_to_renderable logic is correct across all five cases: structured_content → JSON text → String fallback → Error → Cancelled.
  • Five unit tests for mcp_result_to_renderable are present and meaningful.
  • The old should_render_mcp_content / content_text render path is untouched.
  • Code comments are free of spec/process references and explain the "why" locally.

🔶 Minor items (must fix before Phase 3)

M1 — Carry-over from Phase 1: file-level doc comment in json_tree_tests.rs

app/src/ui_components/json_tree_tests.rs line 1:

//! Pure-logic unit tests for the `json_tree` component (Phase 1, APP-2527).

Remove the phase/ticket reference. Reword to something like:

//! Pure-logic unit tests for the `json_tree` component.

M2 — Carry-over from Phase 1: no tests for toggle_string / is_string_expanded

JsonTreeState::toggle_string and is_string_expanded are exercised in the Phase 2 action handler but have no unit tests. Add at least one test each to json_tree_tests.rs (e.g., verify collapsed-by-default, toggling to true, toggling back to false).

M3 — Depth calculation in ToggleJsonNode will be wrong if Phase 3 uses synthetic namespace segments

RequestedCommandViewAction::ToggleJsonNode { path } => {
    let depth = path.len();         // ← inferred from path length
    self.mcp_tree_state.toggle(path.clone(), depth);

The spec says mcp_tree_state uses a synthetic prefix segment (PathSegment::Key("__request__") / PathSegment::Key("__response__")) to namespace request and response paths. If Phase 3 follows that scheme, every path will include this extra leading segment, making path.len() equal to render_depth + 1. That shifts the root node from depth 0 (expanded by default) to depth 1 (collapsed by default), breaking the initial-open UX.

Fix before Phase 3 by one of:

  • Add depth: usize to the ToggleJsonNode action and have the Phase 3 toggle callback supply the render-time depth directly.
  • Or commit to Phase 3 using separate JsonTreeState instances for request and response (no namespace segments), in which case path.len() == render_depth is guaranteed and no action change is needed.

Either approach is valid; the spec should be updated to record the decision.

M4 — Unnecessary .clone() in both action handlers

path is moved out of the enum variant by destructuring, so the clones below are redundant:

// ToggleJsonNode
let depth = path.len();
self.mcp_tree_state.toggle(path.clone(), depth);  // .clone() not needed

// ToggleJsonString
self.mcp_tree_state.toggle_string(path.clone());  // .clone() not needed

Change to pass path directly (the .len() call does not consume it):

let depth = path.len();
self.mcp_tree_state.toggle(path, depth);
self.mcp_tree_state.toggle_string(path);

Clippy (-D warnings) will catch this — it must be clean before the Phase 3 PR is opened.

@cephalonaut cephalonaut left a comment

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.

Phase 2 Re-Review — APP-2527

Verdict: MINOR — M1, M2, M3 fully resolved. M4 still open (redundant clone was relocated, not removed).


✅ M1 — Fixed

File-level doc comment in json_tree_tests.rs now reads:

//! Pure-logic unit tests for the `json_tree` component.

No phase/ticket reference remains.


✅ M2 — Fixed

Three well-designed tests added (string_collapsed_by_default, toggle_string_expands_then_collapses, toggle_string_is_independent_of_node_expansion). Cover the default state, round-trip toggle, and independence from node-expansion state.


✅ M3 — Fixed

Split into mcp_request_tree_state + mcp_response_tree_state. McpTree { Request, Response } enum correctly added. Both action variants carry tree: McpTree and dispatch to the correct state. depth = path.len() is now accurate because no synthetic namespace segments are in the paths. The PartialEq + Eq derives were correctly removed from RequestedCommandViewAction (the Action trait only requires Any + Debug + Send + Sync; no application code compares actions with ==).


🔶 M4 — Still open

The clone was moved from the call site to a let rebinding, but not eliminated:

// ToggleJsonNode handler
RequestedCommandViewAction::ToggleJsonNode { path, tree } => {
    let depth = path.len();
    let path = path.clone();     // ← still a redundant clone
    match tree {
        McpTree::Request => self.mcp_request_tree_state.toggle(path, depth),
        McpTree::Response => self.mcp_response_tree_state.toggle(path, depth),
    }
    ctx.notify();
}

// ToggleJsonString handler
RequestedCommandViewAction::ToggleJsonString { path, tree } => {
    let path = path.clone();     // ← still a redundant clone
    match tree {
        McpTree::Request => self.mcp_request_tree_state.toggle_string(path),
        McpTree::Response => self.mcp_response_tree_state.toggle_string(path),
    }
    ctx.notify();
}

The original path is not used after the clone is made, so clippy::redundant_clone still fires. Because Rust's borrow checker allows the same variable to appear in multiple match arms (only one arm executes, so there is no double-move), path can be moved directly without cloning:

// ToggleJsonNode — fixed
RequestedCommandViewAction::ToggleJsonNode { path, tree } => {
    let depth = path.len();
    match tree {
        McpTree::Request => self.mcp_request_tree_state.toggle(path, depth),
        McpTree::Response => self.mcp_response_tree_state.toggle(path, depth),
    }
    ctx.notify();
}

// ToggleJsonString — fixed
RequestedCommandViewAction::ToggleJsonString { path, tree } => {
    match tree {
        McpTree::Request => self.mcp_request_tree_state.toggle_string(path),
        McpTree::Response => self.mcp_response_tree_state.toggle_string(path),
    }
    ctx.notify();
}

Presubmit runs cargo clippy --workspace --all-targets --all-features --tests -- -D warnings, so this will fail CI before Phase 3 merges.

@cephalonaut cephalonaut left a comment

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.

Phase 2 Final Review — APP-2527

Verdict: PASS — all four minors are resolved; no new issues introduced by the API change.


✅ M4 — Fixed (API changed to &[PathSegment])

toggle and toggle_string now accept &[PathSegment], allocating with .to_vec() internally only at write time. The action handlers pass the destructured path: Vec<PathSegment> directly via auto-deref — no clone, no redundant allocation:

RequestedCommandViewAction::ToggleJsonNode { path, tree } => {
    let depth = path.len();
    match tree {
        McpTree::Request => self.mcp_request_tree_state.toggle(path, depth),
        McpTree::Response => self.mcp_response_tree_state.toggle(path, depth),
    }
    ctx.notify();
}
RequestedCommandViewAction::ToggleJsonString { path, tree } => {
    match tree {
        McpTree::Request => self.mcp_request_tree_state.toggle_string(path),
        McpTree::Response => self.mcp_response_tree_state.toggle_string(path),
    }
    ctx.notify();
}

All existing test call sites in json_tree_tests.rs were updated from path.clone() to &path. No other call sites for JsonTreeState::toggle / toggle_string exist in the codebase (other .toggle() calls in the repo are on unrelated types). The API change is complete and consistent.


All minors resolved

Item Status
M1 — doc comment (Phase 1, APP-2527) ✅ Fixed
M2 — toggle_string / is_string_expanded tests ✅ Fixed
M3 — separate request/response states, McpTree enum ✅ Fixed
M4 — redundant .clone() ✅ Fixed

Phase 2 is clean. Ready to merge and proceed to Phase 3.

@cephalonaut cephalonaut marked this pull request as ready for review June 30, 2026 14:30
@oz-for-oss

oz-for-oss Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

@cephalonaut

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss oz-for-oss 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.

Overview

This PR threads structured MCP request data into the requested-command view and adds normalization helpers/tests for future JSON tree rendering of MCP results. The attached spec context reports that no approved or repository spec context was found, so there is no material spec drift to validate.

Concerns

  • The new Phase 3-only data-layer items are not referenced by non-test code in this diff, while Warp CI runs Clippy with -D warnings; this is likely to fail before the future rendering work lands.
  • The new MCP result normalizer silently drops non-text MCP content, which can make image/resource-only tool results render as an empty string once Phase 3 consumes it.

Verdict

Found: 0 critical, 1 important, 1 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

/// Prefers `structured_content` when present; otherwise tries to parse joined
/// text content as JSON; falls back to wrapping the raw text as a JSON
/// string value so the tree renderer always receives a `serde_json::Value`.
pub(crate) fn mcp_result_to_renderable(result: &CallMCPToolResult) -> McpRenderable {

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.

⚠️ [IMPORTANT] This normalizer is only called by tests in this diff, and the new MCP request storage is likewise only written until Phase 3; CI runs Clippy with -D warnings, so non-test builds are likely to fail on dead_code. Wire these items into the render path in this PR or add a narrowly scoped allow(dead_code) with a removal TODO.

let text = result
.content
.iter()
.filter_map(|c| {

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.

💡 [SUGGESTION] Non-text MCP content is silently discarded here; tools can return images/resources, so a content-only non-text result would become an empty JSON string when this normalizer is used. Preserve unsupported content explicitly or return a renderable fallback instead of dropping it.

@cephalonaut

Copy link
Copy Markdown
Contributor Author

Response to oz-for-oss review:

  • Dead code / Clippy warnings — Phase 3 (PR APP-2527 Phase 3: MCP JSON tree rendering #12829) is stacked directly on this branch and introduces callers for all Phase 2 types. The dead-code state exists only transiently between merges, not in the final merged history. No Clippy suppression annotations needed.
  • Non-text MCP content silently dropped — the prior renderer called serde_json::to_string_pretty(&result) on the whole CallToolResult struct, which would have rendered image/resource content as raw JSON fields (e.g. "type": "image", "data": "<base64>"). The new renderer is no worse: non-text content resolves to an empty string, which renders as a single elided string row. Both the old and new renderers make non-text content non-meaningful to read; this is a follow-up if binary/image MCP results become common.

/// Structured representation of an MCP tool call request, held so the
/// detail view can render it as a JSON tree rather than a flat string.
pub struct McpRequest {
pub name: String,

@peicodes peicodes Jul 1, 2026

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.

Why does request need a name but response doesn't? What is the name, is that the name of the MCP server + name of the tool call?

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.

Good catch — the name field (the tool call name, not the server name) was included for potential future use but turned out to be unused by the tree renderer. The name is already available via command_text for the collapsed header title and doesn't need to be duplicated in McpRequest. Removed in the latest commit.

@cephalonaut cephalonaut force-pushed the matthew/app-2527-phase1 branch from dc10cf6 to 66d3a57 Compare July 1, 2026 03:09
@cephalonaut cephalonaut force-pushed the matthew/app-2527-phase2 branch from 76dffa8 to 2007fbe Compare July 1, 2026 03:09
cephalonaut and others added 3 commits July 1, 2026 00:47
Thread structured serde_json::Value request through to RequestedCommandView
and normalize CallMCPToolResult into a renderable form.

- Add McpRequest struct to hold the tool name and args for tree rendering
- Add mcp_request: Option<McpRequest> and mcp_tree_state: JsonTreeState fields
  to RequestedCommandView
- Add ToggleJsonNode and ToggleJsonString action variants; handle them in
  handle_action by delegating to mcp_tree_state
- Add McpRenderable enum and mcp_result_to_renderable() helper: prefers
  structured_content, falls back to parsing text as JSON, then wraps
  plain text as a JSON String value
- Add update_mcp_request() method on RequestedCommandView
- Extend handle_mcp_tool_stream_update() in block.rs to accept and propagate
  the coerced display_input and name, calling update_mcp_request on the view
- Add unit tests for mcp_result_to_renderable in json_tree_tests.rs

No user-visible change; the old Text render path stays active.

Co-Authored-By: Oz <oz-agent@warp.dev>
M1: Remove phase/spec reference from json_tree_tests.rs doc comment
M2: Add unit tests for toggle_string and is_string_expanded
M3: Split mcp_tree_state into mcp_request_tree_state and
    mcp_response_tree_state; add McpTree enum (Request|Response);
    add tree: McpTree field to ToggleJsonNode/ToggleJsonString so
    handle_action dispatches to the correct state — fixes depth-0
    default expansion for both trees
M4: Eliminate extra .clone() calls in handlers by cloning path once
    then moving it into the match arm

Co-Authored-By: Oz <oz-agent@warp.dev>
Change toggle() and toggle_string() to accept &[PathSegment] instead
of Vec<PathSegment>. The HashMap internally allocates the vec via
to_vec(), but call sites no longer clone. Action handlers now pass
path (a &Vec<PathSegment>) directly — it auto-coerces to &[PathSegment].
Update all test call sites accordingly.

Co-Authored-By: Oz <oz-agent@warp.dev>
@cephalonaut cephalonaut force-pushed the matthew/app-2527-phase2 branch from 2007fbe to d63aab6 Compare July 1, 2026 04:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants