From 47a029adcf7e4464692f3dd314d3e8fe00bec056 Mon Sep 17 00:00:00 2001 From: Brett Chien Date: Tue, 18 Aug 2026 22:09:21 +0800 Subject: [PATCH] feat(remote): log the reverse-MCP initialize handshake + protocol versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #74/#75 revealed the agent DOES connect to the oab server but never calls tools/list. The stall is between connect and list — the inner MCP initialize handshake, which #75 deliberately left unlogged. Log it, since MCP requires the client to initialize right after connecting (it's not the agent's choice). - initialize: log the client's requested `protocolVersion` vs the version the server answered; flag a **PROTOCOL MISMATCH** (warn) when they differ — the suspected reason a connected agent aborts before tools/list. - notifications/initialized: log it (a notification, so it never reached the outcome match). Its presence means the handshake completed; its absence right after an initialize points at a rejected handshake. Logging only — no behaviour change. If the log confirms a version mismatch, the fix (echo the client's protocolVersion instead of the hardcoded 2024-11-05, + advertise tools.listChanged) is a follow-up. No local src-tauri build (dep tree OOMs this box, as with #74/#75); CI build-test covers the compile. --- src-tauri/src/remote.rs | 48 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/remote.rs b/src-tauri/src/remote.rs index 7f4eeb0..a973578 100644 --- a/src-tauri/src/remote.rs +++ b/src-tauri/src/remote.rs @@ -863,6 +863,29 @@ async fn run_once( } else { String::new() }; + // The client's requested MCP protocol version (initialize only), + // captured before `params` moves so we can compare it to what we + // answer — a mismatch is the suspected reason a connected agent + // never reaches tools/list. + let client_proto = if method == "initialize" { + params + .get("protocolVersion") + .and_then(Value::as_str) + .unwrap_or("?") + .to_string() + } else { + String::new() + }; + // `notifications/initialized` is a notification (no reply), so it + // never reaches the match below — log it here. Seeing it means the + // agent accepted our initialize and completed the handshake; its + // absence right after an initialize points at a rejected handshake. + if method == "notifications/initialized" { + let _ = app.emit( + "app-log", + json!({ "level": "info", "msg": format!("remote: {agent} reverse-MCP — client sent initialized (handshake complete)") }), + ); + } if let Some(reply) = handle_inner(&client, id, &method, params).await { // Observability: surface each reverse-MCP call the agent made // against the oab server, and its outcome. `tools/list` is the @@ -913,9 +936,28 @@ async fn run_once( )) } } - // The reverse-MCP inner handshake — Connect already proves the - // agent engaged, so don't add noise for it. - "initialize" => ("", String::new()), + // The reverse-MCP inner handshake. Log the negotiated + // protocol versions so a mismatch (the suspected cause of a + // connected agent never listing tools) is visible. + "initialize" => { + let server_proto = reply + .get("result") + .and_then(|r| r.get("protocolVersion")) + .and_then(Value::as_str) + .unwrap_or("?"); + if client_proto != "?" + && server_proto != "?" + && client_proto != server_proto + { + ("warn", format!( + "remote: {agent} reverse-MCP initialize — PROTOCOL MISMATCH: client requested {client_proto}, server answered {server_proto}" + )) + } else { + ("info", format!( + "remote: {agent} reverse-MCP initialize — client requested {client_proto}, server answered {server_proto}" + )) + } + } other => ("warn", format!( "remote: {agent} reverse-MCP: unsupported method {other} (replied -32601)" )),