@ikuma.cloud/pix-mcp 0.0.6 → 0.0.8

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
@@ -1,8 +1,8 @@
1
1
  # @ikuma.cloud/pix-mcp
2
2
 
3
- A small Pi MCP adapter: discover tools, load their schemas on demand, then call
4
- those tools natively. No scripting engine or model-provider-specific API is
5
- required.
3
+ A small Pi MCP adapter: discover tools, load their schemas on demand, call
4
+ those tools natively, and run user-selected MCP prompts. No scripting engine or
5
+ model-provider-specific API is required.
6
6
 
7
7
  ## Usage
8
8
 
@@ -12,9 +12,10 @@ To load this package in an existing Pi installation:
12
12
  pi -e /absolute/path/to/pix/packages/pix-mcp
13
13
  ```
14
14
 
15
- Disable any other MCP adapter that would collide with the `mcp` tool or flags,
16
- but keep any permission-control extensions enabled. Review the configuration
17
- and automatic startup behavior below before connecting servers.
15
+ Disable any other MCP adapter that would collide with the `mcp` tool,
16
+ `/mcp-prompt` command, or flags, but keep any permission-control extensions
17
+ enabled. Review the configuration and automatic startup behavior below before
18
+ connecting servers.
18
19
 
19
20
  ## Configuration
20
21
 
@@ -82,7 +83,7 @@ URLs, and HTTP headers.
82
83
  | `type` | `stdio`, `http`, or `streamable-http` (alias of `http`); only stdio is inferred, when `command` is present |
83
84
  | `command`, `args`, `env` | Stdio only; executable and argument array, not a shell command |
84
85
  | `url`, `headers` | Streamable HTTP only; explicit `type` required; no legacy SSE fallback or redirect following |
85
- | `timeout` | Hard deadline for each tool invocation, in milliseconds; default 30000 |
86
+ | `timeout` | Hard deadline for each tool invocation or prompt retrieval, in milliseconds; default 30000 |
86
87
  | `cwd` | Stdio extension: working directory relative to the config directory |
87
88
  | `description` | Discovery extension: optional summary, truncated to 500 characters |
88
89
  | `startupTimeoutMs` | pix extension: complete connection/initialization handshake deadline; default 30000 |
@@ -116,10 +117,10 @@ as failed connections. Correct the file and reload Pi to retry.
116
117
 
117
118
  All three deadline fields accept integers from 1 through 2,147,483,647 milliseconds
118
119
  (Node's timer-safe maximum). Each defaults independently to 30 seconds. Setting
119
- `"timeout": 960000` permits a 16-minute tool call without lengthening startup or
120
- discovery. The call clock starts after connection startup;
121
- progress does not reset it. Catalog deadlines cover all pages of one snapshot;
122
- a subsequent list-change refresh starts a new deadline.
120
+ `"timeout": 960000` permits a 16-minute tool call or prompt retrieval without
121
+ lengthening startup or discovery. The invocation clock starts after connection
122
+ startup; progress does not reset it. Catalog deadlines cover all pages of one
123
+ snapshot; a subsequent list-change refresh starts a new deadline.
123
124
 
124
125
  HTTP deadlines cover response headers and bodies, including JSON and SSE. MCP
125
126
  requests borrow the host's HTTP/proxy routing but override its header/body idle
@@ -158,8 +159,9 @@ credentials; debug a failing server separately in a trusted environment.
158
159
  ## Discovery and execution
159
160
 
160
161
  At session startup, the adapter connects to enabled servers and fetches their
161
- paginated tool catalogs. A server's configuration, connection, or discovery
162
- failure does not hide tools from other servers.
162
+ advertised paginated tool and prompt catalogs. A server's configuration,
163
+ connection, or discovery failure does not hide healthy features from other
164
+ servers.
163
165
  Full schemas stay out of model context until selected. **Schema exposure is lazy;
164
166
  initial connections and metadata discovery are not.**
165
167
 
@@ -194,11 +196,11 @@ For example, `draft07-reference` identifies unsupported draft-07 references;
194
196
  returned by list, search, and load, and replaced on each catalog refresh. Raw
195
197
  exceptions, schema contents, invalid tool names, and dialect URLs are not exposed.
196
198
 
197
- An unsupported output schema still fails that server's discovery rather than
199
+ An unsupported output schema still fails that server's tool discovery rather than
198
200
  producing a per-tool rejection. Losing an established HTTP notification stream
199
- also withdraws tools rather than silently keeping a stale catalog; servers that
200
- decline the optional stream with HTTP 405 remain usable. Reload Pi to reconnect
201
- a failed server or reread configuration.
201
+ withdraws tool and prompt catalogs rather than silently keeping stale metadata;
202
+ servers that decline the optional stream with HTTP 405 remain usable. Reload Pi
203
+ to reconnect a failed server or reread configuration.
202
204
 
203
205
  Pi handles provider compatibility. Some providers support transcript-anchored
204
206
  schema additions; others rebuild the tool set and may invalidate prompt caches.
@@ -257,6 +259,52 @@ complete draft-07 converter. For unsupported dialect constructs, the server must
257
259
  supply an equivalent supported schema—not merely remove or change `$schema`.
258
260
  Embedded draft-07 declarations inside a 2020-12 document also remain unsupported.
259
261
 
262
+ ## Prompts
263
+
264
+ MCP prompts are user-controlled and are not exposed as model-callable tools. Use
265
+ the stable `/mcp-prompt` command so catalog changes do not leave stale slash
266
+ commands behind:
267
+
268
+ ```text
269
+ /mcp-prompt
270
+ /mcp-prompt list [server]
271
+ /mcp-prompt run <server> <prompt> [name=value ...]
272
+ ```
273
+
274
+ In TUI mode, `/mcp-prompt` without arguments opens a native prompt selector
275
+ that shows the focused prompt's title and description, followed by native input
276
+ dialogs. Required and optional arguments are requested in declaration order;
277
+ leaving an optional input empty omits it. The rendered prompt is placed in Pi's
278
+ editor so you can review or modify it before sending. Image blocks are stored in
279
+ private temporary files and inserted as `@` references. Escape cancels without
280
+ retrieving the prompt. The explicit `list` and `run` forms remain available in
281
+ every mode; `run` submits immediately and can pass an intentional empty value as
282
+ `name=`.
283
+
284
+ Arguments use shell-style quoting. Positional values map to the prompt's declared
285
+ argument order; `name=value` selects a declared argument explicitly. Quote or escape
286
+ an equals sign in a positional value (for example, `"a=b"` or `a\=b`) to avoid
287
+ assignment parsing. Argument names containing `=` work when the name is quoted,
288
+ as in `"x=y"=value`. The adapter checks required arguments before sending
289
+ `prompts/get`. Prompt retrieval uses the
290
+ server's `timeout`; prompt discovery uses `catalogTimeoutMs` and follows pagination.
291
+ Prompt list-change notifications atomically replace that server's prompt catalog.
292
+ Duplicate or invalid prompt metadata fails only that server's prompt catalog. A
293
+ prompt discovery failure does not hide healthy tools, and a tool discovery failure
294
+ does not hide healthy prompts.
295
+
296
+ Pi cannot insert an arbitrary MCP message sequence with its original roles. A
297
+ single user message is passed through; multi-message prompts are flattened with
298
+ explicit `[user]` and `[assistant]` markers. Text, supported images, and embedded
299
+ text or supported-image resources are retained. Resource links become textual
300
+ references. Audio and other binary content are omitted from the preview and
301
+ preserved in a private full-result artifact. MCP argument completion requests are
302
+ not supported.
303
+
304
+ Prompt metadata and bodies are untrusted server content. Catalog metadata stays
305
+ in command UI; a prompt body enters model context only after the user explicitly
306
+ runs it. Review configured servers and selected prompts accordingly.
307
+
260
308
  ## Output and limits
261
309
 
262
310
  Text, supported images, and structured content are retained. Long text gets a
@@ -266,17 +314,22 @@ content is explicitly omitted from the preview, not silently discarded. Pi's
266
314
  error-result path is text-only, so images in MCP errors are preserved in a
267
315
  full-result artifact rather than displayed inline.
268
316
 
269
- When necessary, the full MCP result is written to `pix-mcp-*/result.json` under
270
- the system temp directory (directory mode 0700, file mode 0600). Pi can inspect it
271
- with `read`. These artifacts may contain sensitive data and are **not deleted at
272
- session shutdown**; remove them when no longer needed. Output limits are not a
273
- complete memory or security sandbox.
317
+ When necessary, the full MCP tool or prompt result is written to
318
+ `pix-mcp-*/result.json` under the system temp directory (directory mode 0700,
319
+ file mode 0600). Pi can inspect it with `read`. These artifacts may contain
320
+ sensitive data and are **not deleted at session shutdown**; remove them when no
321
+ longer needed. Output limits are not a complete memory or security sandbox.
274
322
 
275
323
  Configuration is limited to 256 KiB and 32 servers. Startup connects at most four
276
- servers concurrently. Each catalog is limited to 1000 tools, 100 pagination
277
- cursors, and 2 MiB of metadata; individual input/output schemas are limited to
278
- 64 KiB. Tool names must use 1–128 ASCII letters, digits, underscores, hyphens, or
279
- periods; descriptions are limited to 16 KiB. Stdio messages are limited to 16 MiB.
324
+ servers concurrently. Each tool or prompt catalog is limited to 1000 entries,
325
+ 100 pagination cursors, and 2 MiB of metadata; individual input/output schemas are
326
+ limited to 64 KiB. Prompt arguments are limited to 256 KiB per retrieval;
327
+ prompt and prompt-argument names are limited to 256 bytes, cannot contain Unicode
328
+ control, format, or line-separator characters, and each prompt can declare at most
329
+ 100 arguments. Tool names must use 1–128 ASCII letters, digits,
330
+ underscores, hyphens, or periods. Tool and prompt descriptions, prompt-argument
331
+ descriptions, and prompt titles are limited to 16 KiB. Stdio messages are limited
332
+ to 16 MiB.
280
333
  Schemas are syntax-checked before compilation; see [schema compatibility](#schema-compatibility)
281
334
  for supported dialects and the draft-07 subset. Schema nesting is limited to 64
282
335
  levels, including literal data. External schema references are unsupported, but
@@ -293,9 +346,10 @@ roll back effects.
293
346
 
294
347
  ## Deliberately out of scope
295
348
 
296
- OAuth, legacy SSE transport, MCP prompts/resources APIs, sampling, elicitation,
297
- MCP apps, task execution, semantic search, scripting, config UI, and persistent
298
- catalog caching. Use a fuller adapter when those capabilities are required.
349
+ OAuth, legacy SSE transport, MCP resources APIs, prompt argument completion,
350
+ sampling, elicitation, MCP apps, task execution, semantic search, scripting,
351
+ config UI, and persistent catalog caching. Use a fuller adapter when those
352
+ capabilities are required.
299
353
 
300
354
  ## Contributing
301
355
 
package/mcp.schema.json CHANGED
@@ -33,7 +33,7 @@
33
33
  "description": { "$ref": "#/$defs/text" },
34
34
  "timeout": {
35
35
  "$ref": "#/$defs/timeout",
36
- "description": "Hard deadline for one tool invocation."
36
+ "description": "Hard deadline for one tool invocation or prompt retrieval."
37
37
  },
38
38
  "startupTimeoutMs": {
39
39
  "$ref": "#/$defs/timeout",
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@ikuma.cloud/pix-mcp",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
7
  "type": "module",
8
- "description": "MCP discovery and lazily activated native tools for Pi Coding Agent",
8
+ "description": "MCP prompts and lazily activated native tools for Pi Coding Agent",
9
9
  "keywords": [
10
10
  "pi-package"
11
11
  ],
@@ -28,11 +28,13 @@
28
28
  "peerDependencies": {
29
29
  "@earendil-works/pi-ai": "*",
30
30
  "@earendil-works/pi-coding-agent": "*",
31
+ "@earendil-works/pi-tui": "*",
31
32
  "typebox": "*"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@earendil-works/pi-ai": "0.87.1",
35
36
  "@earendil-works/pi-coding-agent": "0.87.1",
37
+ "@earendil-works/pi-tui": "0.87.1",
36
38
  "typebox": "1.3.27"
37
39
  }
38
40
  }
package/src/catalog.ts CHANGED
@@ -78,7 +78,7 @@ export function summary(item: Entry) {
78
78
 
79
79
  export function compact(value: string, limit: number): string {
80
80
  const text = value
81
- .replace(/\p{Cc}/gu, " ")
81
+ .replace(/[\p{C}\p{Zl}\p{Zp}]/gu, " ")
82
82
  .replace(/\s+/g, " ")
83
83
  .trim();
84
84
  return text.length <= limit ? text : `${text.slice(0, limit)}…`;
package/src/client.ts CHANGED
@@ -7,9 +7,13 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
7
7
  import { mediaTypeEssence } from "@modelcontextprotocol/sdk/shared/mediaType.js";
8
8
  import {
9
9
  CallToolResultSchema,
10
+ GetPromptResultSchema,
11
+ ListPromptsResultSchema,
10
12
  ListToolsResultSchema,
13
+ PromptListChangedNotificationSchema,
11
14
  ToolSchema,
12
15
  ToolListChangedNotificationSchema,
16
+ type Prompt,
13
17
  type Tool,
14
18
  } from "@modelcontextprotocol/sdk/types.js";
15
19
  import type { ServerConfig } from "./config.ts";
@@ -28,25 +32,46 @@ const catalogResultSchema = ListToolsResultSchema.extend({
28
32
  }).array(),
29
33
  });
30
34
 
35
+ // Prompt identifiers are rendered in command UI and diagnostics, so bound them
36
+ // and reject control, format, and line-separator characters before they enter state.
37
+ const promptIdentifier = (value: string) =>
38
+ Buffer.byteLength(value) > 0 &&
39
+ Buffer.byteLength(value) <= 256 &&
40
+ !/[\p{C}\p{Zl}\p{Zp}]/u.test(value);
41
+
31
42
  export class Connection {
32
43
  readonly client: Client;
33
44
  readonly config: ServerConfig;
34
45
  status = "Not connected";
46
+ toolStatus = "Not discovered";
47
+ promptStatus = "Not discovered";
35
48
  instructions = "";
36
49
  #transport: StdioClientTransport | StreamableHTTPClientTransport;
37
50
  #lifetime = new AbortController();
38
51
  #initializing: Promise<void> | undefined;
39
52
  #refreshing: Promise<void> | undefined;
53
+ #promptRefreshing: Promise<void> | undefined;
40
54
  #closing: Promise<void> | undefined;
41
55
  #dirty = false;
56
+ #promptDirty = false;
42
57
  #stopped = false;
58
+ #connected = false;
59
+ #toolsReady = false;
60
+ #promptsReady = false;
43
61
  #changed: (tools: Tool[]) => void;
62
+ #promptsChanged: (prompts: Prompt[]) => void;
63
+ #promptNames = new Set<string>();
44
64
  #outputValidators = new Map<string, ReturnType<typeof compileSchema>>();
45
65
  #operation = new AsyncLocalStorage<AbortSignal>();
46
66
 
47
- constructor(config: ServerConfig, changed: (tools: Tool[]) => void) {
67
+ constructor(
68
+ config: ServerConfig,
69
+ changed: (tools: Tool[]) => void,
70
+ promptsChanged: (prompts: Prompt[]) => void = () => {},
71
+ ) {
48
72
  this.config = config;
49
73
  this.#changed = changed;
74
+ this.#promptsChanged = promptsChanged;
50
75
  this.client = new Client(
51
76
  { name: "pix-mcp", version: "0.0.0" },
52
77
  { capabilities: {}, jsonSchemaValidator: schemaValidator },
@@ -86,19 +111,47 @@ export class Connection {
86
111
  }
87
112
  },
88
113
  );
114
+ this.client.setNotificationHandler(
115
+ PromptListChangedNotificationSchema,
116
+ async () => {
117
+ if (this.#stopped) return;
118
+ try {
119
+ await this.refreshPrompts();
120
+ } catch {
121
+ /* refresh already clears the stale catalog */
122
+ }
123
+ },
124
+ );
89
125
  }
90
126
 
91
127
  #disconnect() {
92
128
  if (this.#stopped || this.#lifetime.signal.aborted) return;
93
129
  this.status = "Disconnected; reload Pi to reconnect";
130
+ this.toolStatus = "Unavailable";
131
+ this.promptStatus = "Unavailable";
132
+ this.#connected = false;
133
+ this.#toolsReady = false;
134
+ this.#promptsReady = false;
94
135
  this.#lifetime.abort(
95
136
  new Error("MCP connection is unavailable; reload Pi to reconnect."),
96
137
  );
97
138
  this.#outputValidators.clear();
139
+ this.#promptNames.clear();
98
140
  this.#changed([]);
141
+ this.#promptsChanged([]);
99
142
  void this.client.close().catch(() => {});
100
143
  }
101
144
 
145
+ #updateStatus() {
146
+ if (!this.#connected || this.#stopped) return;
147
+ if (this.#toolsReady && this.#promptsReady) this.status = "Connected";
148
+ else if (this.#toolsReady)
149
+ this.status = "Connected; prompt discovery failed";
150
+ else if (this.#promptsReady)
151
+ this.status = "Connected; tool discovery failed";
152
+ else this.status = "Discovery failed; reload Pi to retry";
153
+ }
154
+
102
155
  async #deadline<T>(
103
156
  timeout: number,
104
157
  signal: AbortSignal | undefined,
@@ -250,29 +303,51 @@ export class Connection {
250
303
  clearTimeout(handshakeTimer);
251
304
  }
252
305
  this.#lifetime.signal.throwIfAborted();
306
+ this.#connected = true;
307
+ this.status = "Discovering";
253
308
  this.instructions = this.client.getInstructions() ?? "";
254
- await this.refresh();
255
309
  } catch {
256
310
  if (!this.#stopped) {
257
311
  this.status =
258
- "Connection or discovery failed; check configuration/authentication and reload Pi";
312
+ "Connection failed; check configuration/authentication and reload Pi";
313
+ this.toolStatus = "Unavailable";
314
+ this.promptStatus = "Unavailable";
259
315
  this.#changed([]);
316
+ this.#promptsChanged([]);
260
317
  }
261
318
  await this.client.close().catch(() => {});
262
- throw new Error(
263
- `MCP server ${this.config.name}: connection or discovery failed.`,
264
- );
319
+ throw new Error(`MCP server ${this.config.name}: connection failed.`);
265
320
  }
321
+ const results = await Promise.allSettled([
322
+ this.refresh(),
323
+ this.refreshPrompts(),
324
+ ]);
325
+ if (results.some((result) => result.status === "rejected"))
326
+ throw new Error(`MCP server ${this.config.name}: discovery failed.`);
266
327
  }
267
328
 
268
329
  refresh(): Promise<void> {
269
330
  this.#dirty = true;
270
- this.#refreshing ??= this.#refresh().finally(() => {
271
- this.#refreshing = undefined;
272
- });
331
+ this.#refreshing ??= this.#drainToolRefreshes();
273
332
  return this.#refreshing;
274
333
  }
275
334
 
335
+ async #drainToolRefreshes(): Promise<void> {
336
+ let failed = false;
337
+ let failure: unknown;
338
+ try {
339
+ await this.#refresh();
340
+ } catch (error) {
341
+ failed = true;
342
+ failure = error;
343
+ }
344
+ // Clear ownership and inspect dirty state without an await between them. A
345
+ // notification queued as the prior refresh settles must start another pass.
346
+ this.#refreshing = undefined;
347
+ if (this.#dirty && !this.#stopped) return this.refresh();
348
+ if (failed) throw failure;
349
+ }
350
+
276
351
  async #refresh(): Promise<void> {
277
352
  try {
278
353
  while (this.#dirty && !this.#stopped) {
@@ -337,18 +412,187 @@ export class Connection {
337
412
  signal.throwIfAborted();
338
413
  if (!this.#stopped) {
339
414
  this.#outputValidators = validators;
340
- this.status = "Connected";
415
+ this.#toolsReady = true;
416
+ this.toolStatus = "Available";
341
417
  this.#changed(tools);
418
+ this.#updateStatus();
342
419
  }
343
420
  },
344
421
  );
345
422
  }
346
423
  } catch {
347
424
  if (!this.#stopped) {
348
- this.status = "Discovery failed; reload Pi to retry";
425
+ this.#toolsReady = false;
426
+ this.toolStatus = "Discovery failed; reload Pi to retry";
427
+ this.#outputValidators.clear();
349
428
  this.#changed([]);
429
+ this.#updateStatus();
350
430
  }
351
- throw new Error(`MCP server ${this.config.name}: discovery failed.`);
431
+ throw new Error(`MCP server ${this.config.name}: tool discovery failed.`);
432
+ }
433
+ }
434
+
435
+ refreshPrompts(): Promise<void> {
436
+ this.#promptDirty = true;
437
+ this.#promptRefreshing ??= this.#drainPromptRefreshes();
438
+ return this.#promptRefreshing;
439
+ }
440
+
441
+ async #drainPromptRefreshes(): Promise<void> {
442
+ let failed = false;
443
+ let failure: unknown;
444
+ try {
445
+ await this.#refreshPrompts();
446
+ } catch (error) {
447
+ failed = true;
448
+ failure = error;
449
+ }
450
+ this.#promptRefreshing = undefined;
451
+ if (this.#promptDirty && !this.#stopped) return this.refreshPrompts();
452
+ if (failed) throw failure;
453
+ }
454
+
455
+ async #refreshPrompts(): Promise<void> {
456
+ try {
457
+ while (this.#promptDirty && !this.#stopped) {
458
+ this.#promptDirty = false;
459
+ const prompts: Prompt[] = [];
460
+ const cursors = new Set<string>();
461
+ const names = new Set<string>();
462
+ const supportsPrompts = Boolean(
463
+ this.client.getServerCapabilities()?.prompts,
464
+ );
465
+ let cursor: string | undefined;
466
+ let bytes = 0;
467
+ await this.#deadline(
468
+ this.config.catalogTimeoutMs,
469
+ undefined,
470
+ async (signal) => {
471
+ if (supportsPrompts) {
472
+ do {
473
+ const page = await this.#deadline(
474
+ this.config.catalogTimeoutMs,
475
+ signal,
476
+ (requestSignal) =>
477
+ this.client.request(
478
+ {
479
+ method: "prompts/list",
480
+ params: cursor === undefined ? {} : { cursor },
481
+ },
482
+ ListPromptsResultSchema,
483
+ {
484
+ signal: requestSignal,
485
+ timeout: this.config.catalogTimeoutMs,
486
+ },
487
+ ),
488
+ );
489
+ bytes += Buffer.byteLength(JSON.stringify(page));
490
+ if (
491
+ bytes > 2 * 1024 * 1024 ||
492
+ prompts.length + page.prompts.length > 1000
493
+ )
494
+ throw new Error("Catalog limit");
495
+ for (const prompt of page.prompts) {
496
+ if (
497
+ !promptIdentifier(prompt.name) ||
498
+ Buffer.byteLength(prompt.title ?? "") > 16 * 1024 ||
499
+ Buffer.byteLength(prompt.description ?? "") > 16 * 1024 ||
500
+ (prompt.arguments?.length ?? 0) > 100
501
+ )
502
+ throw new Error("Invalid prompt metadata");
503
+ if (names.has(prompt.name))
504
+ throw new Error("Duplicate prompt name");
505
+ const argumentNames = new Set<string>();
506
+ for (const argument of prompt.arguments ?? []) {
507
+ if (
508
+ !promptIdentifier(argument.name) ||
509
+ Buffer.byteLength(argument.description ?? "") > 16 * 1024
510
+ )
511
+ throw new Error("Invalid prompt argument metadata");
512
+ if (argumentNames.has(argument.name))
513
+ throw new Error("Duplicate prompt argument name");
514
+ argumentNames.add(argument.name);
515
+ }
516
+ names.add(prompt.name);
517
+ prompts.push(prompt);
518
+ }
519
+ cursor = page.nextCursor;
520
+ if (cursor !== undefined) {
521
+ if (cursors.has(cursor) || cursors.size >= 100)
522
+ throw new Error("Invalid pagination");
523
+ cursors.add(cursor);
524
+ }
525
+ } while (cursor !== undefined);
526
+ }
527
+ signal.throwIfAborted();
528
+ if (!this.#stopped) {
529
+ this.#promptNames = names;
530
+ this.#promptsReady = true;
531
+ this.promptStatus = supportsPrompts
532
+ ? "Available"
533
+ : "Not supported";
534
+ this.#promptsChanged(prompts);
535
+ this.#updateStatus();
536
+ }
537
+ },
538
+ );
539
+ }
540
+ } catch {
541
+ if (!this.#stopped) {
542
+ this.#promptsReady = false;
543
+ this.promptStatus = "Discovery failed; reload Pi to retry";
544
+ this.#promptNames.clear();
545
+ this.#promptsChanged([]);
546
+ this.#updateStatus();
547
+ }
548
+ throw new Error(
549
+ `MCP server ${this.config.name}: prompt discovery failed.`,
550
+ );
551
+ }
552
+ }
553
+
554
+ async getPrompt(
555
+ name: string,
556
+ args: Record<string, string> | undefined,
557
+ signal?: AbortSignal,
558
+ ) {
559
+ await this.start().catch(() => {});
560
+ const combined = signal
561
+ ? AbortSignal.any([signal, this.#lifetime.signal])
562
+ : this.#lifetime.signal;
563
+ combined.throwIfAborted();
564
+ if (!this.#promptsReady || !this.#promptNames.has(name))
565
+ throw new Error("MCP prompt is unavailable; list prompts again.");
566
+ if (args && Buffer.byteLength(JSON.stringify(args)) > 256 * 1024)
567
+ throw new Error("MCP prompt arguments exceed 256 KiB.");
568
+ try {
569
+ return await this.#deadline(
570
+ this.config.timeout,
571
+ combined,
572
+ (requestSignal) =>
573
+ this.client.request(
574
+ {
575
+ method: "prompts/get",
576
+ params: {
577
+ name,
578
+ ...(args && Object.keys(args).length > 0
579
+ ? { arguments: args }
580
+ : {}),
581
+ },
582
+ },
583
+ GetPromptResultSchema,
584
+ {
585
+ signal: requestSignal,
586
+ timeout: this.config.timeout,
587
+ resetTimeoutOnProgress: false,
588
+ },
589
+ ),
590
+ );
591
+ } catch {
592
+ combined.throwIfAborted();
593
+ throw new Error(
594
+ `MCP server ${this.config.name}: prompt request failed or timed out.`,
595
+ );
352
596
  }
353
597
  }
354
598
 
@@ -357,11 +601,13 @@ export class Connection {
357
601
  args: Record<string, unknown>,
358
602
  signal?: AbortSignal,
359
603
  ) {
360
- await this.start();
604
+ await this.start().catch(() => {});
361
605
  const combined = signal
362
606
  ? AbortSignal.any([signal, this.#lifetime.signal])
363
607
  : this.#lifetime.signal;
364
608
  combined.throwIfAborted();
609
+ if (!this.#toolsReady)
610
+ throw new Error("MCP tool catalog is unavailable; reload Pi to retry.");
365
611
  const outputValidator = this.#outputValidators.get(name);
366
612
  try {
367
613
  // SDK callTool consults a mutable per-page validator after the response and
@@ -408,6 +654,14 @@ export class Connection {
408
654
  this.#stopped = true;
409
655
  this.#lifetime.abort();
410
656
  this.status = "Closed";
657
+ this.toolStatus = "Closed";
658
+ this.promptStatus = "Closed";
659
+ this.#toolsReady = false;
660
+ this.#promptsReady = false;
661
+ this.#outputValidators.clear();
662
+ this.#promptNames.clear();
663
+ this.#changed([]);
664
+ this.#promptsChanged([]);
411
665
  if (
412
666
  this.#transport instanceof StreamableHTTPClientTransport &&
413
667
  this.#transport.sessionId
@@ -417,5 +671,6 @@ export class Connection {
417
671
  await this.client.close().catch(() => {});
418
672
  await this.#initializing?.catch(() => {});
419
673
  await this.#refreshing?.catch(() => {});
674
+ await this.#promptRefreshing?.catch(() => {});
420
675
  }
421
676
  }