@ian-pascoe/pi-mcp 0.1.0 → 0.2.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/README.md +16 -3
- package/dist/pi-mcp-cli.js +137 -101
- package/package.json +8 -8
- package/src/mcp-command-completion.ts +572 -0
- package/src/mcp-command.ts +272 -131
- package/src/mcp-host.ts +56 -49
- package/src/mcp-observer-ui.ts +195 -0
- package/src/mcp-presentation.ts +660 -0
- package/src/mcp-session-files.ts +15 -6
- package/src/mcp-tool-catalog.ts +59 -44
- package/src/pi-mcp-cli.ts +7 -4
- package/src/pi-mcp-extension.ts +369 -310
- package/src/pi-mcp-settings.ts +23 -7
package/README.md
CHANGED
|
@@ -119,11 +119,12 @@ Global and project `mcp` objects merge top-level timeout and retry fields. A pro
|
|
|
119
119
|
| `auth <server> [--no-open] [--callback URL \| --code CODE --state STATE]` | both | Run an explicit OAuth authorization flow. |
|
|
120
120
|
| `logout <server>` / `logout --all --force` | both | Remove one server's credentials, or explicitly reset corrupt auth storage. |
|
|
121
121
|
| `test <server> \| --all [--json]` | both | Connect temporary clients and close them without disturbing live connections; `--json` is standalone-only. |
|
|
122
|
-
| `
|
|
122
|
+
| `help` | `/mcp` | Show concise runtime command help without querying an MCP Server. |
|
|
123
|
+
| `status` | `/mcp` | Show live connection state, retry details, and active Resource subscriptions. |
|
|
123
124
|
| `reconnect <server>` | `/mcp` | Reconnect one live server. |
|
|
124
125
|
| `prompt <server> <prompt> [--arg NAME=VALUE]…` | `/mcp` | Run an MCP Prompt. |
|
|
125
126
|
| `subscribe <server> <uri>` / `unsubscribe <server> <uri>` | `/mcp` | Manage Resource subscriptions. |
|
|
126
|
-
| `logs [server]
|
|
127
|
+
| `logs [server]` | `/mcp` | Read retained server logs without sending an MCP logging-level request. |
|
|
127
128
|
|
|
128
129
|
Mutations default to global scope. `-l` or `--local` selects project scope and is allowed only when Pi has saved trust for that project. The standalone CLI also accepts `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one invocation. In a running Pi session, add/enable persists first and then connects in the background; disable/remove persists first and then disconnects. A failed connection never rolls back the setting.
|
|
129
130
|
|
|
@@ -131,6 +132,18 @@ For a remote server, `add` accepts repeated `--header NAME=VALUE`, `--transport
|
|
|
131
132
|
|
|
132
133
|
OAuth is always explicit: the authorization URL is printed before a best-effort browser launch. Use `--no-open` for remote/headless use, then provide a full callback URL with `--callback`, or a verified `--code` and `--state` pair. The host permits one active authorization flow per process and validates callback state, issuer, resource, and loopback redirect values.
|
|
133
134
|
|
|
135
|
+
## MCP Observer UI
|
|
136
|
+
|
|
137
|
+
The MCP Transcript Presentation gives every Server Tool and fixed Resource tool a compact row with its original MCP Server and operation names, argument previews, content counts, and text-backed success, warning, failure, or cancellation state. Expanding a row shows bounded structured arguments, model-visible text, stored-content metadata, output-schema failures, and Result Spill paths. Progress replaces the current row instead of adding transcript entries. Pi still renders native result images separately.
|
|
138
|
+
|
|
139
|
+
Prompt messages show their MCP Server, Prompt name, message roles, text, and image metadata. Resource Update Notices show their MCP Server and URI, and state that the Resource remains unread until the agent explicitly reads it. These renderers use the existing persisted content and details. They do not invoke a Prompt, read a Resource, or change replay and next-turn delivery.
|
|
140
|
+
|
|
141
|
+
In TUI mode, the MCP Observer UI uses Pi's footer status to show connected, connecting, retrying, authentication, registration, and failed counts. It sends one MCP Attention Notice when invalid settings, authentication, client registration, or terminal failure needs a command. `/mcp status` reports invalid settings separately from an empty configuration and includes attempts, retry timing, redacted causes, and sorted active Resource subscriptions.
|
|
142
|
+
|
|
143
|
+
Interactive TUI and HTML exports use the semantic tool renderers. Prompt and Resource Update custom rendering, footer health, and Attention Notices are TUI-only. Print, JSON, and RPC modes receive no Observer-only output. HTML custom messages keep their durable content-based representation.
|
|
144
|
+
|
|
145
|
+
Observer copy strips terminal controls and applies exact-value redaction for values resolved from settings. It does not guess secrets from field names, and arbitrary Server Tool data remains faithful in model-visible content and stored session data.
|
|
146
|
+
|
|
134
147
|
## What the model can use
|
|
135
148
|
|
|
136
149
|
Every advertised MCP **Server Tool** becomes an individual Pi tool named:
|
|
@@ -176,7 +189,7 @@ Catalog lists are cached for the session, aggregate at most 1,000 pages, reject
|
|
|
176
189
|
|
|
177
190
|
Text and images map to Pi-native content. Embedded text Resources and Resource Links become provenance-labelled text. Structured content is visible as labelled JSON and retained in tool details. Unsupported audio and binary Resources are saved as private, mode-safe session files rather than discarded.
|
|
178
191
|
|
|
179
|
-
All model-facing text uses Pi's 2,000-line / 50-KB limit. Oversized complete output is retained in a private Result Spill and the returned content includes its path. Per-server stderr and MCP logging retain only the newest 256 KB. Stdio stdout is protocol framing only; stderr and MCP logs do not write directly to TUI, JSON, or RPC output.
|
|
192
|
+
All model-facing text uses Pi's 2,000-line / 50-KB limit. Oversized complete output is retained in a private Result Spill and the returned content includes its path. Per-server stderr and MCP logging retain only the newest 256 KB. `/mcp logs` keeps the newest combined text within Pi's display limit and identifies the private retained-log path when it truncates. Stdio stdout is protocol framing only; stderr and MCP logs do not write directly to TUI, JSON, or RPC output.
|
|
180
193
|
|
|
181
194
|
Desired Resource subscriptions and expanded Prompt messages are persisted as versioned Pi custom entries and replay only on the active session branch. Connections and logs are ephemeral. Reload closes the old session generation and its dormant tool definitions, then creates a clean generation; shutdown awaits owned cleanup.
|
|
182
195
|
|
package/dist/pi-mcp-cli.js
CHANGED
|
@@ -6,11 +6,12 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/pi-mcp-cli.ts
|
|
9
|
+
import { realpathSync } from "node:fs";
|
|
9
10
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
|
|
12
13
|
import { createInterface } from "node:readline/promises";
|
|
13
|
-
import {
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
14
15
|
|
|
15
16
|
// src/mcp-command.ts
|
|
16
17
|
var MCP_COMMAND_NAMES = [
|
|
@@ -27,8 +28,42 @@ var MCP_COMMAND_NAMES = [
|
|
|
27
28
|
"prompt",
|
|
28
29
|
"subscribe",
|
|
29
30
|
"unsubscribe",
|
|
30
|
-
"logs"
|
|
31
|
+
"logs",
|
|
32
|
+
"help"
|
|
31
33
|
];
|
|
34
|
+
var MCP_ADD_LOCAL_VALUE_OPTIONS = ["cwd", "environment"];
|
|
35
|
+
var MCP_ADD_OAUTH_VALUE_OPTIONS = [
|
|
36
|
+
"client-id",
|
|
37
|
+
"client-secret",
|
|
38
|
+
"redirect-uri",
|
|
39
|
+
"scope"
|
|
40
|
+
];
|
|
41
|
+
var MCP_ADD_REMOTE_VALUE_OPTIONS = [
|
|
42
|
+
"auth",
|
|
43
|
+
...MCP_ADD_OAUTH_VALUE_OPTIONS,
|
|
44
|
+
"header",
|
|
45
|
+
"token"
|
|
46
|
+
];
|
|
47
|
+
var MCP_COMMAND_OPTIONS = {
|
|
48
|
+
add: {
|
|
49
|
+
flags: ["local"],
|
|
50
|
+
values: [...MCP_ADD_LOCAL_VALUE_OPTIONS, ...MCP_ADD_REMOTE_VALUE_OPTIONS, "transport"]
|
|
51
|
+
},
|
|
52
|
+
auth: { flags: ["no-open"], values: ["callback", "code", "state"] },
|
|
53
|
+
disable: { flags: ["local"], values: [] },
|
|
54
|
+
enable: { flags: ["local"], values: [] },
|
|
55
|
+
help: { flags: [], values: [] },
|
|
56
|
+
list: { flags: ["json"], values: [] },
|
|
57
|
+
logout: { flags: ["all", "force"], values: [] },
|
|
58
|
+
logs: { flags: [], values: [] },
|
|
59
|
+
prompt: { flags: [], values: ["arg"] },
|
|
60
|
+
reconnect: { flags: [], values: [] },
|
|
61
|
+
remove: { flags: ["local", "logout"], values: [] },
|
|
62
|
+
status: { flags: [], values: [] },
|
|
63
|
+
subscribe: { flags: [], values: [] },
|
|
64
|
+
test: { flags: ["all", "json"], values: [] },
|
|
65
|
+
unsubscribe: { flags: [], values: [] }
|
|
66
|
+
};
|
|
32
67
|
var MCP_STANDALONE_COMMAND_NAMES = MCP_COMMAND_NAMES.slice(0, 8);
|
|
33
68
|
var EXIT_CODES = {
|
|
34
69
|
authentication: 4,
|
|
@@ -40,19 +75,6 @@ var EXIT_CODES = {
|
|
|
40
75
|
};
|
|
41
76
|
var GENERAL_USAGE = `Usage: pi-mcp <command> [options]
|
|
42
77
|
Commands: ${MCP_STANDALONE_COMMAND_NAMES.join(", ")}`;
|
|
43
|
-
var MCP_LOG_LEVELS = [
|
|
44
|
-
"debug",
|
|
45
|
-
"info",
|
|
46
|
-
"notice",
|
|
47
|
-
"warning",
|
|
48
|
-
"error",
|
|
49
|
-
"critical",
|
|
50
|
-
"alert",
|
|
51
|
-
"emergency"
|
|
52
|
-
];
|
|
53
|
-
function isMcpLoggingLevel(value) {
|
|
54
|
-
return MCP_LOG_LEVELS.some((candidate) => candidate === value);
|
|
55
|
-
}
|
|
56
78
|
var RUNTIME_HELP = `Commands: ${MCP_COMMAND_NAMES.join(", ")}`;
|
|
57
79
|
var COMMAND_USAGE = {
|
|
58
80
|
add: "Usage: pi-mcp add [-l|--local] <name> <url> [options]\n pi-mcp add [-l|--local] <name> [options] -- <command> [args...]",
|
|
@@ -61,7 +83,8 @@ var COMMAND_USAGE = {
|
|
|
61
83
|
enable: "Usage: pi-mcp enable [-l|--local] <server>",
|
|
62
84
|
list: "Usage: pi-mcp list [--json]",
|
|
63
85
|
logout: "Usage: pi-mcp logout <server> | --all --force",
|
|
64
|
-
logs: "Usage: /mcp logs [server]
|
|
86
|
+
logs: "Usage: /mcp logs [server]",
|
|
87
|
+
help: "Usage: /mcp help",
|
|
65
88
|
prompt: "Usage: /mcp prompt <server> <prompt> [--arg NAME=VALUE]...",
|
|
66
89
|
reconnect: "Usage: /mcp reconnect <server>",
|
|
67
90
|
remove: "Usage: pi-mcp remove [-l|--local] [--logout] <server>",
|
|
@@ -89,16 +112,19 @@ function normalizeOptionName(rawName) {
|
|
|
89
112
|
return rawName.replace(/^--/, "");
|
|
90
113
|
}
|
|
91
114
|
}
|
|
92
|
-
function
|
|
115
|
+
function scanMcpCommandOptions(tokens, command, mode) {
|
|
93
116
|
const flags = /* @__PURE__ */ new Set();
|
|
94
117
|
const positionals = [];
|
|
95
118
|
const values = /* @__PURE__ */ new Map();
|
|
119
|
+
const accepted = MCP_COMMAND_OPTIONS[command];
|
|
120
|
+
const flagNames = new Set(accepted.flags);
|
|
121
|
+
const valueNames = new Set(accepted.values);
|
|
96
122
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
97
123
|
const token = tokens[index];
|
|
98
124
|
if (token === void 0) continue;
|
|
99
125
|
if (token === "--") {
|
|
100
|
-
if (
|
|
101
|
-
return { flags, positionals, tail: tokens.slice(index + 1), values };
|
|
126
|
+
if (command !== "add") return { message: "unexpected -- delimiter", ok: false };
|
|
127
|
+
return { ok: true, options: { flags, positionals, tail: tokens.slice(index + 1), values } };
|
|
102
128
|
}
|
|
103
129
|
if (!token.startsWith("-")) {
|
|
104
130
|
positionals.push(token);
|
|
@@ -108,20 +134,28 @@ function parseOptions(tokens, flagNames, valueNames, allowDelimiter = false) {
|
|
|
108
134
|
const rawName = equalsIndex < 0 ? token : token.slice(0, equalsIndex);
|
|
109
135
|
const name = normalizeOptionName(rawName);
|
|
110
136
|
if (flagNames.has(name)) {
|
|
111
|
-
if (equalsIndex >= 0)
|
|
137
|
+
if (equalsIndex >= 0)
|
|
138
|
+
return { message: `option ${rawName} does not accept a value`, ok: false };
|
|
112
139
|
flags.add(name);
|
|
113
140
|
continue;
|
|
114
141
|
}
|
|
115
|
-
if (!valueNames.has(name)) return `unknown option ${rawName}
|
|
142
|
+
if (!valueNames.has(name)) return { message: `unknown option ${rawName}`, ok: false };
|
|
116
143
|
const value = equalsIndex < 0 ? tokens[index + 1] : token.slice(equalsIndex + 1);
|
|
117
144
|
if (value === void 0 || equalsIndex < 0 && value.startsWith("--")) {
|
|
118
|
-
return
|
|
145
|
+
return mode === "completion" && value === void 0 ? {
|
|
146
|
+
ok: true,
|
|
147
|
+
options: { flags, pendingValue: name, positionals, tail: void 0, values }
|
|
148
|
+
} : { message: `option ${rawName} requires a value`, ok: false };
|
|
119
149
|
}
|
|
120
150
|
if (equalsIndex < 0) index += 1;
|
|
121
151
|
const existing = values.get(name) ?? [];
|
|
122
152
|
values.set(name, [...existing, value]);
|
|
123
153
|
}
|
|
124
|
-
return { flags, positionals, tail: void 0, values };
|
|
154
|
+
return { ok: true, options: { flags, positionals, tail: void 0, values } };
|
|
155
|
+
}
|
|
156
|
+
function parseOptions(tokens, command) {
|
|
157
|
+
const result = scanMcpCommandOptions(tokens, command, "strict");
|
|
158
|
+
return result.ok ? result.options : result.message;
|
|
125
159
|
}
|
|
126
160
|
function oneValue(options, name) {
|
|
127
161
|
return options.values.get(name)?.at(-1);
|
|
@@ -151,31 +185,50 @@ function isHttpUrl(value) {
|
|
|
151
185
|
return false;
|
|
152
186
|
}
|
|
153
187
|
}
|
|
154
|
-
function
|
|
188
|
+
function classifyMcpAddAuthentication(options, mode) {
|
|
155
189
|
const configuredType = oneValue(options, "auth");
|
|
190
|
+
const token = oneValue(options, "token");
|
|
191
|
+
const oauth = MCP_ADD_OAUTH_VALUE_OPTIONS.some((name) => options.values.has(name));
|
|
192
|
+
const type = configuredType ?? (token !== void 0 ? "bearer" : oauth ? "oauth" : void 0);
|
|
193
|
+
if (type === void 0) return { ok: true, type };
|
|
194
|
+
if (type === "none") {
|
|
195
|
+
return token === void 0 && !oauth ? { ok: true, type } : { message: "auth type none cannot include credential options", ok: false };
|
|
196
|
+
}
|
|
197
|
+
if (type === "bearer") {
|
|
198
|
+
if (mode === "strict" && (token === void 0 || token.length === 0))
|
|
199
|
+
return { message: "bearer auth requires --token", ok: false };
|
|
200
|
+
if (oauth) return { message: "bearer auth cannot include OAuth options", ok: false };
|
|
201
|
+
return { ok: true, type };
|
|
202
|
+
}
|
|
203
|
+
if (type === "oauth") {
|
|
204
|
+
return token === void 0 ? { ok: true, type } : { message: "OAuth auth cannot include --token", ok: false };
|
|
205
|
+
}
|
|
206
|
+
return { message: "--auth must be none, bearer, or oauth", ok: false };
|
|
207
|
+
}
|
|
208
|
+
function classifyMcpAddTransportMode(options) {
|
|
209
|
+
const transport = oneValue(options, "transport");
|
|
210
|
+
if (transport !== void 0 && transport !== "http" && transport !== "sse" && transport !== "stdio")
|
|
211
|
+
return "invalid";
|
|
212
|
+
if (options.positionals.length > 2) return "invalid";
|
|
213
|
+
const local = options.tail !== void 0 || transport === "stdio" || MCP_ADD_LOCAL_VALUE_OPTIONS.some((name) => options.values.has(name));
|
|
214
|
+
const remote = options.positionals.length > 1 || transport === "http" || transport === "sse" || MCP_ADD_REMOTE_VALUE_OPTIONS.some((name) => options.values.has(name));
|
|
215
|
+
return local && remote ? "invalid" : local ? "local" : remote ? "remote" : "both";
|
|
216
|
+
}
|
|
217
|
+
function parseRemoteAuth(options) {
|
|
156
218
|
const token = oneValue(options, "token");
|
|
157
219
|
const clientId = oneValue(options, "client-id");
|
|
158
220
|
const clientSecret = oneValue(options, "client-secret");
|
|
159
221
|
const redirectUri = oneValue(options, "redirect-uri");
|
|
160
222
|
const scopes = options.values.get("scope") ?? [];
|
|
161
|
-
const
|
|
162
|
-
|
|
223
|
+
const compatibility = classifyMcpAddAuthentication(options, "strict");
|
|
224
|
+
if (!compatibility.ok) return compatibility.message;
|
|
225
|
+
const type = compatibility.type;
|
|
163
226
|
if (type === void 0) return void 0;
|
|
164
|
-
if (type === "none") {
|
|
165
|
-
if (token !== void 0 || clientId !== void 0 || clientSecret !== void 0 || redirectUri !== void 0 || scopes.length > 0) {
|
|
166
|
-
return "auth type none cannot include credential options";
|
|
167
|
-
}
|
|
168
|
-
return { type: "none" };
|
|
169
|
-
}
|
|
227
|
+
if (type === "none") return { type: "none" };
|
|
170
228
|
if (type === "bearer") {
|
|
171
|
-
if (token === void 0
|
|
172
|
-
if (clientId !== void 0 || clientSecret !== void 0 || redirectUri !== void 0 || scopes.length > 0) {
|
|
173
|
-
return "bearer auth cannot include OAuth options";
|
|
174
|
-
}
|
|
229
|
+
if (token === void 0) return "bearer auth requires --token";
|
|
175
230
|
return { token, type: "bearer" };
|
|
176
231
|
}
|
|
177
|
-
if (type !== "oauth") return "--auth must be none, bearer, or oauth";
|
|
178
|
-
if (token !== void 0) return "OAuth auth cannot include --token";
|
|
179
232
|
return {
|
|
180
233
|
...clientId === void 0 ? {} : { clientId },
|
|
181
234
|
...clientSecret === void 0 ? {} : { clientSecret },
|
|
@@ -185,47 +238,24 @@ function parseRemoteAuth(options) {
|
|
|
185
238
|
};
|
|
186
239
|
}
|
|
187
240
|
function parseAdd(args) {
|
|
188
|
-
const options = parseOptions(
|
|
189
|
-
args,
|
|
190
|
-
/* @__PURE__ */ new Set(["local"]),
|
|
191
|
-
/* @__PURE__ */ new Set([
|
|
192
|
-
"auth",
|
|
193
|
-
"client-id",
|
|
194
|
-
"client-secret",
|
|
195
|
-
"cwd",
|
|
196
|
-
"environment",
|
|
197
|
-
"header",
|
|
198
|
-
"redirect-uri",
|
|
199
|
-
"scope",
|
|
200
|
-
"token",
|
|
201
|
-
"transport"
|
|
202
|
-
]),
|
|
203
|
-
true
|
|
204
|
-
);
|
|
241
|
+
const options = parseOptions(args, "add");
|
|
205
242
|
if (typeof options === "string") return usageFailure("add", options);
|
|
206
243
|
const name = options.positionals[0];
|
|
207
244
|
if (name === void 0 || name.length === 0)
|
|
208
245
|
return usageFailure("add", "server name is required");
|
|
246
|
+
const transportMode = classifyMcpAddTransportMode(options);
|
|
209
247
|
if (options.tail !== void 0) {
|
|
210
248
|
if (options.positionals.length !== 1)
|
|
211
249
|
return usageFailure("add", "a local add cannot also include a URL");
|
|
212
250
|
const command = options.tail[0];
|
|
213
251
|
if (command === void 0 || command.length === 0)
|
|
214
252
|
return usageFailure("add", "local command is required after --");
|
|
215
|
-
for (const remoteOption of
|
|
216
|
-
"auth",
|
|
217
|
-
"client-id",
|
|
218
|
-
"client-secret",
|
|
219
|
-
"header",
|
|
220
|
-
"redirect-uri",
|
|
221
|
-
"scope",
|
|
222
|
-
"token"
|
|
223
|
-
]) {
|
|
253
|
+
for (const remoteOption of MCP_ADD_REMOTE_VALUE_OPTIONS) {
|
|
224
254
|
if (options.values.has(remoteOption))
|
|
225
255
|
return usageFailure("add", `local add cannot include --${remoteOption}`);
|
|
226
256
|
}
|
|
227
257
|
const transport2 = oneValue(options, "transport");
|
|
228
|
-
if (transport2 !== void 0 &&
|
|
258
|
+
if (transport2 !== void 0 && transportMode !== "local") {
|
|
229
259
|
return usageFailure("add", "local transport must be stdio");
|
|
230
260
|
}
|
|
231
261
|
const environment = parseAssignments(options.values.get("environment") ?? [], "environment");
|
|
@@ -250,13 +280,13 @@ function parseAdd(args) {
|
|
|
250
280
|
}
|
|
251
281
|
if (options.positionals.length !== 2)
|
|
252
282
|
return usageFailure("add", "remote add requires a name and URL");
|
|
253
|
-
if (
|
|
283
|
+
if (MCP_ADD_LOCAL_VALUE_OPTIONS.some((name2) => options.values.has(name2))) {
|
|
254
284
|
return usageFailure("add", "remote add cannot include local process options");
|
|
255
285
|
}
|
|
256
286
|
const url = options.positionals[1] ?? "";
|
|
257
287
|
if (!isHttpUrl(url)) return usageFailure("add", "remote URL must be absolute HTTP or HTTPS");
|
|
258
288
|
const transport = oneValue(options, "transport") ?? "http";
|
|
259
|
-
if (transport !== "http" && transport !== "sse")
|
|
289
|
+
if (transportMode !== "remote" || transport !== "http" && transport !== "sse")
|
|
260
290
|
return usageFailure("add", "remote transport must be http or sse");
|
|
261
291
|
const headers = parseAssignments(options.values.get("header") ?? [], "header");
|
|
262
292
|
if (typeof headers === "string") return usageFailure("add", headers);
|
|
@@ -279,11 +309,7 @@ function parseAdd(args) {
|
|
|
279
309
|
};
|
|
280
310
|
}
|
|
281
311
|
function parseScopedServer(kind, args) {
|
|
282
|
-
const options = parseOptions(
|
|
283
|
-
args,
|
|
284
|
-
new Set(kind === "remove" ? ["local", "logout"] : ["local"]),
|
|
285
|
-
/* @__PURE__ */ new Set()
|
|
286
|
-
);
|
|
312
|
+
const options = parseOptions(args, kind);
|
|
287
313
|
if (typeof options === "string") return usageFailure(kind, options);
|
|
288
314
|
if (options.positionals.length !== 1)
|
|
289
315
|
return usageFailure(kind, "exactly one server name is required");
|
|
@@ -310,7 +336,7 @@ function parseMcpCommand(args, surface) {
|
|
|
310
336
|
if (commandName === "remove" || commandName === "enable" || commandName === "disable")
|
|
311
337
|
return parseScopedServer(commandName, rest);
|
|
312
338
|
if (commandName === "list") {
|
|
313
|
-
const options2 = parseOptions(rest,
|
|
339
|
+
const options2 = parseOptions(rest, "list");
|
|
314
340
|
if (typeof options2 === "string" || options2.positionals.length > 0)
|
|
315
341
|
return usageFailure(
|
|
316
342
|
"list",
|
|
@@ -321,11 +347,7 @@ function parseMcpCommand(args, surface) {
|
|
|
321
347
|
return { command: { json: options2.flags.has("json"), kind: "list" }, ok: true };
|
|
322
348
|
}
|
|
323
349
|
if (commandName === "auth") {
|
|
324
|
-
const options2 = parseOptions(
|
|
325
|
-
rest,
|
|
326
|
-
/* @__PURE__ */ new Set(["no-open"]),
|
|
327
|
-
/* @__PURE__ */ new Set(["callback", "code", "state"])
|
|
328
|
-
);
|
|
350
|
+
const options2 = parseOptions(rest, "auth");
|
|
329
351
|
if (typeof options2 === "string") return usageFailure("auth", options2);
|
|
330
352
|
if (options2.positionals.length !== 1)
|
|
331
353
|
return usageFailure(
|
|
@@ -352,7 +374,7 @@ function parseMcpCommand(args, surface) {
|
|
|
352
374
|
};
|
|
353
375
|
}
|
|
354
376
|
if (commandName === "logout") {
|
|
355
|
-
const options2 = parseOptions(rest,
|
|
377
|
+
const options2 = parseOptions(rest, "logout");
|
|
356
378
|
if (typeof options2 === "string") return usageFailure("logout", options2);
|
|
357
379
|
const all = options2.flags.has("all");
|
|
358
380
|
const force = options2.flags.has("force");
|
|
@@ -367,7 +389,7 @@ function parseMcpCommand(args, surface) {
|
|
|
367
389
|
};
|
|
368
390
|
}
|
|
369
391
|
if (commandName === "test") {
|
|
370
|
-
const options2 = parseOptions(rest,
|
|
392
|
+
const options2 = parseOptions(rest, "test");
|
|
371
393
|
if (typeof options2 === "string") return usageFailure("test", options2);
|
|
372
394
|
if (surface === "runtime" && options2.flags.has("json"))
|
|
373
395
|
return usageFailure("test", "--json is standalone-only");
|
|
@@ -388,12 +410,16 @@ function parseMcpCommand(args, surface) {
|
|
|
388
410
|
if (rest.length > 0) return usageFailure("status", "status accepts no arguments");
|
|
389
411
|
return { command: { includeHelp: false, kind: "status" }, ok: true };
|
|
390
412
|
}
|
|
413
|
+
if (commandName === "help") {
|
|
414
|
+
if (rest.length > 0) return usageFailure("help", "help accepts no arguments");
|
|
415
|
+
return { command: { kind: "help" }, ok: true };
|
|
416
|
+
}
|
|
391
417
|
if (commandName === "reconnect") {
|
|
392
418
|
if (rest.length !== 1) return usageFailure("reconnect", "exactly one server name is required");
|
|
393
419
|
return { command: { kind: "reconnect", server: rest[0] ?? "" }, ok: true };
|
|
394
420
|
}
|
|
395
421
|
if (commandName === "prompt") {
|
|
396
|
-
const options2 = parseOptions(rest,
|
|
422
|
+
const options2 = parseOptions(rest, "prompt");
|
|
397
423
|
if (typeof options2 === "string") return usageFailure("prompt", options2);
|
|
398
424
|
if (options2.positionals.length !== 2)
|
|
399
425
|
return usageFailure("prompt", "server and prompt names are required");
|
|
@@ -413,20 +439,15 @@ function parseMcpCommand(args, surface) {
|
|
|
413
439
|
if (rest.length !== 2) return usageFailure(commandName, "server and resource URI are required");
|
|
414
440
|
return { command: { kind: commandName, server: rest[0] ?? "", uri: rest[1] ?? "" }, ok: true };
|
|
415
441
|
}
|
|
416
|
-
const options = parseOptions(rest,
|
|
442
|
+
const options = parseOptions(rest, "logs");
|
|
417
443
|
if (typeof options === "string" || options.positionals.length > 1)
|
|
418
444
|
return usageFailure(
|
|
419
445
|
"logs",
|
|
420
446
|
typeof options === "string" ? options : "logs accepts at most one server name"
|
|
421
447
|
);
|
|
422
|
-
const level = oneValue(options, "level");
|
|
423
|
-
if (level !== void 0 && !isMcpLoggingLevel(level)) {
|
|
424
|
-
return usageFailure("logs", `unknown logging level ${level}`);
|
|
425
|
-
}
|
|
426
448
|
return {
|
|
427
449
|
command: {
|
|
428
450
|
kind: "logs",
|
|
429
|
-
...level === void 0 ? {} : { level },
|
|
430
451
|
...options.positionals[0] === void 0 ? {} : { server: options.positionals[0] }
|
|
431
452
|
},
|
|
432
453
|
ok: true
|
|
@@ -549,12 +570,11 @@ async function executeMcpCommand(command, adapters, surface = "standalone") {
|
|
|
549
570
|
case "logs": {
|
|
550
571
|
const live = liveAdapter(adapters);
|
|
551
572
|
if ("exitCode" in live) return live;
|
|
552
|
-
result = await live.logs({
|
|
553
|
-
...command.level === void 0 ? {} : { level: command.level },
|
|
554
|
-
...command.server === void 0 ? {} : { server: command.server }
|
|
555
|
-
});
|
|
573
|
+
result = await live.logs(command.server === void 0 ? {} : { server: command.server });
|
|
556
574
|
break;
|
|
557
575
|
}
|
|
576
|
+
case "help":
|
|
577
|
+
return successResult({ message: RUNTIME_HELP, ok: true }, false);
|
|
558
578
|
}
|
|
559
579
|
if (!result.ok) return adapterFailure(result.category, result.message);
|
|
560
580
|
const json = (command.kind === "list" || command.kind === "test") && command.json;
|
|
@@ -9960,6 +9980,10 @@ var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
|
9960
9980
|
var MCP_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
9961
9981
|
var JsonValueSchema = typebox_exports.Any();
|
|
9962
9982
|
var NonEmptyStringSchema = typebox_exports.String({ minLength: 1 });
|
|
9983
|
+
var McpServerIdSchema = typebox_exports.String({
|
|
9984
|
+
minLength: 1,
|
|
9985
|
+
pattern: "^[^\\u0000-\\u001F\\u007F-\\u009F]+$"
|
|
9986
|
+
});
|
|
9963
9987
|
var PositiveSafeIntegerSchema = typebox_exports.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
|
|
9964
9988
|
var RetryCountSchema = typebox_exports.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
9965
9989
|
var BackoffFactorSchema = typebox_exports.Number({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
|
|
@@ -10004,7 +10028,7 @@ var McpLayerWireSchema = typebox_exports.Object(
|
|
|
10004
10028
|
requestTimeoutMs: typebox_exports.Optional(PositiveSafeIntegerSchema),
|
|
10005
10029
|
retry: typebox_exports.Optional(McpRetryWireSchema),
|
|
10006
10030
|
servers: typebox_exports.Optional(
|
|
10007
|
-
typebox_exports.Record(
|
|
10031
|
+
typebox_exports.Record(McpServerIdSchema, typebox_exports.Union([McpServerDefinitionWireSchema, typebox_exports.Null()]))
|
|
10008
10032
|
)
|
|
10009
10033
|
},
|
|
10010
10034
|
{ additionalProperties: false }
|
|
@@ -10108,6 +10132,20 @@ function parseMcpLayer(document, scope) {
|
|
|
10108
10132
|
value: {}
|
|
10109
10133
|
};
|
|
10110
10134
|
}
|
|
10135
|
+
if (Object.keys(document.mcp.servers ?? {}).some(
|
|
10136
|
+
(serverId) => !value_exports.Check(McpServerIdSchema, serverId)
|
|
10137
|
+
)) {
|
|
10138
|
+
return {
|
|
10139
|
+
errors: [
|
|
10140
|
+
new McpSettingsError(
|
|
10141
|
+
`${scope} mcp.servers`,
|
|
10142
|
+
"Server Definition names must be non-empty and contain no terminal controls"
|
|
10143
|
+
)
|
|
10144
|
+
],
|
|
10145
|
+
scope,
|
|
10146
|
+
value: {}
|
|
10147
|
+
};
|
|
10148
|
+
}
|
|
10111
10149
|
return { errors: [], scope, value: document.mcp };
|
|
10112
10150
|
}
|
|
10113
10151
|
function parseRemoteUrl(value, path) {
|
|
@@ -10269,9 +10307,7 @@ function mergeServerDefinitions(globalLayer, projectLayer, environment, secrets)
|
|
|
10269
10307
|
const masks = /* @__PURE__ */ new Map();
|
|
10270
10308
|
const errors = [];
|
|
10271
10309
|
for (const [id, wire] of Object.entries(globalLayer.value.servers ?? {})) {
|
|
10272
|
-
if (
|
|
10273
|
-
errors.push(new McpSettingsError("global mcp.servers", "Server Definition ID is empty"));
|
|
10274
|
-
} else if (wire === null || wire.enabled === false && Object.keys(wire).length === 1) {
|
|
10310
|
+
if (wire === null || wire.enabled === false && Object.keys(wire).length === 1) {
|
|
10275
10311
|
errors.push(
|
|
10276
10312
|
new McpSettingsError(
|
|
10277
10313
|
`global mcp.servers.${id}`,
|
|
@@ -10286,9 +10322,7 @@ function mergeServerDefinitions(globalLayer, projectLayer, environment, secrets)
|
|
|
10286
10322
|
const inherited = definitions.has(id);
|
|
10287
10323
|
definitions.delete(id);
|
|
10288
10324
|
masks.delete(id);
|
|
10289
|
-
if (
|
|
10290
|
-
errors.push(new McpSettingsError("project mcp.servers", "Server Definition ID is empty"));
|
|
10291
|
-
} else if (wire === null || wire.enabled === false && Object.keys(wire).length === 1) {
|
|
10325
|
+
if (wire === null || wire.enabled === false && Object.keys(wire).length === 1) {
|
|
10292
10326
|
masks.set(id, { id, inherited, provenance: "project" });
|
|
10293
10327
|
} else {
|
|
10294
10328
|
definitions.set(id, { scope: "project", wire });
|
|
@@ -10686,7 +10720,7 @@ async function listStandaloneServers(state) {
|
|
|
10686
10720
|
transport: definition.transport
|
|
10687
10721
|
});
|
|
10688
10722
|
messages.push(
|
|
10689
|
-
`${definition.id} (
|
|
10723
|
+
`${definition.id} (provenance=${definition.provenance}, ${definition.enabled ? "enabled" : "disabled"}, transport=${definition.transport}, auth=${definition.transport === "stdio" ? "none" : definition.auth?.type ?? "anonymous"}, stored-auth=${storedAuth ? "present" : "absent"})`
|
|
10690
10724
|
);
|
|
10691
10725
|
}
|
|
10692
10726
|
for (const mask of settings2.masks.values()) {
|
|
@@ -10697,7 +10731,9 @@ async function listStandaloneServers(state) {
|
|
|
10697
10731
|
name: mask.id,
|
|
10698
10732
|
provenance: mask.provenance
|
|
10699
10733
|
});
|
|
10700
|
-
messages.push(
|
|
10734
|
+
messages.push(
|
|
10735
|
+
`${mask.id} (provenance=${mask.provenance}, masked, inherited=${mask.inherited ? "yes" : "no"})`
|
|
10736
|
+
);
|
|
10701
10737
|
}
|
|
10702
10738
|
const message = messages.length === 0 ? "No MCP Server Definitions configured" : messages.join("\n");
|
|
10703
10739
|
return commandSuccess(message, { servers });
|
|
@@ -10931,7 +10967,7 @@ async function runPiMcpCli(args, options = {}) {
|
|
|
10931
10967
|
return result.exitCode;
|
|
10932
10968
|
}
|
|
10933
10969
|
var entrypoint = process.argv[1];
|
|
10934
|
-
if (entrypoint !== void 0 && import.meta.url ===
|
|
10970
|
+
if (entrypoint !== void 0 && fileURLToPath(import.meta.url) === realpathSync(entrypoint)) {
|
|
10935
10971
|
void runPiMcpCli(process.argv.slice(2)).then(
|
|
10936
10972
|
(exitCode) => {
|
|
10937
10973
|
process.exitCode = exitCode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A complete Model Context Protocol Host for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -35,12 +35,6 @@
|
|
|
35
35
|
"access": "public",
|
|
36
36
|
"provenance": true
|
|
37
37
|
},
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build:cli": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && esbuild src/pi-mcp-cli.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/pi-mcp-cli.js --external:@modelcontextprotocol/client --external:@modelcontextprotocol/client/*",
|
|
40
|
-
"prepack": "pnpm build:cli",
|
|
41
|
-
"test": "vitest run --config ../../vitest.config.ts --root .",
|
|
42
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
43
|
-
},
|
|
44
38
|
"dependencies": {
|
|
45
39
|
"@modelcontextprotocol/client": "^2.0.0"
|
|
46
40
|
},
|
|
@@ -51,6 +45,7 @@
|
|
|
51
45
|
"peerDependencies": {
|
|
52
46
|
"@earendil-works/pi-ai": "*",
|
|
53
47
|
"@earendil-works/pi-coding-agent": "*",
|
|
48
|
+
"@earendil-works/pi-tui": "*",
|
|
54
49
|
"typebox": "*"
|
|
55
50
|
},
|
|
56
51
|
"engines": {
|
|
@@ -60,5 +55,10 @@
|
|
|
60
55
|
"extensions": [
|
|
61
56
|
"./src/index.ts"
|
|
62
57
|
]
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build:cli": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && esbuild src/pi-mcp-cli.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/pi-mcp-cli.js --external:@modelcontextprotocol/client --external:@modelcontextprotocol/client/*",
|
|
61
|
+
"test": "vitest run --config ../../vitest.config.ts --root .",
|
|
62
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
63
63
|
}
|
|
64
|
-
}
|
|
64
|
+
}
|