From 68f36bff84b757461bc65ad3acd24f3b0d0c4de5 Mon Sep 17 00:00:00 2001 From: brettchien Date: Fri, 14 Aug 2026 10:38:19 +0800 Subject: [PATCH 1/3] docs(adr): Studio as a reverse MCP-over-ACP client (Part B connection model) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Realizes the fleet ADR's Part B path (i) and resolves its Β§5 open questions. Topology: Studio = ACP-WS client + MCP server (dials out, NAT-friendly), Orca = in-pod agent/MCP-client consuming Studio's oab-mcp tools via the OAB MCP Facade under Studio's identity. Reuses openab's as-built reverse MCP-over-ACP (#1447) + wire contract; Studio codes the client side. Decisions: D1 single `oab` server with fleet-parameterized tools (not per-fleet); D2 writes granted by the authed /acp connection (D-29 alignment, no per-call consent); D3 no server-initiated inbound. Names the one genuine openab-side dependency β€” session attachment (land Studio's server on Orca's Discord-driven session) + endpoint reachability/bearer β€” distinct from auth, which is settled. Slices 1-2 (fleet-aware tools, in-process handler) need no openab dep. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) --- docs/adr/reverse-mcp-client.md | 264 +++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 docs/adr/reverse-mcp-client.md diff --git a/docs/adr/reverse-mcp-client.md b/docs/adr/reverse-mcp-client.md new file mode 100644 index 0000000..e52f40e --- /dev/null +++ b/docs/adr/reverse-mcp-client.md @@ -0,0 +1,264 @@ +# ADR: Studio as a reverse MCP-over-ACP client (the Part B connection model) + +- **Status:** Proposed +- **Date:** 2026-08-14 +- **Author:** Orca (`ecs-claude`) +- **Builds on:** [Fleet grouping + Studio connection model](./fleet-grouping-and-connection-model.md) + β€” this ADR realizes its **Part B, path (i)** and resolves its Β§5 open questions + Q1 (write permission) and Q2 (fleet ↔ session mapping). +- **Upstream (accepted + as-built in `openabdev/openab`):** + [Reverse MCP-over-ACP over WebSocket](https://github.com/openabdev/openab/blob/main/docs/adr/acp-server-websocket-reverse-mcp.md) + (#1447) and its client wire contract + [MCP-over-ACP tunnel contract](https://github.com/openabdev/openab/blob/main/docs/mcp-over-acp-tunnel-contract.md). + +--- + +## 1. Context + +The fleet ADR (Β§3) named two first-class ways an agent reaches the fleet: +(i) **reverse MCP** β€” attach to a *running* Studio; (ii) **headless standalone +`oab-mcp`** β€” no Studio. Path (ii) needs no new Studio code (provision the stdio +binary, done). Path (i) is unbuilt: today Studio spawns `oab-mcp` as a **stdio +subprocess** (`rmcp`, `transport-io`) on a private pipe β€” nothing external can +attach. + +The driving consumer is concrete: **Orca**, the always-on ECS agent, should drive +Brett's fleet **through Brett's running Studio** β€” using **Studio's** identity and +credentials, with every fleet operation visible in Studio's UI β€” instead of +carrying its own AWS credentials (which re-introduces the silent +credential-fallback class of bug the fleet work exists to kill). + +The upstream mechanism that makes this possible is **accepted and as-built**: any +ACP WebSocket client may declare one or more `type:acp` MCP servers, and OpenAB +core proxies their tools to the in-pod agent. This ADR decides **what Studio +implements as that client**, and names the **one** thing that is genuinely an +OpenAB-side dependency. + +## 2. Roles & topology + +Reverse MCP inverts the usual "who listens" intuition: the party that **cannot be +dialled** (behind NAT, no inbound port) is the one that **serves** tools, over the +outbound WebSocket it opened. + +| Party | Role | Where | +|---|---|---| +| **Studio** (Brett's laptop) | **ACP WS client + MCP server** β€” dials out, serves `oab-mcp` tools over the socket it holds | laptop (NAT'd) | +| **OpenAB core / gateway** | ACP server + **MCP proxy/aggregator** (the OAB MCP Facade) | hosts Orca's session | +| **Orca** | **the agent = MCP client** β€” discovers & calls Studio's tools via the facade meta-tools, executing under **Studio's** identity | ECS Fargate | + +- **Studio dials out.** It solves NAT: Studio opens the `/acp` WSS; nothing + connects *to* Studio. The fleet is reachable only while Studio runs (accepted + cost of path (i); path (ii) covers the no-Studio case). +- **"Orca attaches to a running Studio"** (fleet ADR Β§3(i)) is the *logical* + view β€” the agent gains Studio's tools. The *physical* dial is + **Studio β†’ gateway**. +- **Studio's identity executes.** `oab-mcp` runs in Studio with Studio's + `fleets.toml` / AWS credentials (the fleet work of Part A). Orca calls the tools; + Studio performs the AWS actions. No AWS credentials are provisioned on Orca. + +``` + Orca (agent, ECS) ──in-pod──▢ OpenAB core (OAB MCP Facade) + β–² + β”‚ /acp WSS (outbound from Studio) + β”‚ MCP-over-ACP, multiplexed with ACP chat + β”‚ + Studio (laptop) = MCP server: oab-mcp tools + β”‚ + β–Ό Studio's fleets.toml + AWS creds + AWS (ECS: the fleet) +``` + +## 3. What Studio reuses from OpenAB (do not rebuild) + +The upstream side is settled; Studio codes against it, it does not re-invent it: + +- **Gateway `/acp` WS + `AcpTunnelRegistry`**, keyed `(channel_id, serverId)`. + Reverse MCP-over-ACP is **generic** (#1447): a client declares **N** `type:acp` + servers on `session/new` (re-declared on `session/resume`), each with `{id, + name}`. +- **The OAB MCP Facade + `AcpTunnelSource`** exposes those servers to the agent + through the two meta-tools `search_capabilities` / `execute_capability`. + Discovery is **pull-based, gateway-initiated**, cached per `(channel_id, name)`. +- **Trust = the `/acp` transport auth alone** (D-29 removed the per-tool + allowlist). Admission over the authed transport *is* the grant; a connected + server publishes every tool it declares. +- **The wire contract is fully specified** β€” `initialize` handshake, `mcp/connect` + β†’ `connectionId`, `mcp/message` framing (flattened inner MCP, correlate by the + **outer** ACP frame id β€” there is **no** inner MCP id), `mcp/disconnect`, + `mcp/cancel`, and the limits table. + +Studio's job is to be a correct **client** of that contract β€” the role the +browser extension plays in the upstream example, with `oab-mcp` in place of the +DOM tools. + +## 4. Decision β€” Studio's reverse-MCP client + +### 4.1 An ACP-WS client component (in `src-tauri`) + +A background task (Tauri side, process-lifetime, decoupled from any UI) that: + +- **Dials** the gateway `GET /acp` WSS. Bearer auth rides the WebSocket + sub-protocol offer `openab.bearer., acp.v1`; the server echoes `acp.v1`. + All frames are JSON-RPC 2.0. +- **Drives the session:** `initialize` (read back `agentCapabilities`; the gateway + advertises reverse-MCP support as the `_meta` key `dev.openab/acp: true`), then + `session/new` declaring the server (Β§4.4). On reconnect, `session/resume` + **re-declaring the whole set** β€” a resume withdraws any `type:acp` server it does + not re-present. +- **Reconnects** with backoff; the `serverId` (`id`) is a fresh UUID **per + connection** while the `name` is stable (the registry keys on `id`, routing + resolves by `name`; upstream Β§6.1). New WS deps for Studio: + `tokio-tungstenite` + `rustls`. + +### 4.2 Tunnel dispatch β€” answer the gateway's MCP requests + +The tunnel is **gateway-initiated**: the gateway asks, Studio answers. Studio +handles, on each `mcp/connect` `connectionId`: + +- `mcp/connect { acpId }` β†’ reply a fresh `connectionId`. +- `mcp/message { connectionId, method, params }` (**request**, has outer `id`): + dispatch the inner MCP method to the in-process `oab-mcp` handler and reply the + inner MCP **result** as the ACP response `result`; an inner MCP error β†’ the outer + JSON-RPC `error`. Inner methods handled as an MCP **server**: `initialize` β†’ + `InitializeResult`; `notifications/initialized` (notification, no reply) β†’ + forward; `tools/list` β†’ the `oab-mcp` tools; `tools/call` β†’ execute, return an + MCP `CallToolResult`. +- `mcp/disconnect { connectionId }` β†’ release, reply `{}`. +- `mcp/cancel { requestId }` (notification; `requestId` = the **outer** frame id) β†’ + abort that in-flight call. + +Correlation is **always** by the outer ACP frame id. **Limits** (from the +contract) Studio must respect: + +| Limit | Value | +|---|---| +| Tunnel request timeout | `170s` default (`180s` ACP idle ceiling) | +| Connect / handshake timeout | `30s` | +| `type:acp` servers per session | `8` | +| Any inbound frame | `8 MiB` (exceed β†’ connection closed) | +| A method-bearing frame (request/notification) | `1 MiB` | + +### 4.3 `oab-mcp` becomes in-process callable + +Today `oab-mcp` is only reachable over stdio (`rmcp` `transport-io`). The tunnel +dispatch needs to invoke the **same** tool logic in-process. Refactor `oab-mcp` so +its `ServerHandler` (the `initialize` / `tools/list` / `tools/call` surface) is +callable directly, and let **both** transports sit on top of it: + +- **path (i)** β€” the reverse-MCP tunnel dispatch (this ADR); +- **path (ii)** β€” the standalone stdio binary (`transport-io`), unchanged for the + headless case. + +One tool implementation, two front doors. No tool logic is duplicated or forked. + +### 4.4 D1 β€” one `oab` server, fleet-parameterized tools (resolves Β§5 Q2) + +Studio declares a **single** `type:acp` server (`name: "oab"`), not one per fleet. +A single server suffices for **any number of fleets** **because the tools are +fleet-aware**: they take a `fleet` (name) argument and resolve members/credential +through the Part-A binding (`FleetBindings::get(name)` / `members`). + +This matters precisely because **fleets can share a cluster** (`orca` and `mira` +both on `oab`): a `cluster` argument alone cannot name a fleet, so the tool surface +must accept a `fleet`. `fleet_config` already enumerates every fleet, so Orca +discovers them and passes the right `fleet` per call. + +- **Rejected β€” one `type:acp` server per fleet.** Its only advantage is + tunnel-layer isolation (a session pinned to one fleet). That is not a boundary we + need: fleets share Studio's identity/credentials (credential is + cluster/account-granular), so isolation here is ergonomic, not security β€” and the + `fleet` argument already provides it. The cost is real: N tunnels/declarations, + the 8-servers/session ceiling, same-name rank/evict handling, and re-declaring + all on every resume. **Deferred** to if/when fleets carry **distinct + credentials/accounts** (fleet ADR Β§5 Q4, cross-account) or a session must be + hard-restricted to one fleet. +- **Consequence β€” a tool-surface evolution.** The tools take `cluster` today, not + `fleet`. Making them `fleet`-aware (resolve by name β†’ members + credential) is + **implementation slice 1** below. It is also the completion of Part A on the MCP + side: it makes fleet-scoped operation real beyond the console. + +### 4.5 D2 β€” writes are granted by the authed connection (resolves Β§5 Q1) + +`deploy_apply` / `deploy_scale` / `deploy_delete` are auto-approved, like reads. +**The authed `/acp` connection is the grant** β€” consistent with upstream D-29 +("the `/acp` transport is the gate") and OpenAB core's D1 (it auto-approves +`session/request_permission`). A client that holds a valid bearer to Studio's +gateway is, by construction, one Brett trusts to operate his fleet; there is no +second per-call consent gate, and the ADR does **not** add one. (A future +fine-grained consent surface is possible but explicitly out of scope β€” it would be +a change to the upstream contract's Β§8, not a Studio-local feature.) + +### 4.6 D3 β€” no server-initiated inbound MCP + +Studio serves a **gateway-initiated** tunnel only: it answers `initialize` / +`tools/list` / `tools/call` and does not push. `oab-mcp`'s tool set is effectively +**static**, so `notifications/tools/list_changed` has no consumer, and +server-originated requests (`sampling`/`elicitation`/`roots`) are not needed. This +matches the upstream contract and the 2026-07-28 MCP spec direction (those +surfaces are on a deprecation offramp). If a dynamic tool set ever appears, adopt +the upstream mechanism then rather than shipping a deprecated form now. + +## 5. The one OpenAB-side dependency + +Everything above is Studio-local. **One** thing is not, and it is *not* auth (auth +is settled: the transport bearer). It is **session attachment + endpoint**: + +> **Studio's declared `oab-mcp` server must land on _Orca's_ session, and Studio +> must be able to reach the gateway that hosts it.** + +Orca's ACP chat session is driven by the **Discord adapter**, not by Studio. In the +upstream browser example the tool-provider *is* the session's chat client, so its +`session/new` naturally carries the tools into that session. Studio is a **third +party** to Orca's session β€” it does not drive Orca's prompts. So the open item is: + +1. **Session binding.** How a connection that did **not** create Orca's session + contributes a `type:acp` server *into* it (keyed by Orca's `channel_id`), rather + than spawning a separate, agent-less session. The registry is already + `(channel_id, serverId)`-keyed; what is undefined is the **admission path for a + non-session-driving attacher** and the bearer that scopes it to that + `channel_id`. +2. **Endpoint reachability.** Which gateway laptop-Studio dials to meet ECS-Orca + (a hosted/shared gateway, or Orca's core exposed), and how Studio obtains the + scoped bearer. + +This is the item to settle with OpenAB (Jelly). It is small and sharply bounded β€” +but it is load-bearing: without it, path (i) cannot connect Studio's tools to Orca +specifically. **Path (ii)** (headless standalone `oab-mcp`) has none of this and is +the fallback that keeps Orca operating the fleet in the meantime. + +## 6. Consequences & implementation slices + +- Studio gains an **outbound network client** and answers gateway-initiated + requests β€” a new posture for the desktop app (previously it only *hosted* a stdio + sidecar). New deps: `tokio-tungstenite`, `rustls`. +- The **`fleet`-aware tool surface** (slice 1) is independently useful: it makes + MCP-side fleet scoping real, and unblocks path (ii) fleet operation too. +- **`oab-mcp` handler reuse** keeps one tool implementation behind both transports. + +**Slices** (each a PR; docs/backend before the network client): + +1. **`oab-mcp` tools become `fleet`-aware** β€” accept `fleet` (name), resolve via + `FleetBindings::get`/`members`; keep `cluster` as a compatible fallback. Backend + + tests; no network. *(Also completes Part A on the MCP side.)* +2. **Extract the in-process `oab-mcp` handler** β€” one `ServerHandler`, the stdio + binary re-expressed on top of it. Pure refactor, behaviour-preserving. +3. **ACP-WS client + session lifecycle** β€” dial, bearer sub-protocol, + `initialize`/`session/new`/`resume`, reconnect. Gated behind config; no tunnel + dispatch yet. +4. **Tunnel dispatch** β€” `mcp/connect` / `mcp/message` / `mcp/disconnect` / + `mcp/cancel` wired to the in-process handler; limits + id correlation. End-to-end + against a real gateway once Β§5 is settled. + +Slices 1–2 need **no** OpenAB dependency and can land immediately. Slices 3–4's +end-to-end verification depends on Β§5. + +## 7. Open questions + +1. **Session attachment + endpoint (Β§5)** β€” the one OpenAB-side item; the + feasibility gate for path (i). +2. **Per-fleet servers / cross-account (Β§4.4, fleet ADR Β§5 Q4)** β€” revisit if a + fleet ever carries distinct credentials or a session must be pinned to one fleet. +3. **Fine-grained write consent (Β§4.5)** β€” deliberately deferred; would be an + upstream contract change, not Studio-local. +4. **Reconnect UX** β€” how Studio surfaces "attached / detached from Orca's session" + in its UI (the human-in-the-loop value of path (i)). From 80c16353eadda85fb4d6599dd782e3207be8cfaa Mon Sep 17 00:00:00 2001 From: brettchien Date: Fri, 14 Aug 2026 11:03:56 +0800 Subject: [PATCH 2/3] =?UTF-8?q?docs(adr):=20adopt=20Model=20A=20=E2=80=94?= =?UTF-8?q?=20Studio=20creates=20the=20session=20(removes=20the=20attach?= =?UTF-8?q?=20gap)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Brett (2026-08-14): Studio is the session creator, dialing the remote agent runtime; the gateway spawns the agent for that session (katashiro pattern), so oab-mcp reaches the session's agent by construction β€” no "attach to a session you didn't create" mechanism needed. A human later drives the session via a chat surface in Studio for fleet management. Β§5 rewritten: the OpenAB-side need shrinks to ordinary ACP-client concerns (endpoint + transport bearer), not a new capability. Model B (inject tools into the always-on Discord Orca's live session) is documented and explicitly deferred. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) --- docs/adr/reverse-mcp-client.md | 118 +++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 48 deletions(-) diff --git a/docs/adr/reverse-mcp-client.md b/docs/adr/reverse-mcp-client.md index e52f40e..45d0510 100644 --- a/docs/adr/reverse-mcp-client.md +++ b/docs/adr/reverse-mcp-client.md @@ -43,18 +43,26 @@ outbound WebSocket it opened. | Party | Role | Where | |---|---|---| | **Studio** (Brett's laptop) | **ACP WS client + MCP server** β€” dials out, serves `oab-mcp` tools over the socket it holds | laptop (NAT'd) | -| **OpenAB core / gateway** | ACP server + **MCP proxy/aggregator** (the OAB MCP Facade) | hosts Orca's session | -| **Orca** | **the agent = MCP client** β€” discovers & calls Studio's tools via the facade meta-tools, executing under **Studio's** identity | ECS Fargate | - -- **Studio dials out.** It solves NAT: Studio opens the `/acp` WSS; nothing - connects *to* Studio. The fleet is reachable only while Studio runs (accepted - cost of path (i); path (ii) covers the no-Studio case). +| **OpenAB core / gateway** | ACP server + **MCP proxy/aggregator** (the OAB MCP Facade); spawns the agent for the session Studio creates | the agent runtime | +| **the agent** (Orca) | **MCP client** β€” discovers & calls Studio's tools via the facade meta-tools, executing under **Studio's** identity | ECS Fargate | + +- **Studio dials out and is the session creator (decision, 2026-08-14).** Studio, + the ACP client, opens the `/acp` WSS to the **remote agent runtime** (Orca's + OpenAB endpoint) and **creates the session** with `session/new`. The gateway + spawns the agent for that session β€” the **katashiro model** (Β§5, "Model A"): the + connection that serves the tools is the same one that owns the session, so its + `oab-mcp` tools reach *that session's* agent by construction. This is a + **dedicated fleet-management session**, distinct from Orca's Discord chat + session; a human later drives it through a chat surface in Studio (deferred β€” + Β§7). It solves NAT (nothing connects *to* Studio) and is reachable only while + Studio runs (path (ii) covers the no-Studio case). - **"Orca attaches to a running Studio"** (fleet ADR Β§3(i)) is the *logical* - view β€” the agent gains Studio's tools. The *physical* dial is - **Studio β†’ gateway**. + reading β€” the agent gains Studio's tools. The *physical* dial is + **Studio β†’ the agent runtime's gateway**, Studio creating the session. - **Studio's identity executes.** `oab-mcp` runs in Studio with Studio's - `fleets.toml` / AWS credentials (the fleet work of Part A). Orca calls the tools; - Studio performs the AWS actions. No AWS credentials are provisioned on Orca. + `fleets.toml` / AWS credentials (the fleet work of Part A). The agent calls the + tools; Studio performs the AWS actions. No AWS credentials are provisioned on + the agent. ``` Orca (agent, ECS) ──in-pod──▢ OpenAB core (OAB MCP Facade) @@ -151,7 +159,7 @@ callable directly, and let **both** transports sit on top of it: One tool implementation, two front doors. No tool logic is duplicated or forked. -### 4.4 D1 β€” one `oab` server, fleet-parameterized tools (resolves Β§5 Q2) +### 4.4 D1 β€” one `oab` server, fleet-parameterized tools (resolves fleet ADR Β§5 Q2) Studio declares a **single** `type:acp` server (`name: "oab"`), not one per fleet. A single server suffices for **any number of fleets** **because the tools are @@ -177,7 +185,7 @@ discovers them and passes the right `fleet` per call. **implementation slice 1** below. It is also the completion of Part A on the MCP side: it makes fleet-scoped operation real beyond the console. -### 4.5 D2 β€” writes are granted by the authed connection (resolves Β§5 Q1) +### 4.5 D2 β€” writes are granted by the authed connection (resolves fleet ADR Β§5 Q1) `deploy_apply` / `deploy_scale` / `deploy_delete` are auto-approved, like reads. **The authed `/acp` connection is the grant** β€” consistent with upstream D-29 @@ -198,33 +206,42 @@ matches the upstream contract and the 2026-07-28 MCP spec direction (those surfaces are on a deprecation offramp). If a dynamic tool set ever appears, adopt the upstream mechanism then rather than shipping a deprecated form now. -## 5. The one OpenAB-side dependency - -Everything above is Studio-local. **One** thing is not, and it is *not* auth (auth -is settled: the transport bearer). It is **session attachment + endpoint**: - -> **Studio's declared `oab-mcp` server must land on _Orca's_ session, and Studio -> must be able to reach the gateway that hosts it.** - -Orca's ACP chat session is driven by the **Discord adapter**, not by Studio. In the -upstream browser example the tool-provider *is* the session's chat client, so its -`session/new` naturally carries the tools into that session. Studio is a **third -party** to Orca's session β€” it does not drive Orca's prompts. So the open item is: - -1. **Session binding.** How a connection that did **not** create Orca's session - contributes a `type:acp` server *into* it (keyed by Orca's `channel_id`), rather - than spawning a separate, agent-less session. The registry is already - `(channel_id, serverId)`-keyed; what is undefined is the **admission path for a - non-session-driving attacher** and the bearer that scopes it to that - `channel_id`. -2. **Endpoint reachability.** Which gateway laptop-Studio dials to meet ECS-Orca - (a hosted/shared gateway, or Orca's core exposed), and how Studio obtains the - scoped bearer. - -This is the item to settle with OpenAB (Jelly). It is small and sharply bounded β€” -but it is load-bearing: without it, path (i) cannot connect Studio's tools to Orca -specifically. **Path (ii)** (headless standalone `oab-mcp`) has none of this and is -the fallback that keeps Orca operating the fleet in the meantime. +## 5. Session model β€” Model A (Studio creates the session) + +There are two ways Studio's tools could reach an agent. **Decision (2026-08-14): +Model A.** + +**Model A β€” Studio creates a dedicated fleet-management session (chosen).** Studio, +as the ACP client, dials the remote agent runtime and **creates the session**; the +gateway spawns the agent for it. This is exactly the **katashiro pattern**: the +connection that serves the tools also owns the session, so `session/new`'s declared +`oab-mcp` server reaches *that session's* agent **by construction**. No +"attach to a session you did not create" mechanism is required β€” the mechanism is +already as-built upstream (#1447). A human later drives this session through a chat +surface in Studio, to *work with the agent* on fleet management (that chat is +deferred β€” Β§7). + +Because Model A reuses the settled path, the only OpenAB-side needs are **ordinary +ACP-client concerns**, not a new capability: + +1. **Endpoint.** Which agent-runtime gateway Studio dials (Orca's OpenAB endpoint, + reachable from the laptop) β€” topology/deployment, not protocol. +2. **Bearer.** How Studio obtains the `/acp` transport bearer β€” the settled + transport auth, nothing new. + +**Model B β€” inject tools into an agent's *already-running* session (deferred).** +Making the always-on, Discord-driven Orca gain `oab-mcp` tools *in its existing +chat session* would require a third party (Studio) to contribute a `type:acp` +server into a session it did **not** create β€” keyed by that session's `channel_id`, +with a bearer scoping the attacher to it. Upstream has `(channel_id, serverId)` +keying and `session/resume` (a session's **own** client reconnecting), but **not** +an admission path for a non-session-driving attacher. That is a genuine new +OpenAB mechanism; it is **out of scope** here and revisited only if operating the +live Discord Orca in-place proves necessary. Model A already delivers +"an agent operates the fleet through Studio." + +**Path (ii)** (headless standalone `oab-mcp`) remains the no-Studio fallback and +depends on none of this. ## 6. Consequences & implementation slices @@ -247,18 +264,23 @@ the fallback that keeps Orca operating the fleet in the meantime. dispatch yet. 4. **Tunnel dispatch** β€” `mcp/connect` / `mcp/message` / `mcp/disconnect` / `mcp/cancel` wired to the in-process handler; limits + id correlation. End-to-end - against a real gateway once Β§5 is settled. + against a real agent-runtime gateway (Β§5, Model A). -Slices 1–2 need **no** OpenAB dependency and can land immediately. Slices 3–4's -end-to-end verification depends on Β§5. +Slices 1–2 need **no** OpenAB dependency and can land immediately. Slices 3–4 need +only a reachable agent-runtime endpoint + bearer (Β§5) β€” ordinary ACP-client +concerns, not a new upstream mechanism. ## 7. Open questions -1. **Session attachment + endpoint (Β§5)** β€” the one OpenAB-side item; the - feasibility gate for path (i). -2. **Per-fleet servers / cross-account (Β§4.4, fleet ADR Β§5 Q4)** β€” revisit if a +1. **Endpoint + bearer (Β§5)** β€” which agent-runtime gateway Studio dials and how it + gets the transport bearer. Deployment/topology, not protocol. +2. **The session chat surface** β€” Model A's session is driven by a human working + *with* the agent on fleet management; Studio needs a chat UI for that session. + Deferred; the mechanism (an ACP chat over the same `/acp` WS) exists. +3. **Per-fleet servers / cross-account (Β§4.4, fleet ADR Β§5 Q4)** β€” revisit if a fleet ever carries distinct credentials or a session must be pinned to one fleet. -3. **Fine-grained write consent (Β§4.5)** β€” deliberately deferred; would be an +4. **Fine-grained write consent (Β§4.5)** β€” deliberately deferred; would be an upstream contract change, not Studio-local. -4. **Reconnect UX** β€” how Studio surfaces "attached / detached from Orca's session" - in its UI (the human-in-the-loop value of path (i)). +5. **Model B (inject into a live session)** β€” only if operating the always-on + Discord Orca in-place is later required; needs the new upstream admission path + described in Β§5. From 7fb5bd73d26a47fdfe02bc6dee2ff6a32a86586c Mon Sep 17 00:00:00 2001 From: brettchien Date: Fri, 14 Aug 2026 12:57:00 +0800 Subject: [PATCH 3/3] =?UTF-8?q?docs(adr):=20=C2=A74.3=20the=20tunnel=20rel?= =?UTF-8?q?ays=20to=20the=20sidecar,=20not=20an=20in-process=20link?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading src-tauri showed the desktop reaches oab-mcp only over the stdio sidecar (McpClient), deliberately keeping aws out of the Tauri build (desktop-core-sidecar ADR; src-tauri is its own workspace). So the tunnel dispatch relays inner MCP to the running sidecar rather than linking oab-mcp in-process. Slice 2's lib split stays useful (a future headless client) but the desktop path is a WS↔sidecar relay. Β§6 slices 2-4 updated to match (acp-tunnel crate + src-tauri relay). πŸ€– Generated with [Claude Code](https://claude.com/claude-code) --- docs/adr/reverse-mcp-client.md | 58 ++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/docs/adr/reverse-mcp-client.md b/docs/adr/reverse-mcp-client.md index 45d0510..7597893 100644 --- a/docs/adr/reverse-mcp-client.md +++ b/docs/adr/reverse-mcp-client.md @@ -146,18 +146,32 @@ contract) Studio must respect: | Any inbound frame | `8 MiB` (exceed β†’ connection closed) | | A method-bearing frame (request/notification) | `1 MiB` | -### 4.3 `oab-mcp` becomes in-process callable - -Today `oab-mcp` is only reachable over stdio (`rmcp` `transport-io`). The tunnel -dispatch needs to invoke the **same** tool logic in-process. Refactor `oab-mcp` so -its `ServerHandler` (the `initialize` / `tools/list` / `tools/call` surface) is -callable directly, and let **both** transports sit on top of it: - -- **path (i)** β€” the reverse-MCP tunnel dispatch (this ADR); -- **path (ii)** β€” the standalone stdio binary (`transport-io`), unchanged for the - headless case. - -One tool implementation, two front doors. No tool logic is duplicated or forked. +### 4.3 The tunnel relays to the sidecar `oab-mcp` (not an in-process link) + +**Revised 2026-08-14, after reading `src-tauri`.** The desktop already reaches +`oab-mcp` **only over stdio**: it spawns the bundled `oab-mcp` sidecar and +multiplexes JSON-RPC over its stdio (`McpClient`, per the *desktop-core-sidecar* +ADR), which deliberately keeps `studio-cp` / `aws-config` **out of the Tauri +build** β€” `src-tauri` is even its own workspace to stay off `cargo build +--workspace`. So the tunnel dispatch **must not** link `oab-mcp` in-process; it +would drag the whole `aws-sdk` into the desktop binary and reverse that ADR. + +**Decision: the tunnel is a WS ↔ sidecar relay.** An inbound tunnel +`initialize` / `tools/list` / `tools/call` is forwarded to the **already-running** +sidecar via `McpClient` (which is already a JSON-RPC multiplexer) and its result +relayed back. Both sides speak MCP JSON-RPC, so this is a thin relay, and the +sidecar is the single instance already holding Studio's identity / `fleets.toml` +bindings β€” so tunnelled tools behave identically to the ones the UI panels call. + +- **path (i)** β€” the reverse-MCP tunnel (this ADR): WS β†’ `McpClient` β†’ sidecar. +- **path (ii)** β€” the standalone stdio binary, unchanged for the headless case. + +**Slice 2 (the `oab-mcp` lib/bin split with a transport-agnostic `dispatch`) is +still merged and useful** β€” it cleanly separates the handler and would let a +*future headless* reverse-MCP client drive `oab-mcp` in-process β€” but the +**desktop** path does not use that in-process API; it relays to the sidecar. The +one-implementation-two-front-doors property holds at the sidecar (`oab-mcp` +itself), not at an in-process link inside `src-tauri`. ### 4.4 D1 β€” one `oab` server, fleet-parameterized tools (resolves fleet ADR Β§5 Q2) @@ -257,14 +271,18 @@ depends on none of this. 1. **`oab-mcp` tools become `fleet`-aware** β€” accept `fleet` (name), resolve via `FleetBindings::get`/`members`; keep `cluster` as a compatible fallback. Backend + tests; no network. *(Also completes Part A on the MCP side.)* -2. **Extract the in-process `oab-mcp` handler** β€” one `ServerHandler`, the stdio - binary re-expressed on top of it. Pure refactor, behaviour-preserving. -3. **ACP-WS client + session lifecycle** β€” dial, bearer sub-protocol, - `initialize`/`session/new`/`resume`, reconnect. Gated behind config; no tunnel - dispatch yet. -4. **Tunnel dispatch** β€” `mcp/connect` / `mcp/message` / `mcp/disconnect` / - `mcp/cancel` wired to the in-process handler; limits + id correlation. End-to-end - against a real agent-runtime gateway (Β§5, Model A). +2. **Split `oab-mcp` into a lib + thin stdio bin** with a transport-agnostic + `dispatch`. Pure refactor, behaviour-preserving. (Enables a future headless + in-process client; the desktop path relays to the sidecar β€” Β§4.3.) +3. **Session lifecycle** β€” a new aws-free `acp-tunnel` crate: the bearer + sub-protocol, JSON-RPC envelopes, and the `initialize`/`session/new`/`resume` + state machine declaring the `oab` server. Pure + unit-tested. (The src-tauri WS + transport that drives it lands with slice 4.) +4. **Tunnel dispatch** β€” inbound `mcp/connect` / `mcp/message` / `mcp/disconnect` / + `mcp/cancel` (in `acp-tunnel`, over a `ToolBackend` trait; unit-tested with a + mock), plus the **src-tauri WS transport**: dial `/acp` (`tokio-tungstenite`), + drive the session, and implement `ToolBackend` by **relaying to the sidecar + `McpClient`** (Β§4.3). End-to-end against a real agent-runtime gateway (Β§5). Slices 1–2 need **no** OpenAB dependency and can land immediately. Slices 3–4 need only a reachable agent-runtime endpoint + bearer (Β§5) β€” ordinary ACP-client