pi-mcp-client 0.8.1 → 0.9.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
@@ -23,7 +23,9 @@ This server doesn't require credentials. Ask Pi:
23
23
 
24
24
  You configure servers and sign in when needed. The model finds and uses
25
25
  relevant tools and resources—you don't need to select tools before asking a
26
- question.
26
+ question. When a server needs more information during a tool call, Pi asks you
27
+ directly and sends only what you submit. See
28
+ [server requests for input](docs/behavior.md#server-requests-for-input).
27
29
 
28
30
  Use these commands in Pi to manage your connections:
29
31
 
package/dist/index.js CHANGED
@@ -1770,9 +1770,9 @@ var defaultOptions = {
1770
1770
  fields: void 0,
1771
1771
  searchOptions: void 0,
1772
1772
  storeFields: [],
1773
- logger: (level, message) => {
1773
+ logger: (level, message2) => {
1774
1774
  if (typeof (console === null || console === void 0 ? void 0 : console[level]) === "function")
1775
- console[level](message);
1775
+ console[level](message2);
1776
1776
  },
1777
1777
  autoVacuum: true
1778
1778
  };
@@ -1878,6 +1878,9 @@ var messages = {
1878
1878
  subscriptions_unsupported: "The server did not accept resource subscriptions.",
1879
1879
  subscription_limit: "The connection has reached its limit of 50 resource subscriptions.",
1880
1880
  catalog_changed: "The catalog kept changing during discovery.",
1881
+ elicitation_required: "The server requires a browser step before this tool can run.",
1882
+ elicitation_declined: "The browser step required by the server was declined or dismissed.",
1883
+ elicitation_completed: "The browser step required by the server is complete.",
1881
1884
  oauth_failed: "OAuth authentication did not complete.",
1882
1885
  oauth_client_required: "OAuth requires a registered client: this server does not support dynamic client registration.",
1883
1886
  oauth_registration_rejected: "The authorization server rejected OAuth client registration.",
@@ -1921,6 +1924,9 @@ function diagnostic(code, context) {
1921
1924
  subscriptions_unsupported: "Choose a server with resource subscription support, or read the resource explicitly when needed.",
1922
1925
  subscription_limit: "Unsubscribe from another resource before adding a new watch.",
1923
1926
  catalog_changed: "Retry discovery once the server catalog has settled.",
1927
+ elicitation_required: "The tool didn't run. Use an interactive Pi session, where the user can review and open the server's page, then retry.",
1928
+ elicitation_declined: "The tool didn't run. Retry only if the user asks.",
1929
+ elicitation_completed: "The tool didn't run yet. Call it again to continue.",
1924
1930
  oauth_failed: `Check the service's OAuth requirements and /mcp get ${target} for the client type, requested scopes, and callback address. Only public clients with PKCE are supported; clients requiring a client secret are not. Retry /mcp login ${target} after correcting the setup.`,
1925
1931
  oauth_client_required: `Configure oauthClientId with a registered public/PKCE client ID in this server's definition. Register the exact callback URL shown by /mcp get ${target}, then run /mcp reload and /mcp login ${target}. Clients requiring a client secret are not supported. --no-browser does not fix client registration.`,
1926
1932
  oauth_registration_rejected: `Check whether the service allows public/native client registration and the callback URL shown by /mcp get ${target}. If registration is restricted, configure an approved public client with oauthClientId, run /mcp reload, then retry /mcp login ${target}.`,
@@ -2033,6 +2039,8 @@ function diagnose(error, context) {
2033
2039
  const code2 = sdkOAuthFallback(current);
2034
2040
  if (code2) return diagnostic(code2, context);
2035
2041
  }
2042
+ if (current instanceof ProtocolError && current.code === ProtocolErrorCode.UrlElicitationRequired)
2043
+ return diagnostic("elicitation_required", context);
2036
2044
  if (current instanceof ProtocolError) return diagnostic(
2037
2045
  context.operation === "read" && (current instanceof ResourceNotFoundError || current.code === ProtocolErrorCode.ResourceNotFound) ? "resource_not_found" : "protocol_error",
2038
2046
  context
@@ -2684,9 +2692,9 @@ function configCommandCompletions(input, servers) {
2684
2692
  const match = /^(add|remove|import)\s+(.*)$/su.exec(input.trimStart());
2685
2693
  if (!match) return void 0;
2686
2694
  const [, action, rest] = match;
2687
- const choices = ["--scope global", "--scope project"];
2695
+ const choices2 = ["--scope global", "--scope project"];
2688
2696
  if (!/^--scope\s+(global|project)\s/u.test(rest)) {
2689
- return choices.filter((choice) => choice.startsWith(rest)).map((choice) => ({
2697
+ return choices2.filter((choice) => choice.startsWith(rest)).map((choice) => ({
2690
2698
  value: `${action} ${choice}`,
2691
2699
  label: choice
2692
2700
  }));
@@ -2706,13 +2714,13 @@ function identifier(value) {
2706
2714
  return typeof value === "string" && !!value && value.length <= 512 && line(value) === value;
2707
2715
  }
2708
2716
  function preparePrompt(server, identity, value) {
2709
- const invalid2 = () => failure("protocol_error", { server, operation: "search" });
2710
- if (!object(value) || !identifier(value.name) || value.description !== void 0 && typeof value.description !== "string" || value.title !== void 0 && typeof value.title !== "string" || value.arguments !== void 0 && !Array.isArray(value.arguments)) throw invalid2();
2717
+ const invalid3 = () => failure("protocol_error", { server, operation: "search" });
2718
+ if (!object(value) || !identifier(value.name) || value.description !== void 0 && typeof value.description !== "string" || value.title !== void 0 && typeof value.title !== "string" || value.arguments !== void 0 && !Array.isArray(value.arguments)) throw invalid3();
2711
2719
  const args = value.arguments ?? [];
2712
- if (args.length > 100) throw invalid2();
2720
+ if (args.length > 100) throw invalid3();
2713
2721
  const seen = /* @__PURE__ */ new Set();
2714
2722
  const arguments_ = args.map((argument) => {
2715
- if (!object(argument) || !identifier(argument.name) || seen.has(argument.name) || argument.description !== void 0 && typeof argument.description !== "string" || argument.required !== void 0 && typeof argument.required !== "boolean") throw invalid2();
2723
+ if (!object(argument) || !identifier(argument.name) || seen.has(argument.name) || argument.description !== void 0 && typeof argument.description !== "string" || argument.required !== void 0 && typeof argument.required !== "boolean") throw invalid3();
2716
2724
  seen.add(argument.name);
2717
2725
  return { name: argument.name, description: plain(argument.description ?? "").slice(0, 8e3), required: argument.required === true };
2718
2726
  });
@@ -2734,14 +2742,14 @@ function promptCommand(prompt) {
2734
2742
  }
2735
2743
  var promptText = (value) => plain(value).replace(/\r\n?/g, "\n");
2736
2744
  function preparePromptSnapshot(result, server) {
2737
- const invalid2 = () => failure("protocol_error", { server, operation: "prompt" });
2738
- if (!object(result) || !Array.isArray(result.messages) || !result.messages.length) throw invalid2();
2745
+ const invalid3 = () => failure("protocol_error", { server, operation: "prompt" });
2746
+ if (!object(result) || !Array.isArray(result.messages) || !result.messages.length) throw invalid3();
2739
2747
  if (Buffer.byteLength(JSON.stringify(result)) > 50 * 1024) throw failure("prompt_too_large", { server, operation: "prompt" });
2740
2748
  let usable = true;
2741
2749
  const previews = [];
2742
- const messages2 = result.messages.map((message, index) => {
2743
- if (!["user", "assistant"].includes(message.role) || !object(message.content)) throw invalid2();
2744
- const content = message.content;
2750
+ const messages2 = result.messages.map((message2, index) => {
2751
+ if (!["user", "assistant"].includes(message2.role) || !object(message2.content)) throw invalid3();
2752
+ const content = message2.content;
2745
2753
  let block;
2746
2754
  let preview2;
2747
2755
  if (content.type === "text" && typeof content.text === "string") {
@@ -2761,9 +2769,9 @@ ${promptText(content.resource.text)}`;
2761
2769
  block = { type: line(String(content.type)), unsupported: true, bytes: Buffer.byteLength(JSON.stringify(content)) };
2762
2770
  preview2 = `Unsupported ${line(String(content.type))} content \xB7 ${Buffer.byteLength(JSON.stringify(content))} bytes`;
2763
2771
  }
2764
- previews.push(`Message ${index + 1} \xB7 ${message.role} (server supplied)
2772
+ previews.push(`Message ${index + 1} \xB7 ${message2.role} (server supplied)
2765
2773
  ${preview2}`);
2766
- return { role: message.role, content: block };
2774
+ return { role: message2.role, content: block };
2767
2775
  });
2768
2776
  const description = typeof result.description === "string" ? promptText(result.description) : void 0;
2769
2777
  const body = JSON.stringify({ server, ...description === void 0 ? {} : { description }, messages: messages2 }, null, 2);
@@ -2775,9 +2783,26 @@ ${preview2}`);
2775
2783
 
2776
2784
  // src/prompt-selector.ts
2777
2785
  import { SelectList, Text } from "@earendil-works/pi-tui";
2778
- function promptSelector(title, choices, theme, keys, done, render) {
2786
+ async function chooseOption(ctx, title, choices2, signal) {
2787
+ if (signal.aborted) return;
2788
+ if (ctx.mode !== "tui") return ctx.ui.select(title, choices2, { signal });
2789
+ return ctx.ui.custom((tui, theme, keys, done) => {
2790
+ let settled = false;
2791
+ const finish = (choice) => {
2792
+ if (settled) return;
2793
+ settled = true;
2794
+ signal.removeEventListener("abort", abort);
2795
+ done(choice);
2796
+ };
2797
+ const abort = () => finish(void 0);
2798
+ signal.addEventListener("abort", abort, { once: true });
2799
+ const selector = promptSelector(title, choices2, theme, keys, finish, () => tui.requestRender());
2800
+ return { ...selector, dispose: () => signal.removeEventListener("abort", abort) };
2801
+ });
2802
+ }
2803
+ function promptSelector(title, choices2, theme, keys, done, render) {
2779
2804
  const [heading, ...body] = title.split("\n");
2780
- const list = new SelectList(choices.map((value) => ({ value, label: value })), 8, {
2805
+ const list = new SelectList(choices2.map((value) => ({ value, label: value })), 8, {
2781
2806
  selectedPrefix: (text) => theme.fg("accent", text),
2782
2807
  selectedText: (text) => theme.fg("accent", text),
2783
2808
  description: (text) => theme.fg("muted", text),
@@ -2799,11 +2824,11 @@ function promptSelector(title, choices, theme, keys, done, render) {
2799
2824
  ];
2800
2825
  },
2801
2826
  handleInput(data) {
2802
- const index = choices.indexOf(list.getSelectedItem()?.value ?? "");
2827
+ const index = choices2.indexOf(list.getSelectedItem()?.value ?? "");
2803
2828
  if (keys.matches(data, "tui.select.cancel")) done(void 0);
2804
2829
  else if (keys.matches(data, "tui.select.confirm")) done(list.getSelectedItem()?.value);
2805
2830
  else if (keys.matches(data, "tui.select.up") || data === "k") list.setSelectedIndex(Math.max(0, index - 1));
2806
- else if (keys.matches(data, "tui.select.down") || data === "j") list.setSelectedIndex(Math.min(choices.length - 1, index + 1));
2831
+ else if (keys.matches(data, "tui.select.down") || data === "j") list.setSelectedIndex(Math.min(choices2.length - 1, index + 1));
2807
2832
  render();
2808
2833
  },
2809
2834
  invalidate() {
@@ -2835,21 +2860,9 @@ async function runPromptCommand(input, ctx, runtime, signal, assertCurrent, use)
2835
2860
  signal.throwIfAborted();
2836
2861
  assertCurrent();
2837
2862
  };
2838
- const select = async (title, choices) => {
2863
+ const select = async (title, choices2) => {
2839
2864
  guard();
2840
- const selected = ctx.mode !== "tui" ? await ctx.ui.select(title, choices, { signal }) : await ctx.ui.custom((tui, theme, keys, done) => {
2841
- let settled = false;
2842
- const finish = (choice) => {
2843
- if (settled) return;
2844
- settled = true;
2845
- signal.removeEventListener("abort", abort);
2846
- done(choice);
2847
- };
2848
- const abort = () => finish(void 0);
2849
- signal.addEventListener("abort", abort, { once: true });
2850
- const selector = promptSelector(title, choices, theme, keys, finish, () => tui.requestRender());
2851
- return { ...selector, dispose: () => signal.removeEventListener("abort", abort) };
2852
- });
2865
+ const selected = await chooseOption(ctx, title, choices2, signal);
2853
2866
  guard();
2854
2867
  return selected;
2855
2868
  };
@@ -3179,10 +3192,10 @@ var TomlError = class extends Error {
3179
3192
  line;
3180
3193
  column;
3181
3194
  codeblock;
3182
- constructor(message, options) {
3195
+ constructor(message2, options) {
3183
3196
  const [line2, column] = getLineColFromPtr(options.toml, options.ptr);
3184
3197
  const codeblock = makeCodeBlock(options.toml, line2, column);
3185
- super(`Invalid TOML document: ${message}
3198
+ super(`Invalid TOML document: ${message2}
3186
3199
 
3187
3200
  ${codeblock}`, options);
3188
3201
  this.line = line2;
@@ -3948,12 +3961,12 @@ function validateImportCandidate(candidate, cwd) {
3948
3961
  }
3949
3962
  return void 0;
3950
3963
  }
3951
- function importPreview(candidate, problem) {
3964
+ function importPreview(candidate, problem2) {
3952
3965
  const definition = candidate.definition;
3953
3966
  return [
3954
3967
  `${candidate.label} \xB7 ${candidate.transport}`,
3955
3968
  ...candidate.group ? [`Source: ${candidate.group}`] : [],
3956
- ...problem ? [problem] : [],
3969
+ ...problem2 ? [problem2] : [],
3957
3970
  ...definition ? [
3958
3971
  "Connection values are hidden; review and trust the source file before importing.",
3959
3972
  `Arguments: ${definition.args?.length ?? 0}; environment: ${Object.keys(definition.env ?? {}).length}; headers: ${Object.keys(definition.headers ?? {}).length} (values hidden)`,
@@ -3986,11 +3999,11 @@ async function runImportCommand(input, ctx, agentDir, projectTrusted, signal, as
3986
3999
  if (projectTrusted() !== trusted)
3987
4000
  throw new ConfigMutationError("Project trust changed during the preview. Run /mcp import again; nothing was saved.");
3988
4001
  };
3989
- const select = async (title, choices) => {
4002
+ const select = async (title, choices2) => {
3990
4003
  guard();
3991
- const result = await ctx.ui.select(title, choices, { signal });
4004
+ const result = await ctx.ui.select(title, choices2, { signal });
3992
4005
  guard();
3993
- return result !== void 0 && choices.includes(result) ? result : void 0;
4006
+ return result !== void 0 && choices2.includes(result) ? result : void 0;
3994
4007
  };
3995
4008
  guard();
3996
4009
  const source = await readImportSource(path, ctx.cwd, signal);
@@ -4030,22 +4043,22 @@ Only MCP server definitions are imported. Other settings, permissions, and crede
4030
4043
  if (group !== "All groups") candidates = candidates.filter((candidate) => candidate.group === group);
4031
4044
  }
4032
4045
  for (const [index, candidate] of candidates.entries()) {
4033
- const problem = validateImportCandidate(candidate, ctx.cwd);
4046
+ const problem2 = validateImportCandidate(candidate, ctx.cwd);
4034
4047
  let name = candidate.name;
4035
4048
  while (true) {
4036
4049
  const duplicate = name !== void 0 && Object.hasOwn(selected, name);
4037
- const action = name && !duplicate && !problem ? importAction(name, scope, scopes) : void 0;
4050
+ const action = name && !duplicate && !problem2 ? importAction(name, scope, scopes) : void 0;
4038
4051
  const conflict = name ? [
4039
4052
  Object.hasOwn(scopes.global, name) ? "global" : void 0,
4040
4053
  Object.hasOwn(scopes.project, name) ? "project" : void 0
4041
4054
  ].filter(Boolean).join(" and ") : "";
4042
4055
  const choice = await select([
4043
4056
  `Import preview ${index + 1}/${candidates.length} \xB7 ${scope} scope \xB7 ${source.format === "codex" ? "Codex TOML" : "JSON"}`,
4044
- importPreview(candidate, problem),
4057
+ importPreview(candidate, problem2),
4045
4058
  ...name ? [`Destination name: ${name}`] : [],
4046
4059
  ...conflict ? [`Existing definition in ${conflict} scope; fields and credentials are never merged.`] : [],
4047
4060
  ...duplicate ? ["This destination name is already selected; choose a different name."] : []
4048
- ].join("\n"), ["Skip", ...action ? [action] : [], ...!problem ? ["Choose a different name"] : [], "Cancel import"]);
4061
+ ].join("\n"), ["Skip", ...action ? [action] : [], ...!problem2 ? ["Choose a different name"] : [], "Cancel import"]);
4049
4062
  if (choice === void 0 || choice === "Cancel import") {
4050
4063
  cancel();
4051
4064
  return;
@@ -4115,7 +4128,7 @@ import {
4115
4128
  // src/oauth-page.ts
4116
4129
  function oauthCallbackHtml(status) {
4117
4130
  const heading = status === "received" ? "Authorization response received" : status === "denied" ? "Authorization not granted" : "Invalid OAuth callback";
4118
- const message = status === "received" ? "Return to Pi to finish signing in. You can close this tab." : "Return to Pi and try signing in again. You can close this tab.";
4131
+ const message2 = status === "received" ? "Return to Pi to finish signing in. You can close this tab." : "Return to Pi and try signing in again. You can close this tab.";
4119
4132
  return `<!doctype html>
4120
4133
  <html lang="en">
4121
4134
  <head>
@@ -4178,7 +4191,7 @@ function oauthCallbackHtml(status) {
4178
4191
  <div class="logo"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" aria-hidden="true"><path fill="#fff" fill-rule="evenodd" d="M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z M282.65 282.65 V400 H400 V282.65 Z"/><path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/></svg></div>
4179
4192
  <p class="brand">Pi MCP Client</p>
4180
4193
  <h1>${heading}</h1>
4181
- <p>${message}</p>
4194
+ <p>${message2}</p>
4182
4195
  </main>
4183
4196
  </body>
4184
4197
  </html>`;
@@ -4189,17 +4202,17 @@ function callbackUrl(options = {}) {
4189
4202
  return `http://127.0.0.1:${options.oauthCallbackPort ?? 19847}/callback`;
4190
4203
  }
4191
4204
  function parseCallback(value, redirect, state) {
4192
- const invalid2 = () => failure("oauth_failed", { operation: "auth" });
4193
- if (value.length > 16384 || /[\u0000-\u0020\u007f]/u.test(value)) throw invalid2();
4205
+ const invalid3 = () => failure("oauth_failed", { operation: "auth" });
4206
+ if (value.length > 16384 || /[\u0000-\u0020\u007f]/u.test(value)) throw invalid3();
4194
4207
  let target;
4195
4208
  try {
4196
4209
  target = new URL(value);
4197
4210
  } catch {
4198
- throw invalid2();
4211
+ throw invalid3();
4199
4212
  }
4200
4213
  const expected = new URL(redirect);
4201
4214
  const params = target.searchParams;
4202
- if (target.origin !== expected.origin || target.pathname !== expected.pathname || target.username || target.password || target.hash || [...params.keys()].some((key) => params.getAll(key).length !== 1) || params.get("state") !== state || params.has("code") === params.has("error") || !(params.get("code") || params.get("error"))) throw invalid2();
4215
+ if (target.origin !== expected.origin || target.pathname !== expected.pathname || target.username || target.password || target.hash || [...params.keys()].some((key) => params.getAll(key).length !== 1) || params.get("state") !== state || params.has("code") === params.has("error") || !(params.get("code") || params.get("error"))) throw invalid3();
4203
4216
  return params;
4204
4217
  }
4205
4218
  function credentialKey({ server, url, clientId }) {
@@ -4706,9 +4719,82 @@ import { join as join2 } from "node:path";
4706
4719
  import { randomUUID as randomUUID3 } from "node:crypto";
4707
4720
  import {
4708
4721
  Client,
4722
+ ProtocolError as ProtocolError3,
4723
+ ProtocolErrorCode as ProtocolErrorCode3,
4724
+ SdkError as SdkError2,
4725
+ SdkErrorCode,
4709
4726
  StreamableHTTPClientTransport,
4710
4727
  UriTemplate as UriTemplate2
4711
4728
  } from "@modelcontextprotocol/client";
4729
+
4730
+ // package.json
4731
+ var package_default = {
4732
+ name: "pi-mcp-client",
4733
+ version: "0.9.0",
4734
+ description: "MCP tools for Pi, discovered on demand and called natively through the official SDK.",
4735
+ type: "module",
4736
+ license: "MIT",
4737
+ author: "Matthias Vallentin",
4738
+ repository: {
4739
+ type: "git",
4740
+ url: "git+https://github.com/mavam/pi-mcp-client.git"
4741
+ },
4742
+ keywords: [
4743
+ "pi-package",
4744
+ "pi-extension",
4745
+ "mcp",
4746
+ "model-context-protocol"
4747
+ ],
4748
+ engines: {
4749
+ node: ">=22"
4750
+ },
4751
+ files: [
4752
+ "dist",
4753
+ "docs",
4754
+ "README.md",
4755
+ "LICENSE"
4756
+ ],
4757
+ pi: {
4758
+ extensions: [
4759
+ "./dist/index.js"
4760
+ ]
4761
+ },
4762
+ publishConfig: {
4763
+ access: "public"
4764
+ },
4765
+ scripts: {
4766
+ build: "rm -rf dist && esbuild src/index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --external:@earendil-works/pi-coding-agent --external:@earendil-works/pi-ai --external:@earendil-works/pi-tui --external:typebox --external:@modelcontextprotocol/client --external:@modelcontextprotocol/client/* --external:@napi-rs/keyring",
4767
+ check: "tsc --noEmit && bun test",
4768
+ test: "bun test",
4769
+ prepack: "bun run build"
4770
+ },
4771
+ dependencies: {
4772
+ "@modelcontextprotocol/client": "2.0.0",
4773
+ "@napi-rs/keyring": "^1.3.0",
4774
+ minisearch: "^7.2.0",
4775
+ "smol-toml": "^1.8.0"
4776
+ },
4777
+ devDependencies: {
4778
+ "@earendil-works/pi-ai": "^0.85.1",
4779
+ "@earendil-works/pi-coding-agent": "^0.85.1",
4780
+ "@earendil-works/pi-tui": "^0.85.1",
4781
+ "@modelcontextprotocol/server": "2.0.0",
4782
+ "@types/bun": "^1.3.0",
4783
+ "@types/node": "^24.0.0",
4784
+ esbuild: "^0.25.0",
4785
+ typebox: "^1.1.0",
4786
+ typescript: "^5.9.0",
4787
+ zod: "^4.0.0"
4788
+ },
4789
+ peerDependencies: {
4790
+ "@earendil-works/pi-ai": ">=0.85.1",
4791
+ "@earendil-works/pi-coding-agent": ">=0.85.1",
4792
+ "@earendil-works/pi-tui": ">=0.85.1",
4793
+ typebox: "^1.1.0"
4794
+ }
4795
+ };
4796
+
4797
+ // src/runtime.ts
4712
4798
  import {
4713
4799
  StdioClientTransport,
4714
4800
  getDefaultEnvironment
@@ -4805,9 +4891,335 @@ var ResourceSubscriptions = class {
4805
4891
  }
4806
4892
  };
4807
4893
 
4894
+ // src/elicitation.ts
4895
+ import {
4896
+ ProtocolError as ProtocolError2,
4897
+ ProtocolErrorCode as ProtocolErrorCode2
4898
+ } from "@modelcontextprotocol/client";
4899
+ var MAX_FIELDS = 50;
4900
+ var MAX_CHOICES = 100;
4901
+ var MAX_TEXT = 8192;
4902
+ var MAX_CONTENT = 64 * 1024;
4903
+ var MAX_REQUIRED = 3;
4904
+ var invalid2 = (message2) => new ProtocolError2(ProtocolErrorCode2.InvalidParams, message2);
4905
+ var short = (value, limit) => typeof value === "string" ? line(value).slice(0, limit) : "";
4906
+ var number = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
4907
+ function message(value) {
4908
+ const text = plain(value).split("\n").map((row) => row.trimEnd()).slice(0, 20).join("\n").trim();
4909
+ return text.slice(0, 2e3) || "(No message supplied.)";
4910
+ }
4911
+ function choices(values) {
4912
+ if (!values.length || values.length > MAX_CHOICES || new Set(values.map((choice) => choice.value)).size !== values.length)
4913
+ throw invalid2("Elicitation choices are empty, duplicated, or too many.");
4914
+ return values.map(({ value, label: label2 }) => ({ value, label: short(label2, 120) || short(value, 120) || JSON.stringify(value) }));
4915
+ }
4916
+ function formFields(schema) {
4917
+ const entries = Object.entries(schema.properties ?? {});
4918
+ if (entries.length > MAX_FIELDS) throw invalid2("Elicitation forms are limited to 50 fields.");
4919
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
4920
+ return entries.map(([name, value]) => {
4921
+ const property = value;
4922
+ const base = {
4923
+ name,
4924
+ label: short(property.title, 120) || short(name, 120) || "(unnamed)",
4925
+ description: short(property.description, 400),
4926
+ required: required.has(name)
4927
+ };
4928
+ if (property.type === "boolean")
4929
+ return { ...base, type: "boolean", default: typeof property.default === "boolean" ? property.default : void 0 };
4930
+ if (property.type === "number" || property.type === "integer")
4931
+ return { ...base, type: property.type, minimum: number(property.minimum), maximum: number(property.maximum), default: number(property.default) };
4932
+ if (property.type === "array") {
4933
+ const items = property.items ?? {};
4934
+ const options = Array.isArray(items.enum) ? items.enum.map((value2) => ({ value: value2, label: value2 })) : (items.anyOf ?? []).map((option) => ({ value: option.const, label: option.title }));
4935
+ return {
4936
+ ...base,
4937
+ type: "multi",
4938
+ options: choices(options),
4939
+ minItems: number(property.minItems),
4940
+ maxItems: number(property.maxItems),
4941
+ default: Array.isArray(property.default) ? property.default : void 0
4942
+ };
4943
+ }
4944
+ if (Array.isArray(property.enum) || Array.isArray(property.oneOf)) {
4945
+ const options = Array.isArray(property.enum) ? property.enum.map((value2, index) => ({ value: value2, label: property.enumNames?.[index] ?? value2 })) : property.oneOf.map((option) => ({ value: option.const, label: option.title }));
4946
+ return { ...base, type: "choice", options: choices(options), default: typeof property.default === "string" ? property.default : void 0 };
4947
+ }
4948
+ return {
4949
+ ...base,
4950
+ type: "string",
4951
+ minLength: number(property.minLength),
4952
+ maxLength: number(property.maxLength),
4953
+ format: typeof property.format === "string" ? property.format : void 0,
4954
+ default: typeof property.default === "string" ? property.default : void 0
4955
+ };
4956
+ });
4957
+ }
4958
+ var validDate = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(`${value}T00:00:00Z`)) && (/* @__PURE__ */ new Date(`${value}T00:00:00Z`)).toISOString().startsWith(value);
4959
+ var formats = {
4960
+ email: { name: "email address", valid: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(value) },
4961
+ uri: { name: "absolute URI", valid: (value) => URL.canParse(value) },
4962
+ date: {
4963
+ name: "date (YYYY-MM-DD)",
4964
+ valid: validDate
4965
+ },
4966
+ "date-time": {
4967
+ name: "date and time (RFC 3339)",
4968
+ valid: (value) => /^\d{4}-\d{2}-\d{2}T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?(Z|[+-]([01]\d|2[0-3]):[0-5]\d)$/i.test(value) && validDate(value.slice(0, 10)) && !Number.isNaN(Date.parse(value))
4969
+ }
4970
+ };
4971
+ var textLimit = (field) => Math.min(field.maxLength ?? MAX_TEXT, MAX_TEXT);
4972
+ function problem(field, value) {
4973
+ switch (field.type) {
4974
+ case "string": {
4975
+ if (typeof value !== "string") return "Enter text.";
4976
+ const length = [...value].length;
4977
+ if (length > textLimit(field)) return `Use at most ${textLimit(field)} characters.`;
4978
+ if (field.minLength !== void 0 && length < field.minLength) return `Use at least ${field.minLength} characters.`;
4979
+ const format = field.format ? formats[field.format] : void 0;
4980
+ if (format && !format.valid(value)) return `Enter a valid ${format.name}.`;
4981
+ return;
4982
+ }
4983
+ case "number":
4984
+ case "integer":
4985
+ if (typeof value !== "number" || !Number.isFinite(value)) return "Enter a number.";
4986
+ if (field.type === "integer" && !Number.isInteger(value)) return "Enter a whole number.";
4987
+ if (field.minimum !== void 0 && value < field.minimum) return `Enter at least ${field.minimum}.`;
4988
+ if (field.maximum !== void 0 && value > field.maximum) return `Enter at most ${field.maximum}.`;
4989
+ return;
4990
+ case "boolean":
4991
+ return typeof value === "boolean" ? void 0 : "Choose yes or no.";
4992
+ case "choice":
4993
+ return typeof value === "string" && field.options.some((option) => option.value === value) ? void 0 : "Choose one of the listed options.";
4994
+ case "multi":
4995
+ if (!Array.isArray(value) || new Set(value).size !== value.length || value.some((item) => !field.options.some((option) => option.value === item))) return "Choose from the listed options.";
4996
+ if (field.minItems !== void 0 && value.length < field.minItems) return `Choose at least ${field.minItems}.`;
4997
+ if (field.maxItems !== void 0 && value.length > field.maxItems) return `Choose at most ${field.maxItems}.`;
4998
+ }
4999
+ }
5000
+ var range = (min, max, unit = "") => min !== void 0 && max !== void 0 ? `${min}\u2013${max}${unit}` : min !== void 0 ? `at least ${min}${unit}` : max !== void 0 ? `at most ${max}${unit}` : "";
5001
+ function constraints(field) {
5002
+ const parts = [];
5003
+ if (field.type === "string") {
5004
+ parts.push(field.format && formats[field.format] ? formats[field.format].name : "text");
5005
+ const length = range(field.minLength, field.maxLength, " characters");
5006
+ if (length) parts.push(length);
5007
+ } else if (field.type === "number" || field.type === "integer") {
5008
+ parts.push(field.type === "integer" ? "whole number" : "number");
5009
+ const bounds = range(field.minimum, field.maximum);
5010
+ if (bounds) parts.push(bounds);
5011
+ } else if (field.type === "boolean") parts.push("yes or no");
5012
+ else if (field.type === "choice") parts.push("choose one");
5013
+ else if (field.type === "multi") parts.push(`choose ${range(field.minItems, field.maxItems) || "any"}`);
5014
+ parts.push(field.required ? "required" : "optional");
5015
+ return parts.join(", ");
5016
+ }
5017
+ var label = (field, value) => field.type === "choice" || field.type === "multi" ? field.options.find((option) => option.value === value)?.label ?? value : value;
5018
+ function shown(field, value) {
5019
+ if (value === void 0) return "not set";
5020
+ if (typeof value === "boolean") return value ? "yes" : "no";
5021
+ if (Array.isArray(value)) return value.map((item) => label(field, item)).join(", ") || "none";
5022
+ if (typeof value === "number") return String(value);
5023
+ if (field.type === "choice") return label(field, value);
5024
+ const text = line(value);
5025
+ return JSON.stringify(text.length > 60 ? `${text.slice(0, 59)}\u2026` : text);
5026
+ }
5027
+ async function edit(ui, field, values, signal) {
5028
+ const current = values.get(field.name);
5029
+ const title = `${field.label}
5030
+ ${[constraints(field), field.description].filter(Boolean).join("\n")}`;
5031
+ const clear = field.required ? [] : ["Clear value"];
5032
+ if (field.type === "boolean" || field.type === "choice") {
5033
+ const options = field.type === "boolean" ? [{ value: true, label: "Yes" }, { value: false, label: "No" }] : field.options.map((option) => ({ value: option.value, label: option.label }));
5034
+ const labels = options.map((option, index) => `${option.value === current ? "\u25CF" : "\u25CB"} ${index + 1}. ${option.label}`);
5035
+ const choice = await ui.select(title, [...labels, ...clear, "Back"], signal);
5036
+ if (choice === "Clear value") values.delete(field.name);
5037
+ else if (choice !== void 0 && labels.includes(choice)) values.set(field.name, options[labels.indexOf(choice)].value);
5038
+ return;
5039
+ }
5040
+ if (field.type === "multi") {
5041
+ const selected = new Set(Array.isArray(current) ? current : []);
5042
+ while (!signal.aborted) {
5043
+ const labels = field.options.map((option2, index) => `${selected.has(option2.value) ? "\u2611" : "\u2610"} ${index + 1}. ${option2.label}`);
5044
+ const choice = await ui.select(`${title}
5045
+ Select options to toggle them, then choose Done.`, [...labels, "Done", ...clear, "Back"], signal);
5046
+ if (choice === void 0 || choice === "Back") return;
5047
+ if (choice === "Clear value") return void values.delete(field.name);
5048
+ if (choice === "Done") {
5049
+ const value2 = field.options.map((option2) => option2.value).filter((value3) => selected.has(value3));
5050
+ const issue2 = problem(field, value2);
5051
+ if (issue2) ui.notify(`${field.label}: ${issue2}`, "error");
5052
+ else return void values.set(field.name, value2);
5053
+ continue;
5054
+ }
5055
+ const option = field.options[labels.indexOf(choice)];
5056
+ if (option && !selected.delete(option.value)) selected.add(option.value);
5057
+ }
5058
+ return;
5059
+ }
5060
+ const text = await ui.editor(`${title}${field.required ? "" : "\nLeave empty to clear the value."}`, current === void 0 ? "" : String(current));
5061
+ if (text === void 0 || signal.aborted) return;
5062
+ if (!text.trim() && !field.required && (field.type !== "string" || !text)) return void values.delete(field.name);
5063
+ const value = field.type === "string" ? text : text.trim() ? Number(text.trim()) : Number.NaN;
5064
+ const issue = problem(field, value);
5065
+ if (issue) ui.notify(`${field.label}: ${issue} The previous value was kept.`, "error");
5066
+ else values.set(field.name, value);
5067
+ }
5068
+ async function elicitForm(ui, server, params, signal) {
5069
+ const fields = formFields(params.requestedSchema);
5070
+ const values = /* @__PURE__ */ new Map();
5071
+ for (const field of fields)
5072
+ if (field.default !== void 0 && !problem(field, field.default)) values.set(field.name, field.default);
5073
+ const title = `${server} requests information
5074
+ ${message(params.message)}
5075
+
5076
+ Review the values below. Only what you submit is sent to ${server}. Never enter passwords, API keys, or payment details here.`;
5077
+ while (!signal.aborted) {
5078
+ const labels = fields.map((field2, index) => `${index + 1}. ${field2.label} (${field2.required ? "required" : "optional"}) \xB7 ${shown(field2, values.get(field2.name))}`);
5079
+ const choice = await ui.select(title, [...labels, "Submit", "Decline", "Cancel"], signal);
5080
+ if (signal.aborted || choice === void 0 || choice === "Cancel") break;
5081
+ if (choice === "Decline") return { action: "decline" };
5082
+ if (choice === "Submit") {
5083
+ const issue = fields.map((field2) => {
5084
+ const value = values.get(field2.name);
5085
+ if (value === void 0) return field2.required ? `${field2.label}: A value is required.` : void 0;
5086
+ const found = problem(field2, value);
5087
+ return found && `${field2.label}: ${found}`;
5088
+ }).find(Boolean);
5089
+ const content = Object.fromEntries(values);
5090
+ if (issue) ui.notify(issue, "error");
5091
+ else if (Buffer.byteLength(JSON.stringify(content)) > MAX_CONTENT) ui.notify("The submitted values exceed 64 KiB.", "error");
5092
+ else return { action: "accept", content };
5093
+ continue;
5094
+ }
5095
+ const field = fields[labels.indexOf(choice)];
5096
+ if (field) await edit(ui, field, values, signal);
5097
+ }
5098
+ return { action: "cancel" };
5099
+ }
5100
+ function safeUrl(value) {
5101
+ if (typeof value !== "string" || value.length > 4096 || !URL.canParse(value)) return;
5102
+ const url = new URL(value);
5103
+ const loopback = url.hostname === "localhost" || url.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url.hostname);
5104
+ if (url.username || url.password || !(url.protocol === "https:" || url.protocol === "http:" && loopback)) return;
5105
+ const notes = [];
5106
+ if (url.hostname.split(".").some((part) => part.startsWith("xn--")))
5107
+ notes.push("\u26A0 The site name contains internationalized characters. Check that it's the site you expect.");
5108
+ if (url.protocol === "http:") notes.push("\u26A0 This page uses an unencrypted connection to this computer.");
5109
+ return { href: url.href, host: url.host, notes };
5110
+ }
5111
+ async function elicitUrl(ui, server, params, signal) {
5112
+ const target = safeUrl(params.url);
5113
+ if (!target) {
5114
+ ui.notify(`${server} asked to open an unsupported URL, so the request was refused. Only HTTPS pages, or HTTP pages on this computer, without embedded credentials can be opened.`, "warning");
5115
+ throw invalid2("Unsupported elicitation URL.");
5116
+ }
5117
+ let failed = false;
5118
+ while (!signal.aborted) {
5119
+ const choice = await ui.select(
5120
+ `${server} asks you to open a web page
5121
+ ${message(params.message)}
5122
+
5123
+ Site: ${target.host}
5124
+ URL: ${target.href}
5125
+ ${target.notes.map((note) => `${note}
5126
+ `).join("")}` + (failed ? `Couldn't open a browser. Copy the URL, then choose "I'll open it myself".
5127
+ ` : "") + "Anything you enter on the page goes to that site, not to Pi. Copy the URL before choosing to open it elsewhere.",
5128
+ ["Open in browser", "I'll open it myself", "Decline"],
5129
+ signal
5130
+ );
5131
+ if (signal.aborted || choice === void 0) break;
5132
+ if (choice === "Decline") return { action: "decline" };
5133
+ if (choice === "I'll open it myself") return { action: "accept" };
5134
+ if (await ui.open(target.href)) return { action: "accept" };
5135
+ failed = true;
5136
+ }
5137
+ return { action: "cancel" };
5138
+ }
5139
+ function elicit(ui, server, params, signal) {
5140
+ const name = line(server);
5141
+ return params.mode === "url" ? elicitUrl(ui, name, params, signal) : elicitForm(ui, name, params, signal);
5142
+ }
5143
+ async function awaitCompletion(ui, server, completed, signal) {
5144
+ const name = line(server);
5145
+ const choice = await ui.select(
5146
+ `${name} \xB7 Finish in your browser
5147
+ Complete the step on the page you opened, then choose Done. This closes automatically if ${name} confirms completion. The tool runs again only if the assistant calls it.`,
5148
+ ["Done", "Cancel"],
5149
+ AbortSignal.any([completed, signal])
5150
+ );
5151
+ return completed.aborted || choice === "Done" && !signal.aborted;
5152
+ }
5153
+ function requiredElicitations(error) {
5154
+ if (!(error instanceof ProtocolError2) || error.code !== ProtocolErrorCode2.UrlElicitationRequired) return;
5155
+ const list = error.data?.elicitations;
5156
+ if (!Array.isArray(list) || !list.length || list.length > MAX_REQUIRED) return;
5157
+ const valid = list.every((item) => typeof item === "object" && item !== null && item.mode === "url" && typeof item.message === "string" && typeof item.url === "string" && typeof item.elicitationId === "string" && item.elicitationId.length > 0 && item.elicitationId.length <= 256);
5158
+ if (!valid || new Set(list.map((item) => item.elicitationId)).size !== list.length) return;
5159
+ return list.map(({ mode, message: message2, url, elicitationId }) => ({ mode, message: message2, url, elicitationId }));
5160
+ }
5161
+
4808
5162
  // src/runtime.ts
4809
5163
  var ToolContractError = class extends Error {
4810
5164
  };
5165
+ var UNBOUNDED_TIMEOUT = 2147483647;
5166
+ var MAX_PENDING_ELICITATIONS = 4;
5167
+ var Deadline = class {
5168
+ constructor(remaining) {
5169
+ this.remaining = remaining;
5170
+ this.start();
5171
+ }
5172
+ controller = new AbortController();
5173
+ signal = this.controller.signal;
5174
+ timer;
5175
+ started = 0;
5176
+ holds = 0;
5177
+ done = false;
5178
+ start() {
5179
+ this.started = Date.now();
5180
+ this.timer = setTimeout(
5181
+ () => this.controller.abort(new SdkError2(SdkErrorCode.RequestTimeout, "Request timed out")),
5182
+ Math.max(0, this.remaining)
5183
+ );
5184
+ }
5185
+ pause() {
5186
+ if (this.holds++ || !this.timer) return;
5187
+ clearTimeout(this.timer);
5188
+ this.timer = void 0;
5189
+ this.remaining -= Date.now() - this.started;
5190
+ }
5191
+ resume() {
5192
+ if (--this.holds || this.done || this.signal.aborted) return;
5193
+ this.start();
5194
+ }
5195
+ dispose() {
5196
+ this.done = true;
5197
+ clearTimeout(this.timer);
5198
+ }
5199
+ };
5200
+ function createClient(config, handlers = {}) {
5201
+ const { elicit: elicit2, elicitationComplete } = handlers;
5202
+ const client = new Client(
5203
+ { name: "pi-mcp-client", version: package_default.version },
5204
+ {
5205
+ ...elicit2 ? { capabilities: { elicitation: { form: {}, url: {} } } } : {},
5206
+ listChanged: {
5207
+ tools: { autoRefresh: false, debounceMs: 0, onChanged: () => handlers.toolsChanged?.() },
5208
+ prompts: { autoRefresh: false, debounceMs: 0, onChanged: () => handlers.promptsChanged?.() },
5209
+ resources: { autoRefresh: false, debounceMs: 0, onChanged: () => handlers.resourcesChanged?.() }
5210
+ },
5211
+ versionNegotiation: {
5212
+ mode: config.protocol ?? "auto",
5213
+ probe: { timeoutMs: config.startupTimeoutMs ?? config.timeoutMs ?? 15e3 }
5214
+ }
5215
+ }
5216
+ );
5217
+ if (elicit2) {
5218
+ client.setRequestHandler("elicitation/create", (request, ctx) => elicit2(request.params, ctx.mcpReq.signal));
5219
+ client.setNotificationHandler("notifications/elicitation/complete", (notification) => elicitationComplete?.(notification.params.elicitationId));
5220
+ }
5221
+ return client;
5222
+ }
4811
5223
  function waitFor2(promise, signal) {
4812
5224
  if (!signal) return promise;
4813
5225
  return new Promise((resolve3, reject) => {
@@ -4826,36 +5238,11 @@ function waitFor2(promise, signal) {
4826
5238
  if (signal.aborted) abort();
4827
5239
  });
4828
5240
  }
4829
- var createSdkConnector = (storeFactory = credentialStore) => async (name, config, signal, onToolsChanged, onResourcesChanged, onPromptsChanged) => {
5241
+ var createSdkConnector = (storeFactory = credentialStore) => async (name, config, signal, handlers) => {
4830
5242
  const timeout = config.timeoutMs ?? 15e3;
4831
5243
  const startupTimeout = config.startupTimeoutMs ?? timeout;
4832
5244
  let connecting = true;
4833
- const client = new Client(
4834
- { name: "pi-mcp-client", version: "0.1.0" },
4835
- {
4836
- listChanged: {
4837
- tools: {
4838
- autoRefresh: false,
4839
- debounceMs: 0,
4840
- onChanged: () => onToolsChanged?.()
4841
- },
4842
- prompts: {
4843
- autoRefresh: false,
4844
- debounceMs: 0,
4845
- onChanged: () => onPromptsChanged?.()
4846
- },
4847
- resources: {
4848
- autoRefresh: false,
4849
- debounceMs: 0,
4850
- onChanged: () => onResourcesChanged?.()
4851
- }
4852
- },
4853
- versionNegotiation: {
4854
- mode: config.protocol ?? "auto",
4855
- probe: { timeoutMs: startupTimeout }
4856
- }
4857
- }
4858
- );
5245
+ const client = createClient(config, handlers);
4859
5246
  const transport = config.command ? new StdioClientTransport({
4860
5247
  command: config.command,
4861
5248
  args: config.args,
@@ -4913,11 +5300,12 @@ var createSdkConnector = (storeFactory = credentialStore) => async (name, config
4913
5300
  };
4914
5301
  var connectSdk = createSdkConnector();
4915
5302
  var McpRuntime = class {
4916
- constructor(config, cwd, cacheDir, connect = connectSdk) {
5303
+ constructor(config, cwd, cacheDir, connect = connectSdk, options = {}) {
4917
5304
  this.config = config;
4918
5305
  this.cwd = cwd;
4919
5306
  this.cacheDir = cacheDir;
4920
5307
  this.connect = connect;
5308
+ this.options = options;
4921
5309
  }
4922
5310
  states = /* @__PURE__ */ new Map();
4923
5311
  lifetime = new AbortController();
@@ -4985,33 +5373,42 @@ var McpRuntime = class {
4985
5373
  this.lifetime.signal
4986
5374
  ),
4987
5375
  this.lifetime.signal,
4988
- () => {
4989
- if (this.closing || this.lifetime.signal.aborted || state.connectionToken !== token)
4990
- return;
4991
- state.catalogGeneration++;
4992
- state.catalogDirty = true;
4993
- state.tools = void 0;
4994
- state.warnings = void 0;
4995
- const path = join2(this.cacheDir, `${identity}.json`);
4996
- state.invalidating = withFileMutationQueue2(
4997
- path,
4998
- () => rm2(path, { force: true })
4999
- ).catch(() => {
5000
- state.warnings = [
5001
- `${name}: stale catalog cache could not be removed.`
5002
- ];
5003
- });
5004
- },
5005
- () => {
5006
- if (this.closing || this.lifetime.signal.aborted || state.connectionToken !== token) return;
5007
- state.resourceGeneration++;
5008
- state.resources = void 0;
5009
- state.resourceWarnings = void 0;
5010
- },
5011
- () => {
5012
- if (this.closing || this.lifetime.signal.aborted || state.connectionToken !== token) return;
5013
- state.promptGeneration++;
5014
- state.prompts = void 0;
5376
+ {
5377
+ toolsChanged: () => {
5378
+ if (this.closing || this.lifetime.signal.aborted || state.connectionToken !== token)
5379
+ return;
5380
+ state.catalogGeneration++;
5381
+ state.catalogDirty = true;
5382
+ state.tools = void 0;
5383
+ state.warnings = void 0;
5384
+ const path = join2(this.cacheDir, `${identity}.json`);
5385
+ state.invalidating = withFileMutationQueue2(
5386
+ path,
5387
+ () => rm2(path, { force: true })
5388
+ ).catch(() => {
5389
+ state.warnings = [
5390
+ `${name}: stale catalog cache could not be removed.`
5391
+ ];
5392
+ });
5393
+ },
5394
+ resourcesChanged: () => {
5395
+ if (this.closing || this.lifetime.signal.aborted || state.connectionToken !== token) return;
5396
+ state.resourceGeneration++;
5397
+ state.resources = void 0;
5398
+ state.resourceWarnings = void 0;
5399
+ },
5400
+ promptsChanged: () => {
5401
+ if (this.closing || this.lifetime.signal.aborted || state.connectionToken !== token) return;
5402
+ state.promptGeneration++;
5403
+ state.prompts = void 0;
5404
+ },
5405
+ ...this.options.interactive ? {
5406
+ elicit: (params, signal) => this.elicit(name, state, token, params, signal),
5407
+ elicitationComplete: (id) => {
5408
+ if (state.connectionToken === token)
5409
+ for (const complete of state.awaitingCompletion?.get(id) ?? []) complete();
5410
+ }
5411
+ } : {}
5015
5412
  }
5016
5413
  );
5017
5414
  if (this.closing || this.lifetime.signal.aborted) {
@@ -5411,7 +5808,7 @@ var McpRuntime = class {
5411
5808
  );
5412
5809
  return result;
5413
5810
  }
5414
- async call(tool, args, signal, progress) {
5811
+ async call(tool, args, signal, progress, ui) {
5415
5812
  const config = this.definition(tool.server);
5416
5813
  if (!allowed(tool.name, config) || this.identity(tool.server) !== tool.identity)
5417
5814
  throw new ToolContractError(
@@ -5424,29 +5821,125 @@ var McpRuntime = class {
5424
5821
  "MCP tool was removed or its schema changed. Use mcp_tools with activate and its exact identifier to activate its current definition."
5425
5822
  );
5426
5823
  const client = await waitFor2(this.client(tool.server), signal);
5824
+ const state = this.state(tool.server);
5825
+ const deadline = new Deadline(config.toolTimeoutMs ?? config.timeoutMs ?? 3e4);
5826
+ const scope = ui && this.options.interactive ? { ui, deadline, progress } : void 0;
5827
+ if (scope) {
5828
+ if (!state.scopes?.size) {
5829
+ state.scopes = /* @__PURE__ */ new Set();
5830
+ state.scopesEnded = new AbortController();
5831
+ }
5832
+ state.scopes.add(scope);
5833
+ }
5427
5834
  try {
5428
5835
  return await client.callTool(
5429
5836
  { name: tool.name, arguments: args },
5430
5837
  {
5431
5838
  signal: AbortSignal.any([
5432
5839
  this.lifetime.signal,
5840
+ deadline.signal,
5433
5841
  ...signal ? [signal] : []
5434
5842
  ]),
5435
- timeout: config.toolTimeoutMs ?? config.timeoutMs ?? 3e4,
5843
+ timeout: UNBOUNDED_TIMEOUT,
5436
5844
  onprogress: (event) => progress?.(
5437
5845
  event.message ?? `${event.progress}${event.total === void 0 ? "" : `/${event.total}`}`
5438
5846
  )
5439
5847
  }
5440
5848
  );
5441
5849
  } catch (error) {
5442
- const value = diagnose(error, {
5850
+ const context = {
5443
5851
  server: tool.server,
5444
5852
  operation: "call",
5445
5853
  oauth: usesOAuth(config),
5446
5854
  signal
5447
- });
5448
- this.state(tool.server).error = value;
5855
+ };
5856
+ if (scope && error instanceof ProtocolError3 && error.code === ProtocolErrorCode3.UrlElicitationRequired) {
5857
+ const required = requiredElicitations(error);
5858
+ throw failure(
5859
+ required ? await this.completeRequired(tool.server, state, scope, required, signal) : "protocol_error",
5860
+ context
5861
+ );
5862
+ }
5863
+ const value = diagnose(error, context);
5864
+ if (value.code !== "elicitation_required") state.error = value;
5449
5865
  throw new DiagnosticError(value);
5866
+ } finally {
5867
+ deadline.dispose();
5868
+ if (scope) {
5869
+ state.scopes?.delete(scope);
5870
+ if (!state.scopes?.size) state.scopesEnded?.abort();
5871
+ }
5872
+ }
5873
+ }
5874
+ /** Server dialogs appear one at a time, in arrival order. */
5875
+ dialogs = Promise.resolve();
5876
+ dialog(work, signal) {
5877
+ const run = this.dialogs.then(() => {
5878
+ signal.throwIfAborted();
5879
+ return work();
5880
+ });
5881
+ this.dialogs = run.catch(() => {
5882
+ });
5883
+ return waitFor2(run, signal);
5884
+ }
5885
+ /** Accept requests only while a tool call to this server can show them. */
5886
+ async elicit(name, state, token, params, signal) {
5887
+ const scopes = [...state.scopes ?? []];
5888
+ const scope = scopes.at(-1);
5889
+ if (!scope || !state.scopesEnded || state.connectionToken !== token || this.closing)
5890
+ throw new ProtocolError3(ProtocolErrorCode3.InvalidRequest, "Elicitation is only accepted during an interactive tool call.");
5891
+ if ((state.pendingElicitations ?? 0) >= MAX_PENDING_ELICITATIONS)
5892
+ throw new ProtocolError3(ProtocolErrorCode3.InvalidRequest, "Too many pending elicitation requests.");
5893
+ state.pendingElicitations = (state.pendingElicitations ?? 0) + 1;
5894
+ for (const each of scopes) each.deadline.pause();
5895
+ const combined = AbortSignal.any([signal, this.lifetime.signal, state.scopesEnded.signal]);
5896
+ try {
5897
+ return await this.dialog(() => {
5898
+ scope.progress?.("Waiting for your input\u2026");
5899
+ return elicit(scope.ui, name, params, combined);
5900
+ }, combined);
5901
+ } catch (error) {
5902
+ if (combined.aborted) return { action: "cancel" };
5903
+ throw error;
5904
+ } finally {
5905
+ state.pendingElicitations = (state.pendingElicitations ?? 1) - 1;
5906
+ for (const each of scopes) each.deadline.resume();
5907
+ if (!combined.aborted) scope.progress?.("Calling\u2026");
5908
+ }
5909
+ }
5910
+ /** Present a required browser step; never replay the refused call. */
5911
+ async completeRequired(name, state, scope, required, signal) {
5912
+ const combined = AbortSignal.any([this.lifetime.signal, ...signal ? [signal] : []]);
5913
+ const pending = new Set(required.map((item) => item.elicitationId));
5914
+ const completed = new AbortController();
5915
+ const awaiting = state.awaitingCompletion ??= /* @__PURE__ */ new Map();
5916
+ const listeners = /* @__PURE__ */ new Map();
5917
+ for (const id of pending) {
5918
+ const complete = () => {
5919
+ pending.delete(id);
5920
+ if (!pending.size) completed.abort();
5921
+ };
5922
+ listeners.set(id, complete);
5923
+ const subscribers = awaiting.get(id) ?? /* @__PURE__ */ new Set();
5924
+ subscribers.add(complete);
5925
+ awaiting.set(id, subscribers);
5926
+ }
5927
+ try {
5928
+ for (const params of required) {
5929
+ scope.progress?.("Waiting for your input\u2026");
5930
+ const result = await this.dialog(() => elicit(scope.ui, name, params, combined), combined);
5931
+ if (result.action !== "accept") return combined.aborted ? "cancelled" : "elicitation_declined";
5932
+ }
5933
+ const done = completed.signal.aborted || await this.dialog(() => awaitCompletion(scope.ui, name, completed.signal, combined), combined);
5934
+ return done ? "elicitation_completed" : combined.aborted ? "cancelled" : "elicitation_declined";
5935
+ } catch {
5936
+ return combined.aborted ? "cancelled" : "elicitation_declined";
5937
+ } finally {
5938
+ for (const [id, complete] of listeners) {
5939
+ const subscribers = awaiting.get(id);
5940
+ subscribers?.delete(complete);
5941
+ if (!subscribers?.size) awaiting.delete(id);
5942
+ }
5450
5943
  }
5451
5944
  }
5452
5945
  /** Close related OAuth connections without reconnecting, including disabled servers. */
@@ -5683,7 +6176,7 @@ Content type: ${line(resource.mimeType ?? "unspecified")}` });
5683
6176
  converted.details.resource = target;
5684
6177
  return converted;
5685
6178
  }
5686
- async function convertResult(result, label) {
6179
+ async function convertResult(result, label2) {
5687
6180
  const texts = [];
5688
6181
  const blocks = [];
5689
6182
  let offset = 0;
@@ -5705,7 +6198,7 @@ async function convertResult(result, label) {
5705
6198
  append(part.resource.text, { mimeType: part.resource.mimeType });
5706
6199
  else if (part.type === "resource_link")
5707
6200
  append(`${part.name}: ${part.uri}${validResourceUri(part.uri) ? `
5708
- Read with mcp_tools(${JSON.stringify({ read: { server: label.split(".")[0], uri: part.uri } })})` : ""}`, {
6201
+ Read with mcp_tools(${JSON.stringify({ read: { server: label2.split(".")[0], uri: part.uri } })})` : ""}`, {
5709
6202
  mimeType: part.mimeType,
5710
6203
  resourceLink: true
5711
6204
  });
@@ -5731,12 +6224,12 @@ Read with mcp_tools(${JSON.stringify({ read: { server: label.split(".")[0], uri:
5731
6224
  ...result.isError ? {
5732
6225
  diagnostics: [
5733
6226
  diagnostic("tool_error", {
5734
- server: label.split(".")[0],
6227
+ server: label2.split(".")[0],
5735
6228
  operation: "call"
5736
6229
  })
5737
6230
  ]
5738
6231
  } : {},
5739
- rows: [{ label, state: result.isError ? "failed" : "done" }]
6232
+ rows: [{ label: label2, state: result.isError ? "failed" : "done" }]
5740
6233
  };
5741
6234
  if (truncated.truncated || needsSpill) {
5742
6235
  const path = join3(await mkdtemp(join3(tmpdir(), "pi-mcp-client-")), "result.json");
@@ -5999,14 +6492,14 @@ var CommandUsageError = class extends Error {
5999
6492
  };
6000
6493
  function errorResult(error, context) {
6001
6494
  const value = error instanceof ToolContractError ? diagnostic("tool_changed", context) : diagnose(error, context);
6002
- const message = formatDiagnostic(value) + (context.operation === "call" ? " The call was not replayed. The server may already have performed the operation; verify before retrying." : "");
6003
- return textResult(message, {
6495
+ const message2 = formatDiagnostic(value) + (context.operation === "call" && !value.code.startsWith("elicitation_") ? " The call was not replayed. The server may already have performed the operation; verify before retrying." : "");
6496
+ return textResult(message2, {
6004
6497
  mcpClient: 1,
6005
6498
  failed: true,
6006
6499
  diagnostics: [value],
6007
6500
  rows: [
6008
6501
  {
6009
- label: value.code === "cancelled" ? "Cancelled" : message,
6502
+ label: value.code === "cancelled" ? "Cancelled" : message2,
6010
6503
  state: value.code === "cancelled" ? "cancelled" : "failed"
6011
6504
  }
6012
6505
  ]
@@ -6028,14 +6521,25 @@ function mcpClient(pi, options = {}) {
6028
6521
  STATUS_ENTRY,
6029
6522
  (entry, _options, theme) => statusPanel(entry.data ?? { servers: [], loaded: [] }, theme)
6030
6523
  );
6031
- pi.registerMessageRenderer("mcp-prompt", (message, { expanded, outputPad }, theme) => {
6032
- const details = object(message.details) ? message.details : {};
6524
+ pi.registerMessageRenderer("mcp-prompt", (message2, { expanded, outputPad }, theme) => {
6525
+ const details = object(message2.details) ? message2.details : {};
6033
6526
  const count = Number(details.count ?? 0);
6034
- const label = `${line(String(details.server ?? "MCP"))} \xB7 ${line(String(details.name ?? "prompt"))} \xB7 ${count} ${count === 1 ? "message" : "messages"}`;
6035
- return new Text4(theme.fg("toolTitle", "mcp prompt") + "\n" + theme.fg("success", "\u2714\uFE0E ") + theme.fg("accent", label) + (expanded && typeof message.content === "string" ? `
6036
- ${message.content}` : ""), outputPad, 0);
6527
+ const label2 = `${line(String(details.server ?? "MCP"))} \xB7 ${line(String(details.name ?? "prompt"))} \xB7 ${count} ${count === 1 ? "message" : "messages"}`;
6528
+ return new Text4(theme.fg("toolTitle", "mcp prompt") + "\n" + theme.fg("success", "\u2714\uFE0E ") + theme.fg("accent", label2) + (expanded && typeof message2.content === "string" ? `
6529
+ ${message2.content}` : ""), outputPad, 0);
6037
6530
  });
6038
6531
  const exposure = new Exposure(pi, registerNative);
6532
+ const launchBrowser = async (target) => {
6533
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
6534
+ const result = await pi.exec(command, [target], { timeout: 5e3 }).catch(() => void 0);
6535
+ return !!result && result.code === 0;
6536
+ };
6537
+ const elicitationUi = (ctx) => ({
6538
+ select: (title, choices2, signal) => chooseOption(ctx, title, choices2, signal),
6539
+ editor: (title, value) => ctx.ui.editor(title, value),
6540
+ notify: (message2, type) => ctx.ui.notify(message2, type),
6541
+ open: launchBrowser
6542
+ });
6039
6543
  const current = () => {
6040
6544
  if (!runtime)
6041
6545
  throw configError ?? failure("configuration_invalid", { operation: "configuration" });
@@ -6059,13 +6563,13 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6059
6563
  renderCall: (args, theme, context) => renderCall(`${tool.server} ${tool.name}`, args, theme, context.expanded),
6060
6564
  renderResult: (result, options2, theme, context) => renderResult(result, options2, theme, context.isError),
6061
6565
  async execute(_id, args, signal, onUpdate, ctx) {
6062
- const label = `${tool.server}.${tool.name}`;
6063
- const emit = (message) => onUpdate?.(
6064
- textResult(message, {
6566
+ const label2 = `${tool.server}.${tool.name}`;
6567
+ const emit = (message2) => onUpdate?.(
6568
+ textResult(message2, {
6065
6569
  mcpClient: 1,
6066
6570
  rows: [
6067
6571
  {
6068
- label: `${label} \xB7 ${line(message).slice(0, 160)}`,
6572
+ label: `${label2} \xB7 ${line(message2).slice(0, 160)}`,
6069
6573
  state: "running"
6070
6574
  }
6071
6575
  ]
@@ -6077,9 +6581,10 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6077
6581
  tool,
6078
6582
  args,
6079
6583
  signal ?? ctx.signal,
6080
- emit
6584
+ emit,
6585
+ ctx.hasUI ? elicitationUi(ctx) : void 0
6081
6586
  );
6082
- return await convertResult(result, label);
6587
+ return await convertResult(result, label2);
6083
6588
  } catch (error) {
6084
6589
  return errorResult(error, {
6085
6590
  server: tool.server,
@@ -6168,7 +6673,8 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6168
6673
  nextConfig,
6169
6674
  ctx.cwd,
6170
6675
  join4(agentDir, "cache", "pi-mcp-client"),
6171
- createSdkConnector(storeFactory)
6676
+ createSdkConnector(storeFactory),
6677
+ { interactive: ctx.hasUI }
6172
6678
  );
6173
6679
  const active = new Set(pi.getActiveTools());
6174
6680
  const retained = [...exposure.definitions.values()].filter((tool) => {
@@ -6202,7 +6708,7 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6202
6708
  const trusted = await resolveProjectTrust(ctx);
6203
6709
  if (generation !== sessionGeneration) return;
6204
6710
  config = await loadConfig(agentDir, ctx.cwd, trusted);
6205
- runtime = new McpRuntime(config, ctx.cwd, join4(agentDir, "cache", "pi-mcp-client"), createSdkConnector(storeFactory));
6711
+ runtime = new McpRuntime(config, ctx.cwd, join4(agentDir, "cache", "pi-mcp-client"), createSdkConnector(storeFactory), { interactive: ctx.hasUI });
6206
6712
  resourceNotifications(runtime, ctx);
6207
6713
  } catch (error) {
6208
6714
  configError = new DiagnosticError(diagnose(error, { operation: "configuration" }));
@@ -6408,7 +6914,7 @@ Variables: ${(candidate.variables ?? []).join(", ") || "none"}. Supply known val
6408
6914
  state: "failed"
6409
6915
  };
6410
6916
  }));
6411
- messages2.push(...discovery.unavailable.map((message) => `Not searched: ${message}`), ...discovery.warnings);
6917
+ messages2.push(...discovery.unavailable.map((message2) => `Not searched: ${message2}`), ...discovery.warnings);
6412
6918
  messages2.push("No tools activated or resource or prompt content read. Activate selected tools or read selected resources using their exact next-call arguments. Prompt commands are for the user to run, not the assistant.");
6413
6919
  } else {
6414
6920
  const resolved = resolveTools(discovery.tools, identifiers);
@@ -6421,8 +6927,8 @@ Variables: ${(candidate.variables ?? []).join(", ") || "none"}. Supply known val
6421
6927
  const ok = tool && loaded.includes(tool);
6422
6928
  const unavailable = discovery.diagnostics.find((value) => value.server && serversFor(identifier2).includes(value.server));
6423
6929
  const reason = unavailable ? `server unavailable: ${formatDiagnostic(unavailable)}` : tool ? collisions.has(tool.nativeName) ? "name collision" : "restricted by Pi" : `unknown identifier${suggestions.length ? `; nearest catalog names: ${suggestions.join(", ")}` : "; no catalog names available for this server. Check the server identifier or discover candidates with query."}`;
6424
- const label = `${line(identifier2)} \u2014 ${ok ? added.includes(tool.nativeName) ? "loaded" : "already loaded" : `not loaded \u2014 ${reason}`}`;
6425
- messages2.push(label);
6930
+ const label2 = `${line(identifier2)} \u2014 ${ok ? added.includes(tool.nativeName) ? "loaded" : "already loaded" : `not loaded \u2014 ${reason}`}`;
6931
+ messages2.push(label2);
6426
6932
  details.rows.push({
6427
6933
  label: activationLabel(identifier2, tool),
6428
6934
  ...ok ? {} : { inlineDescription: reason },
@@ -6531,8 +7037,8 @@ ${snapshot.body}`,
6531
7037
  if (action === "subscriptions") {
6532
7038
  if (server) throw new CommandUsageError("Use /mcp subscriptions without arguments.");
6533
7039
  const watches = current().resourceSubscriptions();
6534
- const message = watches.length ? watches.map((watch) => `${watch.changed ? "\u21BB" : "\u2714\uFE0E"} ${watch.server} \xB7 ${line(watch.uri).slice(0, 160)}${watch.changed ? " \xB7 changed" : ""}`).join("\n") : "No active resource subscriptions.";
6535
- if (ctx.hasUI) ctx.ui.notify(message, "info");
7040
+ const message2 = watches.length ? watches.map((watch) => `${watch.changed ? "\u21BB" : "\u2714\uFE0E"} ${watch.server} \xB7 ${line(watch.uri).slice(0, 160)}${watch.changed ? " \xB7 changed" : ""}`).join("\n") : "No active resource subscriptions.";
7041
+ if (ctx.hasUI) ctx.ui.notify(message2, "info");
6536
7042
  return;
6537
7043
  }
6538
7044
  if (action === "subscribe" || action === "unsubscribe") {
@@ -6551,8 +7057,8 @@ ${snapshot.body}`,
6551
7057
  server = mutation.server;
6552
7058
  await reloadConfiguration(ctx, mutation);
6553
7059
  const remaining = Object.hasOwn(config, server);
6554
- const message = mutation.action === "add" ? `\u2714\uFE0E ${server}: saved in ${mutation.scope} configuration. Connections, authentication, and tool discovery remain on demand.` + (mutation.scope === "global" && projectTrusted(ctx) ? " Trusted project definitions take precedence over global definitions." : "") : `\u2714\uFE0E ${server}: removed from ${mutation.scope} configuration. Credentials were retained.` + (remaining ? " A definition from the other scope remains effective; connections reopen on demand." : " Its connection is closed and its tools are no longer active.");
6555
- if (ctx.hasUI) ctx.ui.notify(message, "info");
7060
+ const message2 = mutation.action === "add" ? `\u2714\uFE0E ${server}: saved in ${mutation.scope} configuration. Connections, authentication, and tool discovery remain on demand.` + (mutation.scope === "global" && projectTrusted(ctx) ? " Trusted project definitions take precedence over global definitions." : "") : `\u2714\uFE0E ${server}: removed from ${mutation.scope} configuration. Credentials were retained.` + (remaining ? " A definition from the other scope remains effective; connections reopen on demand." : " Its connection is closed and its tools are no longer active.");
7061
+ if (ctx.hasUI) ctx.ui.notify(message2, "info");
6556
7062
  return;
6557
7063
  }
6558
7064
  if (action === "reload" && !server) {
@@ -6646,14 +7152,14 @@ ${snapshot.body}`,
6646
7152
  );
6647
7153
  return;
6648
7154
  }
6649
- const choices = [...tools].sort((a, b) => a.name.localeCompare(b.name));
7155
+ const choices2 = [...tools].sort((a, b) => a.name.localeCompare(b.name));
6650
7156
  const columns = ctx.mode === "tui" ? process.stdout.columns || 80 : 80;
6651
- const labels = choices.map((tool2, index) => toolPickerLabel(tool2, index, columns));
7157
+ const labels = choices2.map((tool2, index) => toolPickerLabel(tool2, index, columns));
6652
7158
  const selected = await ctx.ui.select(
6653
7159
  `${server}: ${tools.length} tools (select to inspect; none are activated)`,
6654
7160
  labels
6655
7161
  );
6656
- const tool = selected === void 0 ? void 0 : choices[labels.indexOf(selected)];
7162
+ const tool = selected === void 0 ? void 0 : choices2[labels.indexOf(selected)];
6657
7163
  if (tool)
6658
7164
  ctx.ui.notify(
6659
7165
  inspectTool(tool),
@@ -6674,9 +7180,7 @@ ${snapshot.body}`,
6674
7180
  const options2 = config[server];
6675
7181
  const loginRuntime = current();
6676
7182
  const open2 = async (target) => {
6677
- const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
6678
- const result = await pi.exec(command, [target], { timeout: 5e3 }).catch(() => void 0);
6679
- if (!result || result.code !== 0)
7183
+ if (!await launchBrowser(target))
6680
7184
  throw failure("oauth_failed", { server, operation: "auth" });
6681
7185
  };
6682
7186
  if (loginController) throw failure("busy", { server, operation: "auth" });
@@ -6752,7 +7256,7 @@ Opening browser. Esc to cancel.`
6752
7256
  "info"
6753
7257
  );
6754
7258
  } catch (error) {
6755
- const message = error instanceof CommandUsageError || error instanceof ConfigMutationError || error instanceof PromptCommandError ? error.message : formatDiagnostic(
7259
+ const message2 = error instanceof CommandUsageError || error instanceof ConfigMutationError || error instanceof PromptCommandError ? error.message : formatDiagnostic(
6756
7260
  diagnose(error, {
6757
7261
  server,
6758
7262
  operation: ["reload", "get", "enable", "disable", "add", "remove", "import"].includes(action) ? "configuration" : action === "tools" ? "search" : action === "prompt" ? "prompt" : ["login", "logout"].includes(action) ? "auth" : ["subscribe", "unsubscribe"].includes(action) ? "subscribe" : action === "refresh" ? "refresh" : "reconnect",
@@ -6760,8 +7264,8 @@ Opening browser. Esc to cancel.`
6760
7264
  signal: ctx.signal
6761
7265
  })
6762
7266
  );
6763
- if (ctx.hasUI) ctx.ui.notify(message, "error");
6764
- else throw new Error(message);
7267
+ if (ctx.hasUI) ctx.ui.notify(message2, "error");
7268
+ else throw new Error(message2);
6765
7269
  }
6766
7270
  }
6767
7271
  });
package/docs/behavior.md CHANGED
@@ -51,6 +51,41 @@ configuration reload invalidate pending selections. The agent must be idle when
51
51
  you choose **Use prompt**; another active turn is never silently interrupted.
52
52
  Accepted content becomes session data and may contain sensitive information.
53
53
 
54
+ ## Server requests for input
55
+
56
+ During a tool call, a server can ask you for information or send you to a web
57
+ page. Pi shows each request in a dialog that names the requesting server. The
58
+ model never sees or answers these dialogs, and the tool call waits until you
59
+ respond. Time spent answering doesn't count against the
60
+ [tool timeout](configuration.md#pi-specific-options).
61
+
62
+ Form requests list the requested fields with their current values, including
63
+ server defaults. Select a field to change it; Pi validates each value against
64
+ the field's type and constraints. Only **Submit** sends values to the server.
65
+ **Decline** refuses the request, and **Cancel** or Esc dismisses it; the server
66
+ receives no values either way. Don't enter passwords, API keys, or payment
67
+ details in a form. Servers must use a web page for sensitive information.
68
+
69
+ Web page requests show the site and the full URL before anything opens.
70
+ **Open in browser** launches your default browser. **I'll open it myself**
71
+ confirms without launching one, which is useful over SSH; copy the URL first.
72
+ Pi opens only HTTPS pages, or HTTP pages on this computer, and refuses URLs with
73
+ embedded credentials. Site names with internationalized characters appear in
74
+ their encoded `xn--` form with a warning. Information you enter on the page goes
75
+ to that site, not to Pi.
76
+
77
+ Some servers refuse to run a tool until you complete a step on a web page. After
78
+ you open the page, Pi waits until the server confirms completion or you choose
79
+ **Done**. The tool doesn't run again automatically: the model receives the
80
+ outcome and can call the tool again after you complete the step.
81
+
82
+ Pi accepts server requests only in interactive sessions and only while a tool
83
+ call to that server is running. Requests at other times, including during
84
+ resource reads and prompt previews, are refused without a dialog. Headless
85
+ sessions don't offer this capability to servers, so servers that support it
86
+ fall back to their non-interactive behavior. Dialogs appear one at a time.
87
+ Stopping the tool call or ending the session closes any open request.
88
+
54
89
  ## Discovery and caching
55
90
 
56
91
  Connections start on demand, never while the extension factory loads. A search
@@ -142,7 +142,7 @@ server definition:
142
142
  | `excludeTools` | Denylist applied after `includeTools`. |
143
143
  | `timeoutMs` | Request timeout, from 100 to 600000 ms. Defaults: 15 seconds for discovery/HTTP requests, 30 seconds for stdio tool calls. |
144
144
  | `startupTimeoutMs` | Optional timeout for SDK connection setup and protocol negotiation, from 100 to 600000 ms. Defaults to `timeoutMs` or 15 seconds. |
145
- | `toolTimeoutMs` | Optional timeout for tool calls only, from 100 to 600000 ms. Overrides `timeoutMs` for calls without changing metadata or resource deadlines. |
145
+ | `toolTimeoutMs` | Optional timeout for tool calls only, from 100 to 600000 ms. Overrides `timeoutMs` for calls without changing metadata or resource deadlines. Time spent answering [server requests](behavior.md#server-requests-for-input) doesn't count. |
146
146
  | `protocol` | `auto` (default) for SDK protocol-version negotiation, or `legacy` for an explicit legacy handshake. |
147
147
 
148
148
  OAuth client IDs, scopes, and callback ports require HTTP without an Authorization
@@ -71,6 +71,13 @@ is needed, first use follows three steps: discover, activate, then call the nati
71
71
  tool. There is no invocation proxy. Previously loaded tools remain available;
72
72
  [session behavior](behavior.md#sessions) describes restoration and tool restrictions.
73
73
 
74
+ A call can pause while the user answers a
75
+ [server request for input](behavior.md#server-requests-for-input); the model
76
+ never sees the dialog or its values unless the server includes them in its
77
+ result. When a server requires a browser step first, the call fails with an
78
+ `elicitation_*` diagnostic and isn't replayed. After `elicitation_completed`,
79
+ the model can call the tool again.
80
+
74
81
  ## Read resources as context
75
82
 
76
83
  For a request such as “Use the authentication guide to explain this API,” the
@@ -37,6 +37,9 @@ unavailable server isn't an empty catalog.
37
37
  | `subscriptions_unsupported` | Choose a server with subscription support, or ask the model to read when needed. |
38
38
  | `subscription_limit` | Remove a watch before adding another; the limit is 50 per connection. |
39
39
  | `catalog_changed` | Ask the model to retry discovery after the server catalog settles. |
40
+ | `elicitation_required` | The server needs you to complete a step on a web page before the tool runs. Use an interactive session, then ask the model to retry. |
41
+ | `elicitation_declined` | You declined or dismissed the server's web page request, so the tool didn't run. Ask the model to retry when you're ready. |
42
+ | `elicitation_completed` | You completed the server's web page step. The tool didn't run yet; the model can call it again. |
40
43
  | `oauth_failed` | An unclassified OAuth failure. Check the service's requirements and `/mcp get <server>` for the client type, scopes, and callback URL. Only public/PKCE clients are supported, not clients requiring a secret. |
41
44
  | `oauth_client_required` | The server doesn't support dynamic registration. Configure `oauthClientId` for a registered public/PKCE client and register the exact callback URL. Reload, then log in again. |
42
45
  | `oauth_registration_rejected` | The server rejected dynamic registration. Check public/native client eligibility and the callback URL, or configure an approved public client ID. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-client",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "MCP tools for Pi, discovered on demand and called natively through the official SDK.",
5
5
  "type": "module",
6
6
  "license": "MIT",