pi-codemcp 0.1.0
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/.python-version +1 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/extensions/index.ts +188 -0
- package/package.json +91 -0
- package/sidecar/__init__.py +1 -0
- package/sidecar/catalog_cache.py +89 -0
- package/sidecar/chains.py +316 -0
- package/sidecar/executor.py +591 -0
- package/sidecar/gateway.py +893 -0
- package/sidecar/json_types.py +11 -0
- package/sidecar/mcp_config.py +278 -0
- package/sidecar/models.py +92 -0
- package/sidecar/pyproject.toml +144 -0
- package/sidecar/settings.py +59 -0
- package/sidecar/tool_catalog.py +838 -0
- package/sidecar/uv.lock +1775 -0
- package/src/chains.ts +452 -0
- package/src/config.ts +58 -0
- package/src/errors.ts +9 -0
- package/src/execution-rendering.ts +183 -0
- package/src/json-file.ts +54 -0
- package/src/lifecycle.ts +59 -0
- package/src/mcp-client.ts +303 -0
- package/src/modal.ts +1233 -0
- package/src/output.ts +52 -0
- package/src/settings.ts +144 -0
- package/src/tools.ts +332 -0
package/.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# pi-codemcp
|
|
2
|
+
|
|
3
|
+
Fast, typed, sandboxed **Code Mode for every MCP server configured in Pi**.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
- `codemcp_search` finds relevant tools and returns only their typed SDK stubs.
|
|
8
|
+
- `codemcp_execute` runs one sandboxed Python call graph across one or many MCP servers.
|
|
9
|
+
- `codemcp_save_chain` turns a repeated call graph into a reusable native Pi tool.
|
|
10
|
+
|
|
11
|
+
Intermediate results stay inside the sandbox. The model receives only the compact value returned by the program.
|
|
12
|
+
|
|
13
|
+
## Why Code Mode?
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
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).
|
|
18
|
+
|
|
19
|
+
pi-codemcp applies that idea on the **client side** to the MCP servers you already use in Pi:
|
|
20
|
+
|
|
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.
|
|
26
|
+
|
|
27
|
+
That can make complex MCP workflows faster and substantially more token-efficient. Exact savings depend on the servers, schemas, model, and task.
|
|
28
|
+
|
|
29
|
+
## Built for daily use, not a demo
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
pi-codemcp is deliberately opinionated about operational quality:
|
|
34
|
+
|
|
35
|
+
- Pi startup does not wait for Python or MCP servers.
|
|
36
|
+
- Each upstream connection is lazy and independent.
|
|
37
|
+
- Tool catalogs are cached per server and invalidated independently.
|
|
38
|
+
- Agent-written code is type-checked before execution.
|
|
39
|
+
- Time, memory, call count, and output size are bounded.
|
|
40
|
+
- Failures are explicit; there are no silent retries or compatibility fallbacks.
|
|
41
|
+
- Tool output is compact by default and expands with Pi's normal `Ctrl+O` UI.
|
|
42
|
+
|
|
43
|
+
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
|
+
|
|
45
|
+
## Saved MCP chains
|
|
46
|
+
|
|
47
|
+
Any successful MCP call graph can become a reusable tool with an explicit input and output JSON Schema.
|
|
48
|
+
|
|
49
|
+
A saved chain is exposed in two forms from one manifest:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
mcp_chain_weekly_digest(...) # native Pi tool
|
|
53
|
+
chains.weekly_digest(...) # typed call inside Code Mode
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Chains can call upstream MCP tools, other saved chains, or themselves recursively. This enables reusable composition such as:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
issues = await chains.collect_open_issues({"assignee": input["assignee"]})
|
|
60
|
+
result = await slack.post_message({
|
|
61
|
+
"channel": input["channel"],
|
|
62
|
+
"text": issues["summary"],
|
|
63
|
+
})
|
|
64
|
+
return {"posted": result["ok"], "count": issues["count"]}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Nested chains share the same deadline, cancellation signal, catalog snapshot, and total call budget. Every nested input and output is runtime-validated. Recursion is supported but bounded. Dependency fingerprints mark chains stale when a referenced contract changes.
|
|
68
|
+
|
|
69
|
+
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
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pi install npm:pi-codemcp
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
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
|
+
|
|
81
|
+
## Agent workflow
|
|
82
|
+
|
|
83
|
+
The agent searches for a capability, receives the complete stub it needs, and executes a compact plan:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
issues = await linear.list_issues({"assignee": "me", "limit": 50})
|
|
87
|
+
return {"count": len(issues), "ids": [issue["identifier"] for issue in issues]}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Incomplete upstream schemas become recursive `JsonValue`, not `Any`; unknown values must be narrowed explicitly before typed use.
|
|
91
|
+
|
|
92
|
+
## Safety and limits
|
|
93
|
+
|
|
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.
|
|
95
|
+
|
|
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.
|
|
97
|
+
|
|
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.
|
|
99
|
+
|
|
100
|
+
## Something failed? Please open an issue
|
|
101
|
+
|
|
102
|
+
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.
|
|
103
|
+
|
|
104
|
+
Open an issue at <https://github.com/yolonir/pi-codemcp/issues>.
|
|
105
|
+
|
|
106
|
+
You can ask your coding agent to do the work:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
Reproduce this pi-codemcp problem, redact all credentials and private data,
|
|
110
|
+
collect the pi-codemcp version, Pi version, OS/architecture, MCP transport,
|
|
111
|
+
minimal configuration shape, exact error, and relevant logs, then open a
|
|
112
|
+
GitHub issue at https://github.com/yolonir/pi-codemcp/issues.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
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.
|
|
116
|
+
|
|
117
|
+
## Local development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
just init
|
|
121
|
+
just check
|
|
122
|
+
just release-check
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
To test the checkout without loading an installed copy:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pi -ne -e . --no-session
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`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
|
+
|
|
133
|
+
## Releases
|
|
134
|
+
|
|
135
|
+
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
|
+
|
|
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.
|
|
138
|
+
|
|
139
|
+
## Credits
|
|
140
|
+
|
|
141
|
+
The core search-and-execute philosophy is inspired by Cloudflare's Code Mode work. pi-codemcp is an independent implementation for Pi that composes arbitrary configured MCP servers through FastMCP and a Pydantic Monty sandbox.
|
|
142
|
+
|
|
143
|
+
MIT
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
CONFIG_DIR_NAME,
|
|
4
|
+
type ExtensionAPI,
|
|
5
|
+
type ExtensionCommandContext,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { SavedChainManager } from "../src/chains.js";
|
|
8
|
+
import { setMcpServersEnabled } from "../src/config.js";
|
|
9
|
+
import { summarizeError } from "../src/errors.js";
|
|
10
|
+
import { CodeMcpLifecycle } from "../src/lifecycle.js";
|
|
11
|
+
import type { SidecarClientOptions } from "../src/mcp-client.js";
|
|
12
|
+
import {
|
|
13
|
+
type ChainModalState,
|
|
14
|
+
chainStatesFromViews,
|
|
15
|
+
type ChainEnabledChange as ModalChainEnabledChange,
|
|
16
|
+
type ServerEnabledChange,
|
|
17
|
+
type ServerModalState,
|
|
18
|
+
serverStatesFromStatus,
|
|
19
|
+
showServerManagerModal,
|
|
20
|
+
} from "../src/modal.js";
|
|
21
|
+
import { type CodeMcpSettings, saveCodeMcpSettings } from "../src/settings.js";
|
|
22
|
+
import { registerCodeMcpTools } from "../src/tools.js";
|
|
23
|
+
|
|
24
|
+
export function createCodeMcpExtension(options: SidecarClientOptions = {}) {
|
|
25
|
+
return function codeMcpExtension(pi: ExtensionAPI): void {
|
|
26
|
+
const lifecycle = new CodeMcpLifecycle(options);
|
|
27
|
+
const chains = new SavedChainManager(pi, lifecycle);
|
|
28
|
+
registerCodeMcpTools(pi, lifecycle, chains);
|
|
29
|
+
|
|
30
|
+
pi.registerCommand("codemcp", {
|
|
31
|
+
description: "Manage CodeMCP servers, saved chains, tools, and settings",
|
|
32
|
+
handler: async (_args, ctx) => {
|
|
33
|
+
try {
|
|
34
|
+
bindProjectChainScope(ctx, lifecycle, chains);
|
|
35
|
+
const [status, savedChains, settings] = await Promise.all([
|
|
36
|
+
lifecycle.request("status", {}),
|
|
37
|
+
chains.list(),
|
|
38
|
+
Promise.resolve(lifecycle.loadSettings()),
|
|
39
|
+
]);
|
|
40
|
+
const servers = serverStatesFromStatus(status);
|
|
41
|
+
if (ctx.mode !== "tui") {
|
|
42
|
+
if (ctx.hasUI) ctx.ui.notify(formatStatusSummary(servers), "info");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await showServerManagerModal(ctx, {
|
|
47
|
+
servers,
|
|
48
|
+
chains: chainStatesFromViews(savedChains),
|
|
49
|
+
settings,
|
|
50
|
+
onDiscover: async (server) =>
|
|
51
|
+
requireServerStatus(
|
|
52
|
+
await lifecycle.request("discover", { server: server.name }),
|
|
53
|
+
server.name,
|
|
54
|
+
),
|
|
55
|
+
onSaveChanges: (updated, serverChanges, chainChanges) =>
|
|
56
|
+
saveManagerChanges(lifecycle, chains, updated, serverChanges, chainChanges),
|
|
57
|
+
onResolveUnsaved: async () => {
|
|
58
|
+
const choice = await ctx.ui.select("Unsaved CodeMCP changes", [
|
|
59
|
+
"Save",
|
|
60
|
+
"Discard",
|
|
61
|
+
"Cancel",
|
|
62
|
+
]);
|
|
63
|
+
if (choice === "Save") return "save";
|
|
64
|
+
if (choice === "Discard") return "discard";
|
|
65
|
+
return "cancel";
|
|
66
|
+
},
|
|
67
|
+
onRevalidateChain: async (chain) => {
|
|
68
|
+
await chains.revalidate(chain.name, chain.scope);
|
|
69
|
+
return chainStatesFromViews(await chains.list());
|
|
70
|
+
},
|
|
71
|
+
onDeleteChain: async (chain) =>
|
|
72
|
+
chainStatesFromViews(await chains.delete(chain.name, chain.scope)),
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
ctx.ui.notify(summarizeError(error), "error");
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
pi.on("session_start", (_event, ctx) => {
|
|
81
|
+
bindProjectChainScope(ctx, lifecycle, chains);
|
|
82
|
+
chains.activatePersisted();
|
|
83
|
+
for (const error of chains.startupErrors) ctx.ui.notify(error, "warning");
|
|
84
|
+
let settings: CodeMcpSettings;
|
|
85
|
+
try {
|
|
86
|
+
settings = lifecycle.loadSettings();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
ctx.ui.notify(`CodeMCP settings failed: ${summarizeError(error)}`, "warning");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!settings.backgroundWarmup) return;
|
|
92
|
+
void lifecycle.warmup().catch((error: unknown) => {
|
|
93
|
+
ctx.ui.notify(`CodeMCP background warmup failed: ${summarizeError(error)}`, "warning");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
pi.on("session_shutdown", async () => {
|
|
98
|
+
await lifecycle.shutdown();
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function saveManagerChanges(
|
|
104
|
+
lifecycle: Pick<CodeMcpLifecycle, "configPath" | "settingsPath" | "loadSettings" | "reload">,
|
|
105
|
+
chains: Pick<SavedChainManager, "applyEnabled">,
|
|
106
|
+
updated: CodeMcpSettings,
|
|
107
|
+
serverChanges: readonly ServerEnabledChange[],
|
|
108
|
+
chainChanges: readonly ModalChainEnabledChange[],
|
|
109
|
+
): Promise<{
|
|
110
|
+
settings: CodeMcpSettings;
|
|
111
|
+
servers: ServerModalState[];
|
|
112
|
+
chains: ChainModalState[];
|
|
113
|
+
}> {
|
|
114
|
+
const previousSettings = lifecycle.loadSettings();
|
|
115
|
+
let serverConfigChanged = false;
|
|
116
|
+
try {
|
|
117
|
+
saveCodeMcpSettings(lifecycle.settingsPath, updated);
|
|
118
|
+
if (serverChanges.length > 0) {
|
|
119
|
+
setMcpServersEnabled(
|
|
120
|
+
lifecycle.configPath,
|
|
121
|
+
serverChanges.map((change) => ({ name: change.name, enabled: change.enabled })),
|
|
122
|
+
);
|
|
123
|
+
serverConfigChanged = true;
|
|
124
|
+
await lifecycle.reload();
|
|
125
|
+
}
|
|
126
|
+
const applied = await chains.applyEnabled(
|
|
127
|
+
chainChanges.map((change) => ({
|
|
128
|
+
name: change.name,
|
|
129
|
+
scope: change.scope,
|
|
130
|
+
enabled: change.enabled,
|
|
131
|
+
})),
|
|
132
|
+
);
|
|
133
|
+
return {
|
|
134
|
+
settings: lifecycle.loadSettings(),
|
|
135
|
+
servers: serverStatesFromStatus(applied.status),
|
|
136
|
+
chains: chainStatesFromViews(applied.chains),
|
|
137
|
+
};
|
|
138
|
+
} catch (error) {
|
|
139
|
+
saveCodeMcpSettings(lifecycle.settingsPath, previousSettings);
|
|
140
|
+
if (serverConfigChanged) {
|
|
141
|
+
setMcpServersEnabled(
|
|
142
|
+
lifecycle.configPath,
|
|
143
|
+
serverChanges.map((change) => ({
|
|
144
|
+
name: change.name,
|
|
145
|
+
enabled: change.previousEnabled,
|
|
146
|
+
})),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await lifecycle.reload();
|
|
151
|
+
} catch (rollbackError) {
|
|
152
|
+
throw new AggregateError(
|
|
153
|
+
[error, rollbackError],
|
|
154
|
+
"CodeMCP save failed and runtime rollback also failed",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export default createCodeMcpExtension();
|
|
162
|
+
|
|
163
|
+
function requireServerStatus(
|
|
164
|
+
status: Record<string, unknown>,
|
|
165
|
+
serverName: string,
|
|
166
|
+
): ServerModalState {
|
|
167
|
+
const server = serverStatesFromStatus(status).find((candidate) => candidate.name === serverName);
|
|
168
|
+
if (!server) throw new Error(`CodeMCP returned no status for ${serverName}`);
|
|
169
|
+
return server;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function bindProjectChainScope(
|
|
173
|
+
ctx: Pick<ExtensionCommandContext, "cwd" | "isProjectTrusted">,
|
|
174
|
+
lifecycle: CodeMcpLifecycle,
|
|
175
|
+
chains: SavedChainManager,
|
|
176
|
+
): void {
|
|
177
|
+
const projectChainsPath = ctx.isProjectTrusted()
|
|
178
|
+
? join(ctx.cwd, CONFIG_DIR_NAME, "pi-codemcp", "chains")
|
|
179
|
+
: undefined;
|
|
180
|
+
lifecycle.configureProjectChains(projectChainsPath);
|
|
181
|
+
chains.configureProject(projectChainsPath);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function formatStatusSummary(servers: ServerModalState[]): string {
|
|
185
|
+
const enabled = servers.filter((server) => server.enabled).length;
|
|
186
|
+
const tools = servers.reduce((total, server) => total + server.toolCount, 0);
|
|
187
|
+
return `CodeMCP: ${enabled}/${servers.length} servers · ${tools} enabled tools`;
|
|
188
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-codemcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed, sandboxed Code Mode access to configured MCP servers for Pi",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"packageManager": "bun@1.3.10",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/yolonir/pi-codemcp.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/yolonir/pi-codemcp#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/yolonir/pi-codemcp/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22.19.0"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pi-package",
|
|
24
|
+
"mcp",
|
|
25
|
+
"code-mode"
|
|
26
|
+
],
|
|
27
|
+
"files": [
|
|
28
|
+
"extensions/index.ts",
|
|
29
|
+
"src/*.ts",
|
|
30
|
+
"sidecar/*.py",
|
|
31
|
+
"sidecar/pyproject.toml",
|
|
32
|
+
"sidecar/uv.lock",
|
|
33
|
+
".python-version",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"check": "just check",
|
|
39
|
+
"check:ts": "bun run typecheck && bun run lint && bun run test:ts",
|
|
40
|
+
"format": "biome check --write .",
|
|
41
|
+
"lint": "biome check .",
|
|
42
|
+
"prepublishOnly": "just check && just release-check",
|
|
43
|
+
"test": "bun run test:ts && bun run test:python",
|
|
44
|
+
"test:python": "uv run --project sidecar pytest tests/python -q",
|
|
45
|
+
"test:ts": "bun test tests/typescript",
|
|
46
|
+
"typecheck": "tsc --noEmit"
|
|
47
|
+
},
|
|
48
|
+
"pi": {
|
|
49
|
+
"extensions": [
|
|
50
|
+
"./extensions/index.ts"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@modelcontextprotocol/sdk": "1.29.0"
|
|
55
|
+
},
|
|
56
|
+
"optionalDependencies": {
|
|
57
|
+
"@manzt/uv-darwin-arm64": "0.8.13",
|
|
58
|
+
"@manzt/uv-darwin-x64": "0.8.13",
|
|
59
|
+
"@manzt/uv-linux-arm64": "0.8.13",
|
|
60
|
+
"@manzt/uv-linux-x64": "0.8.13",
|
|
61
|
+
"@manzt/uv-win32-arm64": "0.8.13",
|
|
62
|
+
"@manzt/uv-win32-ia32": "0.8.13",
|
|
63
|
+
"@manzt/uv-win32-x64": "0.8.13"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
67
|
+
"@earendil-works/pi-tui": "*",
|
|
68
|
+
"typebox": "*"
|
|
69
|
+
},
|
|
70
|
+
"peerDependenciesMeta": {
|
|
71
|
+
"@earendil-works/pi-coding-agent": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"@earendil-works/pi-tui": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"typebox": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"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",
|
|
87
|
+
"@types/bun": "1.3.14",
|
|
88
|
+
"typebox": "1.3.6",
|
|
89
|
+
"typescript": "5.9.3"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FastMCP/Pydantic-Monty sidecar for pi-codemcp."""
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import time
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
from typing import TYPE_CHECKING, Literal
|
|
7
|
+
|
|
8
|
+
from mcp import types as mcp_types
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, ValidationError
|
|
10
|
+
|
|
11
|
+
from .json_types import JSON_OBJECT_ADAPTER, JsonObject
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
CACHE_VERSION: Literal[1] = 1
|
|
17
|
+
DEFAULT_MAX_AGE_SECONDS = 24 * 60 * 60
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CachedServerCatalog(BaseModel):
|
|
21
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
22
|
+
|
|
23
|
+
version: Literal[1] = CACHE_VERSION
|
|
24
|
+
server_name: str
|
|
25
|
+
config_fingerprint: str
|
|
26
|
+
updated_at: float
|
|
27
|
+
tools: list[JsonObject]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CatalogCache:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
directory: Path,
|
|
34
|
+
*,
|
|
35
|
+
max_age_seconds: float = DEFAULT_MAX_AGE_SECONDS,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.directory = directory
|
|
38
|
+
self.max_age_seconds = max_age_seconds
|
|
39
|
+
|
|
40
|
+
def load(
|
|
41
|
+
self,
|
|
42
|
+
server_name: str,
|
|
43
|
+
config_fingerprint: str,
|
|
44
|
+
) -> list[mcp_types.Tool] | None:
|
|
45
|
+
path = self._path(server_name)
|
|
46
|
+
try:
|
|
47
|
+
entry = CachedServerCatalog.model_validate_json(path.read_text(encoding="utf-8"))
|
|
48
|
+
except (FileNotFoundError, OSError, ValidationError, ValueError):
|
|
49
|
+
return None
|
|
50
|
+
if entry.server_name != server_name:
|
|
51
|
+
return None
|
|
52
|
+
if entry.config_fingerprint != config_fingerprint:
|
|
53
|
+
return None
|
|
54
|
+
if time.time() - entry.updated_at > self.max_age_seconds:
|
|
55
|
+
return None
|
|
56
|
+
try:
|
|
57
|
+
return [mcp_types.Tool.model_validate(tool) for tool in entry.tools]
|
|
58
|
+
except ValidationError:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def save(
|
|
62
|
+
self,
|
|
63
|
+
server_name: str,
|
|
64
|
+
config_fingerprint: str,
|
|
65
|
+
tools: list[mcp_types.Tool],
|
|
66
|
+
) -> None:
|
|
67
|
+
self.directory.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
with suppress(OSError):
|
|
69
|
+
self.directory.chmod(0o700)
|
|
70
|
+
path = self._path(server_name)
|
|
71
|
+
temporary = path.with_suffix(".tmp")
|
|
72
|
+
entry = CachedServerCatalog(
|
|
73
|
+
server_name=server_name,
|
|
74
|
+
config_fingerprint=config_fingerprint,
|
|
75
|
+
updated_at=time.time(),
|
|
76
|
+
tools=[
|
|
77
|
+
JSON_OBJECT_ADAPTER.validate_python(
|
|
78
|
+
tool.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
79
|
+
)
|
|
80
|
+
for tool in tools
|
|
81
|
+
],
|
|
82
|
+
)
|
|
83
|
+
temporary.write_text(entry.model_dump_json(), encoding="utf-8")
|
|
84
|
+
temporary.chmod(0o600)
|
|
85
|
+
temporary.replace(path)
|
|
86
|
+
|
|
87
|
+
def _path(self, server_name: str) -> Path:
|
|
88
|
+
digest = hashlib.sha256(server_name.encode("utf-8")).hexdigest()[:20]
|
|
89
|
+
return self.directory / f"{digest}.json"
|