Skip to content

feat(mcp): add pi-mcp-py, an extension that connects the MCP servers a client passes - #36

Merged
zy1233 merged 2 commits into
feat/client-mcp-hooksfrom
feat/pi-mcp-extension
Oct 25, 2026
Merged

zy1233 merged 2 commits into
feat/client-mcp-hooksfrom
feat/pi-mcp-extension

Conversation

@zy1233

@zy1233 zy1233 commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

Stacked on #35 (base: feat/client-mcp-hooks, which is on #34). Core and CLI still have no MCP client and do not import this package. This PR adds the optional package pi-mcp-py (module pi_mcp, entry point pi-mcp). It connects the MCP servers that the ACP client passes in session/new|load|resume, and gives their tools to the model.

session/new (mcp_servers) ──► pi.client_mcp_servers                       (#35)
  activate(pi)                                  entry point group pi_agent.extensions
    pi.claim_client_mcp_servers(names)          "I connect these"          (no "not connected" notice)
    session_start ──► Manager.start()           one runner task per server (stdio | http | sse)
                                                connect ─► initialize ─► list_tools ─► register
    pi.add_turn_barrier(manager.wait_started)   the first turn waits at most 10 s for a slow server
    pi.register_cleanup(manager.close)          ends every server process when the session ends
    /mcp, /mcp reconnect <name>

tool call: mcp__<server>__<tool>
  ServerConnection.call                         time limit 120 s; one retry only if the request was NOT sent;
                                                reconnect at most 3 times in a row
  shape_result                                  structuredContent -> text; text cut at 50 KB with a note; images kept
packages/pi-mcp/
├── pi_mcp/__init__.py     activate(), the /mcp command
├── pi_mcp/manager.py      the servers of one session: start, status, reconnect, close
├── pi_mcp/connection.py   one server: state machine, one runner task, calls, reconnect
├── pi_mcp/transport.py    stdio / Streamable HTTP / SSE; safe env of a child; direct client for this machine
├── pi_mcp/tools.py        names (max 64 characters), result shape
├── pi_mcp/errors.py       error text for the model, failure classes
├── pi_mcp/redact.py       env, headers, args and url of a server never reach a log, notice or error
├── pi_mcp/settings.py     PI_MCP_STARTUP_TIMEOUT, PI_MCP_CALL_TIMEOUT
└── tests/                 192 tests, a FastMCP mock server

Wiring: workspace member and testpaths in pyproject.toml; the CI test job installs the package (ci.yml); module-map row in AGENTS.md; README status row; CHANGELOG; the design (docs/specs/2026-10-23-mcp-extension-design.md) is synced with the code.

Release (last commit, ci(release)): release.yml installs the package for the pre-release tests and builds it (Build pi-mcp). publish-pypi uploads every file of dist/ and github-release attaches dist/*, so the next tag publishes pi-mcp-py 0.1.0 with the other packages. The README table and install lines, the package README (it is the PyPI description), the CHANGELOG and spec 9 say "on PyPI from 0.6.0".

Design points that are easy to miss:

  • Tools are named mcp__<server>__<tool>. The harness lets an extension tool replace a tool of the same name, so a server with a tool named bash would take the place of the real bash without the prefix.
  • The MCP SDK needs each cancel scope to end in the task that began it, so each server has one runner task. A transport that is ended by cancellation does not tell the pending requests; a call races a gone future (ConnectionEnded) instead of waiting for its time limit.
  • httpx reads the proxy of the system (on Windows, the registry) and does not skip loopback addresses. A server at localhost, 127.0.0.1 or ::1 uses a client with trust_env=False.
  • A stdio server gets the safe list of the MCP SDK, the proxy and certificate variables, and the env of the editor. It does not get the LLM keys of the agent.

Evidence

  • Before (base of this PR, Zed passes its context7 server): the notice says the server is not connected. After: the model calls it.

    test_a_server_of_the_client_gives_the_model_its_tools
      new_session(mcp_servers=[stdio mock]) -> prompt
      before: "mcp__mock__echo" is not offered to the model
      after : offered; the call returns "echo: via acp"; no "ignored" message to the client
    
  • Real server. The context7 server of Zed, started as Zed passes it (Zed has no system Node: its own node.exe runs the script of its extension): ready in 3.1 s, 2 tools (both with readOnlyHint), resolve-library-id("react") returned 1683 characters, no process left after close.

  • Real model. PiAcpAgent (entry-point discovery, real stream function, config of the maintainer: deepseek / deepseek-flash) and that server in session/new: session/new answered in 0.45 s. The model called mcp__mcp-server-context7__resolve-library-id (argument names libraryName, query), got the 1683 characters, and answered with the library id /reactjs/react.dev. 10.1 s for the turn, stop_reason=end_turn, 0 permission requests (auto mode), 0 node processes after close_session.

  • Tests. The 192 tests of packages/pi-mcp/tests pass on Windows with Python 3.11, 3.12 and 3.13, and on Linux (WSL2) with Python 3.12. Whole suite, run before the last test-only edits: Windows 2905 passed / 42 skipped; Linux 2931 passed / 16 skipped.

  • 28 one-line mutations of pi_mcp (no prefix, whole environment to the child, no redaction, no second try for an unsent request, no time limit, no direct client for loopback, a name given twice, no cut of a long result, and others): every one makes at least one test fail. One survived at first (a tool registered again after a reconnect); the assertion is now exact and the mutation is caught.

  • Release content (last commit). actionlint finds nothing in release.yml. A dry run of Release from this branch (run 40162795623; a manual run publishes nothing): Pre-release tests pass (the job now installs the package and runs its tests), Build Python packages pass with the new step Build pi-mcp, Publish to PyPI is skipped. The dist artifact has 14 files, among them pi_mcp_py-0.1.0-py3-none-any.whl and pi_mcp_py-0.1.0.tar.gz, and twine check --strict passes for all 14. The wheel has the eight modules and the pi-mcp entry point. I cancelled the two TUI build jobs of the run: this change does not touch them.

  • ruff check . and ruff format --check .: clean, also with the CI-pinned ruff 0.16.0.

  • Not done: OAuth; MCP resources, prompts, sampling, roots, elicitation, tools/list_changed; websocket servers. The legacy SSE transport of the SDK swallows the errors of its POST requests, so a call to a dead SSE server ends by its time limit. A session/new that Zed sends before its first worktree loads has an empty mcpServers (zed#64611); the agent cannot tell that from "no servers". Whether Zed gives OAuth tokens to an agent as headers is not verified.

Merge Danger

Door: two-way. It is a new package; nothing in core or CLI imports it. Uninstall pi-mcp-py and the agent is as after #35.

Blast Radius: the CI test job, and everyone who installs the package.

  • The extension starts the commands that the client names (command and args of a stdio server). The editor has already asked the user (Zed keeps the servers of a project off in Restricted Mode until the worktree is trusted), so the agent does not ask a second time (spec 6.3). In ask mode, each MCP tool call is still asked about unless the server marks the tool readOnlyHint (or neither destructive nor open-world).
  • CI: the test job installs one more package. Measured on the runs of feat(extensions): give extensions the MCP servers a client passes, a way to tell the user, and turn barriers #35 and this PR: Python 3.11 2m31s to 4m10s, 3.12 1m48s to 3m58s, 3.13 2m36s to 3m17s. All checks of this PR pass. tui-ci.yml is not changed.
  • Release. The last commit changes release.yml (two edits: the install line of the pre-release tests, and the step Build pi-mcp). The next tag publishes pi-mcp-py 0.1.0 with the other packages. Three things that I cannot check from here:
    • The first upload of a new PyPI project needs a PYPI_API_TOKEN that may create projects (an account-wide token, or a pending publisher for the name). The name pi-mcp-py is free (/pypi/pi-mcp-py/json gives 404). With skip-existing: true, a re-run after a token fix is safe.
    • The floor pi-agent-core-lc>=0.5.0 in packages/pi-mcp/pyproject.toml must become >=0.6.0 with the version bump, because the extension needs the hooks of feat(extensions): give extensions the MCP servers a client passes, a way to tell the user, and turn barriers #35. test_extension_packaging.py fails until it does, so CI says so.
    • "From 0.6.0" in the docs is my assumption for the version of the next release.
  • The notice of feat(extensions): give extensions the MCP servers a client passes, a way to tell the user, and turn barriers #35 tells the user to "install the pi-mcp-py package". That is true after the release that publishes it, so release soon after this merge.
  • Design defaults that the maintainer can change are in spec 10 (Q1 to Q11).

…a client passes

An editor such as Zed passes its MCP servers in session/new. With this package
installed the agent connects them (stdio, Streamable HTTP, SSE) and gives their
tools to the model. Core, harness and CLI still have no MCP client and read no
mcp.json: the extension takes the servers from pi.client_mcp_servers (the hooks
of the previous commit) and claims them. Without the package nothing changes.

Package packages/pi-mcp (pi-mcp-py, import name pi_mcp, entry point pi-mcp):
- One task per server. The MCP SDK uses anyio cancel scopes, which must end in
  the task that began them. session_start only starts the tasks; a turn barrier
  waits at most 10 s for servers that still start, and says so once.
- Tools are named mcp__<server>__<tool> (64 characters at most, a hash when cut,
  _2/_3 for repeats). The prefix matters: an extension tool with the name of a
  built-in tool replaces it. The ask mode asks about every MCP tool call unless
  the tool says it is harmless (readOnlyHint, as for all tools).
- Results: text over 50 KB is cut with a note, a result with only
  structuredContent becomes JSON text, an MCP error result becomes an error.
- Limits: PI_MCP_STARTUP_TIMEOUT (60 s), PI_MCP_CALL_TIMEOUT (120 s); a call
  also ends on abort.
- A server that stops: the in-flight call fails with the reason and is not sent
  again (the server may have run it). The next call connects the server again,
  3 times in a row at most (a call that gets an answer starts the count again).
  A request that never left is sent again once. A call stops waiting when the
  connection ends: the SDK does not tell a request when its transport ends by
  cancellation, so every start has a future that the call races. /mcp and
  /mcp reconnect <name> show and repair the servers.
- Secrets: the notices, /mcp, errors and logs show only the server name, the
  executable name or the host. Token values of env, headers and url are masked
  in error text.
- stdio is opened with the MCP SDK and not with create_session of
  langchain-mcp-adapters, which would expand ${VAR} from the environment of the
  agent (the LLM key). The child gets the SDK default list, proxy, certificate
  and locale variables, and the env of the client. stderr goes to
  <home>/logs/mcp/<name>.log, not to the pipe the client reads.
- A server at localhost, 127.0.0.1 or ::1 is reached without the system proxy:
  httpx reads the Windows proxy settings and does not skip loopback.
- Closing the session ends every server (stdin first, then the process tree).

Wiring: uv workspace member and testpaths, the install line of ci.yml,
test_extension_packaging (the floor of pi-agent-core-lc is the checkout
version; the package says so and does nothing on an older core), AGENTS.md,
README.md, CHANGELOG.md and the design (docs/specs/2026-10-23-mcp-extension-
design.md, brought in line with the code). The tests of the package are not
collected when mcp, langchain_mcp_adapters or pi_mcp is missing (the pre-release
job of CI installs none of them; release.yml is not changed). tui-ci.yml is not
changed either: zypi passes no servers.

Tests (192, real child processes and a real local HTTP server, no mock of the
SDK): units, stdio, HTTP and SSE, the extension in a real AgentHarness, and a
real PiAcpAgent over ACP (ask mode, session/close ends the process). Windows
(Python 3.11 and 3.12) and WSL.
The Release workflow now installs pi-mcp-py for its pre-release tests and builds it with the
other packages. `publish-pypi` uploads every file of `dist/` and `github-release` attaches
`dist/*`, so the build step is the whole publish list.

- release.yml: add `-e "./packages/pi-mcp"` to the install line of the test job, and the step
  "Build pi-mcp". With the package installed, the pre-release `pytest -v` runs
  `packages/pi-mcp/tests` too.
- README.md: the status table and the install lines say that pi-mcp-py is on PyPI from 0.6.0.
- packages/pi-mcp/README.md and pi_mcp/__init__.py: install with `pip install pi-mcp-py`. The
  README is the long description on PyPI, so it must not say "not on PyPI yet".
- CHANGELOG.md: the entry of pi-mcp-py says that the Release workflow builds it.
- docs/specs/2026-10-23-mcp-extension-design.md: section 9 lists what changed for the release.

Checked: actionlint finds nothing in release.yml; the sdist and the wheel build with the command
of the workflow; `twine check --strict` passes for both; the wheel has the eight modules and the
`pi-mcp` entry point.
@zy1233
zy1233 added this pull request to stack #37 October 25, 2026 04:50
@zy1233
zy1233 merged commit e0f2a36 into main Oct 25, 2026
7 of 10 checks passed
@zy1233
zy1233 deleted the feat/pi-mcp-extension branch October 25, 2026 04:50
zy1233 added a commit that referenced this pull request Oct 25, 2026
…erge of #33 to #36 (plan r20) (#38)

* docs(plan): record the MCP direction, the blank-session fix and the merge of #33 to #36 (plan r20)

The owner came back on 2026-10-23 with two problems in Zed: the MCP notice at the start
of each new thread, and a thread without a message that cannot be opened after Zed
restarts. The owner chose an optional extension package for MCP (core, harness and CLI
stay without an MCP client) and asked for the blank-session fix as a PR at once. On
2026-10-25 the owner asked for the release content first, then for the merge of #33 to
#36.

The merge is done. #33 with `gh pr merge --squash` (ca2ab85). #34 to #36 with
`gh stack link 34 35 36` (stack #37) and `gh stack merge 37 --yes --squash` (fa4887b,
34c11d9, e0f2a36). The tree of each commit is the tree of its PR head plus the two plan
files of #33. CI and TUI CI pass on e0f2a36 (pytest 2,908 passed on Python 3.11, 3.12
and 3.13; nextest 8,490 passed, 0 failed, 22 skipped).

PLAN-RUST-AGENT-RUNTIME-REMOVAL.md:
- Decision 17 gets a correction (#34: session/load and session/resume open an empty
  session for a canonical UUID that has no file).
- New decisions: 19 (the MCP direction, decided), 20 (the versions of the extension
  packages and their core floors), 21 (the 11 defaults of the MCP design).
- V0.6: 14 commits, one more package. Conditions 1, 3, 5 and 6 and the known issues are
  rewritten.
- r19 said the three extension packages stay as they are. That is wrong. The packaging
  test requires that each floor equals the workspace version, so a core of 0.6.0 needs
  `>=0.6.0` in all four extension packages (checked: 4 tests fail). This is decision 20.
- The status, section 7, appendix A.1 and A.2, the index and the revision log carry the
  same facts.

The execution log gets section 10.23: the cause of both problems, the three PRs, the
release content, the merge, the check of the trees, CI on main, the floors, and what is
not verified.

docs/TUI-AND-CODE-AGENT.md: the paragraph about MCP servers now says that `pi-mcp-py`
connects them and what the agent says without it.

No condition of V0.6 is met yet. Only documents change.

* docs(plan): record the answer of the owner to decisions 20 and 21 (plan r20)

The owner answered "follow your lean" for the two open decisions of r20.

Decision 20 (versions and core floors of the extension packages): option (a). At the
version bump (V0.6 condition 5) the four extension packages get the core floor
`>=0.6.0`. pi-web-access-py, pi-goal-x-py and pi-dynamic-workflows-py go from 0.2.0 to
0.3.0, as V0.5 did in 7e7d4d4 (0.1.0 to 0.2.0). pi-mcp-py stays at 0.1.0 (first upload).
The lean did not say which digit to raise. 0.3.0 is my choice from the V0.5 precedent;
the plan and the execution log say so.

Decision 21 (the 11 defaults of the MCP design): all defaults stay, including Q3
(annotations are trusted) and Q5 (no way to configure servers in zypi). No code change.

Neither decision changes code. The change of decision 20 belongs to the PR of
condition 5, which is not open yet.

Files: the plan (status, V0.6 section, next steps, decisions 20 and 21, the lists of the
decision section, index, revision log, one link definition), the execution log (section
10.23 and its revision-log entry), the MCP design (section 10: one sentence). Only
documents change.
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