feat(mcp): wire ACP MCP servers through to pi (stdio/http/sse + acp transport) - #67
feat(mcp): wire ACP MCP servers through to pi (stdio/http/sse + acp transport)#67hancengiz wants to merge 2 commits into
Conversation
8a23545 to
53e87a3
Compare
ChristianLuciani
left a comment
There was a problem hiding this comment.
@hancengiz this is really impressive work — clean architecture, thorough tests, and the .pi/mcp.json backup/restore mechanism with crash recovery is exactly the right level of care for touching user config files. The mcp-shim at 79 lines with zero dependencies is a model of good module design. I ran the suite locally (typecheck + lint + 105 tests) and everything is green.
I was about to open an issue proposing exactly this integration when I found your PR — you have already built it, and built it well. 😄
A couple of things I wanted to raise for discussion:
1. Backup failure edge case in writeManagedMcpConfig (config.ts)
I think there is a subtle bug here: if copyFileSync fails during backup (disk full, permissions), backedUp stays false, the code proceeds to overwrite the user original .pi/mcp.json with the managed version, and then restore() — seeing backedUp === false and no backup file — deletes it. The user loses their config.
The fix is small — if the backup fails, skip the write entirely:
let backupFailed = false
try {
if (existsSync(path) && !existsSync(backupPath)) {
copyFileSync(path, backupPath)
}
} catch {
backupFailed = true
}
if (backupFailed) {
return { path, restore: () => {} } // do not touch the file
}Edge case for sure (backup of a small JSON file failing is rare), but since it is user config, being defensive feels warranted.
2. Should acp transport have its own gate?
getMcpCapabilities() gates http, sse, and acp on a single boolean. The PR description notes that acp transport is UNSTABLE/experimental per the ACP schema. If that transport needs to be disabled independently (spec change, stability), there is currently no knob — you have to turn off everything.
Not a blocker at all, but wanted to flag it in case you or @svkozak want a PI_ACP_ENABLE_MCP_ACP or similar before this lands. Happy either way.
One observation on size: at ~1300 lines / 12 files, this is a substantial review. The MCP-over-ACP transport (bridge + shim, ~420 lines + tests) is self-contained enough that it could ship as a follow-up. But that is a strategic call for the maintainer — the code is solid either way.
I would love to help move this forward — testing against a real pi-mcp-adapter setup, reviewing follow-up changes, whatever is useful. Thanks for building this!
…ransport)
pi-acp previously advertised mcpCapabilities {http:false, sse:false} and
silently dropped all MCP servers from session/new. pi has no built-in MCP
support, but the pi-mcp-adapter extension reads and spawns/
connects MCP servers. This change wires ACP-provided MCP servers through to
pi via that mechanism.
Changes:
- Advertise mcpCapabilities {http:true, sse:true, acp:true} in initialize.
- stdio/http/sse servers: translated into pi-mcp-adapter config entries and
merged into the project-local for the session. The original
file is backed up and restored on session close.
- acp-transport servers (MCP-over-ACP RFD): pi-acp spawns a tiny stdio shim
(dist/mcp-shim.js) that pi launches via pi-mcp-adapter; the shim relays
newline-delimited JSON-RPC to a local socket owned by AcpMcpBridge, which
routes messages over the ACP channel using mcp/connect, mcp/message, and
mcp/disconnect.
- Add Agent.extMethod/extNotification to route inbound (server-originated)
mcp/message notifications to the owning session bridge. Server-originated
requests (e.g. sampling) are declined.
- Restore and dispose bridges/sockets on session close and on
spawn failure.
- Build the shim as a second tsup entry.
- Add README "MCP support" section and update Limitations.
- Tests: config translation + merge/restore (unit), AcpMcpBridge connect/
message/notification/inbound round-trips (component).
Requires the pi-mcp-adapter extension to be installed in pi for pi to actually
connect to the servers.
…lation, Windows pipes - writeManagedMcpConfig: if the backup of the user's .pi/mcp.json cannot be created, skip MCP wiring instead of overwriting the file (restore() would otherwise delete the user's config) - setupMcpServers: isolate per-server setup failures so one broken ACP-transport server no longer drops all other MCP servers - AcpMcpBridge: bound ACP round-trips with timeouts (mcp/connect 15s, mcp/message 5min, mcp/disconnect 5s); an unresponsive client now yields a JSON-RPC error to pi instead of a hung request - AcpMcpBridge: send mcp/disconnect as a request on shim socket close (SDK schema defines it as a request, not a notification) - Use a named pipe for the shim socket on Windows (unix socket paths do not work with net.Server.listen there) - mcp-shim: remove dead/incorrect cleanup path in pipe() - Tests: timeout behavior (connect + message), error propagation to pi, disconnect-as-request, per-server failure isolation, backup-failure safety
53e87a3 to
ca53a64
Compare
|
@svkozak Sergii any comments on this? |
|
@hancengiz appreciate the effort, but after reviewing I don't think this is a good fit for pi-acp:
Other than that, there are a few other issues that may further complicate things. |
|
@svkozak thanks for the thoughtful response — this is really useful clarity. The distinction between "ACP translator" and "configuration manager" makes complete sense: the adapter should start Pi and translate events, not own MCP config, merge precedence rules, or coordinate state across processes. That is a clean scope boundary and I am aligned with it. Appreciate you taking the time to explain the reasoning rather than just closing. It helps everyone who comes to this issue understand why, not just what. |
Summary
pi-acppreviously advertisedmcpCapabilities { http: false, sse: false }and silently dropped every MCP server passed insession/new(the only handling was// Pi doesn't support mcpServers, but we accept and store.). This PR implements real MCP wiring through to pi without claiming MCP support for plain pi by default.pi has no built-in MCP support, but the community pi-mcp-adapter extension reads
.pi/mcp.jsonand spawns/connects MCP servers. This change routes ACP-provided MCP servers through that mechanism when MCP is enabled/detected.Capability advertisement
pi-acpnow advertises MCP capabilities conditionally:{ "http": false, "sse": false, "acp": false }pi-mcp-adapteris detected globally (package dir or settings package entry), orPI_ACP_ENABLE_MCP=trueis set:{ "http": true, "sse": true, "acp": true }PI_ACP_ENABLE_MCP=falseforces MCP capability advertisement off.This avoids telling ACP clients that MCP is supported when plain pi cannot actually consume MCP config.
What it does when MCP is enabled
.pi/mcp.jsonfor the session. The original file is backed up (.pi/mcp.json.pi-acp.bak) and restored on session close.pi-acpspawns a tiny dependency-free stdio shim (dist/mcp-shim.js) that pi launches via pi-mcp-adapter. The shim relays newline-delimited JSON-RPC to a local socket owned byAcpMcpBridge, which routes messages over the ACP channel usingmcp/connect,mcp/message, andmcp/disconnect.mcp/messagenotifications are forwarded to the owning session's bridge to the shim to pi. Server-originated requests (e.g.sampling/createMessage) are declined, since handling them would require pi's MCP client to also act as an MCP server..pi/mcp.jsonand disposes bridges/sockets on session close and on spawn failure.Robustness hardening (second commit)
Follow-up commit addressing review feedback and hardening the failure paths:
.pi/mcp.jsoncannot be created, MCP wiring is skipped for the session instead of overwriting the file — previouslyrestore()could delete the user's config in that case.acp-transport server (e.g. its socket cannot be created) is skipped instead of silently dropping all MCP servers for the session.mcp/connect15s,mcp/message5min (MCP tool calls can legitimately run long),mcp/disconnect5s. An unresponsive client now yields a JSON-RPC error to pi instead of a request that hangs forever.\\.\pipe\named pipe on win32 (unix socket paths don't work withnet.Server.listenthere).mcp/disconnecton shim socket close is now sent as a request (the SDK schema defines it with a response), matching the dispose path.Architecture
Requires
The pi-mcp-adapter extension must be installed in pi (
pi install npm:pi-mcp-adapter). Without it, pi has no MCP support; the capability defaults to false unless explicitly forced withPI_ACP_ENABLE_MCP=true.Limitations / notes
initializehas no cwd, so project-local-onlypi-mcp-adapterinstalls cannot always be detected at capability-advertisement time. UsePI_ACP_ENABLE_MCP=trueif you rely on a project-local MCP adapter..pi/mcp.jsonis restored on clean session close. Ifpi-acpis killed abruptly, the managed servers remain in.pi/mcp.json(a.pi-acp.bakis left for manual recovery).acpcapability is currently marked UNSTABLE/experimental by the ACP schema, matching the RFD status.Testing
npm run typecheck,npm run lint,npm run buildall clean.main, includes the new upstream tests)..pi/mcp.jsonmerge + restore, and a regression test for the backup-failure data-loss case (fails against the pre-fix code).AcpMcpBridgeconnect round-trip (mcp/connect+mcp/messagerequest/response), notification forwarding (no spurious response), inbound server-originated notification forwarding to the shim, connection tracking,mcp/messagerejection surfacing as a JSON-RPC error, message and connect timeout behavior,mcp/disconnectsent as a request on socket close, and per-server failure isolation insetupMcpServers(real socket-failure injection).mcp-shim.jsspawned as a subprocess relays atools/callrequest through a real socket and returns the response. PASSFiles
src/mcp-shim.ts— standalone stdio to socket relay shim (built todist/mcp-shim.js).src/acp/mcp/config.ts— ACP to pi-mcp-adapter config translation + managed.pi/mcp.json.src/acp/mcp/bridge.ts—AcpMcpBridge: socket server,mcp/connect/message/disconnectrelay.src/acp/mcp/index.ts—setupMcpServersorchestrator.src/acp/pi-settings.ts— MCP capability detection and env override.src/acp/session.ts/src/acp/agent.ts— lifecycle wiring, conditional capabilities, inbound routing.tsup.config.ts— build the shim as a second entry.Closes the "MCP servers are accepted in ACP params and stored in session state, but not wired through to pi" limitation while keeping default capability advertising honest for plain pi.