@ian-pascoe/pi-mcp 0.1.1 → 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 +134 -99
- 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 +4 -2
- 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
|
@@ -28,8 +28,42 @@ var MCP_COMMAND_NAMES = [
|
|
|
28
28
|
"prompt",
|
|
29
29
|
"subscribe",
|
|
30
30
|
"unsubscribe",
|
|
31
|
-
"logs"
|
|
31
|
+
"logs",
|
|
32
|
+
"help"
|
|
32
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
|
+
};
|
|
33
67
|
var MCP_STANDALONE_COMMAND_NAMES = MCP_COMMAND_NAMES.slice(0, 8);
|
|
34
68
|
var EXIT_CODES = {
|
|
35
69
|
authentication: 4,
|
|
@@ -41,19 +75,6 @@ var EXIT_CODES = {
|
|
|
41
75
|
};
|
|
42
76
|
var GENERAL_USAGE = `Usage: pi-mcp <command> [options]
|
|
43
77
|
Commands: ${MCP_STANDALONE_COMMAND_NAMES.join(", ")}`;
|
|
44
|
-
var MCP_LOG_LEVELS = [
|
|
45
|
-
"debug",
|
|
46
|
-
"info",
|
|
47
|
-
"notice",
|
|
48
|
-
"warning",
|
|
49
|
-
"error",
|
|
50
|
-
"critical",
|
|
51
|
-
"alert",
|
|
52
|
-
"emergency"
|
|
53
|
-
];
|
|
54
|
-
function isMcpLoggingLevel(value) {
|
|
55
|
-
return MCP_LOG_LEVELS.some((candidate) => candidate === value);
|
|
56
|
-
}
|
|
57
78
|
var RUNTIME_HELP = `Commands: ${MCP_COMMAND_NAMES.join(", ")}`;
|
|
58
79
|
var COMMAND_USAGE = {
|
|
59
80
|
add: "Usage: pi-mcp add [-l|--local] <name> <url> [options]\n pi-mcp add [-l|--local] <name> [options] -- <command> [args...]",
|
|
@@ -62,7 +83,8 @@ var COMMAND_USAGE = {
|
|
|
62
83
|
enable: "Usage: pi-mcp enable [-l|--local] <server>",
|
|
63
84
|
list: "Usage: pi-mcp list [--json]",
|
|
64
85
|
logout: "Usage: pi-mcp logout <server> | --all --force",
|
|
65
|
-
logs: "Usage: /mcp logs [server]
|
|
86
|
+
logs: "Usage: /mcp logs [server]",
|
|
87
|
+
help: "Usage: /mcp help",
|
|
66
88
|
prompt: "Usage: /mcp prompt <server> <prompt> [--arg NAME=VALUE]...",
|
|
67
89
|
reconnect: "Usage: /mcp reconnect <server>",
|
|
68
90
|
remove: "Usage: pi-mcp remove [-l|--local] [--logout] <server>",
|
|
@@ -90,16 +112,19 @@ function normalizeOptionName(rawName) {
|
|
|
90
112
|
return rawName.replace(/^--/, "");
|
|
91
113
|
}
|
|
92
114
|
}
|
|
93
|
-
function
|
|
115
|
+
function scanMcpCommandOptions(tokens, command, mode) {
|
|
94
116
|
const flags = /* @__PURE__ */ new Set();
|
|
95
117
|
const positionals = [];
|
|
96
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);
|
|
97
122
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
98
123
|
const token = tokens[index];
|
|
99
124
|
if (token === void 0) continue;
|
|
100
125
|
if (token === "--") {
|
|
101
|
-
if (
|
|
102
|
-
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 } };
|
|
103
128
|
}
|
|
104
129
|
if (!token.startsWith("-")) {
|
|
105
130
|
positionals.push(token);
|
|
@@ -109,20 +134,28 @@ function parseOptions(tokens, flagNames, valueNames, allowDelimiter = false) {
|
|
|
109
134
|
const rawName = equalsIndex < 0 ? token : token.slice(0, equalsIndex);
|
|
110
135
|
const name = normalizeOptionName(rawName);
|
|
111
136
|
if (flagNames.has(name)) {
|
|
112
|
-
if (equalsIndex >= 0)
|
|
137
|
+
if (equalsIndex >= 0)
|
|
138
|
+
return { message: `option ${rawName} does not accept a value`, ok: false };
|
|
113
139
|
flags.add(name);
|
|
114
140
|
continue;
|
|
115
141
|
}
|
|
116
|
-
if (!valueNames.has(name)) return `unknown option ${rawName}
|
|
142
|
+
if (!valueNames.has(name)) return { message: `unknown option ${rawName}`, ok: false };
|
|
117
143
|
const value = equalsIndex < 0 ? tokens[index + 1] : token.slice(equalsIndex + 1);
|
|
118
144
|
if (value === void 0 || equalsIndex < 0 && value.startsWith("--")) {
|
|
119
|
-
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 };
|
|
120
149
|
}
|
|
121
150
|
if (equalsIndex < 0) index += 1;
|
|
122
151
|
const existing = values.get(name) ?? [];
|
|
123
152
|
values.set(name, [...existing, value]);
|
|
124
153
|
}
|
|
125
|
-
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;
|
|
126
159
|
}
|
|
127
160
|
function oneValue(options, name) {
|
|
128
161
|
return options.values.get(name)?.at(-1);
|
|
@@ -152,31 +185,50 @@ function isHttpUrl(value) {
|
|
|
152
185
|
return false;
|
|
153
186
|
}
|
|
154
187
|
}
|
|
155
|
-
function
|
|
188
|
+
function classifyMcpAddAuthentication(options, mode) {
|
|
156
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) {
|
|
157
218
|
const token = oneValue(options, "token");
|
|
158
219
|
const clientId = oneValue(options, "client-id");
|
|
159
220
|
const clientSecret = oneValue(options, "client-secret");
|
|
160
221
|
const redirectUri = oneValue(options, "redirect-uri");
|
|
161
222
|
const scopes = options.values.get("scope") ?? [];
|
|
162
|
-
const
|
|
163
|
-
|
|
223
|
+
const compatibility = classifyMcpAddAuthentication(options, "strict");
|
|
224
|
+
if (!compatibility.ok) return compatibility.message;
|
|
225
|
+
const type = compatibility.type;
|
|
164
226
|
if (type === void 0) return void 0;
|
|
165
|
-
if (type === "none") {
|
|
166
|
-
if (token !== void 0 || clientId !== void 0 || clientSecret !== void 0 || redirectUri !== void 0 || scopes.length > 0) {
|
|
167
|
-
return "auth type none cannot include credential options";
|
|
168
|
-
}
|
|
169
|
-
return { type: "none" };
|
|
170
|
-
}
|
|
227
|
+
if (type === "none") return { type: "none" };
|
|
171
228
|
if (type === "bearer") {
|
|
172
|
-
if (token === void 0
|
|
173
|
-
if (clientId !== void 0 || clientSecret !== void 0 || redirectUri !== void 0 || scopes.length > 0) {
|
|
174
|
-
return "bearer auth cannot include OAuth options";
|
|
175
|
-
}
|
|
229
|
+
if (token === void 0) return "bearer auth requires --token";
|
|
176
230
|
return { token, type: "bearer" };
|
|
177
231
|
}
|
|
178
|
-
if (type !== "oauth") return "--auth must be none, bearer, or oauth";
|
|
179
|
-
if (token !== void 0) return "OAuth auth cannot include --token";
|
|
180
232
|
return {
|
|
181
233
|
...clientId === void 0 ? {} : { clientId },
|
|
182
234
|
...clientSecret === void 0 ? {} : { clientSecret },
|
|
@@ -186,47 +238,24 @@ function parseRemoteAuth(options) {
|
|
|
186
238
|
};
|
|
187
239
|
}
|
|
188
240
|
function parseAdd(args) {
|
|
189
|
-
const options = parseOptions(
|
|
190
|
-
args,
|
|
191
|
-
/* @__PURE__ */ new Set(["local"]),
|
|
192
|
-
/* @__PURE__ */ new Set([
|
|
193
|
-
"auth",
|
|
194
|
-
"client-id",
|
|
195
|
-
"client-secret",
|
|
196
|
-
"cwd",
|
|
197
|
-
"environment",
|
|
198
|
-
"header",
|
|
199
|
-
"redirect-uri",
|
|
200
|
-
"scope",
|
|
201
|
-
"token",
|
|
202
|
-
"transport"
|
|
203
|
-
]),
|
|
204
|
-
true
|
|
205
|
-
);
|
|
241
|
+
const options = parseOptions(args, "add");
|
|
206
242
|
if (typeof options === "string") return usageFailure("add", options);
|
|
207
243
|
const name = options.positionals[0];
|
|
208
244
|
if (name === void 0 || name.length === 0)
|
|
209
245
|
return usageFailure("add", "server name is required");
|
|
246
|
+
const transportMode = classifyMcpAddTransportMode(options);
|
|
210
247
|
if (options.tail !== void 0) {
|
|
211
248
|
if (options.positionals.length !== 1)
|
|
212
249
|
return usageFailure("add", "a local add cannot also include a URL");
|
|
213
250
|
const command = options.tail[0];
|
|
214
251
|
if (command === void 0 || command.length === 0)
|
|
215
252
|
return usageFailure("add", "local command is required after --");
|
|
216
|
-
for (const remoteOption of
|
|
217
|
-
"auth",
|
|
218
|
-
"client-id",
|
|
219
|
-
"client-secret",
|
|
220
|
-
"header",
|
|
221
|
-
"redirect-uri",
|
|
222
|
-
"scope",
|
|
223
|
-
"token"
|
|
224
|
-
]) {
|
|
253
|
+
for (const remoteOption of MCP_ADD_REMOTE_VALUE_OPTIONS) {
|
|
225
254
|
if (options.values.has(remoteOption))
|
|
226
255
|
return usageFailure("add", `local add cannot include --${remoteOption}`);
|
|
227
256
|
}
|
|
228
257
|
const transport2 = oneValue(options, "transport");
|
|
229
|
-
if (transport2 !== void 0 &&
|
|
258
|
+
if (transport2 !== void 0 && transportMode !== "local") {
|
|
230
259
|
return usageFailure("add", "local transport must be stdio");
|
|
231
260
|
}
|
|
232
261
|
const environment = parseAssignments(options.values.get("environment") ?? [], "environment");
|
|
@@ -251,13 +280,13 @@ function parseAdd(args) {
|
|
|
251
280
|
}
|
|
252
281
|
if (options.positionals.length !== 2)
|
|
253
282
|
return usageFailure("add", "remote add requires a name and URL");
|
|
254
|
-
if (
|
|
283
|
+
if (MCP_ADD_LOCAL_VALUE_OPTIONS.some((name2) => options.values.has(name2))) {
|
|
255
284
|
return usageFailure("add", "remote add cannot include local process options");
|
|
256
285
|
}
|
|
257
286
|
const url = options.positionals[1] ?? "";
|
|
258
287
|
if (!isHttpUrl(url)) return usageFailure("add", "remote URL must be absolute HTTP or HTTPS");
|
|
259
288
|
const transport = oneValue(options, "transport") ?? "http";
|
|
260
|
-
if (transport !== "http" && transport !== "sse")
|
|
289
|
+
if (transportMode !== "remote" || transport !== "http" && transport !== "sse")
|
|
261
290
|
return usageFailure("add", "remote transport must be http or sse");
|
|
262
291
|
const headers = parseAssignments(options.values.get("header") ?? [], "header");
|
|
263
292
|
if (typeof headers === "string") return usageFailure("add", headers);
|
|
@@ -280,11 +309,7 @@ function parseAdd(args) {
|
|
|
280
309
|
};
|
|
281
310
|
}
|
|
282
311
|
function parseScopedServer(kind, args) {
|
|
283
|
-
const options = parseOptions(
|
|
284
|
-
args,
|
|
285
|
-
new Set(kind === "remove" ? ["local", "logout"] : ["local"]),
|
|
286
|
-
/* @__PURE__ */ new Set()
|
|
287
|
-
);
|
|
312
|
+
const options = parseOptions(args, kind);
|
|
288
313
|
if (typeof options === "string") return usageFailure(kind, options);
|
|
289
314
|
if (options.positionals.length !== 1)
|
|
290
315
|
return usageFailure(kind, "exactly one server name is required");
|
|
@@ -311,7 +336,7 @@ function parseMcpCommand(args, surface) {
|
|
|
311
336
|
if (commandName === "remove" || commandName === "enable" || commandName === "disable")
|
|
312
337
|
return parseScopedServer(commandName, rest);
|
|
313
338
|
if (commandName === "list") {
|
|
314
|
-
const options2 = parseOptions(rest,
|
|
339
|
+
const options2 = parseOptions(rest, "list");
|
|
315
340
|
if (typeof options2 === "string" || options2.positionals.length > 0)
|
|
316
341
|
return usageFailure(
|
|
317
342
|
"list",
|
|
@@ -322,11 +347,7 @@ function parseMcpCommand(args, surface) {
|
|
|
322
347
|
return { command: { json: options2.flags.has("json"), kind: "list" }, ok: true };
|
|
323
348
|
}
|
|
324
349
|
if (commandName === "auth") {
|
|
325
|
-
const options2 = parseOptions(
|
|
326
|
-
rest,
|
|
327
|
-
/* @__PURE__ */ new Set(["no-open"]),
|
|
328
|
-
/* @__PURE__ */ new Set(["callback", "code", "state"])
|
|
329
|
-
);
|
|
350
|
+
const options2 = parseOptions(rest, "auth");
|
|
330
351
|
if (typeof options2 === "string") return usageFailure("auth", options2);
|
|
331
352
|
if (options2.positionals.length !== 1)
|
|
332
353
|
return usageFailure(
|
|
@@ -353,7 +374,7 @@ function parseMcpCommand(args, surface) {
|
|
|
353
374
|
};
|
|
354
375
|
}
|
|
355
376
|
if (commandName === "logout") {
|
|
356
|
-
const options2 = parseOptions(rest,
|
|
377
|
+
const options2 = parseOptions(rest, "logout");
|
|
357
378
|
if (typeof options2 === "string") return usageFailure("logout", options2);
|
|
358
379
|
const all = options2.flags.has("all");
|
|
359
380
|
const force = options2.flags.has("force");
|
|
@@ -368,7 +389,7 @@ function parseMcpCommand(args, surface) {
|
|
|
368
389
|
};
|
|
369
390
|
}
|
|
370
391
|
if (commandName === "test") {
|
|
371
|
-
const options2 = parseOptions(rest,
|
|
392
|
+
const options2 = parseOptions(rest, "test");
|
|
372
393
|
if (typeof options2 === "string") return usageFailure("test", options2);
|
|
373
394
|
if (surface === "runtime" && options2.flags.has("json"))
|
|
374
395
|
return usageFailure("test", "--json is standalone-only");
|
|
@@ -389,12 +410,16 @@ function parseMcpCommand(args, surface) {
|
|
|
389
410
|
if (rest.length > 0) return usageFailure("status", "status accepts no arguments");
|
|
390
411
|
return { command: { includeHelp: false, kind: "status" }, ok: true };
|
|
391
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
|
+
}
|
|
392
417
|
if (commandName === "reconnect") {
|
|
393
418
|
if (rest.length !== 1) return usageFailure("reconnect", "exactly one server name is required");
|
|
394
419
|
return { command: { kind: "reconnect", server: rest[0] ?? "" }, ok: true };
|
|
395
420
|
}
|
|
396
421
|
if (commandName === "prompt") {
|
|
397
|
-
const options2 = parseOptions(rest,
|
|
422
|
+
const options2 = parseOptions(rest, "prompt");
|
|
398
423
|
if (typeof options2 === "string") return usageFailure("prompt", options2);
|
|
399
424
|
if (options2.positionals.length !== 2)
|
|
400
425
|
return usageFailure("prompt", "server and prompt names are required");
|
|
@@ -414,20 +439,15 @@ function parseMcpCommand(args, surface) {
|
|
|
414
439
|
if (rest.length !== 2) return usageFailure(commandName, "server and resource URI are required");
|
|
415
440
|
return { command: { kind: commandName, server: rest[0] ?? "", uri: rest[1] ?? "" }, ok: true };
|
|
416
441
|
}
|
|
417
|
-
const options = parseOptions(rest,
|
|
442
|
+
const options = parseOptions(rest, "logs");
|
|
418
443
|
if (typeof options === "string" || options.positionals.length > 1)
|
|
419
444
|
return usageFailure(
|
|
420
445
|
"logs",
|
|
421
446
|
typeof options === "string" ? options : "logs accepts at most one server name"
|
|
422
447
|
);
|
|
423
|
-
const level = oneValue(options, "level");
|
|
424
|
-
if (level !== void 0 && !isMcpLoggingLevel(level)) {
|
|
425
|
-
return usageFailure("logs", `unknown logging level ${level}`);
|
|
426
|
-
}
|
|
427
448
|
return {
|
|
428
449
|
command: {
|
|
429
450
|
kind: "logs",
|
|
430
|
-
...level === void 0 ? {} : { level },
|
|
431
451
|
...options.positionals[0] === void 0 ? {} : { server: options.positionals[0] }
|
|
432
452
|
},
|
|
433
453
|
ok: true
|
|
@@ -550,12 +570,11 @@ async function executeMcpCommand(command, adapters, surface = "standalone") {
|
|
|
550
570
|
case "logs": {
|
|
551
571
|
const live = liveAdapter(adapters);
|
|
552
572
|
if ("exitCode" in live) return live;
|
|
553
|
-
result = await live.logs({
|
|
554
|
-
...command.level === void 0 ? {} : { level: command.level },
|
|
555
|
-
...command.server === void 0 ? {} : { server: command.server }
|
|
556
|
-
});
|
|
573
|
+
result = await live.logs(command.server === void 0 ? {} : { server: command.server });
|
|
557
574
|
break;
|
|
558
575
|
}
|
|
576
|
+
case "help":
|
|
577
|
+
return successResult({ message: RUNTIME_HELP, ok: true }, false);
|
|
559
578
|
}
|
|
560
579
|
if (!result.ok) return adapterFailure(result.category, result.message);
|
|
561
580
|
const json = (command.kind === "list" || command.kind === "test") && command.json;
|
|
@@ -9961,6 +9980,10 @@ var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
|
9961
9980
|
var MCP_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
9962
9981
|
var JsonValueSchema = typebox_exports.Any();
|
|
9963
9982
|
var NonEmptyStringSchema = typebox_exports.String({ minLength: 1 });
|
|
9983
|
+
var McpServerIdSchema = typebox_exports.String({
|
|
9984
|
+
minLength: 1,
|
|
9985
|
+
pattern: "^[^\\u0000-\\u001F\\u007F-\\u009F]+$"
|
|
9986
|
+
});
|
|
9964
9987
|
var PositiveSafeIntegerSchema = typebox_exports.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
|
|
9965
9988
|
var RetryCountSchema = typebox_exports.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
9966
9989
|
var BackoffFactorSchema = typebox_exports.Number({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
|
|
@@ -10005,7 +10028,7 @@ var McpLayerWireSchema = typebox_exports.Object(
|
|
|
10005
10028
|
requestTimeoutMs: typebox_exports.Optional(PositiveSafeIntegerSchema),
|
|
10006
10029
|
retry: typebox_exports.Optional(McpRetryWireSchema),
|
|
10007
10030
|
servers: typebox_exports.Optional(
|
|
10008
|
-
typebox_exports.Record(
|
|
10031
|
+
typebox_exports.Record(McpServerIdSchema, typebox_exports.Union([McpServerDefinitionWireSchema, typebox_exports.Null()]))
|
|
10009
10032
|
)
|
|
10010
10033
|
},
|
|
10011
10034
|
{ additionalProperties: false }
|
|
@@ -10109,6 +10132,20 @@ function parseMcpLayer(document, scope) {
|
|
|
10109
10132
|
value: {}
|
|
10110
10133
|
};
|
|
10111
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
|
+
}
|
|
10112
10149
|
return { errors: [], scope, value: document.mcp };
|
|
10113
10150
|
}
|
|
10114
10151
|
function parseRemoteUrl(value, path) {
|
|
@@ -10270,9 +10307,7 @@ function mergeServerDefinitions(globalLayer, projectLayer, environment, secrets)
|
|
|
10270
10307
|
const masks = /* @__PURE__ */ new Map();
|
|
10271
10308
|
const errors = [];
|
|
10272
10309
|
for (const [id, wire] of Object.entries(globalLayer.value.servers ?? {})) {
|
|
10273
|
-
if (
|
|
10274
|
-
errors.push(new McpSettingsError("global mcp.servers", "Server Definition ID is empty"));
|
|
10275
|
-
} else if (wire === null || wire.enabled === false && Object.keys(wire).length === 1) {
|
|
10310
|
+
if (wire === null || wire.enabled === false && Object.keys(wire).length === 1) {
|
|
10276
10311
|
errors.push(
|
|
10277
10312
|
new McpSettingsError(
|
|
10278
10313
|
`global mcp.servers.${id}`,
|
|
@@ -10287,9 +10322,7 @@ function mergeServerDefinitions(globalLayer, projectLayer, environment, secrets)
|
|
|
10287
10322
|
const inherited = definitions.has(id);
|
|
10288
10323
|
definitions.delete(id);
|
|
10289
10324
|
masks.delete(id);
|
|
10290
|
-
if (
|
|
10291
|
-
errors.push(new McpSettingsError("project mcp.servers", "Server Definition ID is empty"));
|
|
10292
|
-
} else if (wire === null || wire.enabled === false && Object.keys(wire).length === 1) {
|
|
10325
|
+
if (wire === null || wire.enabled === false && Object.keys(wire).length === 1) {
|
|
10293
10326
|
masks.set(id, { id, inherited, provenance: "project" });
|
|
10294
10327
|
} else {
|
|
10295
10328
|
definitions.set(id, { scope: "project", wire });
|
|
@@ -10687,7 +10720,7 @@ async function listStandaloneServers(state) {
|
|
|
10687
10720
|
transport: definition.transport
|
|
10688
10721
|
});
|
|
10689
10722
|
messages.push(
|
|
10690
|
-
`${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"})`
|
|
10691
10724
|
);
|
|
10692
10725
|
}
|
|
10693
10726
|
for (const mask of settings2.masks.values()) {
|
|
@@ -10698,7 +10731,9 @@ async function listStandaloneServers(state) {
|
|
|
10698
10731
|
name: mask.id,
|
|
10699
10732
|
provenance: mask.provenance
|
|
10700
10733
|
});
|
|
10701
|
-
messages.push(
|
|
10734
|
+
messages.push(
|
|
10735
|
+
`${mask.id} (provenance=${mask.provenance}, masked, inherited=${mask.inherited ? "yes" : "no"})`
|
|
10736
|
+
);
|
|
10702
10737
|
}
|
|
10703
10738
|
const message = messages.length === 0 ? "No MCP Server Definitions configured" : messages.join("\n");
|
|
10704
10739
|
return commandSuccess(message, { servers });
|
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
|
+
}
|