pi-codemcp 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,9 +4,11 @@ Fast, typed, sandboxed **Code Mode for every MCP server configured in Pi**.
4
4
 
5
5
  Instead of putting every upstream MCP tool definition into the model context, pi-codemcp gives the agent a small interface for discovery, execution, and reuse:
6
6
 
7
- - `codemcp_search` finds relevant tools and returns only their typed SDK stubs.
7
+ - `codemcp_search` ranks capabilities or pages through a compact inventory without loading full schemas.
8
+ - `codemcp_inspect` returns exact typed SDK stubs only for selected calls.
8
9
  - `codemcp_execute` runs one sandboxed Python call graph across one or many MCP servers.
9
10
  - `codemcp_save_chain` turns a repeated call graph into a reusable native Pi tool.
11
+ - `codemcp_manage_chains` lists chains or performs an explicitly confirmed enable, disable, revalidate, or delete.
10
12
 
11
13
  Intermediate results stay inside the sandbox. The model receives only the compact value returned by the program.
12
14
 
@@ -18,11 +20,12 @@ Cloudflare described a better pattern in [Code Mode: give agents an entire API i
18
20
 
19
21
  pi-codemcp applies that idea on the **client side** to the MCP servers you already use in Pi:
20
22
 
21
- 1. Search the combined catalog without loading every schema into context.
22
- 2. Type-check a compact Python plan before any upstream call happens.
23
- 3. Execute dependent or parallel calls without model round-trips between them.
24
- 4. Return only the final data the agent actually needs.
25
- 5. Save stable plans as native tools and reuse them without rewriting the call graph.
23
+ 1. Search the combined catalog or page through a compact inventory.
24
+ 2. Inspect exact schemas only for the calls selected for the task.
25
+ 3. Type-check a compact Python plan before any upstream call happens.
26
+ 4. Execute dependent or parallel calls without model round-trips between them.
27
+ 5. Return only the final data the agent actually needs.
28
+ 6. Save stable plans as native tools and reuse them without rewriting the call graph.
26
29
 
27
30
  That can make complex MCP workflows faster and substantially more token-efficient. Exact savings depend on the servers, schemas, model, and task.
28
31
 
@@ -39,6 +42,7 @@ pi-codemcp is deliberately opinionated about operational quality:
39
42
  - Time, memory, call count, and output size are bounded.
40
43
  - Failures are explicit; there are no silent retries or compatibility fallbacks.
41
44
  - Tool output is compact by default and expands with Pi's normal `Ctrl+O` UI.
45
+ - Bounded local telemetry uses fixed rollups rather than session event logs and appears in the `/codemcp` Stats tab.
42
46
 
43
47
  There is always room to make it faster and more reliable. If something is not working well, please report it rather than silently giving up on the extension.
44
48
 
@@ -68,6 +72,8 @@ Nested chains share the same deadline, cancellation signal, catalog snapshot, an
68
72
 
69
73
  New manifests default to project scope under `<project>/.pi/pi-codemcp/chains`; explicitly global chains live under `<agent-dir>/pi-codemcp/chains`. A project chain overrides a same-named global chain without deleting it. Manifests contain sandboxed code and schemas, never credentials or execution results. `/codemcp` labels both scopes and can revalidate, enable, disable, or delete chains.
70
74
 
75
+ There is deliberately no implicit “save last execution” state: the agent must submit the exact successfully tested code plus explicit input and output contracts. This keeps persistence reviewable and avoids saving the wrong attempt from a long session.
76
+
71
77
  ## Install
72
78
 
73
79
  ```bash
@@ -78,24 +84,136 @@ It reads Pi's existing `<agent-dir>/mcp.json` and supports stdio, Streamable HTT
78
84
 
79
85
  Package users do not need Python, uv, Bun, or just. A pinned uv binary bootstraps the locked Python 3.13 runtime under Pi's writable agent directory on first use; the first bootstrap needs network access unless already cached.
80
86
 
81
- ## Agent workflow
87
+ ## MCP configuration examples
88
+
89
+ `pi-codemcp` reads the same MCP config Pi uses. Either shape is accepted:
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "filesystem": {
95
+ "command": "npx",
96
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ or a root server map:
103
+
104
+ ```json
105
+ {
106
+ "linear": {
107
+ "type": "http",
108
+ "url": "https://mcp.linear.app/mcp",
109
+ "auth": "oauth"
110
+ },
111
+ "grafana": {
112
+ "type": "sse",
113
+ "url": "https://grafana.example.com/sse",
114
+ "headers": {
115
+ "authorization": "Bearer ${GRAFANA_MCP_TOKEN}"
116
+ }
117
+ },
118
+ "disabled-example": {
119
+ "command": "example-server",
120
+ "disabled": true
121
+ }
122
+ }
123
+ ```
124
+
125
+ For stdio servers, pi-codemcp passes a small safe base environment plus variables listed in `MY_PI_CHILD_ENV_ALLOWLIST` or `MY_PI_MCP_ENV_ALLOWLIST`. Explicit `env` values in `mcp.json` are also passed. Remote headers can reference allowed environment variables with `${NAME}`. Pi-only fields such as `directTools`, `lifecycle`, `idleTimeout`, `enabled`, and `disabled` are understood locally and are not forwarded to FastMCP.
126
+
127
+ ## Settings JSON
128
+
129
+ Settings live at `<agent-dir>/pi-codemcp/settings.json` and can also be edited in `/codemcp`:
130
+
131
+ ```json
132
+ {
133
+ "version": 2,
134
+ "backgroundWarmup": true,
135
+ "cacheTtlHours": 24,
136
+ "executionTimeoutSeconds": 30,
137
+ "toolTimeoutSeconds": 30,
138
+ "maxCalls": 50,
139
+ "resultLimitKiB": 16,
140
+ "outputLimitKiB": 50,
141
+ "disabledTools": {
142
+ "linear": ["delete_issue"]
143
+ }
144
+ }
145
+ ```
146
+
147
+ The Python sidecar enforces catalog cache TTL, execution timeout, per-tool timeout, max MCP calls, result size, and disabled-tool policy. The TypeScript Pi layer uses `backgroundWarmup` and `outputLimitKiB` for session warmup and rendered-output truncation; the sidecar still validates those fields so the settings file has one strict shared schema. Version-one files are migrated when loaded, and the removed `outputLineLimit` field is omitted on the next save.
82
148
 
83
- The agent searches for a capability, receives the complete stub it needs, and executes a compact plan:
149
+ ## Search and execute flow
150
+
151
+ The agent searches for a capability, inspects the selected exact stub when needed, and executes a compact plan:
84
152
 
85
153
  ```python
86
154
  issues = await linear.list_issues({"assignee": "me", "limit": 50})
87
155
  return {"count": len(issues), "ids": [issue["identifier"] for issue in issues]}
88
156
  ```
89
157
 
90
- Incomplete upstream schemas become recursive `JsonValue`, not `Any`; unknown values must be narrowed explicitly before typed use.
158
+ The same flow is available through the internal CLI for debugging:
159
+
160
+ ```bash
161
+ uv run --project sidecar --frozen -m sidecar.cli search "issues assigned to me"
162
+ uv run --project sidecar --frozen -m sidecar.cli execute --code-file plan.py
163
+ ```
164
+
165
+ A direct one-shot plan can call multiple servers without model round trips between calls:
166
+
167
+ ```bash
168
+ uv run --project sidecar --frozen -m sidecar.cli execute --code '
169
+ number = await alpha.get_number({"seed": 41})
170
+ saved = await beta.save_number({"value": number["value"]})
171
+ return {"number": number["value"], "identifier": saved["identifier"]}
172
+ '
173
+ ```
174
+
175
+ Incomplete upstream schemas become recursive `JsonValue`, not `Any`; unknown values must be narrowed explicitly before typed use. For unfamiliar outputs, `inspect_json(value, samples=2, max_depth=3)` returns a byte-bounded structural summary, cardinality, field sizes, and samples. Preflight type errors happen before any upstream call is made, and oversized final results fail explicitly with the same actionable inspection data.
176
+
177
+ ## Saved-chain CLI flow
178
+
179
+ Saved chains are JSON manifests with sandboxed code plus explicit input/output JSON Schemas. Project-scoped chains live under `<project>/.pi/pi-codemcp/chains`; global chains live under `<agent-dir>/pi-codemcp/chains`.
180
+
181
+ ```bash
182
+ uv run --project sidecar --frozen -m sidecar.cli chain save save_number \
183
+ --description "Fetch and save one generated number." \
184
+ --code 'number = await alpha.get_number({"seed": input["seed"]})
185
+ return await beta.save_number({"value": number["value"]})' \
186
+ --input-schema '{"type":"object","properties":{"seed":{"type":"integer"}},"required":["seed"],"additionalProperties":false}' \
187
+ --output-schema '{"type":"object","properties":{"saved":{"type":"boolean"},"identifier":{"type":"string"}},"required":["saved","identifier"],"additionalProperties":false}'
188
+
189
+ uv run --project sidecar --frozen -m sidecar.cli chain list
190
+ uv run --project sidecar --frozen -m sidecar.cli chain run save_number --input '{"seed":41}'
191
+ uv run --project sidecar --frozen -m sidecar.cli chain revalidate save_number --scope project
192
+ uv run --project sidecar --frozen -m sidecar.cli chain delete save_number --scope project
193
+ ```
194
+
195
+ Revalidation checks the saved code against the current enabled catalog. Deletion refuses to remove chains still referenced by other chains. Disabling a project chain does not fall back to a same-named global chain; project scope continues to shadow global scope until the project manifest is deleted.
196
+
197
+ ## Output and result normalization
198
+
199
+ When an upstream tool declares an output schema, pi-codemcp requires `structuredContent`, validates it, dumps it back to JSON-compatible values, and preserves declared structured string fields as strings. FastMCP-wrapped `result` strings are intentionally unwrapped and parsed because those wrappers commonly carry JSON payloads as text. When no output schema exists, single text responses that look like JSON objects, arrays, `null`, `true`, or `false` are normalized into native JSON values; non-JSON text remains a string.
200
+
201
+ Execution results report explicit stages:
202
+
203
+ - `preflight`: code did not run and no upstream call was made.
204
+ - `runtime`: the sandbox or an upstream call failed after execution started.
205
+ - `timeout` / `cancelled`: execution was stopped.
206
+ - `result`: the call graph completed, but the returned value exceeded `resultLimitKiB`.
207
+
208
+ Rendered Pi output is separately truncated by `outputLimitKiB`; the full oversized rendered value is not persisted.
91
209
 
92
210
  ## Safety and limits
93
211
 
94
- FastMCP owns MCP transports, runtime validation, and OAuth. [Pydantic Monty](https://github.com/pydantic/monty) type-checks and executes agent-written Python without host filesystem, environment, network, or subprocess access.
212
+ FastMCP owns MCP transports, runtime validation, and OAuth. [Pydantic Monty](https://github.com/pydantic/monty) type-checks and executes agent-written Python without host filesystem, environment, network, or subprocess access. Code Mode can only call the typed MCP tool and saved-chain facades exposed in the generated stubs.
95
213
 
96
- `/codemcp` configures servers, saved chains, per-tool policy, timeouts, call limits, output limits, cache TTL, and warmup. Server, chain, tool-policy, and setting toggles stay local and instantaneous until one `Ctrl+S` batch save/reload. Discovery, revalidation, and deletion remain explicit immediate actions. The sandbox also has a fixed memory ceiling; executions are serialized per Pi session. There are no automatic retries or cross-service rollback.
214
+ `/codemcp` configures servers, saved chains, per-tool policy, timeouts, call limits, output limits, cache TTL, and warmup, and shows bounded lifetime/recent telemetry in its Stats tab. Server, chain, tool-policy, and setting toggles stay local and instantaneous until one `Ctrl+S` batch save/reload. Discovery, revalidation, and deletion remain explicit immediate actions. The sandbox also has a fixed memory ceiling; executions are serialized per Pi session. There are no automatic retries or cross-service rollback.
97
215
 
98
- Enabled tools retain their upstream permissions. Saved chains never bypass server or per-tool policy and are checked against the current enabled catalog whenever they run.
216
+ Enabled tools retain their upstream permissions. Saved chains never bypass server or per-tool policy and are checked against the current enabled catalog whenever they run. Preflight safety does not make upstream tools transactional: if a later call fails after earlier calls succeeded, pi-codemcp does not roll those upstream side effects back.
99
217
 
100
218
  ## Something failed? Please open an issue
101
219
 
@@ -122,19 +240,32 @@ just check
122
240
  just release-check
123
241
  ```
124
242
 
243
+ Development and packaged runtime checks target Python 3.13. The sidecar metadata, `.python-version`, mypy, ty, and CI all align on that version.
244
+
125
245
  To test the checkout without loading an installed copy:
126
246
 
127
247
  ```bash
128
248
  pi -ne -e . --no-session
129
249
  ```
130
250
 
251
+ The sidecar also has a stable internal CLI for development, debugging, and future runtime adapters:
252
+
253
+ ```bash
254
+ uv run --project sidecar --frozen -m sidecar.cli serve --stdio
255
+ uv run --project sidecar --frozen -m sidecar.cli status --agent-dir ~/.pi/agent
256
+ uv run --project sidecar --frozen -m sidecar.cli search "linear issues"
257
+ uv run --project sidecar --frozen -m sidecar.cli execute --code-file plan.py
258
+ uv run --project sidecar --frozen -m sidecar.cli chain list
259
+ uv run --project sidecar --frozen -m sidecar.cli doctor --agent-dir ~/.pi/agent
260
+ ```
261
+
131
262
  `just check` runs lockfile checks, TypeScript, Biome, Bun tests, Ruff, mypy, ty, and pytest. `just release-check` additionally packs the npm artifact, installs it into a clean consumer directory, and runs the packaged sidecar without a system uv on `PATH`.
132
263
 
133
264
  ## Releases
134
265
 
135
266
  Release Please derives versions and release notes from Conventional Commit titles on `main`: `fix:` publishes a patch, `feat:` publishes a minor, and a `!` or `BREAKING CHANGE:` publishes a major. It maintains the release PR, `CHANGELOG.md`, `package.json`, version tag, and GitHub Release.
136
267
 
137
- Merging a release PR publishes the verified package to npm from `.github/workflows/release.yml` using npm trusted publishing and provenance. The publish job checks out the release tag and runs the package's full prepublish and clean-install gates before uploading it.
268
+ Merging a release PR publishes the verified package to npm from `.github/workflows/release.yml` using trusted publishing and provenance. Quality gates the exact merge commit on Linux, macOS, and Windows; the publish job checks out its release tag, packs it with Bun, and uses npm only for the final OIDC-authenticated upload.
138
269
 
139
270
  ## Credits
140
271
 
@@ -17,6 +17,7 @@ import {
17
17
  type ServerModalState,
18
18
  serverStatesFromStatus,
19
19
  showServerManagerModal,
20
+ statsStateFromSnapshot,
20
21
  } from "../src/modal.js";
21
22
  import { type CodeMcpSettings, saveCodeMcpSettings } from "../src/settings.js";
22
23
  import { registerCodeMcpTools } from "../src/tools.js";
@@ -32,10 +33,11 @@ export function createCodeMcpExtension(options: SidecarClientOptions = {}) {
32
33
  handler: async (_args, ctx) => {
33
34
  try {
34
35
  bindProjectChainScope(ctx, lifecycle, chains);
35
- const [status, savedChains, settings] = await Promise.all([
36
+ const [status, savedChains, settings, stats] = await Promise.all([
36
37
  lifecycle.request("status", {}),
37
38
  chains.list(),
38
39
  Promise.resolve(lifecycle.loadSettings()),
40
+ lifecycle.request("stats", {}),
39
41
  ]);
40
42
  const servers = serverStatesFromStatus(status);
41
43
  if (ctx.mode !== "tui") {
@@ -47,6 +49,7 @@ export function createCodeMcpExtension(options: SidecarClientOptions = {}) {
47
49
  servers,
48
50
  chains: chainStatesFromViews(savedChains),
49
51
  settings,
52
+ stats: statsStateFromSnapshot(stats),
50
53
  onDiscover: async (server) =>
51
54
  requireServerStatus(
52
55
  await lifecycle.request("discover", { server: server.name }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codemcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Typed, sandboxed Code Mode access to configured MCP servers for Pi",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.10",
@@ -41,14 +41,15 @@
41
41
  "lint": "biome check .",
42
42
  "prepublishOnly": "just check && just release-check",
43
43
  "test": "bun run test:ts && bun run test:python",
44
- "test:python": "uv run --project sidecar pytest tests/python -q",
44
+ "test:python": "uv run --project sidecar pytest -c sidecar/pyproject.toml tests/python -q",
45
45
  "test:ts": "bun test tests/typescript",
46
46
  "typecheck": "tsc --noEmit"
47
47
  },
48
48
  "pi": {
49
49
  "extensions": [
50
50
  "./extensions/index.ts"
51
- ]
51
+ ],
52
+ "image": "https://raw.githubusercontent.com/yolonir/pi-codemcp/main/media/preview.png"
52
53
  },
53
54
  "dependencies": {
54
55
  "@modelcontextprotocol/sdk": "1.29.0"
@@ -79,11 +80,11 @@
79
80
  }
80
81
  },
81
82
  "devDependencies": {
82
- "@biomejs/biome": "2.5.3",
83
- "@earendil-works/pi-agent-core": "0.80.7",
84
- "@earendil-works/pi-ai": "0.80.7",
85
- "@earendil-works/pi-coding-agent": "0.80.7",
86
- "@earendil-works/pi-tui": "0.80.7",
83
+ "@biomejs/biome": "2.5.4",
84
+ "@earendil-works/pi-agent-core": "0.80.10",
85
+ "@earendil-works/pi-ai": "0.80.10",
86
+ "@earendil-works/pi-coding-agent": "0.80.10",
87
+ "@earendil-works/pi-tui": "0.80.10",
87
88
  "@types/bun": "1.3.14",
88
89
  "typebox": "1.3.6",
89
90
  "typescript": "5.9.3"
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import hashlib
4
+ import os
4
5
  import time
5
6
  from contextlib import suppress
6
7
  from typing import TYPE_CHECKING, Literal
@@ -68,7 +69,7 @@ class CatalogCache:
68
69
  with suppress(OSError):
69
70
  self.directory.chmod(0o700)
70
71
  path = self._path(server_name)
71
- temporary = path.with_suffix(".tmp")
72
+ temporary = self.directory / f".{path.stem}.{os.getpid()}.{time.time_ns()}.tmp"
72
73
  entry = CachedServerCatalog(
73
74
  server_name=server_name,
74
75
  config_fingerprint=config_fingerprint,