pi-codemcp 1.4.0 → 1.5.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 +92 -196
- package/extensions/index.ts +63 -5
- package/package.json +3 -2
- package/sidecar/executor.py +14 -1
- package/sidecar/gateway.py +29 -5
- package/sidecar/settings.py +7 -0
- package/src/execution-rendering.ts +16 -0
- package/src/jev-router.ts +323 -0
- package/src/lifecycle.ts +8 -2
- package/src/mcp-client.ts +15 -1
- package/src/modal.ts +28 -5
- package/src/settings.ts +24 -4
- package/src/tools.ts +132 -0
package/README.md
CHANGED
|
@@ -1,94 +1,71 @@
|
|
|
1
1
|
# pi-codemcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> [!IMPORTANT]
|
|
4
|
+
> **Something failed? Please open an issue.**
|
|
5
|
+
>
|
|
6
|
+
> Please do not assume your failure is too specific or not worth reporting. Platform differences, strange schemas, slow startup, confusing rendering, OAuth problems, and rough edges are exactly the reports that make this project better.
|
|
7
|
+
>
|
|
8
|
+
> Open an issue at <https://github.com/yolonir/pi-codemcp/issues>, or use **Extension is broken!** in `/codemcp` → Settings to ask the agent to investigate and prepare one.
|
|
9
|
+
>
|
|
10
|
+
> You can ask your coding agent to do the work:
|
|
11
|
+
>
|
|
12
|
+
> ```text
|
|
13
|
+
> Reproduce this pi-codemcp problem, redact all credentials and private data,
|
|
14
|
+
> collect the pi-codemcp version, Pi version, OS/architecture, MCP transport,
|
|
15
|
+
> minimal configuration shape, exact error, and relevant logs, then open a
|
|
16
|
+
> GitHub issue at https://github.com/yolonir/pi-codemcp/issues.
|
|
17
|
+
> ```
|
|
18
|
+
>
|
|
19
|
+
> If the agent cannot create the issue, ask it to prepare the title and body for you. I would much rather receive an incomplete report than have someone hit a problem, abandon the package, and never say anything. I will read the issues and work through them.
|
|
20
|
+
|
|
21
|
+
Typed, sandboxed **Code Mode for your MCP servers in Pi**.
|
|
22
|
+
|
|
23
|
+
The agent discovers the tools it needs, writes a Python program, and runs dependent or parallel calls across MCP servers. Intermediate data stays in the sandbox; only the program's compact return value goes back to the model.
|
|
4
24
|
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
9
|
-
- `codemcp_execute` runs one sandboxed Python call graph across one or many MCP servers.
|
|
10
|
-
- `codemcp_edit` applies one exact replacement to the previous execution and reruns it without resending the full code.
|
|
11
|
-
- `codemcp_save_chain` turns a repeated call graph into a reusable native Pi tool.
|
|
12
|
-
- `codemcp_manage_chains` lists chains or performs an explicitly confirmed enable, disable, revalidate, or delete.
|
|
13
|
-
|
|
14
|
-
Intermediate results stay inside the sandbox. The model receives only the compact value returned by the program.
|
|
15
|
-
|
|
16
|
-
## Why Code Mode?
|
|
17
|
-
|
|
18
|
-
MCP has an uncomfortable scaling property: the more tools an agent can use, the more tool schemas compete with the actual task for context. Multi-step work also tends to bounce every intermediate result through the model, adding tokens, latency, and opportunities for mistakes.
|
|
19
|
-
|
|
20
|
-
Cloudflare described a better pattern in [Code Mode: give agents an entire API in 1,000 tokens](https://blog.cloudflare.com/code-mode-mcp/): expose a small search-and-execute surface, let the model write code against a typed SDK, and execute that code in a sandbox. Their work reports a fixed tool footprint and dramatic context savings for very large APIs. The open-source implementation lives in [`@cloudflare/codemode`](https://github.com/cloudflare/agents/tree/main/packages/codemode).
|
|
21
|
-
|
|
22
|
-
pi-codemcp applies that idea on the **client side** to the MCP servers you already use in Pi:
|
|
23
|
-
|
|
24
|
-
1. Search the combined catalog or page through a compact inventory.
|
|
25
|
-
2. Inspect exact schemas only for the calls selected for the task.
|
|
26
|
-
3. Type-check a compact Python plan before any upstream call happens.
|
|
27
|
-
4. Execute dependent or parallel calls without model round-trips between them.
|
|
28
|
-
5. Return only the final data the agent actually needs.
|
|
29
|
-
6. Save stable plans as native tools and reuse them without rewriting the call graph.
|
|
30
|
-
|
|
31
|
-
That can make complex MCP workflows faster and substantially more token-efficient. Exact savings depend on the servers, schemas, model, and task.
|
|
32
|
-
|
|
33
|
-
## Built for daily use, not a demo
|
|
34
|
-
|
|
35
|
-
I built this because I care a lot about software that is genuinely fast, efficient, and predictable enough to use every day. Too many AI extensions look good in a short demo but become slow, noisy, fragile, or effectively unusable in real work.
|
|
36
|
-
|
|
37
|
-
pi-codemcp is deliberately opinionated about operational quality:
|
|
25
|
+
## Install
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- Tool catalogs are cached per server and invalidated independently.
|
|
43
|
-
- Agent-written code is type-checked before execution.
|
|
44
|
-
- Time, memory, call count, and output size are bounded.
|
|
45
|
-
- Failures are explicit; there are no silent retries or compatibility fallbacks.
|
|
46
|
-
- Tool output is compact by default and expands with Pi's normal `Ctrl+O` UI.
|
|
47
|
-
- Bounded local telemetry uses fixed rollups rather than session event logs and appears in the `/codemcp` Stats tab.
|
|
27
|
+
```bash
|
|
28
|
+
pi install npm:pi-codemcp
|
|
29
|
+
```
|
|
48
30
|
|
|
49
|
-
|
|
31
|
+
Reads `<agent-dir>/mcp.json`. Supports stdio, Streamable HTTP, SSE, bearer authentication, and FastMCP-managed OAuth.
|
|
50
32
|
|
|
51
|
-
|
|
33
|
+
Open **`/codemcp`** to manage servers, individual tools, saved chains, settings, and usage stats. Changes are saved immediately. Tool output expands with Pi's normal `Ctrl+O`.
|
|
52
34
|
|
|
53
|
-
|
|
35
|
+
No separate Python, uv, Bun, or just installation is needed. The package bootstraps a locked Python 3.13 runtime on first use; this needs network access unless already cached. Pi startup does not wait for MCP connections.
|
|
54
36
|
|
|
55
|
-
|
|
37
|
+
## How it works
|
|
56
38
|
|
|
57
39
|
```text
|
|
58
|
-
|
|
59
|
-
chains.weekly_digest(...) # typed call inside Code Mode
|
|
40
|
+
Discover tools → load selected contracts → execute a program → return compact data
|
|
60
41
|
```
|
|
61
42
|
|
|
62
|
-
|
|
43
|
+
| Tool | Purpose |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `codemcp_search` | Search capabilities or browse the compact catalog. |
|
|
46
|
+
| `codemcp_route` | Optional Jev routing: select tools, roles, and composition guidance. |
|
|
47
|
+
| `codemcp_inspect` | Load exact typed SDK contracts for selected calls. |
|
|
48
|
+
| `codemcp_execute` | Type-check and run a sandboxed Python program. |
|
|
49
|
+
| `codemcp_edit` | Patch the previous program and rerun it. |
|
|
50
|
+
| `codemcp_save_chain` | Save a tested program as a reusable tool, with user approval. |
|
|
51
|
+
| `codemcp_manage_chains` | List chains; explicitly confirm enable, disable, revalidate, or delete. |
|
|
63
52
|
|
|
64
|
-
|
|
65
|
-
issues = await chains.collect_open_issues({"assignee": input["assignee"]})
|
|
66
|
-
result = await slack.post_message({
|
|
67
|
-
"channel": input["channel"],
|
|
68
|
-
"text": issues["summary"],
|
|
69
|
-
})
|
|
70
|
-
return {"posted": result["ok"], "count": issues["count"]}
|
|
71
|
-
```
|
|
53
|
+
Independent calls can use `asyncio.gather`; dependent calls pass earlier outputs into later inputs. Filtering and aggregation happen inside the sandbox, without model round trips for every intermediate result.
|
|
72
54
|
|
|
73
|
-
|
|
55
|
+
## Optional Jev routing
|
|
74
56
|
|
|
75
|
-
|
|
57
|
+
Provide `TYPESAFE_API_KEY`, then set **Enable Jev → true** in `/codemcp` → Settings. This replaces `codemcp_search` with `codemcp_route` as the agent's discovery tool.
|
|
76
58
|
|
|
77
|
-
|
|
59
|
+
1. The agent calls `codemcp_route` with a short `intent`, such as “read staging logs to diagnose the approval error.” The extension includes the original user request and up to three preceding user/assistant messages as context. A changed subtask can be routed again with a new intent.
|
|
60
|
+
2. Enabled tool names and descriptions go to Jev in parallel chunks of up to 40. Jev scores relevance to that subtask (including prerequisites), assigns workflow roles, and checks whether an intermediate model decision or user approval is needed.
|
|
61
|
+
3. CodeMCP ranks and filters the answers, selects at most eight tools, derives composition guidance, and fetches their exact typed contracts.
|
|
62
|
+
4. The agent writes the actual `codemcp_execute` program, keeping a checkpoint between stages when needed.
|
|
78
63
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
pi install npm:pi-codemcp
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
It reads Pi's existing `<agent-dir>/mcp.json` and supports stdio, Streamable HTTP, SSE, bearer authentication, and FastMCP-managed OAuth. Open `/codemcp` to manage servers, per-tool policy, saved chains, cache, and execution limits.
|
|
64
|
+
Jev does not execute tools or block ordinary messages. **Routing sends the intent, your request, recent context, and tool descriptions to TypeSafe**, so it is opt-in. Without a key, local search stays active; a failed route enables search as a fallback. The SDK also honors `TYPESAFE_BASE_URL` and `TYPESAFE_DEFAULT_MODEL`.
|
|
86
65
|
|
|
87
|
-
|
|
66
|
+
## MCP configuration
|
|
88
67
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
`pi-codemcp` reads the same MCP config Pi uses. Either shape is accepted:
|
|
68
|
+
Use an `mcpServers` object or a root-level server map:
|
|
92
69
|
|
|
93
70
|
```json
|
|
94
71
|
{
|
|
@@ -96,43 +73,29 @@ Package users do not need Python, uv, Bun, or just. A pinned uv binary bootstrap
|
|
|
96
73
|
"filesystem": {
|
|
97
74
|
"command": "npx",
|
|
98
75
|
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
76
|
+
},
|
|
77
|
+
"linear": {
|
|
78
|
+
"type": "http",
|
|
79
|
+
"url": "https://mcp.linear.app/mcp",
|
|
80
|
+
"auth": "oauth"
|
|
99
81
|
}
|
|
100
82
|
}
|
|
101
83
|
}
|
|
102
84
|
```
|
|
103
85
|
|
|
104
|
-
or a
|
|
105
|
-
|
|
106
|
-
```json
|
|
107
|
-
{
|
|
108
|
-
"linear": {
|
|
109
|
-
"type": "http",
|
|
110
|
-
"url": "https://mcp.linear.app/mcp",
|
|
111
|
-
"auth": "oauth"
|
|
112
|
-
},
|
|
113
|
-
"grafana": {
|
|
114
|
-
"type": "sse",
|
|
115
|
-
"url": "https://grafana.example.com/sse",
|
|
116
|
-
"headers": {
|
|
117
|
-
"authorization": "Bearer ${GRAFANA_MCP_TOKEN}"
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
"disabled-example": {
|
|
121
|
-
"command": "example-server",
|
|
122
|
-
"disabled": true
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
```
|
|
86
|
+
Set `disabled: true` or `enabled: false` to disable a server. For stdio, only a safe base environment, comma-separated variables in `MY_PI_CHILD_ENV_ALLOWLIST` / `MY_PI_MCP_ENV_ALLOWLIST`, and explicit server `env` values are passed through. Remote `headers` can reference allowed environment variables with `${NAME}`.
|
|
126
87
|
|
|
127
|
-
|
|
88
|
+
## Settings
|
|
128
89
|
|
|
129
|
-
|
|
90
|
+
Edit settings in `/codemcp` or `<agent-dir>/pi-codemcp/settings.json`. Defaults: 30-second execution and tool timeouts, 50 upstream calls, 16 KiB returned data, 50 KiB rendered output, and a 24-hour catalog cache.
|
|
130
91
|
|
|
131
|
-
|
|
92
|
+
<details>
|
|
93
|
+
<summary>Settings JSON</summary>
|
|
132
94
|
|
|
133
95
|
```json
|
|
134
96
|
{
|
|
135
97
|
"version": 2,
|
|
98
|
+
"jevEnabled": false,
|
|
136
99
|
"backgroundWarmup": true,
|
|
137
100
|
"cacheTtlHours": 24,
|
|
138
101
|
"executionTimeoutSeconds": 30,
|
|
@@ -146,135 +109,68 @@ Settings live at `<agent-dir>/pi-codemcp/settings.json` and can also be edited i
|
|
|
146
109
|
}
|
|
147
110
|
```
|
|
148
111
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
## Search and execute flow
|
|
152
|
-
|
|
153
|
-
The agent searches for a capability, inspects the selected exact stub when needed, and executes a compact plan. Unscoped searches discover stale or missing server catalogs independently: available servers still return results, while `discovery_failures` explicitly reports unavailable servers. Server-scoped searches remain fail-fast.
|
|
154
|
-
|
|
155
|
-
```python
|
|
156
|
-
issues = await linear.list_issues({"assignee": "me", "limit": 50})
|
|
157
|
-
return {"count": len(issues), "ids": [issue["identifier"] for issue in issues]}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
The same flow is available through the internal CLI for debugging:
|
|
112
|
+
Older settings are migrated on load, including `discoveryMode` → `jevEnabled`.
|
|
161
113
|
|
|
162
|
-
|
|
163
|
-
uv run --project sidecar --frozen -m sidecar.cli search "issues assigned to me"
|
|
164
|
-
uv run --project sidecar --frozen -m sidecar.cli execute --code-file plan.py
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
A direct one-shot plan can call multiple servers without model round trips between calls:
|
|
114
|
+
</details>
|
|
168
115
|
|
|
169
|
-
|
|
170
|
-
uv run --project sidecar --frozen -m sidecar.cli execute --code '
|
|
171
|
-
number = await alpha.get_number({"seed": 41})
|
|
172
|
-
saved = await beta.save_number({"value": number["value"]})
|
|
173
|
-
return {"number": number["value"], "identifier": saved["identifier"]}
|
|
174
|
-
'
|
|
175
|
-
```
|
|
116
|
+
## Saved chains
|
|
176
117
|
|
|
177
|
-
|
|
118
|
+
With your approval, a successfully executed program can be saved with explicit input/output JSON Schemas. One manifest exposes both:
|
|
178
119
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
## Saved-chain CLI flow
|
|
184
|
-
|
|
185
|
-
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`.
|
|
186
|
-
|
|
187
|
-
```bash
|
|
188
|
-
uv run --project sidecar --frozen -m sidecar.cli chain save save_number \
|
|
189
|
-
--description "Fetch and save one generated number." \
|
|
190
|
-
--code 'number = await alpha.get_number({"seed": input["seed"]})
|
|
191
|
-
return await beta.save_number({"value": number["value"]})' \
|
|
192
|
-
--input-schema '{"type":"object","properties":{"seed":{"type":"integer"}},"required":["seed"],"additionalProperties":false}' \
|
|
193
|
-
--output-schema '{"type":"object","properties":{"saved":{"type":"boolean"},"identifier":{"type":"string"}},"required":["saved","identifier"],"additionalProperties":false}'
|
|
194
|
-
|
|
195
|
-
uv run --project sidecar --frozen -m sidecar.cli chain list
|
|
196
|
-
uv run --project sidecar --frozen -m sidecar.cli chain run save_number --input '{"seed":41}'
|
|
197
|
-
uv run --project sidecar --frozen -m sidecar.cli chain revalidate save_number --scope project
|
|
198
|
-
uv run --project sidecar --frozen -m sidecar.cli chain delete save_number --scope project
|
|
120
|
+
```text
|
|
121
|
+
mcp_chain_weekly_digest(...) # native Pi tool
|
|
122
|
+
chains.weekly_digest(...) # typed call inside another CodeMCP program
|
|
199
123
|
```
|
|
200
124
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
## Output and result normalization
|
|
125
|
+
Chains can call MCP tools or other chains. Nested calls share time and call budgets; inputs and outputs are validated. Changed dependency contracts mark chains stale for revalidation.
|
|
204
126
|
|
|
205
|
-
|
|
127
|
+
- **Project:** `<project>/.pi/pi-codemcp/chains` (activated in trusted projects).
|
|
128
|
+
- **Global:** `<agent-dir>/pi-codemcp/chains`, when explicitly requested.
|
|
206
129
|
|
|
207
|
-
|
|
130
|
+
Project chains override same-named global chains, even when disabled. Manifests store code and schemas, not credentials or execution results. Manage them through `/codemcp`.
|
|
208
131
|
|
|
209
|
-
|
|
210
|
-
- `runtime`: the sandbox or an upstream call failed after execution started.
|
|
211
|
-
- `timeout` / `cancelled`: execution was stopped.
|
|
212
|
-
- `result`: the call graph completed, but the returned value exceeded `resultLimitKiB`.
|
|
132
|
+
## Execution and safety
|
|
213
133
|
|
|
214
|
-
|
|
134
|
+
[FastMCP](https://github.com/jlowin/fastmcp) handles transports, validation, and OAuth. [Pydantic Monty](https://github.com/pydantic/monty) type-checks and executes Python without host filesystem, environment, network, or subprocess access. External access is only through the exposed MCP and saved-chain calls.
|
|
215
135
|
|
|
216
|
-
|
|
136
|
+
- Type errors stop execution **before any upstream call**. Time, memory, call count, and output size are bounded.
|
|
137
|
+
- Enabled tools retain their upstream permissions. Chains cannot bypass server or per-tool policy.
|
|
138
|
+
- No automatic call retries or cross-service rollback: a later failure does not undo earlier side effects.
|
|
139
|
+
- `codemcp_edit` reruns the **whole program**, including upstream calls; it is not a continuation.
|
|
140
|
+
- Unscoped search returns available results plus explicit `discovery_failures`; server-scoped search fails if that server is unavailable.
|
|
217
141
|
|
|
218
|
-
|
|
142
|
+
<details>
|
|
143
|
+
<summary>Working with unknown or oversized results</summary>
|
|
219
144
|
|
|
220
|
-
|
|
145
|
+
Incomplete schemas use recursive `JsonValue`, not `Any`. Use the prebound `expect_object`, `expect_list`, `expect_string`, and `expect_integer` helpers to narrow values. `inspect_json(value, samples=2, max_depth=3)` returns a bounded structural summary.
|
|
221
146
|
|
|
222
|
-
|
|
147
|
+
Oversized results fail explicitly. If the value fits the in-memory refinement cache, the error includes a `result_ref`. Pass it as `inputRef` in a follow-up `codemcp_execute` to filter the retained value as `input` without repeating upstream calls. References expire after five minutes and are valid only in the originating sidecar.
|
|
223
148
|
|
|
224
|
-
|
|
149
|
+
Failures distinguish `preflight`, `runtime`, `timeout`, `cancelled`, and oversized `result` stages. Rendered output has a separate `outputLimitKiB` cap.
|
|
225
150
|
|
|
226
|
-
|
|
151
|
+
</details>
|
|
227
152
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
You can ask your coding agent to do the work:
|
|
231
|
-
|
|
232
|
-
```text
|
|
233
|
-
Reproduce this pi-codemcp problem, redact all credentials and private data,
|
|
234
|
-
collect the pi-codemcp version, Pi version, OS/architecture, MCP transport,
|
|
235
|
-
minimal configuration shape, exact error, and relevant logs, then open a
|
|
236
|
-
GitHub issue at https://github.com/yolonir/pi-codemcp/issues.
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
If the agent cannot create the issue, ask it to prepare the title and body for you. I would much rather receive an incomplete report than have someone hit a problem, abandon the package, and never say anything. I will read the issues and work through them.
|
|
240
|
-
|
|
241
|
-
## Local development
|
|
242
|
-
|
|
243
|
-
```bash
|
|
244
|
-
just init
|
|
245
|
-
just check
|
|
246
|
-
just release-check
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
Development and packaged runtime checks target Python 3.13. The sidecar metadata, `.python-version`, mypy, ty, and CI all align on that version.
|
|
250
|
-
|
|
251
|
-
To test the checkout without loading an installed copy:
|
|
153
|
+
## Development
|
|
252
154
|
|
|
253
155
|
```bash
|
|
156
|
+
just init # locked dependencies and hooks
|
|
157
|
+
just check # locks, lint, types, and tests
|
|
158
|
+
just release-check # also verify the packed clean-install path
|
|
254
159
|
pi -ne -e . --no-session
|
|
255
160
|
```
|
|
256
161
|
|
|
257
|
-
|
|
162
|
+
Uses Bun and uv with Python 3.13. For sidecar debugging:
|
|
258
163
|
|
|
259
164
|
```bash
|
|
260
|
-
uv run --project sidecar --frozen -m sidecar.cli
|
|
261
|
-
uv run --project sidecar --frozen -m sidecar.cli status --agent-dir ~/.pi/agent
|
|
165
|
+
uv run --project sidecar --frozen -m sidecar.cli doctor --agent-dir ~/.pi/agent
|
|
262
166
|
uv run --project sidecar --frozen -m sidecar.cli search "linear issues"
|
|
263
167
|
uv run --project sidecar --frozen -m sidecar.cli execute --code-file plan.py
|
|
264
|
-
uv run --project sidecar --frozen -m sidecar.cli chain list
|
|
265
|
-
uv run --project sidecar --frozen -m sidecar.cli doctor --agent-dir ~/.pi/agent
|
|
266
168
|
```
|
|
267
169
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
## Releases
|
|
271
|
-
|
|
272
|
-
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.
|
|
273
|
-
|
|
274
|
-
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.
|
|
170
|
+
Conventional Commit titles drive Release Please. Feature PRs do not publish; merging the release PR triggers the verified npm release.
|
|
275
171
|
|
|
276
172
|
## Credits
|
|
277
173
|
|
|
278
|
-
|
|
174
|
+
Inspired by [Cloudflare's Code Mode](https://blog.cloudflare.com/code-mode-mcp/). Independent implementation for Pi using FastMCP and Pydantic Monty.
|
|
279
175
|
|
|
280
176
|
MIT
|
package/extensions/index.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
2
3
|
import {
|
|
3
4
|
CONFIG_DIR_NAME,
|
|
4
5
|
type ExtensionAPI,
|
|
5
6
|
type ExtensionCommandContext,
|
|
6
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { TypeSafeClient } from "@typesafe-ai/sdk";
|
|
7
9
|
import { newCodeMcpTraceId, SavedChainManager } from "../src/chains.js";
|
|
8
10
|
import { setMcpServerEnabled } from "../src/config.js";
|
|
9
11
|
import { summarizeError } from "../src/errors.js";
|
|
12
|
+
import { JevRouter } from "../src/jev-router.js";
|
|
13
|
+
import { readJsonObject, writeJsonObjectAtomically } from "../src/json-file.js";
|
|
10
14
|
import { CodeMcpLifecycle } from "../src/lifecycle.js";
|
|
11
15
|
import type { SidecarClientOptions } from "../src/mcp-client.js";
|
|
12
16
|
import {
|
|
@@ -22,13 +26,21 @@ import {
|
|
|
22
26
|
setEditableSetting,
|
|
23
27
|
setToolEnabled,
|
|
24
28
|
} from "../src/settings.js";
|
|
25
|
-
import { registerCodeMcpTools } from "../src/tools.js";
|
|
29
|
+
import { registerCodeMcpTools, registerJevRouteTool } from "../src/tools.js";
|
|
26
30
|
|
|
27
|
-
export
|
|
31
|
+
export interface CodeMcpExtensionOptions extends SidecarClientOptions {
|
|
32
|
+
jevClient?: TypeSafeClient;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createCodeMcpExtension(options: CodeMcpExtensionOptions = {}) {
|
|
28
36
|
return function codeMcpExtension(pi: ExtensionAPI): void {
|
|
29
|
-
const
|
|
37
|
+
const { jevClient, ...sidecarOptions } = options;
|
|
38
|
+
const lifecycle = new CodeMcpLifecycle(sidecarOptions);
|
|
30
39
|
const chains = new SavedChainManager(pi, lifecycle);
|
|
40
|
+
const routerClient = jevClient ?? createJevClient();
|
|
41
|
+
const jevRouter = routerClient ? new JevRouter(lifecycle, routerClient) : undefined;
|
|
31
42
|
registerCodeMcpTools(pi, lifecycle, chains);
|
|
43
|
+
registerJevRouteTool(pi, () => jevRouter);
|
|
32
44
|
|
|
33
45
|
pi.registerCommand("codemcp", {
|
|
34
46
|
description: "Manage CodeMCP servers, saved chains, tools, and settings",
|
|
@@ -71,7 +83,11 @@ export function createCodeMcpExtension(options: SidecarClientOptions = {}) {
|
|
|
71
83
|
onSetSetting: async (key, value) => {
|
|
72
84
|
const updated = setEditableSetting(lifecycle.loadSettings(), key, value);
|
|
73
85
|
saveCodeMcpSettings(lifecycle.settingsPath, updated);
|
|
74
|
-
await lifecycle.request("reload_settings", {});
|
|
86
|
+
if (key !== "jevEnabled") await lifecycle.request("reload_settings", {});
|
|
87
|
+
setDiscoveryTools(pi, updated.jevEnabled && jevRouter !== undefined);
|
|
88
|
+
if (updated.jevEnabled && !jevRouter) {
|
|
89
|
+
ctx.ui.notify("Jev mode requires TYPESAFE_API_KEY; using local search", "warning");
|
|
90
|
+
}
|
|
75
91
|
return updated;
|
|
76
92
|
},
|
|
77
93
|
onSetChainEnabled: async (chain, enabled) => {
|
|
@@ -97,6 +113,11 @@ export function createCodeMcpExtension(options: SidecarClientOptions = {}) {
|
|
|
97
113
|
});
|
|
98
114
|
|
|
99
115
|
pi.on("session_start", (_event, ctx) => {
|
|
116
|
+
try {
|
|
117
|
+
showChangelogOnce(ctx, join(dirname(lifecycle.settingsPath), "changelog.json"));
|
|
118
|
+
} catch (error) {
|
|
119
|
+
ctx.ui.notify(`CodeMCP changelog failed: ${summarizeError(error)}`, "warning");
|
|
120
|
+
}
|
|
100
121
|
bindProjectChainScope(ctx, lifecycle, chains);
|
|
101
122
|
chains.activatePersisted();
|
|
102
123
|
for (const error of chains.startupErrors) ctx.ui.notify(error, "warning");
|
|
@@ -107,6 +128,11 @@ export function createCodeMcpExtension(options: SidecarClientOptions = {}) {
|
|
|
107
128
|
ctx.ui.notify(`CodeMCP settings failed: ${summarizeError(error)}`, "warning");
|
|
108
129
|
return;
|
|
109
130
|
}
|
|
131
|
+
const useJev = settings.jevEnabled && jevRouter !== undefined;
|
|
132
|
+
setDiscoveryTools(pi, useJev);
|
|
133
|
+
if (settings.jevEnabled && !jevRouter) {
|
|
134
|
+
ctx.ui.notify("Jev mode requires TYPESAFE_API_KEY; using local search", "warning");
|
|
135
|
+
}
|
|
110
136
|
if (!settings.backgroundWarmup) return;
|
|
111
137
|
void lifecycle.warmup().catch((error: unknown) => {
|
|
112
138
|
ctx.ui.notify(`CodeMCP background warmup failed: ${summarizeError(error)}`, "warning");
|
|
@@ -154,6 +180,38 @@ export async function setServerEnabledFromManager(
|
|
|
154
180
|
|
|
155
181
|
export default createCodeMcpExtension();
|
|
156
182
|
|
|
183
|
+
const CHANGELOG_ID = "jev-routing-v1";
|
|
184
|
+
const CHANGELOG_MESSAGE =
|
|
185
|
+
"pi-codemcp update: optional Jev routing is now available! Enable Jev in /codemcp → Settings to let Jev select and compose MCP calls, it's pretty cool";
|
|
186
|
+
|
|
187
|
+
export function showChangelogOnce(
|
|
188
|
+
ctx: Pick<ExtensionCommandContext, "mode" | "ui">,
|
|
189
|
+
path: string,
|
|
190
|
+
): void {
|
|
191
|
+
if (ctx.mode !== "tui") return;
|
|
192
|
+
const state = existsSync(path) ? readJsonObject(path, "CodeMCP changelog state") : {};
|
|
193
|
+
if (state.lastSeen === CHANGELOG_ID) return;
|
|
194
|
+
ctx.ui.notify(CHANGELOG_MESSAGE, "info");
|
|
195
|
+
writeJsonObjectAtomically(path, { lastSeen: CHANGELOG_ID });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function createJevClient(): TypeSafeClient | undefined {
|
|
199
|
+
return process.env.TYPESAFE_API_KEY?.trim() ? new TypeSafeClient() : undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function setDiscoveryTools(pi: ExtensionAPI, useJev: boolean): void {
|
|
203
|
+
const selected = useJev ? "codemcp_route" : "codemcp_search";
|
|
204
|
+
const current = pi.getActiveTools();
|
|
205
|
+
if (
|
|
206
|
+
current.includes(selected) &&
|
|
207
|
+
!current.includes(useJev ? "codemcp_search" : "codemcp_route")
|
|
208
|
+
) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const active = current.filter((name) => name !== "codemcp_search" && name !== "codemcp_route");
|
|
212
|
+
pi.setActiveTools([...active, selected]);
|
|
213
|
+
}
|
|
214
|
+
|
|
157
215
|
export async function promptForProblemReport(
|
|
158
216
|
pi: Pick<ExtensionAPI, "sendUserMessage">,
|
|
159
217
|
ctx: Pick<ExtensionCommandContext, "ui">,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-codemcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"image": "https://raw.githubusercontent.com/yolonir/pi-codemcp/main/media/preview.png"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@modelcontextprotocol/sdk": "1.29.0"
|
|
55
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
56
|
+
"@typesafe-ai/sdk": "0.6.0"
|
|
56
57
|
},
|
|
57
58
|
"optionalDependencies": {
|
|
58
59
|
"@manzt/uv-darwin-arm64": "0.8.13",
|
package/sidecar/executor.py
CHANGED
|
@@ -93,11 +93,13 @@ class ExecutionContext:
|
|
|
93
93
|
call_tool: ContextToolCall,
|
|
94
94
|
settings: ExecutionSettings,
|
|
95
95
|
deadline: float,
|
|
96
|
+
progress: ProgressReporter | None = None,
|
|
96
97
|
) -> None:
|
|
97
98
|
self.catalog = catalog
|
|
98
99
|
self.call_tool = call_tool
|
|
99
100
|
self.settings = settings
|
|
100
101
|
self.deadline = deadline
|
|
102
|
+
self.progress = progress
|
|
101
103
|
self.calls_made = 0
|
|
102
104
|
self.chain_calls = 0
|
|
103
105
|
self.metrics = ExecutionMetrics()
|
|
@@ -117,6 +119,7 @@ class ExecutionContext:
|
|
|
117
119
|
|
|
118
120
|
ToolCall = Callable[[str, JsonObject], Awaitable[JsonValue]]
|
|
119
121
|
ContextToolCall = Callable[[str, JsonObject, ExecutionContext], Awaitable[JsonValue]]
|
|
122
|
+
ProgressReporter = Callable[[int, str], Awaitable[None]]
|
|
120
123
|
ExternalFunction = Callable[..., Awaitable[JsonValue]]
|
|
121
124
|
ResultRetainer = Callable[[JsonValue], RetainedResult | None]
|
|
122
125
|
|
|
@@ -291,9 +294,10 @@ class MontyExecutor:
|
|
|
291
294
|
*,
|
|
292
295
|
input_value: JsonValue = None,
|
|
293
296
|
retain_result: ResultRetainer | None = None,
|
|
297
|
+
progress: ProgressReporter | None = None,
|
|
294
298
|
) -> ExecutionResponse:
|
|
295
299
|
async with self._execution_lock:
|
|
296
|
-
context = self._new_context(self.catalog, call_tool)
|
|
300
|
+
context = self._new_context(self.catalog, call_tool, progress=progress)
|
|
297
301
|
return await self._execute_program(
|
|
298
302
|
code,
|
|
299
303
|
context,
|
|
@@ -397,6 +401,8 @@ class MontyExecutor:
|
|
|
397
401
|
self,
|
|
398
402
|
catalog: ToolCatalog,
|
|
399
403
|
call_tool: ContextToolCall,
|
|
404
|
+
*,
|
|
405
|
+
progress: ProgressReporter | None = None,
|
|
400
406
|
) -> ExecutionContext:
|
|
401
407
|
loop = asyncio.get_running_loop()
|
|
402
408
|
return ExecutionContext(
|
|
@@ -404,6 +410,7 @@ class MontyExecutor:
|
|
|
404
410
|
call_tool=call_tool,
|
|
405
411
|
settings=self.settings,
|
|
406
412
|
deadline=loop.time() + self.settings.timeout_seconds,
|
|
413
|
+
progress=progress,
|
|
407
414
|
)
|
|
408
415
|
|
|
409
416
|
async def _execute_program( # ruff:ignore[complex-structure, too-many-statements]
|
|
@@ -502,9 +509,13 @@ class MontyExecutor:
|
|
|
502
509
|
raise ValueError(message) from error
|
|
503
510
|
if spec.kind == "saved_chain":
|
|
504
511
|
context.chain_calls += 1
|
|
512
|
+
if context.progress is not None:
|
|
513
|
+
await context.progress(context.total_calls, spec.call)
|
|
505
514
|
return await context.call_tool(name, validated, context)
|
|
506
515
|
|
|
507
516
|
context.calls_made += 1
|
|
517
|
+
if context.progress is not None:
|
|
518
|
+
await context.progress(context.total_calls, spec.call)
|
|
508
519
|
remaining = context.remaining_seconds()
|
|
509
520
|
if remaining <= 0:
|
|
510
521
|
raise TimeoutError
|
|
@@ -607,6 +618,8 @@ class MontyExecutor:
|
|
|
607
618
|
"max_duration_secs": remaining,
|
|
608
619
|
"max_memory": context.settings.max_memory_bytes,
|
|
609
620
|
}
|
|
621
|
+
if context.progress is not None:
|
|
622
|
+
await context.progress(context.total_calls, "executing")
|
|
610
623
|
runtime_started = time.perf_counter()
|
|
611
624
|
try:
|
|
612
625
|
async with asyncio.timeout(remaining + 0.1):
|