@ian-pascoe/pi-mcp 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/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/pi-mcp-cli.js +10948 -0
- package/package.json +64 -0
- package/src/index.ts +2 -0
- package/src/mcp-auth-store.ts +393 -0
- package/src/mcp-command.ts +893 -0
- package/src/mcp-content.ts +212 -0
- package/src/mcp-host.ts +971 -0
- package/src/mcp-oauth.ts +740 -0
- package/src/mcp-server-client.ts +375 -0
- package/src/mcp-session-files.ts +127 -0
- package/src/mcp-settings-store.ts +455 -0
- package/src/mcp-tool-catalog.ts +464 -0
- package/src/pi-mcp-cli.ts +507 -0
- package/src/pi-mcp-extension.ts +1013 -0
- package/src/pi-mcp-settings.ts +619 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// oxlint-disable anti-slop/no-conditional-empty-object-spread -- Exact optional command data requires omitting absent fields at the shared command boundary.
|
|
4
|
+
// oxlint-disable anti-slop/no-runtime-typeof, anti-slop/no-unknown-parameters -- This entrypoint owns trust-store parsing before values reach typed settings adapters.
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import {
|
|
11
|
+
runMcpCommandTokens,
|
|
12
|
+
type McpCommandAdapterResult,
|
|
13
|
+
type McpCommandAdapters,
|
|
14
|
+
type McpCommandJsonValue,
|
|
15
|
+
} from "./mcp-command.js";
|
|
16
|
+
import { McpAuthStore } from "./mcp-auth-store.js";
|
|
17
|
+
import {
|
|
18
|
+
authenticateMcpOAuth,
|
|
19
|
+
DEFAULT_MCP_OAUTH_REDIRECT_URL,
|
|
20
|
+
McpOAuthProvider,
|
|
21
|
+
} from "./mcp-oauth.js";
|
|
22
|
+
import { McpServerClient } from "./mcp-server-client.js";
|
|
23
|
+
import { McpSettingsStore, type McpSettingsScope } from "./mcp-settings-store.js";
|
|
24
|
+
import {
|
|
25
|
+
resolveMcpSettings,
|
|
26
|
+
type McpServerDefinition,
|
|
27
|
+
type ResolvedMcpSettings,
|
|
28
|
+
} from "./pi-mcp-settings.js";
|
|
29
|
+
|
|
30
|
+
const HELP = `Usage: pi-mcp <command> [options]
|
|
31
|
+
|
|
32
|
+
Commands:
|
|
33
|
+
list List effective MCP Server Definitions without connecting
|
|
34
|
+
add Add or replace a Server Definition
|
|
35
|
+
remove Remove a Server Definition
|
|
36
|
+
enable Enable a Server Definition
|
|
37
|
+
disable Disable a Server Definition
|
|
38
|
+
auth Authenticate an OAuth Server Definition
|
|
39
|
+
logout Remove stored OAuth credentials
|
|
40
|
+
test Test one server, or every enabled server with --all
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
-h, --help Show this help message
|
|
44
|
+
-a, --approve Trust project settings for this invocation
|
|
45
|
+
-na, --no-approve Ignore project settings for this invocation
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
/** Terminal and construction effects used by the standalone MCP command adapter. */
|
|
49
|
+
export interface PiMcpCliOptions {
|
|
50
|
+
/** Build persistent, authentication, and temporary-test command adapters. */
|
|
51
|
+
readonly createAdapters?: (projectTrusted?: boolean) => Promise<McpCommandAdapters>;
|
|
52
|
+
/** Write command failures and usage text. */
|
|
53
|
+
readonly writeStderr?: (text: string) => void;
|
|
54
|
+
/** Write successful command output and help text. */
|
|
55
|
+
readonly writeStdout?: (text: string) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function commandSuccess(message: string, data?: McpCommandJsonValue): McpCommandAdapterResult {
|
|
59
|
+
return { ...(data === undefined ? {} : { data }), message, ok: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function commandFailure(
|
|
63
|
+
category: "authentication" | "connection" | "runtime" | "settings",
|
|
64
|
+
message: string,
|
|
65
|
+
): McpCommandAdapterResult {
|
|
66
|
+
return { category, message, ok: false };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function readSavedProjectTrust(agentDirectory: string, cwd: string): Promise<boolean> {
|
|
70
|
+
let document: unknown;
|
|
71
|
+
try {
|
|
72
|
+
document = JSON.parse(await readFile(join(agentDirectory, "trust.json"), "utf8"));
|
|
73
|
+
} catch (cause) {
|
|
74
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") return false;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (document === null || typeof document !== "object" || Array.isArray(document)) return false;
|
|
78
|
+
let path = resolve(cwd);
|
|
79
|
+
while (true) {
|
|
80
|
+
const decision = Object.hasOwn(document, path)
|
|
81
|
+
? Object.getOwnPropertyDescriptor(document, path)?.value
|
|
82
|
+
: undefined;
|
|
83
|
+
if (decision === true || decision === false) return decision;
|
|
84
|
+
const parent = dirname(path);
|
|
85
|
+
if (parent === path) return false;
|
|
86
|
+
path = parent;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function readPiMcpCliOAuthPaste(signal: AbortSignal): Promise<string> {
|
|
91
|
+
const lineReader = createInterface({ input: process.stdin, terminal: false });
|
|
92
|
+
try {
|
|
93
|
+
return (await lineReader.question("", { signal })).trim();
|
|
94
|
+
} finally {
|
|
95
|
+
lineReader.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function authClientIdentity(definition: McpServerDefinition): string {
|
|
100
|
+
return definition.transport === "stdio" || definition.auth?.type !== "oauth"
|
|
101
|
+
? "@ian-pascoe/pi-mcp"
|
|
102
|
+
: (definition.auth.clientId ?? "@ian-pascoe/pi-mcp");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface StandaloneMcpState {
|
|
106
|
+
readonly authStore: McpAuthStore;
|
|
107
|
+
readonly settingsStore: McpSettingsStore;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readStandaloneSettings(state: StandaloneMcpState): Promise<{
|
|
111
|
+
readonly settings?: ResolvedMcpSettings;
|
|
112
|
+
readonly failure?: McpCommandAdapterResult;
|
|
113
|
+
}> {
|
|
114
|
+
const layers = await state.settingsStore.readLayers();
|
|
115
|
+
if (!layers.ok) return { failure: commandFailure("settings", layers.error.message) };
|
|
116
|
+
const settings = resolveMcpSettings({
|
|
117
|
+
getGlobalSettings: () => layers.value.global.document,
|
|
118
|
+
getProjectSettings: () => layers.value.project?.document ?? {},
|
|
119
|
+
});
|
|
120
|
+
if (!settings.valid) {
|
|
121
|
+
return {
|
|
122
|
+
failure: commandFailure("settings", settings.errors.map((error) => error.message).join("; ")),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return { settings };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function listStandaloneServers(state: StandaloneMcpState): Promise<McpCommandAdapterResult> {
|
|
129
|
+
const resolved = await readStandaloneSettings(state);
|
|
130
|
+
if (resolved.failure !== undefined) return resolved.failure;
|
|
131
|
+
const settings = resolved.settings;
|
|
132
|
+
if (settings === undefined) return commandFailure("runtime", "settings resolution failed");
|
|
133
|
+
const servers: McpCommandJsonValue[] = [];
|
|
134
|
+
const messages: string[] = [];
|
|
135
|
+
for (const definition of settings.servers.values()) {
|
|
136
|
+
let storedAuth = false;
|
|
137
|
+
if (definition.transport !== "stdio") {
|
|
138
|
+
const stored = await state.authStore.readEntry({
|
|
139
|
+
clientIdentity: authClientIdentity(definition),
|
|
140
|
+
serverUrl: definition.url,
|
|
141
|
+
});
|
|
142
|
+
if (!stored.ok) return commandFailure("authentication", stored.error.message);
|
|
143
|
+
storedAuth = stored.value !== undefined;
|
|
144
|
+
}
|
|
145
|
+
servers.push({
|
|
146
|
+
auth: definition.transport === "stdio" ? "none" : (definition.auth?.type ?? "anonymous"),
|
|
147
|
+
enabled: definition.enabled,
|
|
148
|
+
name: definition.id,
|
|
149
|
+
provenance: definition.provenance,
|
|
150
|
+
storedAuth,
|
|
151
|
+
transport: definition.transport,
|
|
152
|
+
});
|
|
153
|
+
messages.push(
|
|
154
|
+
`${definition.id} (${definition.provenance}, ${definition.enabled ? "enabled" : "disabled"})`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
for (const mask of settings.masks.values()) {
|
|
158
|
+
servers.push({
|
|
159
|
+
enabled: false,
|
|
160
|
+
inherited: mask.inherited,
|
|
161
|
+
masked: true,
|
|
162
|
+
name: mask.id,
|
|
163
|
+
provenance: mask.provenance,
|
|
164
|
+
});
|
|
165
|
+
messages.push(`${mask.id} (${mask.provenance}, disabled mask)`);
|
|
166
|
+
}
|
|
167
|
+
const message =
|
|
168
|
+
messages.length === 0 ? "No MCP Server Definitions configured" : messages.join("\n");
|
|
169
|
+
return commandSuccess(message, { servers });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function settingsMutationResult(
|
|
173
|
+
result: Awaited<ReturnType<McpSettingsStore["removeServerDefinition"]>>,
|
|
174
|
+
action: string,
|
|
175
|
+
): McpCommandAdapterResult {
|
|
176
|
+
return result.ok
|
|
177
|
+
? commandSuccess(
|
|
178
|
+
`${action} ${result.value.changed ? "updated" : "unchanged"}: ${result.value.path}`,
|
|
179
|
+
)
|
|
180
|
+
: commandFailure("settings", result.error.message);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function enabledServerDefinitions(
|
|
184
|
+
state: StandaloneMcpState,
|
|
185
|
+
selected: string | undefined,
|
|
186
|
+
): Promise<
|
|
187
|
+
| { readonly ok: true; readonly definitions: readonly McpServerDefinition[] }
|
|
188
|
+
| { readonly ok: false; readonly failure: McpCommandAdapterResult }
|
|
189
|
+
> {
|
|
190
|
+
const resolved = await readStandaloneSettings(state);
|
|
191
|
+
if (resolved.failure !== undefined) return { failure: resolved.failure, ok: false };
|
|
192
|
+
const settings = resolved.settings;
|
|
193
|
+
if (settings === undefined) {
|
|
194
|
+
return { failure: commandFailure("runtime", "settings resolution failed"), ok: false };
|
|
195
|
+
}
|
|
196
|
+
if (selected === undefined) {
|
|
197
|
+
return {
|
|
198
|
+
definitions: [...settings.servers.values()].filter(({ enabled }) => enabled),
|
|
199
|
+
ok: true,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const definition = settings.servers.get(selected);
|
|
203
|
+
return definition === undefined
|
|
204
|
+
? { failure: commandFailure("settings", `unknown MCP Server ${selected}`), ok: false }
|
|
205
|
+
: { definitions: [definition], ok: true };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function testServerDefinition(
|
|
209
|
+
definition: McpServerDefinition,
|
|
210
|
+
settings: ResolvedMcpSettings,
|
|
211
|
+
cwd: string,
|
|
212
|
+
authStore: McpAuthStore,
|
|
213
|
+
): Promise<McpCommandAdapterResult> {
|
|
214
|
+
let client: McpServerClient | undefined;
|
|
215
|
+
try {
|
|
216
|
+
const oauth =
|
|
217
|
+
definition.transport !== "stdio" && definition.auth?.type === "oauth"
|
|
218
|
+
? definition.auth
|
|
219
|
+
: undefined;
|
|
220
|
+
const authProvider =
|
|
221
|
+
definition.transport === "stdio" ||
|
|
222
|
+
definition.auth?.type === "none" ||
|
|
223
|
+
definition.auth?.type === "bearer"
|
|
224
|
+
? undefined
|
|
225
|
+
: new McpOAuthProvider({
|
|
226
|
+
authStore,
|
|
227
|
+
clientIdentity: authClientIdentity(definition),
|
|
228
|
+
...(oauth?.clientId === undefined ? {} : { clientId: oauth.clientId }),
|
|
229
|
+
...(oauth?.clientSecret === undefined ? {} : { clientSecret: oauth.clientSecret }),
|
|
230
|
+
onAuthorizationUrl: () => undefined,
|
|
231
|
+
redirectUrl: oauth?.redirectUri ?? DEFAULT_MCP_OAUTH_REDIRECT_URL,
|
|
232
|
+
scopes: oauth?.scopes ?? [],
|
|
233
|
+
serverUrl: definition.url,
|
|
234
|
+
});
|
|
235
|
+
const connectOptions = {
|
|
236
|
+
clientInfo: { name: "pi-mcp", version: "0.1.0" },
|
|
237
|
+
connectTimeoutMs: settings.connectTimeoutMs,
|
|
238
|
+
definition,
|
|
239
|
+
piCwd: cwd,
|
|
240
|
+
requestTimeoutMs: settings.requestTimeoutMs,
|
|
241
|
+
serverId: definition.id,
|
|
242
|
+
};
|
|
243
|
+
client = await McpServerClient.connect(
|
|
244
|
+
authProvider === undefined ? connectOptions : { ...connectOptions, authProvider },
|
|
245
|
+
);
|
|
246
|
+
return commandSuccess(
|
|
247
|
+
`${definition.id}: connected (${client.negotiatedProtocolVersion ?? "unknown protocol"})`,
|
|
248
|
+
{
|
|
249
|
+
connected: true,
|
|
250
|
+
protocolVersion: client.negotiatedProtocolVersion ?? null,
|
|
251
|
+
server: definition.id,
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
} catch {
|
|
255
|
+
return commandFailure("connection", `${definition.id}: connection failed`);
|
|
256
|
+
} finally {
|
|
257
|
+
await client?.close();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function removeStandaloneServer(
|
|
262
|
+
state: StandaloneMcpState,
|
|
263
|
+
options: Parameters<McpCommandAdapters["settings"]["remove"]>[0],
|
|
264
|
+
): Promise<McpCommandAdapterResult> {
|
|
265
|
+
if (options.logout) {
|
|
266
|
+
const resolved = await readStandaloneSettings(state);
|
|
267
|
+
if (resolved.failure !== undefined) return resolved.failure;
|
|
268
|
+
const definition = resolved.settings?.servers.get(options.name);
|
|
269
|
+
if (definition === undefined) {
|
|
270
|
+
return commandFailure("settings", `unknown MCP Server ${options.name}`);
|
|
271
|
+
}
|
|
272
|
+
if (definition.transport !== "stdio") {
|
|
273
|
+
const removed = await state.authStore.removeEntry({
|
|
274
|
+
clientIdentity: authClientIdentity(definition),
|
|
275
|
+
serverUrl: definition.url,
|
|
276
|
+
});
|
|
277
|
+
if (!removed.ok) return commandFailure("authentication", removed.error.message);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return settingsMutationResult(
|
|
281
|
+
await state.settingsStore.removeServerDefinition(options.scope, options.name),
|
|
282
|
+
`MCP Server ${options.name}`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Paths and trust state used to compose standalone-compatible command adapters. */
|
|
287
|
+
export interface StandaloneMcpCommandAdapterOptions {
|
|
288
|
+
/** Pi's global agent directory containing settings and authentication state. */
|
|
289
|
+
readonly agentDirectory?: string;
|
|
290
|
+
/** Working directory that owns optional project MCP settings. */
|
|
291
|
+
readonly cwd?: string;
|
|
292
|
+
/** Whether project-local settings and mutations are trusted for this invocation. */
|
|
293
|
+
readonly projectTrusted?: boolean;
|
|
294
|
+
/** Read a pasted OAuth callback when command flags did not supply one. */
|
|
295
|
+
readonly waitForOAuthPaste?: (signal: AbortSignal) => Promise<string>;
|
|
296
|
+
/** Print the OAuth authorization URL before any browser opener runs. */
|
|
297
|
+
readonly writeAuthorizationUrl?: (url: string) => void | Promise<void>;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Build plain-Node persistent and test adapters without importing the Pi runtime. */
|
|
301
|
+
export async function createStandaloneMcpCommandAdapters(
|
|
302
|
+
options?: StandaloneMcpCommandAdapterOptions,
|
|
303
|
+
): Promise<McpCommandAdapters> {
|
|
304
|
+
const agentDirectory =
|
|
305
|
+
options?.agentDirectory ?? process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
306
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
307
|
+
const projectTrusted =
|
|
308
|
+
options?.projectTrusted ?? (await readSavedProjectTrust(agentDirectory, cwd));
|
|
309
|
+
const state: StandaloneMcpState = {
|
|
310
|
+
authStore: new McpAuthStore(agentDirectory),
|
|
311
|
+
settingsStore: new McpSettingsStore({ agentDirectory, cwd, projectTrusted }),
|
|
312
|
+
};
|
|
313
|
+
const scope = (value: "global" | "project"): McpSettingsScope => value;
|
|
314
|
+
return {
|
|
315
|
+
auth: {
|
|
316
|
+
authenticate: async (command) => {
|
|
317
|
+
const selected = await enabledServerDefinitions(state, command.server);
|
|
318
|
+
if (!selected.ok) return selected.failure;
|
|
319
|
+
const definition = selected.definitions[0];
|
|
320
|
+
if (
|
|
321
|
+
definition === undefined ||
|
|
322
|
+
definition.transport === "stdio" ||
|
|
323
|
+
definition.auth?.type === "none" ||
|
|
324
|
+
definition.auth?.type === "bearer"
|
|
325
|
+
) {
|
|
326
|
+
return commandFailure(
|
|
327
|
+
"authentication",
|
|
328
|
+
`MCP Server ${command.server} is not configured for OAuth`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const suppliedPaste =
|
|
332
|
+
command.callback ??
|
|
333
|
+
(command.code === undefined || command.state === undefined
|
|
334
|
+
? undefined
|
|
335
|
+
: `${command.code} ${command.state}`);
|
|
336
|
+
const oauth = definition.auth?.type === "oauth" ? definition.auth : undefined;
|
|
337
|
+
const result = await authenticateMcpOAuth({
|
|
338
|
+
authStore: state.authStore,
|
|
339
|
+
clientIdentity: authClientIdentity(definition),
|
|
340
|
+
...(oauth?.clientId === undefined ? {} : { clientId: oauth.clientId }),
|
|
341
|
+
...(oauth?.clientSecret === undefined ? {} : { clientSecret: oauth.clientSecret }),
|
|
342
|
+
noOpen: command.noOpen,
|
|
343
|
+
...(oauth?.redirectUri === undefined ? {} : { redirectUrl: oauth.redirectUri }),
|
|
344
|
+
scopes: oauth?.scopes ?? [],
|
|
345
|
+
serverId: definition.id,
|
|
346
|
+
serverUrl: definition.url,
|
|
347
|
+
waitForPaste:
|
|
348
|
+
suppliedPaste === undefined
|
|
349
|
+
? (options?.waitForOAuthPaste ?? readPiMcpCliOAuthPaste)
|
|
350
|
+
: async () => suppliedPaste,
|
|
351
|
+
writeAuthorizationUrl:
|
|
352
|
+
options?.writeAuthorizationUrl ?? ((url) => void process.stdout.write(`${url}\n`)),
|
|
353
|
+
});
|
|
354
|
+
return result.ok
|
|
355
|
+
? commandSuccess(`${command.server}: authenticated`)
|
|
356
|
+
: commandFailure("authentication", result.error.message);
|
|
357
|
+
},
|
|
358
|
+
logout: async (options) => {
|
|
359
|
+
if (options.all && options.force) {
|
|
360
|
+
const reset = await state.authStore.forceReset();
|
|
361
|
+
return reset.ok
|
|
362
|
+
? commandSuccess("MCP authentication store reset")
|
|
363
|
+
: commandFailure("authentication", reset.error.message);
|
|
364
|
+
}
|
|
365
|
+
const selected = await enabledServerDefinitions(state, options.server);
|
|
366
|
+
if (!selected.ok) return selected.failure;
|
|
367
|
+
const definition = selected.definitions[0];
|
|
368
|
+
if (definition === undefined || definition.transport === "stdio") {
|
|
369
|
+
return commandFailure(
|
|
370
|
+
"authentication",
|
|
371
|
+
`MCP Server ${options.server} has no remote credentials`,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
const removed = await state.authStore.removeEntry({
|
|
375
|
+
clientIdentity: authClientIdentity(definition),
|
|
376
|
+
serverUrl: definition.url,
|
|
377
|
+
});
|
|
378
|
+
return removed.ok
|
|
379
|
+
? commandSuccess(`${options.server}: logged out`)
|
|
380
|
+
: commandFailure("authentication", removed.error.message);
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
live: undefined,
|
|
384
|
+
settings: {
|
|
385
|
+
add: async (options) =>
|
|
386
|
+
settingsMutationResult(
|
|
387
|
+
await state.settingsStore.setServerDefinition(
|
|
388
|
+
scope(options.scope),
|
|
389
|
+
options.name,
|
|
390
|
+
options.definition,
|
|
391
|
+
),
|
|
392
|
+
`MCP Server ${options.name}`,
|
|
393
|
+
),
|
|
394
|
+
disable: async (options) => {
|
|
395
|
+
const resolved = await readStandaloneSettings(state);
|
|
396
|
+
if (resolved.failure !== undefined) return resolved.failure;
|
|
397
|
+
const inherited =
|
|
398
|
+
options.scope === "project" &&
|
|
399
|
+
resolved.settings?.servers.get(options.name)?.provenance === "global";
|
|
400
|
+
return settingsMutationResult(
|
|
401
|
+
await state.settingsStore.disableServerDefinition(
|
|
402
|
+
scope(options.scope),
|
|
403
|
+
options.name,
|
|
404
|
+
inherited,
|
|
405
|
+
),
|
|
406
|
+
`MCP Server ${options.name}`,
|
|
407
|
+
);
|
|
408
|
+
},
|
|
409
|
+
enable: async (options) =>
|
|
410
|
+
settingsMutationResult(
|
|
411
|
+
await state.settingsStore.enableServerDefinition(scope(options.scope), options.name),
|
|
412
|
+
`MCP Server ${options.name}`,
|
|
413
|
+
),
|
|
414
|
+
list: () => listStandaloneServers(state),
|
|
415
|
+
remove: async (options) => removeStandaloneServer(state, options),
|
|
416
|
+
},
|
|
417
|
+
test: {
|
|
418
|
+
test: async (options) => {
|
|
419
|
+
const resolved = await readStandaloneSettings(state);
|
|
420
|
+
if (resolved.failure !== undefined) return resolved.failure;
|
|
421
|
+
const settings = resolved.settings;
|
|
422
|
+
if (settings === undefined) return commandFailure("runtime", "settings resolution failed");
|
|
423
|
+
const selected = await enabledServerDefinitions(
|
|
424
|
+
state,
|
|
425
|
+
options.all ? undefined : options.server,
|
|
426
|
+
);
|
|
427
|
+
if (!selected.ok) return selected.failure;
|
|
428
|
+
const results: McpCommandJsonValue[] = [];
|
|
429
|
+
const messages: string[] = [];
|
|
430
|
+
for (const definition of selected.definitions) {
|
|
431
|
+
const result = await testServerDefinition(definition, settings, cwd, state.authStore);
|
|
432
|
+
if (!result.ok) return result;
|
|
433
|
+
messages.push(result.message);
|
|
434
|
+
results.push(result.data ?? null);
|
|
435
|
+
}
|
|
436
|
+
return commandSuccess(messages.join("\n") || "No enabled MCP Servers", { results });
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
interface PiMcpTrustOverride {
|
|
443
|
+
readonly args: readonly string[];
|
|
444
|
+
readonly projectTrusted?: boolean;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function parsePiMcpTrustOverride(args: readonly string[]): PiMcpTrustOverride | undefined {
|
|
448
|
+
const filtered: string[] = [];
|
|
449
|
+
let projectTrusted: boolean | undefined;
|
|
450
|
+
for (const argument of args) {
|
|
451
|
+
const decision =
|
|
452
|
+
argument === "--approve" || argument === "-a"
|
|
453
|
+
? true
|
|
454
|
+
: argument === "--no-approve" || argument === "-na"
|
|
455
|
+
? false
|
|
456
|
+
: undefined;
|
|
457
|
+
if (decision === undefined) {
|
|
458
|
+
filtered.push(argument);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (projectTrusted !== undefined && projectTrusted !== decision) return undefined;
|
|
462
|
+
projectTrusted = decision;
|
|
463
|
+
}
|
|
464
|
+
return projectTrusted === undefined ? { args: filtered } : { args: filtered, projectTrusted };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Run the standalone CLI and return a stable process exit code. */
|
|
468
|
+
export async function runPiMcpCli(
|
|
469
|
+
args: readonly string[],
|
|
470
|
+
options: PiMcpCliOptions = {},
|
|
471
|
+
): Promise<number> {
|
|
472
|
+
const writeStdout = options.writeStdout ?? ((text: string) => process.stdout.write(text));
|
|
473
|
+
const writeStderr = options.writeStderr ?? ((text: string) => process.stderr.write(text));
|
|
474
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
475
|
+
writeStdout(HELP);
|
|
476
|
+
return 0;
|
|
477
|
+
}
|
|
478
|
+
const parsedTrust = parsePiMcpTrustOverride(args);
|
|
479
|
+
if (parsedTrust === undefined) {
|
|
480
|
+
writeStderr("Pi MCP: --approve and --no-approve cannot be combined\n");
|
|
481
|
+
return 2;
|
|
482
|
+
}
|
|
483
|
+
const adapters =
|
|
484
|
+
options.createAdapters === undefined
|
|
485
|
+
? await createStandaloneMcpCommandAdapters(
|
|
486
|
+
parsedTrust.projectTrusted === undefined
|
|
487
|
+
? undefined
|
|
488
|
+
: { projectTrusted: parsedTrust.projectTrusted },
|
|
489
|
+
)
|
|
490
|
+
: await options.createAdapters(parsedTrust.projectTrusted);
|
|
491
|
+
const result = await runMcpCommandTokens(parsedTrust.args, "standalone", adapters);
|
|
492
|
+
(result.ok ? writeStdout : writeStderr)(result.output);
|
|
493
|
+
return result.exitCode;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const entrypoint = process.argv[1];
|
|
497
|
+
if (entrypoint !== undefined && import.meta.url === pathToFileURL(entrypoint).href) {
|
|
498
|
+
void runPiMcpCli(process.argv.slice(2)).then(
|
|
499
|
+
(exitCode) => {
|
|
500
|
+
process.exitCode = exitCode;
|
|
501
|
+
},
|
|
502
|
+
() => {
|
|
503
|
+
process.stderr.write("Pi MCP: command failed unexpectedly\n");
|
|
504
|
+
process.exitCode = 6;
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
}
|