MCP tool calls show no in-flight feedback
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/progresshas 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, andapp-server-test-clienthas 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:
- The workspace's serde_json
arbitrary_precisionfeature breaks rmcp 1.7's notification parsing.ServerNotificationis#[serde(untagged)], and the buffered-number representation makesProgressNotificationParam.progress_token(aNumberOrString) fail to deserialize. As a result, every incomingnotifications/progresssilently falls through toCustomNotificationand never reacheson_progress. Probably warrants an upstream report on modelcontextprotocol/rust-sdk; in the meantime it needs a re-parse shim on the client service. - 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
progressincrease 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/progressnotifications arrive beforeitem/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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗