@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 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
- | `status` | `/mcp` | Show live connection state. |
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] [--level LEVEL]` | `/mcp` | Read retained server logs. |
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
 
@@ -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 { pathToFileURL } from "node:url";
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] [--level LEVEL]",
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 parseOptions(tokens, flagNames, valueNames, allowDelimiter = false) {
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 (!allowDelimiter) return "unexpected -- delimiter";
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) return `option ${rawName} does not accept a value`;
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 `option ${rawName} requires a value`;
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 parseRemoteAuth(options) {
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 inferredType = token !== void 0 ? "bearer" : clientId !== void 0 || clientSecret !== void 0 || redirectUri !== void 0 || scopes.length > 0 ? "oauth" : void 0;
162
- const type = configuredType ?? inferredType;
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 || token.length === 0) return "bearer auth requires --token";
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 && transport2 !== "stdio") {
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 (options.values.has("cwd") || options.values.has("environment")) {
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, /* @__PURE__ */ new Set(["json"]), /* @__PURE__ */ new Set());
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, /* @__PURE__ */ new Set(["all", "force"]), /* @__PURE__ */ new Set());
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, /* @__PURE__ */ new Set(["all", "json"]), /* @__PURE__ */ new Set());
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, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(["arg"]));
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, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(["level"]));
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(typebox_exports.String(), typebox_exports.Union([McpServerDefinitionWireSchema, typebox_exports.Null()]))
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 (id.length === 0) {
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 (id.length === 0) {
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} (${definition.provenance}, ${definition.enabled ? "enabled" : "disabled"})`
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(`${mask.id} (${mask.provenance}, disabled mask)`);
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 === pathToFileURL(entrypoint).href) {
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.1.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
+ }