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