Skip to content

feat(extensions): give extensions the MCP servers a client passes, a way to tell the user, and turn barriers - #35

Merged
zy1233 merged 1 commit into
fix/load-blank-sessionfrom
feat/client-mcp-hooks
Oct 25, 2026
Merged

zy1233 merged 1 commit into
fix/load-blank-sessionfrom
feat/client-mcp-hooks

Conversation

@zy1233

@zy1233 zy1233 commented Oct 23, 2026

Copy link
Copy Markdown
Owner

Summary

Stacked on #34 (base: fix/load-blank-session). The agent still has no MCP client. This PR gives an extension what it needs to connect the servers a client passes: the servers as data, a way to tell the user something, and a way to hold a turn back. The extension itself (pi-mcp-py) is the next PR.

session/new | session/load | session/resume   (mcp_servers)
  client_mcp.client_mcp_servers()           ACP models -> ClientMcpServer (names cleaned, unique)
    harness.set_client_mcp_servers()        before the extensions load
      activate(pi)
        pi.client_mcp_servers               read the servers
        pi.claim_client_mcp_servers(names)  "I connect these"
        pi.add_turn_barrier(wait)           "do not start a turn before this is done"
        pi.notify(text)                     one line for the user (not for the model, not saved)
  deferred setup (after the session/new response is sent, zed#60199)
    commands -> held notices -> project trust -> failed extensions -> "not connected" notice
                                                                       (unclaimed servers only)
prompt()
  wait_for_turn_barriers()                  session/cancel, close, and "open again" end the wait
    tool list of the turn is built
pi_agent_core/extensions/            ClientMcpServer; ExtensionAPI +5 members; HarnessBridge +4
packages/pi-agent-harness/           set_client_mcp_servers, add_turn_barrier, NoticeEvent
packages/pi-agent-cli/
├── pi_agent_cli/client_mcp.py (new) ACP -> records
├── pi_agent_cli/agent.py            hand-over, held notices, barrier wait, capabilities, rebind fix
├── pi_agent_cli/mcp_notice.py       names only the unclaimed servers; correct for 1 and for many
└── pi_agent_cli/events.py           notice event -> agent message
docs/specs/2026-10-23-mcp-extension-design.md (new)   design of the whole feature (PR-A, PR-B)

Also fixed here: a client that opens a session again (a second session/load, Zed's Retry) got a new harness, and the old one was dropped without close(). The cleanup of its extensions never ran, so an extension that starts a child process left it running. The new harness now takes the place first, then the old one is closed.

Evidence

  • Before (the notice Zed showed, one server): This agent does not connect to MCP servers, so the 1 MCP server the client passed (mcp-server-context7) were ignored and their tools are not available in this session. After: The client passed 1 MCP server (mcp-server-context7). This agent does not connect to MCP servers by itself, so its tools are not available in this session. To use it, install the pi-mcp-py package. With the pi-mcp entry point installed and nobody claiming the server: ... No extension took it over, so its tools are not available in this session.

  • Wire: initialize still sends the same mcpCapabilities object (all false) when the pi-mcp entry point is not installed (a test pins {"http": False, "sse": False, "acp": False, "_meta": None}). With it installed, http and sse are true. Only package metadata is read; nothing is imported.

  • Tests: 103 new (test_client_mcp_hooks.py 18, test_client_mcp_harness.py 32, test_client_mcp.py 53) and test_mcp_notice.py rewritten. Four one-line mutations are each caught:

    turn does not wait for the barriers        -> 9 tests fail
    replaced harness is not closed             -> 1 fails (test_opening_an_open_session_again_closes_the_harness_it_replaces)
    close/delete does not release a barrier    -> 1 fails (test_closing_the_session_ends_the_wait)
    notice names claimed servers too           -> 4 fail
    
  • pytest: Windows 2710 passed / 42 skipped; Linux (WSL2) 2732 passed / 20 skipped. ruff check . and ruff format --check .: clean (also with the CI-pinned ruff 0.16.0).

  • Not done: no MCP client, no real server was connected (PR-B). The Zed behaviour of session/new before the first worktree loads (zed#64611: empty mcpServers) is not handled; the agent cannot tell that case from "no servers".

Merge Danger

Door: two-way, except the new public methods of ExtensionAPI (client_mcp_servers, claim_client_mcp_servers, notify, register_cleanup, add_turn_barrier): when an extension package uses them, removing them breaks that package.

Blast Radius: every ACP client that passes mcp_servers; every author of a HarnessBridge double.

  • HarnessBridge has four more members. A test double that lacks them is no longer an instance of the Protocol (four test doubles in this repo needed the new members).
  • The notice text changes. It tells the user to install pi-mcp-py, and that package is in the next PR and not on PyPI yet. Merge the two together, or accept a short time in which the hint points to a package that does not exist.
  • A re-opened session now closes its earlier harness. A turn that ran on the earlier harness is aborted (before: it was left to run).

…way to tell the user, and turn barriers

The agent still has no MCP client and does not read mcp.json. The user chose
an optional extension package (pi-mcp-py) that connects the servers a client
passes in session/new, session/load and session/resume. Core, harness and CLI
only get the data and the hooks that such an extension needs.

Data and hooks:
- ClientMcpServer (pi_agent_core.extensions): one server as plain data. The
  fields that can hold a token (command, args, env, url, headers) are
  repr=False.
- ExtensionAPI.client_mcp_servers and claim_client_mcp_servers(names).
  AgentHarness.set_client_mcp_servers() takes the list before the extensions
  load; unclaimed_client_mcp_servers says which servers nobody took over.
- ExtensionAPI.notify(text) shows one line to the user (NoticeEvent, an agent
  message in ACP). The model does not see it and the session does not save it.
- ExtensionAPI.register_cleanup(callback): the public name of what the bridge
  already had.
- ExtensionAPI.add_turn_barrier(wait): the harness awaits it before it builds
  the tool list of a turn. A wait that is cancelled leaves the phase idle.
- HarnessBridge has four more members; test doubles must add them.

CLI:
- client_mcp.py turns the ACP models into records and hands them to the harness
  on session/new, load, resume and the reopen of a blank session.
- The notice about ignored MCP servers names only the servers nobody claimed,
  and its text is correct for one server and for many. With the pi-mcp entry
  point installed it does not tell the user to install anything.
- initialize offers http and sse servers only when the pi-mcp entry point is
  installed (package metadata only; nothing is imported). The wire shape is the
  same as before when it is not.
- A line from notify() waits until the deferred setup has advertised the
  commands (the client drops earlier updates, zed#60199), then goes out in order.
- prompt() waits for the turn barriers and lets session/cancel, closing the
  session and opening it again end the wait.

Fix: a session that the client opened again (a second session/load, Zed's
Retry) built a new harness and dropped the old one without close(), so the
cleanup callbacks of its extensions never ran. The new harness now replaces
the old one first, and the old one is closed.

Design: docs/specs/2026-10-23-mcp-extension-design.md
@zy1233
zy1233 added this pull request to stack #37 October 25, 2026 04:50
@zy1233
zy1233 merged commit 34c11d9 into main Oct 25, 2026
7 checks passed
@zy1233
zy1233 deleted the feat/client-mcp-hooks branch October 25, 2026 04:50
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