MCP tool calls show no in-flight feedback

Open 💬 1 comment Opened Jun 13, 2026 by wycats

TL;DR item/mcpToolCall/progress exists but was never wired up. There was a previous attempt in #16942 to wire it up, but it went stale and no longer applies cleanly. I have a branch for illustration purposes that finishes that work up. I'm happy to see this go forward in any way that works for you folks.

Problem

While an MCP tool call runs, every Codex surface shows a static invocation line: the tool name, with arguments visible inline (TUI) or behind an expansion (app). For quick calls that's fine; it breaks down for tools that do many different things or run for a while.

My motivating case: my MCP server exposes one CLI-shaped tool that dispatches many subcommands. That's a deliberate pattern that keeps the model's tool surface small instead of paying schema cost for dozens of tools. In Codex, every one of those calls renders identically. When I built the same tool natively for VS Code, the extension API let each invocation describe itself as it ran, and the difference in transcript legibility is substantial.

MCP has a sanctioned channel for exactly this: notifications/progress carries a per-call, human-readable message, gated on the client sending a progressToken, which rmcp already auto-injects into every outgoing request. So compliant servers can send progress today; Codex just drops it on receipt (the rmcp client handler logs and discards each notification).

The pieces mostly exist already

  • McpToolCallProgressNotification / item/mcpToolCall/progress has been in the app-server v2 protocol since #6212 (Nov 2025) and is exported to the TS/Python SDKs, but nothing emits it. The TUI ignores it in an empty match arm, and app-server-test-client has a print handler for it that has never fired.
  • #16942 (@jn-openai) built most of the missing pipeline back in April: rmcp progress dispatch, a core event, and the app-server mapping. It went stale before review and was auto-closed by the bot.

Two bugs that would have blocked #16942 from working

While porting that approach onto current main (the codebase has moved since April: session split, TurnItem lifecycle, ElicitationClientService), I found two issues that would have prevented the original pipeline from functioning at all:

  1. The workspace's serde_json arbitrary_precision feature breaks rmcp 1.7's notification parsing. ServerNotification is #[serde(untagged)], and the buffered-number representation makes ProgressNotificationParam.progress_token (a NumberOrString) fail to deserialize. As a result, every incoming notifications/progress silently falls through to CustomNotification and never reaches on_progress. Probably warrants an upstream report on modelcontextprotocol/rust-sdk; in the meantime it needs a re-parse shim on the client service.
  2. rmcp delivers each notification on its own spawned task, so closely spaced notifications can reach the handler out of order. An integration test observed step 3, 2, 1. The spec's requirement that progress increase monotonically gives a clean way to drop stale ones client-side.

Working branch

I have a branch that completes the #16942 pipeline against current main and adds the part it didn't get to: rendering the message in the TUI (a dim status line under the in-flight call cell, cleared on completion):

• Calling search.find_docs({"query":"ratatui styling","limit":3})
  └ Indexing styles.md (2/3)

Branch: https://github.com/wycats/codex/tree/wycats/mcp-tool-call-progress (diff vs main)

Since item/mcpToolCall/progress is emitted at the app-server layer, any app-server client can render it. The TUI change in the branch is one consumer of it.

Coverage on the branch:

  • rmcp-client integration test (stdio): forwards messages in order; same-progress updates pass when the text changes; no-callback path unaffected
  • app-server integration test: model-driven call over streamable HTTP, asserting ordered item/mcpToolCall/progress notifications arrive before item/completed
  • unit tests for message formatting/sanitization (trim, control-char strip, 256-char cap, "2 / 5" numeric fallback)
  • insta snapshot for the in-flight TUI cell

Forwarding is debounced (100ms, latest-wins, flushed before the response completes) per the spec's rate-limiting guidance, and progress events are excluded from rollout persistence like other transient deltas. Schema fixtures regenerate with no diff; the protocol surface already existed.

I'd be glad to open the branch as a PR if invited. Mostly I'd like to see the capability land in whatever form is easiest to review.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗