Skip to content

feat(core): add MCP transport over incoming client sockets - #57502

Merged
jlongster merged 4 commits into
devfrom
client-mcp-transport
Oct 22, 2026
Merged

jlongster merged 4 commits into
devfrom
client-mcp-transport

Conversation

@jlongster

@jlongster jlongster commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Stack 1/5, based on dev. Merge in order.

Part of the client MCP stack (experimental): a client (TUI/desktop/web) opens a WebSocket to the server, and the server treats that incoming socket as an internal MCP server scoped to a location. Tool calls carry _meta["ai.opencode/sessionID"] so the client acts on the calling session. MCP has no official WebSocket transport, so this is a custom transport, which the spec allows.

This PR

Adds McpSocketClient (packages/core/src/mcp/socket-client.ts): an MCP SDK Transport over an already-accepted client WebSocket. The server never dials out.

  • One JSON-RPC message per text frame; invalid frames go to onerror and the connection stays open
  • Frames that arrive before start() are buffered
  • deliver(text) and ended() hooks for the socket loop; close() closes with 1000; onclose fires once
  • No replay after a disconnect

Tests: unit tests over a fake socket, plus a real SDK Client ↔ McpServer round trip through two transports.

Stack

→ 1. #57502 feat(core): add MCP transport over incoming client sockets
2. #57503 feat(core): attach client-hosted MCP servers
3. #57504 feat(client): add connectClientMcp helper for client-hosted MCP tools
4. #57505 feat(server): add client MCP websocket endpoint (experimental)
5. #57506 feat(tui): expose open_terminal over client MCP (experimental)

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks sound. I ran the new tests in packages/core (11 pass, stable across 10 runs, including the real SDK Client ↔ McpServer round trip), and bun run typecheck passes. I also traced the ready/open/closed transitions: onclose fires once whether the socket closes via close() or ended(), frames buffered before start are dropped if the socket ends first, and send is rejected outside the open state. I have no concerns that would block this.

Public API is unchanged: this is a new internal module in packages/core that nothing calls yet.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit looks sound. close() now rejects when the socket's close throws, and onclose still fires once. Frames delivered while start() is flushing the buffer now keep their order, and parsing uses Result.try instead of running an Effect. All 15 tests in test/mcp-socket-client.test.ts pass, and bun run typecheck in packages/core passes. I have no new concerns.

Public API is unchanged: this is an internal module in packages/core.

@jlongster
jlongster enabled auto-merge (squash) October 22, 2026 18:30
@jlongster
jlongster force-pushed the client-mcp-transport branch from 9e8f20c to 3d7233f Compare October 22, 2026 18:53
@jlongster
jlongster disabled auto-merge October 22, 2026 18:58
@jlongster
jlongster merged commit 0030a70 into dev Oct 22, 2026
12 checks passed
@jlongster
jlongster deleted the client-mcp-transport branch October 22, 2026 18:58

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The socket transport is unchanged since my last review. This push folds in #57503 (Mcp.attach, the attached McpClient.connect type, and routing tool calls to attached clients), so this PR now covers stack steps 1 and 2. In packages/core, test/mcp-socket-client.test.ts and test/mcp-attach.test.ts pass (28 tests; the attach tests passed six repeated runs), and bun run typecheck passes.

Public API is unchanged: Mcp.Interface.attach is internal to core, and the attached connection type is never read from config.

Concerns:

  • An attached client whose name matches a configured server takes over that server's tools and permission rules (see the inline comment).
  • test/mcp-attach.test.ts is 402 lines for 13 cases and relies on polling helpers (waitUntil, registered). Please cut it down to the core cases: routing with the session ID, removal on close, fallback to the previous connection, and staying hidden from server status. That would make it easier to follow and less sensitive to slow CI.

callTool: Effect.fn("MCP.callTool")(function* (input) {
const target = yield* requireServer(ServerName.make(input.server))
const server = ServerName.make(input.server)
// An attached client owns the tools it lists, even when a configured server shares its name, and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an attached client uses the same name as a configured MCP server, its tools take over that server's tools of the same name, both here and in tools(). They also pick up that server's permission rules, because McpTool builds the permission action from the server name (<server>_<tool>). A rule like github_*: allow would then also allow tools served by a client that attached as github. Could attach reject a name that matches a configured server, or put attached names in their own namespace? Then attached tools could never override configured ones or inherit their permissions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant