@ian-pascoe/pi-mcp 0.1.1 → 0.3.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
 
@@ -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] [--level LEVEL]",
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 parseOptions(tokens, flagNames, valueNames, allowDelimiter = false) {
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 (!allowDelimiter) return "unexpected -- delimiter";
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) return `option ${rawName} does not accept a value`;
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 `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 };
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 parseRemoteAuth(options) {
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 inferredType = token !== void 0 ? "bearer" : clientId !== void 0 || clientSecret !== void 0 || redirectUri !== void 0 || scopes.length > 0 ? "oauth" : void 0;
163
- const type = configuredType ?? inferredType;
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 || token.length === 0) return "bearer auth requires --token";
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 && transport2 !== "stdio") {
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 (options.values.has("cwd") || options.values.has("environment")) {
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, /* @__PURE__ */ new Set(["json"]), /* @__PURE__ */ new Set());
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, /* @__PURE__ */ new Set(["all", "force"]), /* @__PURE__ */ new Set());
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, /* @__PURE__ */ new Set(["all", "json"]), /* @__PURE__ */ new Set());
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, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(["arg"]));
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, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(["level"]));
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(typebox_exports.String(), typebox_exports.Union([McpServerDefinitionWireSchema, typebox_exports.Null()]))
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 (id.length === 0) {
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 (id.length === 0) {
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} (${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"})`
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(`${mask.id} (${mask.provenance}, disabled mask)`);
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.1.1",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "A complete Model Context Protocol Host for Pi",
6
6
  "keywords": [
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "files": [
28
28
  "src",
29
+ "skills",
29
30
  "dist",
30
31
  "README.md",
31
32
  "LICENSE"
@@ -35,12 +36,6 @@
35
36
  "access": "public",
36
37
  "provenance": true
37
38
  },
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
39
  "dependencies": {
45
40
  "@modelcontextprotocol/client": "^2.0.0"
46
41
  },
@@ -51,6 +46,7 @@
51
46
  "peerDependencies": {
52
47
  "@earendil-works/pi-ai": "*",
53
48
  "@earendil-works/pi-coding-agent": "*",
49
+ "@earendil-works/pi-tui": "*",
54
50
  "typebox": "*"
55
51
  },
56
52
  "engines": {
@@ -59,6 +55,14 @@
59
55
  "pi": {
60
56
  "extensions": [
61
57
  "./src/index.ts"
58
+ ],
59
+ "skills": [
60
+ "./skills"
62
61
  ]
62
+ },
63
+ "scripts": {
64
+ "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/*",
65
+ "test": "vitest run --config ../../vitest.config.ts --root .",
66
+ "typecheck": "tsc --noEmit -p tsconfig.json"
63
67
  }
64
- }
68
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: pi-mcp
3
+ description: Configure or diagnose Pi MCP when settings are invalid, Servers fail to connect, authentication fails, or dynamic tools are missing.
4
+ license: MIT
5
+ ---
6
+
7
+ # Pi MCP
8
+
9
+ Keep edits scoped to `mcp`. Store secrets behind `${NAME}` environment references and redact resolved values. Treat `/mcp` as human-operated: when unavailable, request one exact command and wait for its output. Use `/mcp test` for connectivity instead of invoking Server Tools, Prompts, or Resources.
10
+
11
+ 1. Read the relevant settings, authentication, or connection section of [`../../README.md`](../../README.md).
12
+ 2. Run `/mcp list`, then `/mcp status`. Stop when either identifies a decisive settings or state failure.
13
+ 3. For a runtime failure, inspect `/mcp logs <server>`, then use `/mcp test <server>` after the cause is repaired.
14
+ 4. Use `/mcp reconnect <server>` only for a repaired live definition.
15
+ 5. Prefer the MCP Command Surface for requested mutations, then repeat `list`, `status`, and an approved `test`.
16
+ 6. Finish when provenance is correct and the Server reaches its intended connected, disabled, or authentication-required state without exposing a secret.