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/README.md +6 -2
- package/dist/index.js +732 -155
- package/docs/behavior.md +62 -1
- package/docs/commands.md +5 -3
- package/docs/configuration.md +21 -7
- package/docs/tool-reference.md +7 -0
- package/docs/troubleshooting.md +18 -0
- package/package.json +1 -1
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,
|
|
1773
|
+
logger: (level, message2) => {
|
|
1773
1774
|
if (typeof (console === null || console === void 0 ? void 0 : console[level]) === "function")
|
|
1774
|
-
console[level](
|
|
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
|
|
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
|
|
2695
|
+
const choices2 = ["--scope global", "--scope project"];
|
|
2687
2696
|
if (!/^--scope\s+(global|project)\s/u.test(rest)) {
|
|
2688
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2737
|
-
if (!object(result) || !Array.isArray(result.messages) || !result.messages.length) throw
|
|
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((
|
|
2742
|
-
if (!["user", "assistant"].includes(
|
|
2743
|
-
const 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 ${
|
|
2772
|
+
previews.push(`Message ${index + 1} \xB7 ${message2.role} (server supplied)
|
|
2764
2773
|
${preview2}`);
|
|
2765
|
-
return { role:
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
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,
|
|
2863
|
+
const select = async (title, choices2) => {
|
|
2838
2864
|
guard();
|
|
2839
|
-
const selected =
|
|
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(
|
|
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: ${
|
|
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,
|
|
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
|
-
...
|
|
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 =
|
|
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
|
|
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 (
|
|
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,
|
|
4002
|
+
const select = async (title, choices2) => {
|
|
3989
4003
|
guard();
|
|
3990
|
-
const result = await ctx.ui.select(title,
|
|
4004
|
+
const result = await ctx.ui.select(title, choices2, { signal });
|
|
3991
4005
|
guard();
|
|
3992
|
-
return 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
|
|
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 && !
|
|
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,
|
|
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] : [], ...!
|
|
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
|
|
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>${
|
|
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
|
|
4192
|
-
if (value.length > 16384 || /[\u0000-\u0020\u007f]/u.test(value)) throw
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
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:
|
|
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
|
|
5850
|
+
const context = {
|
|
5442
5851
|
server: tool.server,
|
|
5443
5852
|
operation: "call",
|
|
5444
5853
|
oauth: usesOAuth(config),
|
|
5445
5854
|
signal
|
|
5446
|
-
}
|
|
5447
|
-
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
|
5967
|
-
return textResult(
|
|
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" :
|
|
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", (
|
|
5994
|
-
const details = object(
|
|
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
|
|
5997
|
-
return new Text4(theme.fg("toolTitle", "mcp prompt") + "\n" + theme.fg("success", "\u2714\uFE0E ") + theme.fg("accent",
|
|
5998
|
-
${
|
|
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
|
|
6025
|
-
const emit = (
|
|
6026
|
-
textResult(
|
|
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: `${
|
|
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,
|
|
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
|
-
|
|
6666
|
+
trusted,
|
|
6094
6667
|
mutation,
|
|
6095
6668
|
validate
|
|
6096
6669
|
);
|
|
6097
|
-
const nextConfig = update ? update.config : await loadConfig(agentDir, ctx.cwd,
|
|
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
|
-
|
|
6134
|
-
|
|
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((
|
|
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
|
|
6353
|
-
messages2.push(
|
|
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
|
|
6462
|
-
if (ctx.hasUI) ctx.ui.notify(
|
|
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
|
|
6482
|
-
if (ctx.hasUI) ctx.ui.notify(
|
|
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
|
|
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 =
|
|
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 :
|
|
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
|
-
|
|
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
|
|
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(
|
|
6691
|
-
else throw new Error(
|
|
7267
|
+
if (ctx.hasUI) ctx.ui.notify(message2, "error");
|
|
7268
|
+
else throw new Error(message2);
|
|
6692
7269
|
}
|
|
6693
7270
|
}
|
|
6694
7271
|
});
|