From 1177701d8b6d9dfc2c96b35608d22ce3d3e745fc Mon Sep 17 00:00:00 2001 From: Brett Chien Date: Mon, 17 Aug 2026 22:15:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(remote):=20honest=20reverse-MCP=20logging?= =?UTF-8?q?=20=E2=80=94=20declared=20vs.=20consumed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The management connection logged "oab tools published" the moment Studio got a sessionId back from session/new — i.e. when Studio *declared* the `oab` server, not when the agent consumed it. So an operator saw "published" while the agent reported no oab tools, with nothing in the Activity log to explain the gap. The tools only appear once the agent (via the gateway) connects back to the declared server over the reverse-MCP tunnel and lists them. Make that observable and fix the wording: - Declaration log: "oab tools published/republished" → "oab server (re-)declared (awaiting agent connect)". Declared ≠ consumed. - Inbound::Connect: log when the agent opens the reverse-MCP tunnel to the oab server — the proof the declaration was consumed. - Inbound::Message: log the reverse-MCP call; for `tools/list`, log the served tool count — the definitive "the agent pulled N oab tools" signal. Now the Activity log distinguishes the two failure modes: no Connect / no tools/list after "declared" ⇒ the gateway/agent runtime isn't tunnelling the reverse direction (upstream); a tools/list served with 0 (or an error) ⇒ a Studio-side issue. Behaviour is unchanged — logging only. No local src-tauri build (its dep tree OOMs this box, as with #69); CI build-test covers the compile. Change is additive logging using patterns already in the file. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- src-tauri/src/remote.rs | 46 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/remote.rs b/src-tauri/src/remote.rs index f0b475e..fa3f5d1 100644 --- a/src-tauri/src/remote.rs +++ b/src-tauri/src/remote.rs @@ -767,13 +767,18 @@ async fn run_once( st.status = "connected".to_string(); } emit_status(app, agent, "connected"); - // Only the management binding publishes `oab` tools; an - // agent console runs chat-only (least privilege). + // Only the management binding declares the `oab` server; an + // agent console runs chat-only (least privilege). Wording: + // this is Studio *declaring* the server on session/new — the + // tools appear only once the agent connects back to it over + // the reverse-MCP tunnel (see the Inbound::Connect / + // tools/list logs below). "declared", not "published", so the + // log doesn't read as "the agent has them". let tools = if management { if resume_attempted { - " — oab tools republished" + " — oab server re-declared (awaiting agent connect)" } else { - " — oab tools published" + " — oab server declared (awaiting agent connect)" } } else { "" @@ -815,6 +820,15 @@ async fn run_once( // or a streamed chat chunk. match acp::parse_inbound(&frame) { Inbound::Connect { id, .. } => { + // The agent (via the gateway) opened the reverse-MCP tunnel to + // Studio's declared `oab` server — the proof the declaration was + // consumed. If this never logs after "oab server declared", the + // gateway/agent runtime isn't tunnelling the reverse direction + // (upstream), which is why the agent sees no oab tools. + let _ = app.emit( + "app-log", + json!({ "level": "info", "msg": format!("remote: {agent} reverse-MCP — agent connected to the oab server") }), + ); if let Err(e) = send(&mut write, &acp::connect_reply(id, conn_id)).await { outcome = Err(e); break 'conn; @@ -829,8 +843,32 @@ async fn run_once( // its own id, so out-of-order completion is fine for MCP. let client = client.clone(); let reply_tx = reply_tx.clone(); + let app = app.clone(); + let agent = agent.to_string(); tauri::async_runtime::spawn(async move { if let Some(reply) = handle_inner(&client, id, &method, params).await { + // Observability: surface the reverse-MCP call the agent made + // against the oab server. `tools/list` is the definitive + // "the agent pulled N oab tools" signal — its absence (with + // no Connect either) means the declaration was never consumed + // upstream, not that Studio failed to serve. + if method == "tools/list" { + let n = reply + .get("result") + .and_then(|r| r.get("tools")) + .and_then(Value::as_array) + .map(|a| a.len()); + let msg = match n { + Some(n) => format!("remote: {agent} reverse-MCP tools/list — served {n} oab tool(s)"), + None => format!("remote: {agent} reverse-MCP tools/list — served (unexpected shape)"), + }; + let _ = app.emit("app-log", json!({ "level": "info", "msg": msg })); + } else if method == "tools/call" { + let _ = app.emit( + "app-log", + json!({ "level": "info", "msg": format!("remote: {agent} reverse-MCP tools/call") }), + ); + } let _ = reply_tx.send(reply); } });