pi-mcp-client 0.8.0 → 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/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/index.ts
2
+ import { existsSync } from "node:fs";
2
3
  import { join as join4 } from "node:path";
3
4
  import {
4
5
  BorderedLoader as BorderedLoader2,
@@ -1769,9 +1770,9 @@ var defaultOptions = {
1769
1770
  fields: void 0,
1770
1771
  searchOptions: void 0,
1771
1772
  storeFields: [],
1772
- logger: (level, message) => {
1773
+ logger: (level, message2) => {
1773
1774
  if (typeof (console === null || console === void 0 ? void 0 : console[level]) === "function")
1774
- console[level](message);
1775
+ console[level](message2);
1775
1776
  },
1776
1777
  autoVacuum: true
1777
1778
  };
@@ -1877,6 +1878,9 @@ var messages = {
1877
1878
  subscriptions_unsupported: "The server did not accept resource subscriptions.",
1878
1879
  subscription_limit: "The connection has reached its limit of 50 resource subscriptions.",
1879
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.",
1880
1884
  oauth_failed: "OAuth authentication did not complete.",
1881
1885
  oauth_client_required: "OAuth requires a registered client: this server does not support dynamic client registration.",
1882
1886
  oauth_registration_rejected: "The authorization server rejected OAuth client registration.",
@@ -1920,6 +1924,9 @@ function diagnostic(code, context) {
1920
1924
  subscriptions_unsupported: "Choose a server with resource subscription support, or read the resource explicitly when needed.",
1921
1925
  subscription_limit: "Unsubscribe from another resource before adding a new watch.",
1922
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.",
1923
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.`,
1924
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.`,
1925
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}.`,
@@ -2032,6 +2039,8 @@ function diagnose(error, context) {
2032
2039
  const code2 = sdkOAuthFallback(current);
2033
2040
  if (code2) return diagnostic(code2, context);
2034
2041
  }
2042
+ if (current instanceof ProtocolError && current.code === ProtocolErrorCode.UrlElicitationRequired)
2043
+ return diagnostic("elicitation_required", context);
2035
2044
  if (current instanceof ProtocolError) return diagnostic(
2036
2045
  context.operation === "read" && (current instanceof ResourceNotFoundError || current.code === ProtocolErrorCode.ResourceNotFound) ? "resource_not_found" : "protocol_error",
2037
2046
  context
@@ -2329,7 +2338,7 @@ async function inspectConfigScopes(agentDir, cwd, trusted) {
2329
2338
  async function updateServerConfig(agentDir, cwd, trusted, mutation, validate) {
2330
2339
  const scoped = mutation.action !== "toggle";
2331
2340
  if (scoped && mutation.scope === "project" && !trusted)
2332
- throw new ConfigMutationError("Project configuration requires a trusted project. Use global scope or trust the project first.");
2341
+ throw new ConfigMutationError("Project configuration requires a trusted project. Use global scope, or run /trust to trust this folder first.");
2333
2342
  const paths = [join(agentDir, "mcp.json"), ...trusted ? [join(cwd, ".mcp.json")] : []];
2334
2343
  const targets = await Promise.all(paths.map(configTarget));
2335
2344
  if (scoped && targets.length === 2 && targets[0] === targets[1])
@@ -2683,9 +2692,9 @@ function configCommandCompletions(input, servers) {
2683
2692
  const match = /^(add|remove|import)\s+(.*)$/su.exec(input.trimStart());
2684
2693
  if (!match) return void 0;
2685
2694
  const [, action, rest] = match;
2686
- const choices = ["--scope global", "--scope project"];
2695
+ const choices2 = ["--scope global", "--scope project"];
2687
2696
  if (!/^--scope\s+(global|project)\s/u.test(rest)) {
2688
- return choices.filter((choice) => choice.startsWith(rest)).map((choice) => ({
2697
+ return choices2.filter((choice) => choice.startsWith(rest)).map((choice) => ({
2689
2698
  value: `${action} ${choice}`,
2690
2699
  label: choice
2691
2700
  }));
@@ -2705,13 +2714,13 @@ function identifier(value) {
2705
2714
  return typeof value === "string" && !!value && value.length <= 512 && line(value) === value;
2706
2715
  }
2707
2716
  function preparePrompt(server, identity, value) {
2708
- const invalid2 = () => failure("protocol_error", { server, operation: "search" });
2709
- 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();
2710
2719
  const args = value.arguments ?? [];
2711
- if (args.length > 100) throw invalid2();
2720
+ if (args.length > 100) throw invalid3();
2712
2721
  const seen = /* @__PURE__ */ new Set();
2713
2722
  const arguments_ = args.map((argument) => {
2714
- 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();
2715
2724
  seen.add(argument.name);
2716
2725
  return { name: argument.name, description: plain(argument.description ?? "").slice(0, 8e3), required: argument.required === true };
2717
2726
  });
@@ -2733,14 +2742,14 @@ function promptCommand(prompt) {
2733
2742
  }
2734
2743
  var promptText = (value) => plain(value).replace(/\r\n?/g, "\n");
2735
2744
  function preparePromptSnapshot(result, server) {
2736
- const invalid2 = () => failure("protocol_error", { server, operation: "prompt" });
2737
- 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();
2738
2747
  if (Buffer.byteLength(JSON.stringify(result)) > 50 * 1024) throw failure("prompt_too_large", { server, operation: "prompt" });
2739
2748
  let usable = true;
2740
2749
  const previews = [];
2741
- const messages2 = result.messages.map((message, index) => {
2742
- if (!["user", "assistant"].includes(message.role) || !object(message.content)) throw invalid2();
2743
- 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;
2744
2753
  let block;
2745
2754
  let preview2;
2746
2755
  if (content.type === "text" && typeof content.text === "string") {
@@ -2760,9 +2769,9 @@ ${promptText(content.resource.text)}`;
2760
2769
  block = { type: line(String(content.type)), unsupported: true, bytes: Buffer.byteLength(JSON.stringify(content)) };
2761
2770
  preview2 = `Unsupported ${line(String(content.type))} content \xB7 ${Buffer.byteLength(JSON.stringify(content))} bytes`;
2762
2771
  }
2763
- previews.push(`Message ${index + 1} \xB7 ${message.role} (server supplied)
2772
+ previews.push(`Message ${index + 1} \xB7 ${message2.role} (server supplied)
2764
2773
  ${preview2}`);
2765
- return { role: message.role, content: block };
2774
+ return { role: message2.role, content: block };
2766
2775
  });
2767
2776
  const description = typeof result.description === "string" ? promptText(result.description) : void 0;
2768
2777
  const body = JSON.stringify({ server, ...description === void 0 ? {} : { description }, messages: messages2 }, null, 2);
@@ -2774,9 +2783,26 @@ ${preview2}`);
2774
2783
 
2775
2784
  // src/prompt-selector.ts
2776
2785
  import { SelectList, Text } from "@earendil-works/pi-tui";
2777
- 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) {
2778
2804
  const [heading, ...body] = title.split("\n");
2779
- const list = new SelectList(choices.map((value) => ({ value, label: value })), 8, {
2805
+ const list = new SelectList(choices2.map((value) => ({ value, label: value })), 8, {
2780
2806
  selectedPrefix: (text) => theme.fg("accent", text),
2781
2807
  selectedText: (text) => theme.fg("accent", text),
2782
2808
  description: (text) => theme.fg("muted", text),
@@ -2798,11 +2824,11 @@ function promptSelector(title, choices, theme, keys, done, render) {
2798
2824
  ];
2799
2825
  },
2800
2826
  handleInput(data) {
2801
- const index = choices.indexOf(list.getSelectedItem()?.value ?? "");
2827
+ const index = choices2.indexOf(list.getSelectedItem()?.value ?? "");
2802
2828
  if (keys.matches(data, "tui.select.cancel")) done(void 0);
2803
2829
  else if (keys.matches(data, "tui.select.confirm")) done(list.getSelectedItem()?.value);
2804
2830
  else if (keys.matches(data, "tui.select.up") || data === "k") list.setSelectedIndex(Math.max(0, index - 1));
2805
- 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));
2806
2832
  render();
2807
2833
  },
2808
2834
  invalidate() {
@@ -2834,21 +2860,9 @@ async function runPromptCommand(input, ctx, runtime, signal, assertCurrent, use)
2834
2860
  signal.throwIfAborted();
2835
2861
  assertCurrent();
2836
2862
  };
2837
- const select = async (title, choices) => {
2863
+ const select = async (title, choices2) => {
2838
2864
  guard();
2839
- const selected = ctx.mode !== "tui" ? await ctx.ui.select(title, choices, { signal }) : await ctx.ui.custom((tui, theme, keys, done) => {
2840
- let settled = false;
2841
- const finish = (choice) => {
2842
- if (settled) return;
2843
- settled = true;
2844
- signal.removeEventListener("abort", abort);
2845
- done(choice);
2846
- };
2847
- const abort = () => finish(void 0);
2848
- signal.addEventListener("abort", abort, { once: true });
2849
- const selector = promptSelector(title, choices, theme, keys, finish, () => tui.requestRender());
2850
- return { ...selector, dispose: () => signal.removeEventListener("abort", abort) };
2851
- });
2865
+ const selected = await chooseOption(ctx, title, choices2, signal);
2852
2866
  guard();
2853
2867
  return selected;
2854
2868
  };
@@ -3178,10 +3192,10 @@ var TomlError = class extends Error {
3178
3192
  line;
3179
3193
  column;
3180
3194
  codeblock;
3181
- constructor(message, options) {
3195
+ constructor(message2, options) {
3182
3196
  const [line2, column] = getLineColFromPtr(options.toml, options.ptr);
3183
3197
  const codeblock = makeCodeBlock(options.toml, line2, column);
3184
- super(`Invalid TOML document: ${message}
3198
+ super(`Invalid TOML document: ${message2}
3185
3199
 
3186
3200
  ${codeblock}`, options);
3187
3201
  this.line = line2;
@@ -3947,12 +3961,12 @@ function validateImportCandidate(candidate, cwd) {
3947
3961
  }
3948
3962
  return void 0;
3949
3963
  }
3950
- function importPreview(candidate, problem) {
3964
+ function importPreview(candidate, problem2) {
3951
3965
  const definition = candidate.definition;
3952
3966
  return [
3953
3967
  `${candidate.label} \xB7 ${candidate.transport}`,
3954
3968
  ...candidate.group ? [`Source: ${candidate.group}`] : [],
3955
- ...problem ? [problem] : [],
3969
+ ...problem2 ? [problem2] : [],
3956
3970
  ...definition ? [
3957
3971
  "Connection values are hidden; review and trust the source file before importing.",
3958
3972
  `Arguments: ${definition.args?.length ?? 0}; environment: ${Object.keys(definition.env ?? {}).length}; headers: ${Object.keys(definition.headers ?? {}).length} (values hidden)`,
@@ -3973,23 +3987,23 @@ function importAction(name, scope, scopes) {
3973
3987
  if (scope === "project" && Object.hasOwn(scopes.global, name)) return "Override global definition";
3974
3988
  return `Add ${scope} definition`;
3975
3989
  }
3976
- async function runImportCommand(input, ctx, agentDir, signal, assertCurrent, save) {
3990
+ async function runImportCommand(input, ctx, agentDir, projectTrusted, signal, assertCurrent, save) {
3977
3991
  if (!ctx.hasUI) throw new ConfigMutationError("Configuration imports require an interactive session (TUI or RPC).");
3978
3992
  const { scope, path } = parseImportCommand(input);
3979
- const trusted = ctx.isProjectTrusted();
3993
+ const trusted = projectTrusted();
3980
3994
  if (scope === "project" && !trusted)
3981
- throw new ConfigMutationError("Project configuration requires a trusted project. Use global scope or trust the project first.");
3995
+ throw new ConfigMutationError("Project configuration requires a trusted project. Use global scope, or run /trust to trust this folder first.");
3982
3996
  const guard = () => {
3983
3997
  signal.throwIfAborted();
3984
3998
  assertCurrent();
3985
- if (ctx.isProjectTrusted() !== trusted)
3999
+ if (projectTrusted() !== trusted)
3986
4000
  throw new ConfigMutationError("Project trust changed during the preview. Run /mcp import again; nothing was saved.");
3987
4001
  };
3988
- const select = async (title, choices) => {
4002
+ const select = async (title, choices2) => {
3989
4003
  guard();
3990
- const result = await ctx.ui.select(title, choices, { signal });
4004
+ const result = await ctx.ui.select(title, choices2, { signal });
3991
4005
  guard();
3992
- return result !== void 0 && choices.includes(result) ? result : void 0;
4006
+ return result !== void 0 && choices2.includes(result) ? result : void 0;
3993
4007
  };
3994
4008
  guard();
3995
4009
  const source = await readImportSource(path, ctx.cwd, signal);
@@ -4029,22 +4043,22 @@ Only MCP server definitions are imported. Other settings, permissions, and crede
4029
4043
  if (group !== "All groups") candidates = candidates.filter((candidate) => candidate.group === group);
4030
4044
  }
4031
4045
  for (const [index, candidate] of candidates.entries()) {
4032
- const problem = validateImportCandidate(candidate, ctx.cwd);
4046
+ const problem2 = validateImportCandidate(candidate, ctx.cwd);
4033
4047
  let name = candidate.name;
4034
4048
  while (true) {
4035
4049
  const duplicate = name !== void 0 && Object.hasOwn(selected, name);
4036
- const action = name && !duplicate && !problem ? importAction(name, scope, scopes) : void 0;
4050
+ const action = name && !duplicate && !problem2 ? importAction(name, scope, scopes) : void 0;
4037
4051
  const conflict = name ? [
4038
4052
  Object.hasOwn(scopes.global, name) ? "global" : void 0,
4039
4053
  Object.hasOwn(scopes.project, name) ? "project" : void 0
4040
4054
  ].filter(Boolean).join(" and ") : "";
4041
4055
  const choice = await select([
4042
4056
  `Import preview ${index + 1}/${candidates.length} \xB7 ${scope} scope \xB7 ${source.format === "codex" ? "Codex TOML" : "JSON"}`,
4043
- importPreview(candidate, problem),
4057
+ importPreview(candidate, problem2),
4044
4058
  ...name ? [`Destination name: ${name}`] : [],
4045
4059
  ...conflict ? [`Existing definition in ${conflict} scope; fields and credentials are never merged.`] : [],
4046
4060
  ...duplicate ? ["This destination name is already selected; choose a different name."] : []
4047
- ].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"]);
4048
4062
  if (choice === void 0 || choice === "Cancel import") {
4049
4063
  cancel();
4050
4064
  return;
@@ -4114,7 +4128,7 @@ import {
4114
4128
  // src/oauth-page.ts
4115
4129
  function oauthCallbackHtml(status) {
4116
4130
  const heading = status === "received" ? "Authorization response received" : status === "denied" ? "Authorization not granted" : "Invalid OAuth callback";
4117
- 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.";
4118
4132
  return `<!doctype html>
4119
4133
  <html lang="en">
4120
4134
  <head>
@@ -4177,7 +4191,7 @@ function oauthCallbackHtml(status) {
4177
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>
4178
4192
  <p class="brand">Pi MCP Client</p>
4179
4193
  <h1>${heading}</h1>
4180
- <p>${message}</p>
4194
+ <p>${message2}</p>
4181
4195
  </main>
4182
4196
  </body>
4183
4197
  </html>`;
@@ -4188,17 +4202,17 @@ function callbackUrl(options = {}) {
4188
4202
  return `http://127.0.0.1:${options.oauthCallbackPort ?? 19847}/callback`;
4189
4203
  }
4190
4204
  function parseCallback(value, redirect, state) {
4191
- const invalid2 = () => failure("oauth_failed", { operation: "auth" });
4192
- 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();
4193
4207
  let target;
4194
4208
  try {
4195
4209
  target = new URL(value);
4196
4210
  } catch {
4197
- throw invalid2();
4211
+ throw invalid3();
4198
4212
  }
4199
4213
  const expected = new URL(redirect);
4200
4214
  const params = target.searchParams;
4201
- 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();
4202
4216
  return params;
4203
4217
  }
4204
4218
  function credentialKey({ server, url, clientId }) {
@@ -4705,9 +4719,82 @@ import { join as join2 } from "node:path";
4705
4719
  import { randomUUID as randomUUID3 } from "node:crypto";
4706
4720
  import {
4707
4721
  Client,
4722
+ ProtocolError as ProtocolError3,
4723
+ ProtocolErrorCode as ProtocolErrorCode3,
4724
+ SdkError as SdkError2,
4725
+ SdkErrorCode,
4708
4726
  StreamableHTTPClientTransport,
4709
4727
  UriTemplate as UriTemplate2
4710
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
4711
4798
  import {
4712
4799
  StdioClientTransport,
4713
4800
  getDefaultEnvironment
@@ -4804,9 +4891,335 @@ var ResourceSubscriptions = class {
4804
4891
  }
4805
4892
  };
4806
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
+
4807
5162
  // src/runtime.ts
4808
5163
  var ToolContractError = class extends Error {
4809
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
+ }
4810
5223
  function waitFor2(promise, signal) {
4811
5224
  if (!signal) return promise;
4812
5225
  return new Promise((resolve3, reject) => {
@@ -4825,36 +5238,11 @@ function waitFor2(promise, signal) {
4825
5238
  if (signal.aborted) abort();
4826
5239
  });
4827
5240
  }
4828
- var createSdkConnector = (storeFactory = credentialStore) => async (name, config, signal, onToolsChanged, onResourcesChanged, onPromptsChanged) => {
5241
+ var createSdkConnector = (storeFactory = credentialStore) => async (name, config, signal, handlers) => {
4829
5242
  const timeout = config.timeoutMs ?? 15e3;
4830
5243
  const startupTimeout = config.startupTimeoutMs ?? timeout;
4831
5244
  let connecting = true;
4832
- const client = new Client(
4833
- { name: "pi-mcp-client", version: "0.1.0" },
4834
- {
4835
- listChanged: {
4836
- tools: {
4837
- autoRefresh: false,
4838
- debounceMs: 0,
4839
- onChanged: () => onToolsChanged?.()
4840
- },
4841
- prompts: {
4842
- autoRefresh: false,
4843
- debounceMs: 0,
4844
- onChanged: () => onPromptsChanged?.()
4845
- },
4846
- resources: {
4847
- autoRefresh: false,
4848
- debounceMs: 0,
4849
- onChanged: () => onResourcesChanged?.()
4850
- }
4851
- },
4852
- versionNegotiation: {
4853
- mode: config.protocol ?? "auto",
4854
- probe: { timeoutMs: startupTimeout }
4855
- }
4856
- }
4857
- );
5245
+ const client = createClient(config, handlers);
4858
5246
  const transport = config.command ? new StdioClientTransport({
4859
5247
  command: config.command,
4860
5248
  args: config.args,
@@ -4912,11 +5300,12 @@ var createSdkConnector = (storeFactory = credentialStore) => async (name, config
4912
5300
  };
4913
5301
  var connectSdk = createSdkConnector();
4914
5302
  var McpRuntime = class {
4915
- constructor(config, cwd, cacheDir, connect = connectSdk) {
5303
+ constructor(config, cwd, cacheDir, connect = connectSdk, options = {}) {
4916
5304
  this.config = config;
4917
5305
  this.cwd = cwd;
4918
5306
  this.cacheDir = cacheDir;
4919
5307
  this.connect = connect;
5308
+ this.options = options;
4920
5309
  }
4921
5310
  states = /* @__PURE__ */ new Map();
4922
5311
  lifetime = new AbortController();
@@ -4984,33 +5373,42 @@ var McpRuntime = class {
4984
5373
  this.lifetime.signal
4985
5374
  ),
4986
5375
  this.lifetime.signal,
4987
- () => {
4988
- if (this.closing || this.lifetime.signal.aborted || state.connectionToken !== token)
4989
- return;
4990
- state.catalogGeneration++;
4991
- state.catalogDirty = true;
4992
- state.tools = void 0;
4993
- state.warnings = void 0;
4994
- const path = join2(this.cacheDir, `${identity}.json`);
4995
- state.invalidating = withFileMutationQueue2(
4996
- path,
4997
- () => rm2(path, { force: true })
4998
- ).catch(() => {
4999
- state.warnings = [
5000
- `${name}: stale catalog cache could not be removed.`
5001
- ];
5002
- });
5003
- },
5004
- () => {
5005
- if (this.closing || this.lifetime.signal.aborted || state.connectionToken !== token) return;
5006
- state.resourceGeneration++;
5007
- state.resources = void 0;
5008
- state.resourceWarnings = void 0;
5009
- },
5010
- () => {
5011
- if (this.closing || this.lifetime.signal.aborted || state.connectionToken !== token) return;
5012
- state.promptGeneration++;
5013
- 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
+ } : {}
5014
5412
  }
5015
5413
  );
5016
5414
  if (this.closing || this.lifetime.signal.aborted) {
@@ -5410,7 +5808,7 @@ var McpRuntime = class {
5410
5808
  );
5411
5809
  return result;
5412
5810
  }
5413
- async call(tool, args, signal, progress) {
5811
+ async call(tool, args, signal, progress, ui) {
5414
5812
  const config = this.definition(tool.server);
5415
5813
  if (!allowed(tool.name, config) || this.identity(tool.server) !== tool.identity)
5416
5814
  throw new ToolContractError(
@@ -5423,29 +5821,125 @@ var McpRuntime = class {
5423
5821
  "MCP tool was removed or its schema changed. Use mcp_tools with activate and its exact identifier to activate its current definition."
5424
5822
  );
5425
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
+ }
5426
5834
  try {
5427
5835
  return await client.callTool(
5428
5836
  { name: tool.name, arguments: args },
5429
5837
  {
5430
5838
  signal: AbortSignal.any([
5431
5839
  this.lifetime.signal,
5840
+ deadline.signal,
5432
5841
  ...signal ? [signal] : []
5433
5842
  ]),
5434
- timeout: config.toolTimeoutMs ?? config.timeoutMs ?? 3e4,
5843
+ timeout: UNBOUNDED_TIMEOUT,
5435
5844
  onprogress: (event) => progress?.(
5436
5845
  event.message ?? `${event.progress}${event.total === void 0 ? "" : `/${event.total}`}`
5437
5846
  )
5438
5847
  }
5439
5848
  );
5440
5849
  } catch (error) {
5441
- const value = diagnose(error, {
5850
+ const context = {
5442
5851
  server: tool.server,
5443
5852
  operation: "call",
5444
5853
  oauth: usesOAuth(config),
5445
5854
  signal
5446
- });
5447
- 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;
5448
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
+ }
5449
5943
  }
5450
5944
  }
5451
5945
  /** Close related OAuth connections without reconnecting, including disabled servers. */
@@ -5682,7 +6176,7 @@ Content type: ${line(resource.mimeType ?? "unspecified")}` });
5682
6176
  converted.details.resource = target;
5683
6177
  return converted;
5684
6178
  }
5685
- async function convertResult(result, label) {
6179
+ async function convertResult(result, label2) {
5686
6180
  const texts = [];
5687
6181
  const blocks = [];
5688
6182
  let offset = 0;
@@ -5704,7 +6198,7 @@ async function convertResult(result, label) {
5704
6198
  append(part.resource.text, { mimeType: part.resource.mimeType });
5705
6199
  else if (part.type === "resource_link")
5706
6200
  append(`${part.name}: ${part.uri}${validResourceUri(part.uri) ? `
5707
- 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 } })})` : ""}`, {
5708
6202
  mimeType: part.mimeType,
5709
6203
  resourceLink: true
5710
6204
  });
@@ -5730,12 +6224,12 @@ Read with mcp_tools(${JSON.stringify({ read: { server: label.split(".")[0], uri:
5730
6224
  ...result.isError ? {
5731
6225
  diagnostics: [
5732
6226
  diagnostic("tool_error", {
5733
- server: label.split(".")[0],
6227
+ server: label2.split(".")[0],
5734
6228
  operation: "call"
5735
6229
  })
5736
6230
  ]
5737
6231
  } : {},
5738
- rows: [{ label, state: result.isError ? "failed" : "done" }]
6232
+ rows: [{ label: label2, state: result.isError ? "failed" : "done" }]
5739
6233
  };
5740
6234
  if (truncated.truncated || needsSpill) {
5741
6235
  const path = join3(await mkdtemp(join3(tmpdir(), "pi-mcp-client-")), "result.json");
@@ -5958,19 +6452,54 @@ function statusPanel(snapshot, theme) {
5958
6452
  };
5959
6453
  }
5960
6454
 
6455
+ // src/trust.ts
6456
+ import {
6457
+ hasTrustRequiringProjectResources,
6458
+ ProjectTrustStore,
6459
+ SettingsManager
6460
+ } from "@earendil-works/pi-coding-agent";
6461
+ var CHOICES = /* @__PURE__ */ new Map([
6462
+ ["Trust", { trusted: true, save: true }],
6463
+ ["Trust (this session only)", { trusted: true, save: false }],
6464
+ ["Do not trust", { trusted: false, save: true }],
6465
+ ["Do not trust (this session only)", { trusted: false, save: false }]
6466
+ ]);
6467
+ function projectTrustDecision(agentDir, cwd, piTrusted) {
6468
+ if (!piTrusted) return false;
6469
+ if (hasTrustRequiringProjectResources(cwd)) return true;
6470
+ const saved = new ProjectTrustStore(agentDir).get(cwd);
6471
+ if (saved !== null) return saved;
6472
+ const policy = SettingsManager.create(cwd, agentDir, { projectTrusted: false }).getDefaultProjectTrust();
6473
+ return policy === "ask" ? void 0 : policy === "always";
6474
+ }
6475
+ async function askProjectTrust(agentDir, cwd, select, signal) {
6476
+ const answer = await select(
6477
+ `Trust project folder?
6478
+ ${cwd}
6479
+
6480
+ Its .mcp.json defines MCP servers, which can run local commands with your permissions. Saved decisions use Pi's project trust and also apply to Pi project resources.`,
6481
+ [...CHOICES.keys()],
6482
+ { signal }
6483
+ );
6484
+ const choice = answer === void 0 ? void 0 : CHOICES.get(answer);
6485
+ if (!choice || signal.aborted) return void 0;
6486
+ if (choice.save) new ProjectTrustStore(agentDir).set(cwd, choice.trusted);
6487
+ return choice.trusted;
6488
+ }
6489
+
5961
6490
  // src/index.ts
5962
6491
  var CommandUsageError = class extends Error {
5963
6492
  };
5964
6493
  function errorResult(error, context) {
5965
6494
  const value = error instanceof ToolContractError ? diagnostic("tool_changed", context) : diagnose(error, context);
5966
- const message = formatDiagnostic(value) + (context.operation === "call" ? " The call was not replayed. The server may already have performed the operation; verify before retrying." : "");
5967
- 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, {
5968
6497
  mcpClient: 1,
5969
6498
  failed: true,
5970
6499
  diagnostics: [value],
5971
6500
  rows: [
5972
6501
  {
5973
- label: value.code === "cancelled" ? "Cancelled" : message,
6502
+ label: value.code === "cancelled" ? "Cancelled" : message2,
5974
6503
  state: value.code === "cancelled" ? "cancelled" : "failed"
5975
6504
  }
5976
6505
  ]
@@ -5986,18 +6515,31 @@ function mcpClient(pi, options = {}) {
5986
6515
  let loginController;
5987
6516
  let promptController;
5988
6517
  let importController;
6518
+ let trustController;
6519
+ let sessionTrust;
5989
6520
  pi.registerEntryRenderer(
5990
6521
  STATUS_ENTRY,
5991
6522
  (entry, _options, theme) => statusPanel(entry.data ?? { servers: [], loaded: [] }, theme)
5992
6523
  );
5993
- pi.registerMessageRenderer("mcp-prompt", (message, { expanded, outputPad }, theme) => {
5994
- const details = object(message.details) ? message.details : {};
6524
+ pi.registerMessageRenderer("mcp-prompt", (message2, { expanded, outputPad }, theme) => {
6525
+ const details = object(message2.details) ? message2.details : {};
5995
6526
  const count = Number(details.count ?? 0);
5996
- const label = `${line(String(details.server ?? "MCP"))} \xB7 ${line(String(details.name ?? "prompt"))} \xB7 ${count} ${count === 1 ? "message" : "messages"}`;
5997
- return new Text4(theme.fg("toolTitle", "mcp prompt") + "\n" + theme.fg("success", "\u2714\uFE0E ") + theme.fg("accent", label) + (expanded && typeof message.content === "string" ? `
5998
- ${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);
5999
6530
  });
6000
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
+ });
6001
6543
  const current = () => {
6002
6544
  if (!runtime)
6003
6545
  throw configError ?? failure("configuration_invalid", { operation: "configuration" });
@@ -6021,13 +6563,13 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6021
6563
  renderCall: (args, theme, context) => renderCall(`${tool.server} ${tool.name}`, args, theme, context.expanded),
6022
6564
  renderResult: (result, options2, theme, context) => renderResult(result, options2, theme, context.isError),
6023
6565
  async execute(_id, args, signal, onUpdate, ctx) {
6024
- const label = `${tool.server}.${tool.name}`;
6025
- const emit = (message) => onUpdate?.(
6026
- textResult(message, {
6566
+ const label2 = `${tool.server}.${tool.name}`;
6567
+ const emit = (message2) => onUpdate?.(
6568
+ textResult(message2, {
6027
6569
  mcpClient: 1,
6028
6570
  rows: [
6029
6571
  {
6030
- label: `${label} \xB7 ${line(message).slice(0, 160)}`,
6572
+ label: `${label2} \xB7 ${line(message2).slice(0, 160)}`,
6031
6573
  state: "running"
6032
6574
  }
6033
6575
  ]
@@ -6039,9 +6581,10 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6039
6581
  tool,
6040
6582
  args,
6041
6583
  signal ?? ctx.signal,
6042
- emit
6584
+ emit,
6585
+ ctx.hasUI ? elicitationUi(ctx) : void 0
6043
6586
  );
6044
- return await convertResult(result, label);
6587
+ return await convertResult(result, label2);
6045
6588
  } catch (error) {
6046
6589
  return errorResult(error, {
6047
6590
  server: tool.server,
@@ -6053,6 +6596,35 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6053
6596
  }
6054
6597
  });
6055
6598
  }
6599
+ const trustDecision = (ctx) => projectTrustDecision(agentDir, ctx.cwd, ctx.isProjectTrusted()) ?? (sessionTrust?.cwd === ctx.cwd ? sessionTrust.trusted : void 0);
6600
+ const projectTrusted = (ctx) => trustDecision(ctx) ?? false;
6601
+ async function resolveProjectTrust(ctx) {
6602
+ let trusted = trustDecision(ctx);
6603
+ if (!existsSync(join4(ctx.cwd, ".mcp.json"))) return trusted ?? false;
6604
+ let answered = false;
6605
+ if (trusted === void 0 && ctx.hasUI) {
6606
+ const controller = new AbortController();
6607
+ trustController = controller;
6608
+ try {
6609
+ trusted = await askProjectTrust(
6610
+ agentDir,
6611
+ ctx.cwd,
6612
+ ctx.ui.select.bind(ctx.ui),
6613
+ AbortSignal.any([controller.signal, ...ctx.signal ? [ctx.signal] : []])
6614
+ );
6615
+ } finally {
6616
+ if (trustController === controller) trustController = void 0;
6617
+ }
6618
+ if (controller.signal.aborted) return false;
6619
+ if (trusted !== void 0) {
6620
+ sessionTrust = { cwd: ctx.cwd, trusted };
6621
+ answered = true;
6622
+ }
6623
+ }
6624
+ if (!trusted && !answered && ctx.hasUI && ctx.isProjectTrusted())
6625
+ ctx.ui.notify("Project .mcp.json ignored: this folder isn't trusted for MCP servers. Run /trust to save a decision, then /mcp reload.", "warning");
6626
+ return trusted ?? false;
6627
+ }
6056
6628
  const restore = (ctx) => {
6057
6629
  const tools = restoredTools(ctx.sessionManager.getBranch()).filter((tool) => {
6058
6630
  try {
@@ -6087,20 +6659,22 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6087
6659
  }
6088
6660
  };
6089
6661
  ctx.signal?.throwIfAborted();
6662
+ const trusted = mutation ? projectTrusted(ctx) : await resolveProjectTrust(ctx);
6090
6663
  const update = mutation && await updateServerConfig(
6091
6664
  agentDir,
6092
6665
  ctx.cwd,
6093
- ctx.isProjectTrusted(),
6666
+ trusted,
6094
6667
  mutation,
6095
6668
  validate
6096
6669
  );
6097
- const nextConfig = update ? update.config : await loadConfig(agentDir, ctx.cwd, ctx.isProjectTrusted());
6670
+ const nextConfig = update ? update.config : await loadConfig(agentDir, ctx.cwd, trusted);
6098
6671
  validate(nextConfig);
6099
6672
  const next = new McpRuntime(
6100
6673
  nextConfig,
6101
6674
  ctx.cwd,
6102
6675
  join4(agentDir, "cache", "pi-mcp-client"),
6103
- createSdkConnector(storeFactory)
6676
+ createSdkConnector(storeFactory),
6677
+ { interactive: ctx.hasUI }
6104
6678
  );
6105
6679
  const active = new Set(pi.getActiveTools());
6106
6680
  const retained = [...exposure.definitions.values()].filter((tool) => {
@@ -6122,16 +6696,19 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6122
6696
  return update?.scope;
6123
6697
  }
6124
6698
  pi.on("session_start", async (_event, ctx) => {
6125
- sessionGeneration++;
6699
+ const generation = ++sessionGeneration;
6126
6700
  promptController?.abort();
6127
6701
  importController?.abort();
6702
+ trustController?.abort();
6128
6703
  await runtime?.close();
6129
6704
  runtime = void 0;
6130
6705
  config = {};
6131
6706
  configError = void 0;
6132
6707
  try {
6133
- config = await loadConfig(agentDir, ctx.cwd, ctx.isProjectTrusted());
6134
- runtime = new McpRuntime(config, ctx.cwd, join4(agentDir, "cache", "pi-mcp-client"), createSdkConnector(storeFactory));
6708
+ const trusted = await resolveProjectTrust(ctx);
6709
+ if (generation !== sessionGeneration) return;
6710
+ config = await loadConfig(agentDir, ctx.cwd, trusted);
6711
+ runtime = new McpRuntime(config, ctx.cwd, join4(agentDir, "cache", "pi-mcp-client"), createSdkConnector(storeFactory), { interactive: ctx.hasUI });
6135
6712
  resourceNotifications(runtime, ctx);
6136
6713
  } catch (error) {
6137
6714
  configError = new DiagnosticError(diagnose(error, { operation: "configuration" }));
@@ -6150,6 +6727,7 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
6150
6727
  loginController?.abort();
6151
6728
  promptController?.abort();
6152
6729
  importController?.abort();
6730
+ trustController?.abort();
6153
6731
  const old = runtime;
6154
6732
  runtime = void 0;
6155
6733
  await old?.close();
@@ -6336,7 +6914,7 @@ Variables: ${(candidate.variables ?? []).join(", ") || "none"}. Supply known val
6336
6914
  state: "failed"
6337
6915
  };
6338
6916
  }));
6339
- messages2.push(...discovery.unavailable.map((message) => `Not searched: ${message}`), ...discovery.warnings);
6917
+ messages2.push(...discovery.unavailable.map((message2) => `Not searched: ${message2}`), ...discovery.warnings);
6340
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.");
6341
6919
  } else {
6342
6920
  const resolved = resolveTools(discovery.tools, identifiers);
@@ -6349,8 +6927,8 @@ Variables: ${(candidate.variables ?? []).join(", ") || "none"}. Supply known val
6349
6927
  const ok = tool && loaded.includes(tool);
6350
6928
  const unavailable = discovery.diagnostics.find((value) => value.server && serversFor(identifier2).includes(value.server));
6351
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."}`;
6352
- const label = `${line(identifier2)} \u2014 ${ok ? added.includes(tool.nativeName) ? "loaded" : "already loaded" : `not loaded \u2014 ${reason}`}`;
6353
- 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);
6354
6932
  details.rows.push({
6355
6933
  label: activationLabel(identifier2, tool),
6356
6934
  ...ok ? {} : { inlineDescription: reason },
@@ -6419,6 +6997,7 @@ Variables: ${(candidate.variables ?? []).join(", ") || "none"}. Supply known val
6419
6997
  args,
6420
6998
  ctx,
6421
6999
  agentDir,
7000
+ () => projectTrusted(ctx),
6422
7001
  AbortSignal.any([controller.signal, ...ctx.signal ? [ctx.signal] : []]),
6423
7002
  () => {
6424
7003
  if (sessionGeneration !== generation || runtime !== activeRuntime)
@@ -6458,8 +7037,8 @@ ${snapshot.body}`,
6458
7037
  if (action === "subscriptions") {
6459
7038
  if (server) throw new CommandUsageError("Use /mcp subscriptions without arguments.");
6460
7039
  const watches = current().resourceSubscriptions();
6461
- 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.";
6462
- 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");
6463
7042
  return;
6464
7043
  }
6465
7044
  if (action === "subscribe" || action === "unsubscribe") {
@@ -6478,8 +7057,8 @@ ${snapshot.body}`,
6478
7057
  server = mutation.server;
6479
7058
  await reloadConfiguration(ctx, mutation);
6480
7059
  const remaining = Object.hasOwn(config, server);
6481
- const message = mutation.action === "add" ? `\u2714\uFE0E ${server}: saved in ${mutation.scope} configuration. Connections, authentication, and tool discovery remain on demand.` + (mutation.scope === "global" && ctx.isProjectTrusted() ? " 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.");
6482
- 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");
6483
7062
  return;
6484
7063
  }
6485
7064
  if (action === "reload" && !server) {
@@ -6573,14 +7152,14 @@ ${snapshot.body}`,
6573
7152
  );
6574
7153
  return;
6575
7154
  }
6576
- const choices = [...tools].sort((a, b) => a.name.localeCompare(b.name));
7155
+ const choices2 = [...tools].sort((a, b) => a.name.localeCompare(b.name));
6577
7156
  const columns = ctx.mode === "tui" ? process.stdout.columns || 80 : 80;
6578
- const labels = choices.map((tool2, index) => toolPickerLabel(tool2, index, columns));
7157
+ const labels = choices2.map((tool2, index) => toolPickerLabel(tool2, index, columns));
6579
7158
  const selected = await ctx.ui.select(
6580
7159
  `${server}: ${tools.length} tools (select to inspect; none are activated)`,
6581
7160
  labels
6582
7161
  );
6583
- const tool = selected === void 0 ? void 0 : choices[labels.indexOf(selected)];
7162
+ const tool = selected === void 0 ? void 0 : choices2[labels.indexOf(selected)];
6584
7163
  if (tool)
6585
7164
  ctx.ui.notify(
6586
7165
  inspectTool(tool),
@@ -6601,9 +7180,7 @@ ${snapshot.body}`,
6601
7180
  const options2 = config[server];
6602
7181
  const loginRuntime = current();
6603
7182
  const open2 = async (target) => {
6604
- const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
6605
- const result = await pi.exec(command, [target], { timeout: 5e3 }).catch(() => void 0);
6606
- if (!result || result.code !== 0)
7183
+ if (!await launchBrowser(target))
6607
7184
  throw failure("oauth_failed", { server, operation: "auth" });
6608
7185
  };
6609
7186
  if (loginController) throw failure("busy", { server, operation: "auth" });
@@ -6679,7 +7256,7 @@ Opening browser. Esc to cancel.`
6679
7256
  "info"
6680
7257
  );
6681
7258
  } catch (error) {
6682
- 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(
6683
7260
  diagnose(error, {
6684
7261
  server,
6685
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",
@@ -6687,8 +7264,8 @@ Opening browser. Esc to cancel.`
6687
7264
  signal: ctx.signal
6688
7265
  })
6689
7266
  );
6690
- if (ctx.hasUI) ctx.ui.notify(message, "error");
6691
- else throw new Error(message);
7267
+ if (ctx.hasUI) ctx.ui.notify(message2, "error");
7268
+ else throw new Error(message2);
6692
7269
  }
6693
7270
  }
6694
7271
  });