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/src/json-file.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
statSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
|
|
12
|
+
export type JsonRecord = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
export function readJsonObject(path: string, label: string): JsonRecord {
|
|
15
|
+
let parsed: unknown;
|
|
16
|
+
try {
|
|
17
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw new Error(`${label} is not valid JSON`, { cause: error });
|
|
20
|
+
}
|
|
21
|
+
return requireJsonObject(parsed, label);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function requireJsonObject(value: unknown, label: string): JsonRecord {
|
|
25
|
+
if (!isJsonRecord(value)) {
|
|
26
|
+
throw new TypeError(`${label} must be an object`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function writeJsonObjectAtomically(path: string, value: JsonRecord): void {
|
|
32
|
+
const directory = dirname(path);
|
|
33
|
+
mkdirSync(directory, { recursive: true });
|
|
34
|
+
const temporary = join(directory, `.${process.pid}.${Date.now()}.tmp`);
|
|
35
|
+
const mode = existsSync(path) ? statSync(path).mode : 0o600;
|
|
36
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
mode,
|
|
39
|
+
});
|
|
40
|
+
try {
|
|
41
|
+
renameSync(temporary, path);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
try {
|
|
44
|
+
unlinkSync(temporary);
|
|
45
|
+
} catch {
|
|
46
|
+
// Preserve the original write error.
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isJsonRecord(value: unknown): value is JsonRecord {
|
|
53
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
54
|
+
}
|
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SidecarClient, type SidecarClientOptions, type SidecarToolName } from "./mcp-client.js";
|
|
2
|
+
import { type CodeMcpSettings, loadCodeMcpSettings } from "./settings.js";
|
|
3
|
+
|
|
4
|
+
export class CodeMcpLifecycle {
|
|
5
|
+
readonly sidecar: SidecarClient;
|
|
6
|
+
private reloadBarrier: Promise<void> = Promise.resolve();
|
|
7
|
+
|
|
8
|
+
constructor(options: SidecarClientOptions = {}) {
|
|
9
|
+
this.sidecar = new SidecarClient(options);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get configPath(): string {
|
|
13
|
+
return this.sidecar.configPath;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get settingsPath(): string {
|
|
17
|
+
return this.sidecar.settingsPath;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get chainsPath(): string {
|
|
21
|
+
return this.sidecar.chainsPath;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get projectChainsPath(): string | undefined {
|
|
25
|
+
return this.sidecar.projectChainsPath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
configureProjectChains(path: string | undefined): void {
|
|
29
|
+
this.sidecar.configureProjectChains(path);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
loadSettings(): CodeMcpSettings {
|
|
33
|
+
return loadCodeMcpSettings(this.settingsPath);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async request(
|
|
37
|
+
name: SidecarToolName,
|
|
38
|
+
args: Record<string, unknown>,
|
|
39
|
+
signal?: AbortSignal,
|
|
40
|
+
): Promise<Record<string, unknown>> {
|
|
41
|
+
await this.reloadBarrier;
|
|
42
|
+
return this.sidecar.call(name, args, signal);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async warmup(): Promise<void> {
|
|
46
|
+
await this.request("status", {});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
reload(): Promise<void> {
|
|
50
|
+
const operation = this.reloadBarrier.then(() => this.sidecar.close());
|
|
51
|
+
this.reloadBarrier = operation.catch(() => undefined);
|
|
52
|
+
return operation;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async shutdown(): Promise<void> {
|
|
56
|
+
await this.reloadBarrier;
|
|
57
|
+
await this.sidecar.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
15
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
16
|
+
import { loadCodeMcpSettings } from "./settings.js";
|
|
17
|
+
|
|
18
|
+
type JsonObject = Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
export type SidecarToolName =
|
|
21
|
+
| "search"
|
|
22
|
+
| "discover"
|
|
23
|
+
| "reload_settings"
|
|
24
|
+
| "apply_manager_changes"
|
|
25
|
+
| "execute"
|
|
26
|
+
| "save_chain"
|
|
27
|
+
| "list_chains"
|
|
28
|
+
| "execute_chain"
|
|
29
|
+
| "revalidate_chain"
|
|
30
|
+
| "delete_chain"
|
|
31
|
+
| "status";
|
|
32
|
+
|
|
33
|
+
const LONG_RUNNING_TOOLS = new Set<SidecarToolName>([
|
|
34
|
+
"execute",
|
|
35
|
+
"save_chain",
|
|
36
|
+
"execute_chain",
|
|
37
|
+
"revalidate_chain",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export interface SidecarClientOptions {
|
|
41
|
+
packageRoot?: string;
|
|
42
|
+
agentDir?: string;
|
|
43
|
+
environment?: Record<string, string>;
|
|
44
|
+
projectChainsPath?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class SidecarClient {
|
|
48
|
+
private readonly packageRoot: string;
|
|
49
|
+
private readonly packageVersion: string;
|
|
50
|
+
private readonly agentDir: string;
|
|
51
|
+
private readonly environment: Record<string, string>;
|
|
52
|
+
private client: Client | undefined;
|
|
53
|
+
private transport: StdioClientTransport | undefined;
|
|
54
|
+
private startPromise: Promise<void> | undefined;
|
|
55
|
+
private closePromise: Promise<void> | undefined;
|
|
56
|
+
private projectChainsDirectory: string | undefined;
|
|
57
|
+
private stderrTail = "";
|
|
58
|
+
|
|
59
|
+
constructor(options: SidecarClientOptions = {}) {
|
|
60
|
+
this.packageRoot = resolve(
|
|
61
|
+
options.packageRoot ?? fileURLToPath(new URL("..", import.meta.url)),
|
|
62
|
+
);
|
|
63
|
+
this.packageVersion = readPackageVersion(this.packageRoot);
|
|
64
|
+
this.agentDir = resolve(options.agentDir ?? getAgentDir());
|
|
65
|
+
this.projectChainsDirectory = options.projectChainsPath
|
|
66
|
+
? resolve(options.projectChainsPath)
|
|
67
|
+
: undefined;
|
|
68
|
+
this.environment = {
|
|
69
|
+
...definedProcessEnvironment(),
|
|
70
|
+
...(options.environment ?? {}),
|
|
71
|
+
PI_CODEMCP_AGENT_DIR: this.agentDir,
|
|
72
|
+
...(this.projectChainsDirectory === undefined
|
|
73
|
+
? {}
|
|
74
|
+
: { PI_CODEMCP_PROJECT_CHAINS_DIR: this.projectChainsDirectory }),
|
|
75
|
+
UV_PROJECT_ENVIRONMENT: join(this.agentDir, "pi-codemcp", "runtime", "venv"),
|
|
76
|
+
};
|
|
77
|
+
if (this.projectChainsDirectory === undefined) {
|
|
78
|
+
delete this.environment.PI_CODEMCP_PROJECT_CHAINS_DIR;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get configPath(): string {
|
|
83
|
+
return join(this.agentDir, "mcp.json");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get settingsPath(): string {
|
|
87
|
+
return join(this.agentDir, "pi-codemcp", "settings.json");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get chainsPath(): string {
|
|
91
|
+
return join(this.agentDir, "pi-codemcp", "chains");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get projectChainsPath(): string | undefined {
|
|
95
|
+
return this.projectChainsDirectory;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
configureProjectChains(path: string | undefined): void {
|
|
99
|
+
const resolved = path === undefined ? undefined : resolve(path);
|
|
100
|
+
if (resolved === this.projectChainsDirectory) return;
|
|
101
|
+
if (this.client || this.transport || this.startPromise || this.closePromise) {
|
|
102
|
+
throw new Error("Cannot change CodeMCP project chain scope after the sidecar has started");
|
|
103
|
+
}
|
|
104
|
+
this.projectChainsDirectory = resolved;
|
|
105
|
+
if (resolved === undefined) delete this.environment.PI_CODEMCP_PROJECT_CHAINS_DIR;
|
|
106
|
+
else this.environment.PI_CODEMCP_PROJECT_CHAINS_DIR = resolved;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get pid(): number | null {
|
|
110
|
+
return this.transport?.pid ?? null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get connected(): boolean {
|
|
114
|
+
return this.client !== undefined && this.transport?.pid !== null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async call(name: SidecarToolName, args: JsonObject, signal?: AbortSignal): Promise<JsonObject> {
|
|
118
|
+
await this.ensureStarted(signal);
|
|
119
|
+
const client = this.client;
|
|
120
|
+
if (!client) throw new Error("Sidecar client failed to initialize");
|
|
121
|
+
const timeout = LONG_RUNNING_TOOLS.has(name)
|
|
122
|
+
? loadCodeMcpSettings(this.settingsPath).executionTimeoutSeconds * 1_000 + 5_000
|
|
123
|
+
: 30_000;
|
|
124
|
+
const result = await client.callTool({ name, arguments: args }, undefined, {
|
|
125
|
+
timeout,
|
|
126
|
+
...(signal === undefined ? {} : { signal }),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (result.isError) {
|
|
130
|
+
throw new Error(textContent(result.content) || `Sidecar tool ${name} failed`);
|
|
131
|
+
}
|
|
132
|
+
if (isJsonObject(result.structuredContent)) {
|
|
133
|
+
return result.structuredContent;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const text = textContent(result.content);
|
|
137
|
+
if (!text) {
|
|
138
|
+
throw new Error(`Sidecar tool ${name} returned no structured result`);
|
|
139
|
+
}
|
|
140
|
+
const parsed: unknown = JSON.parse(text);
|
|
141
|
+
if (!isJsonObject(parsed)) {
|
|
142
|
+
throw new Error(`Sidecar tool ${name} returned a non-object result`);
|
|
143
|
+
}
|
|
144
|
+
return parsed;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async close(): Promise<void> {
|
|
148
|
+
if (this.closePromise) return this.closePromise;
|
|
149
|
+
this.closePromise = this.closeInternal();
|
|
150
|
+
try {
|
|
151
|
+
await this.closePromise;
|
|
152
|
+
} finally {
|
|
153
|
+
this.closePromise = undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private async ensureStarted(signal?: AbortSignal): Promise<void> {
|
|
158
|
+
if (this.client && this.transport && !this.startPromise) return;
|
|
159
|
+
if (!this.startPromise) {
|
|
160
|
+
this.startPromise = this.start(signal).catch(async (error: unknown) => {
|
|
161
|
+
await this.closeInternal();
|
|
162
|
+
const detail = this.stderrTail.trim();
|
|
163
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
+
throw new Error(detail ? `${message}\n${detail}` : message, { cause: error });
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const startPromise = this.startPromise;
|
|
169
|
+
try {
|
|
170
|
+
await startPromise;
|
|
171
|
+
} finally {
|
|
172
|
+
if (this.startPromise === startPromise) this.startPromise = undefined;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private async start(signal?: AbortSignal): Promise<void> {
|
|
177
|
+
this.stderrTail = "";
|
|
178
|
+
const transport = new StdioClientTransport({
|
|
179
|
+
command: prepareBundledUv(this.packageRoot, this.agentDir),
|
|
180
|
+
args: [
|
|
181
|
+
"run",
|
|
182
|
+
...(isTruthy(process.env.PI_OFFLINE) ? ["--offline"] : []),
|
|
183
|
+
"--project",
|
|
184
|
+
"sidecar",
|
|
185
|
+
"--frozen",
|
|
186
|
+
"--no-dev",
|
|
187
|
+
"-m",
|
|
188
|
+
"sidecar.gateway",
|
|
189
|
+
],
|
|
190
|
+
cwd: this.packageRoot,
|
|
191
|
+
env: this.environment,
|
|
192
|
+
stderr: "pipe",
|
|
193
|
+
});
|
|
194
|
+
transport.stderr?.on("data", (chunk: Buffer | string) => {
|
|
195
|
+
this.stderrTail = `${this.stderrTail}${chunk.toString()}`.slice(-8192);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const client = new Client({
|
|
199
|
+
name: "pi-codemcp",
|
|
200
|
+
version: this.packageVersion,
|
|
201
|
+
});
|
|
202
|
+
this.transport = transport;
|
|
203
|
+
this.client = client;
|
|
204
|
+
await client.connect(transport, {
|
|
205
|
+
timeout: 310_000,
|
|
206
|
+
...(signal === undefined ? {} : { signal }),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async closeInternal(): Promise<void> {
|
|
211
|
+
const client = this.client;
|
|
212
|
+
const transport = this.transport;
|
|
213
|
+
this.client = undefined;
|
|
214
|
+
this.transport = undefined;
|
|
215
|
+
this.startPromise = undefined;
|
|
216
|
+
|
|
217
|
+
if (client) {
|
|
218
|
+
try {
|
|
219
|
+
await client.close();
|
|
220
|
+
} finally {
|
|
221
|
+
if (transport) await transport.close();
|
|
222
|
+
}
|
|
223
|
+
} else if (transport) {
|
|
224
|
+
await transport.close();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function readPackageVersion(packageRoot: string): string {
|
|
230
|
+
const packageJson = join(packageRoot, "package.json");
|
|
231
|
+
const metadata: unknown = JSON.parse(readFileSync(packageJson, "utf8"));
|
|
232
|
+
if (!isJsonObject(metadata) || typeof metadata.version !== "string") {
|
|
233
|
+
throw new Error(`pi-codemcp package has invalid metadata: ${packageJson}`);
|
|
234
|
+
}
|
|
235
|
+
return metadata.version;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function prepareBundledUv(packageRoot: string, agentDir: string): string {
|
|
239
|
+
const packageRequire = createRequire(join(packageRoot, "package.json"));
|
|
240
|
+
const platformPackage = `@manzt/uv-${process.platform}-${process.arch}`;
|
|
241
|
+
let packageJson: string;
|
|
242
|
+
try {
|
|
243
|
+
packageJson = packageRequire.resolve(`${platformPackage}/package.json`);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Bundled uv is unavailable for ${process.platform}/${process.arch}; reinstall pi-codemcp with optional dependencies enabled`,
|
|
247
|
+
{ cause: error },
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const metadata: unknown = JSON.parse(readFileSync(packageJson, "utf8"));
|
|
251
|
+
if (!isJsonObject(metadata) || typeof metadata.version !== "string") {
|
|
252
|
+
throw new Error(`Bundled uv package has invalid metadata: ${packageJson}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const binaryName = process.platform === "win32" ? "uv.exe" : "uv";
|
|
256
|
+
const source = join(dirname(packageJson), "bin", binaryName);
|
|
257
|
+
if (!existsSync(source)) {
|
|
258
|
+
throw new Error(`Bundled uv executable is missing: ${source}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const runtimeDirectory = join(agentDir, "pi-codemcp", "runtime", "uv", metadata.version);
|
|
262
|
+
const executable = join(runtimeDirectory, binaryName);
|
|
263
|
+
if (existsSync(executable)) return executable;
|
|
264
|
+
|
|
265
|
+
mkdirSync(runtimeDirectory, { recursive: true });
|
|
266
|
+
const temporary = `${executable}.${process.pid}.tmp`;
|
|
267
|
+
copyFileSync(source, temporary);
|
|
268
|
+
if (process.platform !== "win32") chmodSync(temporary, 0o755);
|
|
269
|
+
try {
|
|
270
|
+
renameSync(temporary, executable);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (!existsSync(executable)) throw error;
|
|
273
|
+
unlinkSync(temporary);
|
|
274
|
+
}
|
|
275
|
+
return executable;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function definedProcessEnvironment(): Record<string, string> {
|
|
279
|
+
return Object.fromEntries(
|
|
280
|
+
Object.entries(process.env).filter(
|
|
281
|
+
(entry): entry is [string, string] => entry[1] !== undefined,
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isTruthy(value: string | undefined): boolean {
|
|
287
|
+
return value !== undefined && ["1", "true", "yes"].includes(value.toLowerCase());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function isJsonObject(value: unknown): value is JsonObject {
|
|
291
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function textContent(content: unknown): string {
|
|
295
|
+
if (!Array.isArray(content)) return "";
|
|
296
|
+
return content
|
|
297
|
+
.filter(
|
|
298
|
+
(item): item is { type: "text"; text: string } =>
|
|
299
|
+
isJsonObject(item) && item.type === "text" && typeof item.text === "string",
|
|
300
|
+
)
|
|
301
|
+
.map((item) => item.text)
|
|
302
|
+
.join("\n");
|
|
303
|
+
}
|