Skip to content

Commit e0f2a36

Browse files
authored
feat(mcp): add pi-mcp-py, an extension that connects the MCP servers a client passes (#36)
* feat(mcp): add pi-mcp-py, an extension that connects the MCP servers 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. * ci(release): build and publish pi-mcp-py with the other packages 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.
1 parent 34c11d9 commit e0f2a36

26 files changed

Lines changed: 3995 additions & 22 deletions

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@ jobs:
3939
python-version: ${{ matrix.python-version }}
4040
cache: pip
4141
- name: Install packages (dev)
42-
run: pip install -e ".[dev]" -e "./packages/pi-agent-harness" -e "./packages/pi-agent-cli" -e "./packages/pi-web-access" -e "./packages/pi-goal-x" -e "./packages/pi-dynamic-workflows"
42+
run: pip install -e ".[dev]" -e "./packages/pi-agent-harness" -e "./packages/pi-agent-cli" -e "./packages/pi-web-access" -e "./packages/pi-goal-x" -e "./packages/pi-dynamic-workflows" -e "./packages/pi-mcp"
4343
- name: Run tests
4444
run: pytest -v

‎.github/workflows/release.yml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
python-version: "3.12"
2323
cache: pip
2424
- name: Install packages (dev)
25-
run: pip install -e ".[dev]" -e "./packages/pi-agent-harness" -e "./packages/pi-agent-cli" -e "./packages/pi-web-access" -e "./packages/pi-goal-x" -e "./packages/pi-dynamic-workflows"
25+
run: pip install -e ".[dev]" -e "./packages/pi-agent-harness" -e "./packages/pi-agent-cli" -e "./packages/pi-web-access" -e "./packages/pi-goal-x" -e "./packages/pi-dynamic-workflows" -e "./packages/pi-mcp"
2626
- name: Run tests
2727
run: pytest -v
2828
- name: Lint
@@ -62,6 +62,9 @@ jobs:
6262
- name: Build pi-dynamic-workflows
6363
run: python -m build --outdir dist/ packages/pi-dynamic-workflows
6464

65+
- name: Build pi-mcp
66+
run: python -m build --outdir dist/ packages/pi-mcp
67+
6568
- uses: actions/upload-artifact@v4
6669
with:
6770
name: dist

‎AGENTS.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ AgentMessage[] → transform_context() → convert_to_llm() → LangChain BaseMe
3434
| `pi_agent_core/coding_tools/` | Built-in coding tools (`read`/`bash`/`edit`/`write`/`grep`/`find`/`ls`) | pi coding tools |
3535
| `packages/pi-agent-harness/` | Sessions, AgentHarness, compaction, skills, LocalExecutionEnv | `harness/` |
3636
| `packages/pi-dynamic-workflows/` | `workflow` tool and runtime. Scripts run in a child process (`sandbox/host.py` ⇄ stdlib-only `sandbox/child.py`, JSON lines): the kernel limits are the boundary, the audit hook is a speed bump. Sandbox work starts from `docs/specs/2026-10-01-workflow-sandbox-design.md` and runs `tests/test_sandbox_*.py` on Windows **and** WSL (the layers differ per platform) | `@quintinshaw/pi-dynamic-workflows` |
37+
| `packages/pi-mcp/` | Optional extension `pi-mcp-py` (entry point `pi-mcp`): connects the MCP servers (stdio, http, sse) that an ACP client passes in `session/new`, one runner task per server, tools named `mcp__<server>__<tool>`, `/mcp` command. Core, harness and CLI have no MCP client; they hand the servers over as data (`ClientMcpServer`, `ExtensionAPI.client_mcp_servers`). Design: `docs/specs/2026-10-23-mcp-extension-design.md`. Its tests are left out of a run that did not install the package (the header says so) | — |
3738
| `packages/pi-agent-cli/` | Standard-ACP CLI (`python -m pi_agent_cli`); config `agent.example.toml`, home `~/.pi-python`; TUI binary `zypi` under `tui/`. See [`packages/pi-agent-cli/AGENTS.md`](packages/pi-agent-cli/AGENTS.md) for spawn, config, and prompt pipeline | — |
3839

3940
### Invariants
@@ -80,7 +81,7 @@ Only if a venv is broken — recreate from scratch:
8081
```powershell
8182
# Dev
8283
uv venv --python 3.12 .venv
83-
uv pip install --python .venv -e ".[dev]" -e "./packages/pi-agent-harness" -e "./packages/pi-agent-cli" -e "./packages/pi-web-access" -e "./packages/pi-goal-x" -e "./packages/pi-dynamic-workflows"
84+
uv pip install --python .venv -e ".[dev]" -e "./packages/pi-agent-harness" -e "./packages/pi-agent-cli" -e "./packages/pi-web-access" -e "./packages/pi-goal-x" -e "./packages/pi-dynamic-workflows" -e "./packages/pi-mcp"
8485
8586
# Real-LLM
8687
uv venv --python 3.12 .venv-test-real

‎CHANGELOG.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- **Tools that find tests which share process-wide state, and a helper that keeps such a test apart (TUI tests)**: `scripts/tui_envspy/` and `in_own_process`. All tests of one Rust test binary run as threads of one process, so they share the environment variables, the working directory and every `static`. The vendored tests were written for `cargo nextest` (one process per test), so their locks are partial, and a test that changes such state can break another test in some runs only. `envspy.sh` runs `cargo test` with an `LD_PRELOAD` shim over `getenv`, `setenv`, `unsetenv`, `putenv` and `chdir`, and `analyze.py` lists the tests that change a variable while another test runs, leave it changed, or read what another test has changed. `stress.sh` runs the lib tests of one package many times, each time in a random order (`--shuffle-seed`, so a failing run can be repeated) and with many test threads. It can also run a few named tests (a suspect pair, or a victim beside all tests of one module), and with `ENVSPY_TAG` it runs the tests through the spy. Linux only. 10 new `pytest` tests cover the analyzer. `crate::test_util::in_own_process(|| { … })` in `pi-pager` runs the body of a test in a child process (the same test binary, `--exact`, one test thread), and the test fails when the child fails; 57 tests use it, directly or through `with_rtl_bidi`, `with_theme_test_env` and `with_test_env`, and 2 new tests check it. The README of `scripts/tui_envspy/` has the rules for a test that changes process-wide state, and what the tools do not find. Numbers: section 3.4 of `docs/baselines/tui.md`.
13-
- **Extensions can take over the MCP servers a client passes, tell the user something, and hold a turn back (`pi-agent-core`, `pi-agent-harness`, `pi-agent-cli`)**: the agent still has no MCP client, and it still does not read `mcp.json`. What a client passes in `mcp_servers` now goes to the harness as data: `ClientMcpServer` (`name`, `transport`, `command`, `args`, `env`, `url`, `headers`; its `repr` shows only `name` and `transport`, because the other fields can hold tokens), `AgentHarness.set_client_mcp_servers()` (before the extensions load), and `ExtensionAPI.client_mcp_servers` with `claim_client_mcp_servers(names)`. Three general entry points come with it. `ExtensionAPI.notify(text)` shows one line to the user (a `NoticeEvent`, which is an agent message in ACP; the model does not see it, the session does not save it, and the agent holds it until the client knows the session). `ExtensionAPI.register_cleanup(callback)` is the public name of what the bridge already had. `ExtensionAPI.add_turn_barrier(wait)` makes the harness await `wait` before it builds the tool list of a turn; `session/cancel`, closing the session and opening it again end that wait in the CLI. `initialize` says that the agent takes `http` and `sse` MCP servers only when the `pi-mcp` extension entry point is installed. Nothing uses these entry points yet: the extension that does (`pi-mcp-py`) is a later change. Design: `docs/specs/2026-10-23-mcp-extension-design.md`.
13+
- **Extensions can take over the MCP servers a client passes, tell the user something, and hold a turn back (`pi-agent-core`, `pi-agent-harness`, `pi-agent-cli`)**: the agent still has no MCP client, and it still does not read `mcp.json`. What a client passes in `mcp_servers` now goes to the harness as data: `ClientMcpServer` (`name`, `transport`, `command`, `args`, `env`, `url`, `headers`; its `repr` shows only `name` and `transport`, because the other fields can hold tokens), `AgentHarness.set_client_mcp_servers()` (before the extensions load), and `ExtensionAPI.client_mcp_servers` with `claim_client_mcp_servers(names)`. Three general entry points come with it. `ExtensionAPI.notify(text)` shows one line to the user (a `NoticeEvent`, which is an agent message in ACP; the model does not see it, the session does not save it, and the agent holds it until the client knows the session). `ExtensionAPI.register_cleanup(callback)` is the public name of what the bridge already had. `ExtensionAPI.add_turn_barrier(wait)` makes the harness await `wait` before it builds the tool list of a turn; `session/cancel`, closing the session and opening it again end that wait in the CLI. `initialize` says that the agent takes `http` and `sse` MCP servers only when the `pi-mcp` extension entry point is installed. The extension that uses these entry points is `pi-mcp-py` (next entry). Design: `docs/specs/2026-10-23-mcp-extension-design.md`.
14+
- **`pi-mcp-py`: an optional extension that connects the MCP servers an ACP client passes (new package `packages/pi-mcp`, version 0.1.0)**: an editor such as Zed passes its MCP servers in `session/new`. With this package installed, the agent connects them (stdio, Streamable HTTP and SSE) and gives their tools to the model. The core, the harness and the CLI still have no MCP client, and the agent still reads no `mcp.json`; the extension takes the servers from `pi.client_mcp_servers` and claims them, so the notice "its tools are not available" does not appear. Without the package, nothing changes. **Tools**: each tool is named `mcp__<server>__<tool>` (letters, digits, `_` and `-`; at most 64 characters, cut with a hash; a repeated name gets `_2`, `_3`). The prefix is needed: a tool of an extension that has the name of a built-in tool replaces it, and an MCP tool named `bash` would take over `bash`. The permission mode `ask` asks about every MCP tool call unless the server declares the tool harmless (`readOnlyHint`, or neither destructive nor open-world), as for all tools. A result is text; text over 50 KB is cut with a note, a result with only `structuredContent` is sent as JSON text, and an MCP error result becomes an error tool result. **One task for each server**: the MCP SDK uses `anyio` cancel scopes, which must end in the task that began them. `session_start` starts the servers and returns at once; a turn waits at most 10 s for servers that are still starting (`session/cancel` ends the wait) and then goes on without them. A server that is slower sends one notice, and its tools join in a later turn. **Limits** (environment variables): `PI_MCP_STARTUP_TIMEOUT` (60 s) and `PI_MCP_CALL_TIMEOUT` (120 s); a call also ends when the user cancels the turn. **When a server stops**: the user gets one notice for each server and kind of event (did not start, stopped, cannot reconnect), and a good start gets none, so a thread without MCP trouble stays blank. The next tool call connects the server again, at most 3 times in a row (a call that gets an answer starts the count again). A call that was already sent when the server died is not repeated, because the server may have run it. `/mcp` lists the servers (name, transport, state, number of tools), and `/mcp reconnect <name>` connects one again with no limit. **Secrets**: `env`, `headers`, `args` and `url` hold tokens. Notices, `/mcp`, errors and logs show only the server name, the name of the executable, or the host; the values of `env`, the values of `headers` and the URL are masked in error text. **Environment of a stdio server**: the default list of the MCP SDK, the proxy, certificate, locale and time zone variables, and the `env` of the client. `${VAR}` is not expanded (`langchain-mcp-adapters` would expand it from the environment of the agent, which has the LLM key, so the extension starts stdio servers itself), and the keys of the LLM provider are not passed. stderr of a server goes to `<home>/logs/mcp/<name>.log`, not to the stderr of the agent, where a full pipe would stop it. **Servers on this machine** are reached without the proxy of the system: `httpx` reads the proxy settings of Windows and does not skip loopback addresses. **End**: when the session closes, each server gets its stdin closed first, and then its process tree is stopped; a test checks that no process stays, on Windows and on Linux. **Not supported**: OAuth (it works only if the editor passes a token in `headers`; not verified), MCP resources, prompts, sampling and elicitation, a server shared by two sessions (each session starts its own), and a configuration file. A legacy SSE server whose POST fails ends a call only with the call timeout (the SDK hides the error). **Limits of the package**: it needs `pi-agent-core-lc` 0.6.0 or later; an older core makes it log a warning and do nothing. The `Release` workflow installs the package for its pre-release tests and builds it with the other packages, so the upload to PyPI has it (`pip install pi-mcp-py`). Tests: 192 in `packages/pi-mcp/tests` with real child processes (stdio, HTTP, SSE), the real `AgentHarness`, and the real `PiAcpAgent` over ACP, run on Windows and WSL. Design: `docs/specs/2026-10-23-mcp-extension-design.md`.
1415

1516
### Changed
1617

‎README.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ This repository is a monorepo containing:
2121
- `pi-web-access-py` / `pi_web_access`: web search and URL fetch extension.
2222
- `pi-goal-x-py` / `pi_goal_x`: goal-planning extension.
2323
- `pi-dynamic-workflows-py` / `pi_dynamic_workflows`: workflow orchestration extension.
24+
- `pi-mcp-py` / `pi_mcp`: optional extension that connects the MCP servers an ACP client (an editor) passes (see [`packages/pi-mcp`](packages/pi-mcp/README.md)).
2425

2526
**Rust TUI** (beta — prebuilt binary in [GitHub Releases](https://github.com/zy1233/pi-python/releases)):
2627

@@ -36,6 +37,7 @@ This repository is a monorepo containing:
3637
| `pi-web-access-py` | **Beta** | [PyPI](https://pypi.org/project/pi-web-access-py/) |
3738
| `pi-goal-x-py` | **Beta** | [PyPI](https://pypi.org/project/pi-goal-x-py/) |
3839
| `pi-dynamic-workflows-py` | **Beta** | [PyPI](https://pypi.org/project/pi-dynamic-workflows-py/) |
40+
| `pi-mcp-py` | **Alpha** | [PyPI](https://pypi.org/project/pi-mcp-py/) (from 0.6.0) |
3941
| `zypi` (Rust TUI) | **Beta** | [GitHub Releases](https://github.com/zy1233/pi-python/releases) (Linux x86_64) |
4042

4143
## Features
@@ -65,6 +67,7 @@ This repository is a monorepo containing:
6567

6668
- Built-in coding tools (`pi_agent_core.coding_tools`): read / bash / edit / write / grep / find / ls with pi-faithful truncation notices and `details` payloads
6769
- LangChain `BaseTool` → `AgentTool` adapter (`from_langchain_tool`) — MCP tools via `langchain-mcp-adapters` work out of the box
70+
- The CLI itself has no MCP client. The optional `pi-mcp-py` extension connects the MCP servers that an ACP client passes in `session/new` (stdio, Streamable HTTP, SSE) and shows their tools as `mcp__<server>__<tool>`; without it, the agent says that their tools are not available in the session
6871

6972
**Coding Agent CLI** (Phase 4 — beta)
7073

@@ -100,6 +103,7 @@ pip install pi-agent-cli-lc # optional: ACP stdio / headless agent
100103
pip install pi-web-access-py # optional: web search / fetch extension
101104
pip install pi-goal-x-py # optional: goal planning extension
102105
pip install pi-dynamic-workflows-py # optional: workflow extension
106+
pip install pi-mcp-py # optional: MCP servers that the editor passes
103107

104108
# Optional LLM providers:
105109
pip install pi-agent-core-lc[openai] # ChatOpenAI
@@ -118,6 +122,7 @@ pip install -e "./packages/pi-agent-cli"
118122
pip install -e "./packages/pi-web-access"
119123
pip install -e "./packages/pi-goal-x"
120124
pip install -e "./packages/pi-dynamic-workflows"
125+
pip install -e "./packages/pi-mcp" # optional: MCP servers that the editor passes
121126
```
122127

123128
</details>
@@ -420,7 +425,7 @@ Use `.venv-test-real` for pytest real-LLM tests; the smoke script can run from t
420425
- **Phase 3.5 (tool ecosystem)** — done: all 7 built-in tools (read/bash/edit/write/grep/find/ls), group factories, and the LangChain `BaseTool` adapter ([design doc](docs/specs/2026-07-03-phase3.5-tool-ecosystem-design.md))
421426
- **Phase 4 (Coding Agent CLI — beta)** — done: `tui/` vendored grok-build fork (Apache-2.0); `pi_agent_cli` standard ACP agent; TUI binary `zypi`; `x.ai/*` vendor RPCs removed; session management (`/new` `/resume` `/quit`); headless `-p`; `config.toml` + Windows notes ([design doc](docs/specs/2026-08-25-phase4-coding-agent-cli-design.md), [Windows](docs/WINDOWS.md), [TUI guide](docs/TUI-AND-CODE-AGENT.md))
422427
- **Phase 5 (Coding Agent prompt engine)** — done: pi-aligned `build_system_prompt` + tool contributions + context files + `<available_skills>` ([design doc](docs/specs/2026-09-02-phase5-prompt-engine-design.md))
423-
- **Phase 6 (extended integrations)** — done: git workspace context in the system prompt; opt-in multi-provider live matrix. MCP stays a caller-side `BaseTool` import ([design doc](docs/specs/2026-09-24-phase6-extended-integrations-design.md))
428+
- **Phase 6 (extended integrations)** — done: git workspace context in the system prompt; opt-in multi-provider live matrix. MCP stays a caller-side `BaseTool` import ([design doc](docs/specs/2026-09-24-phase6-extended-integrations-design.md)); the optional `pi-mcp-py` extension (2026-10) connects the servers an editor passes, and the core, harness and CLI still hold no MCP client ([design doc](docs/specs/2026-10-23-mcp-extension-design.md))
424429
- **Phase 7 (extension packages)** — done: `pi-web-access-py`, `pi-goal-x-py`, `pi-dynamic-workflows-py` ([design doc](docs/specs/2026-09-22-phase7-extension-api-design.md))
425430
- **FrontierHarness Eval** — done: 30-task benchmark (21 Terminal-Bench + 9 DeepSWE); Windows + WSL Docker runtimes; side-by-side Pi comparison ([report](docs/benchmarks/FRONTIER-HARNESS-30-EVAL-REPORT.md))
426431

0 commit comments

Comments
 (0)