@fitchmultz/pi-mcp-adapter 8.0.5 → 8.0.7
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/CHANGELOG.md +140 -7
- package/OAUTH.md +13 -16
- package/README.md +8 -8
- package/cli.js +67 -28
- package/dist/abort.js +18 -9
- package/dist/client-operations.js +4 -2
- package/dist/commands.js +77 -43
- package/dist/config.js +194 -105
- package/dist/connection-runtime.d.ts +2 -2
- package/dist/connection-runtime.js +29 -18
- package/dist/consent-manager.d.ts +2 -2
- package/dist/consent-manager.js +5 -2
- package/dist/direct-tools.d.ts +1 -1
- package/dist/direct-tools.js +13 -7
- package/dist/elicitation-handler.d.ts +2 -3
- package/dist/elicitation-handler.js +113 -58
- package/dist/gateway-arguments.d.ts +1 -1
- package/dist/gateway-arguments.js +90 -23
- package/dist/glimpse-ui.js +10 -5
- package/dist/host-html-template.d.ts +3 -1
- package/dist/host-html-template.js +9 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +264 -119
- package/dist/init.d.ts +1 -3
- package/dist/init.js +91 -51
- package/dist/legacy-migration.d.ts +1 -1
- package/dist/legacy-migration.js +17 -12
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +33 -17
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +4 -2
- package/dist/mcp-auth-flow.js +150 -58
- package/dist/mcp-auth.d.ts +7 -7
- package/dist/mcp-auth.js +189 -138
- package/dist/mcp-callback-server.js +19 -8
- package/dist/mcp-code.js +168 -54
- package/dist/mcp-keyring-helper.cjs +57 -23
- package/dist/mcp-oauth-provider.d.ts +6 -7
- package/dist/mcp-oauth-provider.js +146 -72
- package/dist/mcp-oauth-runtime.js +29 -16
- package/dist/mcp-output-guard.js +193 -65
- package/dist/mcp-output-preview.js +41 -20
- package/dist/mcp-panel.d.ts +9 -9
- package/dist/mcp-panel.js +141 -70
- package/dist/mcp-script-worker.mjs +135 -43
- package/dist/mcp-setup-panel.d.ts +8 -8
- package/dist/mcp-setup-panel.js +130 -44
- package/dist/mcp-status.js +14 -7
- package/dist/mcp-trace.d.ts +10 -5
- package/dist/mcp-trace.js +127 -61
- package/dist/metadata-cache.d.ts +3 -4
- package/dist/metadata-cache.js +83 -43
- package/dist/npx-resolver.js +101 -60
- package/dist/onboarding-state.js +4 -2
- package/dist/plain-result-reader.js +78 -31
- package/dist/prompts.js +36 -20
- package/dist/proxy-modes.js +670 -193
- package/dist/resource-tools.js +30 -9
- package/dist/runtime-owner.js +36 -18
- package/dist/sampling-handler.d.ts +1 -2
- package/dist/sampling-handler.js +41 -21
- package/dist/search-ranking.d.ts +1 -1
- package/dist/search-ranking.js +67 -34
- package/dist/server-manager.d.ts +11 -11
- package/dist/server-manager.js +283 -166
- package/dist/session-recovery.js +113 -61
- package/dist/tool-approval.js +9 -3
- package/dist/tool-loader.js +154 -55
- package/dist/tool-metadata.d.ts +1 -1
- package/dist/tool-metadata.js +51 -23
- package/dist/tool-registrar.js +32 -10
- package/dist/tool-result-renderer.d.ts +3 -3
- package/dist/tool-result-renderer.js +65 -44
- package/dist/types.d.ts +2 -2
- package/dist/types.js +39 -18
- package/dist/ui-message-history.js +33 -13
- package/dist/ui-resource-handler.d.ts +3 -3
- package/dist/ui-resource-handler.js +37 -21
- package/dist/ui-server.js +101 -43
- package/dist/ui-session.js +66 -29
- package/dist/ui-stream-types.js +14 -4
- package/dist/ui-tool-visibility.js +12 -6
- package/dist/unix-socket-transport.d.ts +1 -2
- package/dist/unix-socket-transport.js +19 -12
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +60 -35
- package/package.json +29 -5
- package/skills/mcp-scripting/SKILL.md +6 -6
|
@@ -493,11 +493,11 @@ export declare class ManagedClient extends Client {
|
|
|
493
493
|
}>;
|
|
494
494
|
}
|
|
495
495
|
export { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
496
|
-
export { SdkHttpError, StreamableHTTPClientTransport, SSEClientTransport, auth, extractWWWAuthenticateParams, UnauthorizedError } from "@modelcontextprotocol/client";
|
|
496
|
+
export { SdkHttpError, StreamableHTTPClientTransport, SSEClientTransport, auth, extractWWWAuthenticateParams, UnauthorizedError, } from "@modelcontextprotocol/client";
|
|
497
497
|
export { UnixSocketClientTransport } from "./unix-socket-transport.ts";
|
|
498
498
|
export { trackToolTransport, trackToolTransportFailure } from "./session-recovery.ts";
|
|
499
499
|
export { McpOAuthProvider } from "./mcp-oauth-provider.ts";
|
|
500
|
-
export { extractOAuthConfig, isOAuthChallenge, recordOAuthChallenge, getOAuthRequest, bindOAuthRequestIssuer } from "./mcp-auth-flow.ts";
|
|
500
|
+
export { extractOAuthConfig, isOAuthChallenge, recordOAuthChallenge, getOAuthRequest, bindOAuthRequestIssuer, } from "./mcp-auth-flow.ts";
|
|
501
501
|
export { registerSamplingHandler } from "./sampling-handler.ts";
|
|
502
502
|
export { registerElicitationHandler } from "./elicitation-handler.ts";
|
|
503
503
|
export { serverStreamResultPatchNotificationSchema } from "./ui-stream-types.ts";
|
|
@@ -8,10 +8,13 @@ export class ManagedClient extends Client {
|
|
|
8
8
|
oauthProvider;
|
|
9
9
|
own(operation, kind) {
|
|
10
10
|
let pending = clientOperations.get(this);
|
|
11
|
-
if (!pending)
|
|
12
|
-
clientOperations.set(this, pending = new Map());
|
|
11
|
+
if (!pending) {
|
|
12
|
+
clientOperations.set(this, (pending = new Map()));
|
|
13
|
+
}
|
|
13
14
|
pending.set(operation, kind);
|
|
14
|
-
const settled = () => {
|
|
15
|
+
const settled = () => {
|
|
16
|
+
pending.delete(operation);
|
|
17
|
+
};
|
|
15
18
|
void operation.then(settled, settled);
|
|
16
19
|
return operation;
|
|
17
20
|
}
|
|
@@ -33,44 +36,52 @@ export class ManagedClient extends Client {
|
|
|
33
36
|
? this.oauthProvider.runWithSignal(controller.signal, () => {
|
|
34
37
|
// Native legacy cancellation notifications must retain their operation's auth context.
|
|
35
38
|
const abort = AsyncLocalStorage.bind(() => controller.abort(signal?.reason));
|
|
36
|
-
if (signal?.aborted)
|
|
39
|
+
if (signal?.aborted) {
|
|
37
40
|
abort();
|
|
38
|
-
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
39
43
|
signal?.addEventListener("abort", abort, { once: true });
|
|
44
|
+
}
|
|
40
45
|
removeAbortListener = () => signal?.removeEventListener("abort", abort);
|
|
41
46
|
return start(controller.signal);
|
|
42
47
|
})
|
|
43
48
|
: start(signal);
|
|
44
|
-
void operation.then(removeAbortListener, () => {
|
|
49
|
+
void operation.then(removeAbortListener, () => {
|
|
50
|
+
controller?.abort();
|
|
51
|
+
removeAbortListener();
|
|
52
|
+
});
|
|
45
53
|
return this.own(operation, "request/refresh");
|
|
46
54
|
}
|
|
47
55
|
callTool(...args) {
|
|
48
|
-
return this.track(signal => super.callTool(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
56
|
+
return this.track((signal) => super.callTool(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
49
57
|
}
|
|
50
58
|
readResource(...args) {
|
|
51
|
-
return this.track(signal => super.readResource(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
59
|
+
return this.track((signal) => super.readResource(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
52
60
|
}
|
|
53
61
|
getPrompt(...args) {
|
|
54
|
-
return this.track(signal => super.getPrompt(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
62
|
+
return this.track((signal) => super.getPrompt(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
55
63
|
}
|
|
56
64
|
listTools(...args) {
|
|
57
|
-
return this.track(signal => super.listTools(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
65
|
+
return this.track((signal) => super.listTools(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
58
66
|
}
|
|
59
67
|
listResources(...args) {
|
|
60
|
-
return this.track(signal => super.listResources(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
68
|
+
return this.track((signal) => super.listResources(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
61
69
|
}
|
|
62
70
|
listPrompts(...args) {
|
|
63
|
-
return this.track(signal => super.listPrompts(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
71
|
+
return this.track((signal) => super.listPrompts(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
64
72
|
}
|
|
65
73
|
complete(...args) {
|
|
66
|
-
return this.track(signal => super.complete(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
74
|
+
return this.track((signal) => super.complete(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
67
75
|
}
|
|
68
76
|
setLoggingLevel(...args) {
|
|
69
|
-
return this.track(signal => super.setLoggingLevel(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
77
|
+
return this.track((signal) => super.setLoggingLevel(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
70
78
|
}
|
|
71
79
|
listen(...args) {
|
|
72
80
|
return this.track(async (signal) => {
|
|
73
|
-
const subscription = await super.listen(args[0], {
|
|
81
|
+
const subscription = await super.listen(args[0], {
|
|
82
|
+
...args[1],
|
|
83
|
+
...(signal ? { signal } : {}),
|
|
84
|
+
});
|
|
74
85
|
this.own(subscription.closed, "subscription");
|
|
75
86
|
return subscription;
|
|
76
87
|
}, args[1]?.signal);
|
|
@@ -82,17 +93,17 @@ export class ManagedClient extends Client {
|
|
|
82
93
|
}, args[1]?.signal);
|
|
83
94
|
}
|
|
84
95
|
unsubscribeResource(...args) {
|
|
85
|
-
return this.track(signal => super.unsubscribeResource(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
96
|
+
return this.track((signal) => super.unsubscribeResource(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
86
97
|
}
|
|
87
98
|
}
|
|
88
99
|
// Loaded only by the manager's connection path. Keep native constructors and
|
|
89
100
|
// synchronous connection setup together, after the one asynchronous import.
|
|
90
101
|
export { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
91
|
-
export { SdkHttpError, StreamableHTTPClientTransport, SSEClientTransport, auth, extractWWWAuthenticateParams, UnauthorizedError } from "@modelcontextprotocol/client";
|
|
102
|
+
export { SdkHttpError, StreamableHTTPClientTransport, SSEClientTransport, auth, extractWWWAuthenticateParams, UnauthorizedError, } from "@modelcontextprotocol/client";
|
|
92
103
|
export { UnixSocketClientTransport } from "./unix-socket-transport.js";
|
|
93
104
|
export { trackToolTransport, trackToolTransportFailure } from "./session-recovery.js";
|
|
94
105
|
export { McpOAuthProvider } from "./mcp-oauth-provider.js";
|
|
95
|
-
export { extractOAuthConfig, isOAuthChallenge, recordOAuthChallenge, getOAuthRequest, bindOAuthRequestIssuer } from "./mcp-auth-flow.js";
|
|
106
|
+
export { extractOAuthConfig, isOAuthChallenge, recordOAuthChallenge, getOAuthRequest, bindOAuthRequestIssuer, } from "./mcp-auth-flow.js";
|
|
96
107
|
export { registerSamplingHandler } from "./sampling-handler.js";
|
|
97
108
|
export { registerElicitationHandler } from "./elicitation-handler.js";
|
|
98
109
|
export { serverStreamResultPatchNotificationSchema } from "./ui-stream-types.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare class ConsentManager {
|
|
2
|
-
private decisions;
|
|
3
|
-
private log;
|
|
2
|
+
private readonly decisions;
|
|
3
|
+
private readonly log;
|
|
4
4
|
hasDecisions(): boolean;
|
|
5
5
|
requiresPrompt(serverName: string): boolean;
|
|
6
6
|
registerDecision(serverName: string, approved: boolean): void;
|
package/dist/consent-manager.js
CHANGED
|
@@ -2,7 +2,9 @@ import { logger } from "./logger.js";
|
|
|
2
2
|
export class ConsentManager {
|
|
3
3
|
decisions = new Map();
|
|
4
4
|
log = logger.child({ component: "ConsentManager" });
|
|
5
|
-
hasDecisions() {
|
|
5
|
+
hasDecisions() {
|
|
6
|
+
return this.decisions.size > 0;
|
|
7
|
+
}
|
|
6
8
|
requiresPrompt(serverName) {
|
|
7
9
|
return this.decisions.get(serverName) !== true;
|
|
8
10
|
}
|
|
@@ -12,8 +14,9 @@ export class ConsentManager {
|
|
|
12
14
|
}
|
|
13
15
|
ensureApproved(serverName) {
|
|
14
16
|
const decision = this.decisions.get(serverName);
|
|
15
|
-
if (decision === true)
|
|
17
|
+
if (decision === true) {
|
|
16
18
|
return;
|
|
19
|
+
}
|
|
17
20
|
throw new Error(decision === false
|
|
18
21
|
? `Tool calls for "${serverName}" were denied for this session`
|
|
19
22
|
: `Tool call approval required for "${serverName}"`);
|
package/dist/direct-tools.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { McpExtensionState } from "./state.ts";
|
|
3
|
-
import type { DirectToolSpec, McpAdapterOptions, McpConfig } from "./types.ts";
|
|
4
3
|
export { getMissingConfiguredDirectToolServers } from "./metadata-cache.ts";
|
|
4
|
+
import { type DirectToolSpec, type McpAdapterOptions, type McpConfig } from "./types.ts";
|
|
5
5
|
export declare function directToolSelection(config: McpConfig, serverName: string, envOverride?: string[]): true | string[] | false;
|
|
6
6
|
export declare function buildProxyDescription(config: McpConfig): string;
|
|
7
7
|
type DirectToolExecute = (toolCallId: string, params: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined, ctx: ExtensionContext) => Promise<AgentToolResult<Record<string, unknown>>>;
|
package/dist/direct-tools.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { abortable, throwIfAborted } from "./abort.js";
|
|
2
2
|
import { parseDirectToolSelectors } from "./metadata-cache.js";
|
|
3
3
|
export { getMissingConfiguredDirectToolServers } from "./metadata-cache.js";
|
|
4
|
-
import { isServerDisabled } from "./types.js";
|
|
4
|
+
import { isServerDisabled, } from "./types.js";
|
|
5
5
|
import { nativeMcpResult } from "./tool-registrar.js";
|
|
6
6
|
export function directToolSelection(config, serverName, envOverride) {
|
|
7
7
|
if (envOverride) {
|
|
8
8
|
const selection = parseDirectToolSelectors(envOverride);
|
|
9
|
-
if (selection.servers.has(serverName))
|
|
9
|
+
if (selection.servers.has(serverName)) {
|
|
10
10
|
return true;
|
|
11
|
+
}
|
|
11
12
|
return selection.tools.has(serverName) ? [...selection.tools.get(serverName)] : false;
|
|
12
13
|
}
|
|
13
14
|
return config.mcpServers[serverName]?.directTools ?? config.settings?.directTools ?? false;
|
|
14
15
|
}
|
|
15
16
|
export function buildProxyDescription(config) {
|
|
16
|
-
const servers = Object.entries(config.mcpServers)
|
|
17
|
+
const servers = Object.entries(config.mcpServers)
|
|
18
|
+
.filter(([, definition]) => !isServerDisabled(definition))
|
|
19
|
+
.map(([name]) => name)
|
|
20
|
+
.sort();
|
|
17
21
|
return [
|
|
18
22
|
'MCP gateway. Use mcp_search to discover typed tools; enable mcp_script with mcp_search({ enable: ["script"] }) for multi-call composition. Native Pi tools are called directly.',
|
|
19
23
|
`Configured servers: ${servers.join(", ") || "none"}. Catalog discovery is lazy.`,
|
|
@@ -36,18 +40,20 @@ export function createDirectToolExecutor(getState, getInitPromise, spec, beforeE
|
|
|
36
40
|
return nativeMcpResult({
|
|
37
41
|
content: [{ type: "text", text: `MCP initialization failed: ${message}` }],
|
|
38
42
|
details: { error: "init_failed", message },
|
|
39
|
-
},
|
|
43
|
+
}, Boolean(spec.outputSchema));
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
if (!state) {
|
|
43
47
|
return nativeMcpResult({
|
|
44
48
|
content: [{ type: "text", text: "MCP not initialized" }],
|
|
45
49
|
details: { error: "not_initialized" },
|
|
46
|
-
},
|
|
50
|
+
}, Boolean(spec.outputSchema));
|
|
47
51
|
}
|
|
48
52
|
const { executeCall } = await import("./proxy-modes.js");
|
|
49
|
-
const result = await executeCall(state, spec.originalName, params, spec.serverName, undefined, signal, { toolCallId }, beforeExecute
|
|
53
|
+
const result = await executeCall(state, spec.originalName, params, spec.serverName, undefined, signal, { toolCallId }, beforeExecute
|
|
54
|
+
? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation)
|
|
55
|
+
: undefined, { exactOriginalName: true, ...(spec.resourceUri ? { resourceUri: spec.resourceUri } : {}) });
|
|
50
56
|
const { mode: _mode, ...details } = result.details;
|
|
51
|
-
return nativeMcpResult({ ...result, details },
|
|
57
|
+
return nativeMcpResult({ ...result, details }, Boolean(spec.outputSchema));
|
|
52
58
|
};
|
|
53
59
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type
|
|
3
|
-
import { type ElicitRequest, type ElicitRequestFormParams, type ElicitRequestURLParams, type ElicitResult } from "@modelcontextprotocol/client";
|
|
2
|
+
import { type Client, type ElicitRequest, type ElicitRequestFormParams, type ElicitRequestURLParams, type ElicitResult } from "@modelcontextprotocol/client";
|
|
4
3
|
export type ElicitationValue = string | number | boolean | string[] | undefined;
|
|
5
|
-
export type ElicitationUIContext = ExtensionUIContext
|
|
4
|
+
export type ElicitationUIContext = Pick<ExtensionUIContext, "select" | "input" | "notify">;
|
|
6
5
|
export interface ElicitationHandlerOptions {
|
|
7
6
|
serverName: string;
|
|
8
7
|
ui: ElicitationUIContext;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ProtocolErrorCode, ProtocolError, } from "@modelcontextprotocol/client";
|
|
2
2
|
import { AjvJsonSchemaValidator } from "@modelcontextprotocol/client/validators/ajv";
|
|
3
3
|
import open from "open";
|
|
4
|
-
export function registerElicitationHandler(client, options, own = run => run()) {
|
|
5
|
-
client.setRequestHandler("elicitation/create", request => own(() => handleElicitationRequest(options, request)));
|
|
4
|
+
export function registerElicitationHandler(client, options, own = (run) => run()) {
|
|
5
|
+
client.setRequestHandler("elicitation/create", (request) => own(() => handleElicitationRequest(options, request)));
|
|
6
6
|
}
|
|
7
7
|
export async function handleElicitationRequest(options, request) {
|
|
8
8
|
return request.params.mode === "url"
|
|
@@ -11,38 +11,47 @@ export async function handleElicitationRequest(options, request) {
|
|
|
11
11
|
}
|
|
12
12
|
export async function handleFormElicitation(options, params) {
|
|
13
13
|
const decision = await options.ui.select(`MCP Input Request\nServer: ${options.serverName}\n\n${params.message}`, ["Continue", "Decline"]);
|
|
14
|
-
if (decision === undefined)
|
|
14
|
+
if (decision === undefined) {
|
|
15
15
|
return { action: "cancel" };
|
|
16
|
-
|
|
16
|
+
}
|
|
17
|
+
if (decision === "Decline") {
|
|
17
18
|
return { action: "decline" };
|
|
19
|
+
}
|
|
18
20
|
const values = {};
|
|
19
21
|
const properties = Object.entries(params.requestedSchema.properties);
|
|
20
22
|
for (const [name, schema] of properties) {
|
|
21
23
|
const value = await collectValidField(options.ui, params, name, schema);
|
|
22
|
-
if (!("value" in value))
|
|
24
|
+
if (!("value" in value)) {
|
|
23
25
|
return { action: "cancel" };
|
|
26
|
+
}
|
|
24
27
|
values[name] = value.value;
|
|
25
28
|
}
|
|
26
29
|
while (true) {
|
|
27
30
|
const content = coerceAndValidateFormValues(params, values);
|
|
28
31
|
const action = await options.ui.select(formatReview(options.serverName, properties, content), properties.length > 0 ? ["Submit", "Edit", "Decline"] : ["Submit", "Decline"]);
|
|
29
|
-
if (action === undefined)
|
|
32
|
+
if (action === undefined) {
|
|
30
33
|
return { action: "cancel" };
|
|
31
|
-
|
|
34
|
+
}
|
|
35
|
+
if (action === "Decline") {
|
|
32
36
|
return { action: "decline" };
|
|
33
|
-
|
|
37
|
+
}
|
|
38
|
+
if (action === "Submit") {
|
|
34
39
|
return { action: "accept", content };
|
|
40
|
+
}
|
|
35
41
|
const labels = properties.map(([name, schema]) => `${schema.title ?? humanizeName(name)} (${name})`);
|
|
36
42
|
const selected = await options.ui.select("Choose a field to edit", labels);
|
|
37
|
-
if (selected === undefined)
|
|
43
|
+
if (selected === undefined) {
|
|
38
44
|
return { action: "cancel" };
|
|
45
|
+
}
|
|
39
46
|
const property = properties[labels.indexOf(selected)];
|
|
40
|
-
if (!property)
|
|
47
|
+
if (!property) {
|
|
41
48
|
continue;
|
|
49
|
+
}
|
|
42
50
|
const [name, schema] = property;
|
|
43
51
|
const value = await collectValidField(options.ui, params, name, schema, values[name]);
|
|
44
|
-
if (!("value" in value))
|
|
52
|
+
if (!("value" in value)) {
|
|
45
53
|
return { action: "cancel" };
|
|
54
|
+
}
|
|
46
55
|
values[name] = value.value;
|
|
47
56
|
}
|
|
48
57
|
}
|
|
@@ -50,8 +59,9 @@ async function collectValidField(ui, params, name, schema, current) {
|
|
|
50
59
|
const required = params.requestedSchema.required?.includes(name) === true;
|
|
51
60
|
while (true) {
|
|
52
61
|
const result = await collectField(ui, params, name, schema, current);
|
|
53
|
-
if (!("value" in result))
|
|
62
|
+
if (!("value" in result)) {
|
|
54
63
|
return result;
|
|
64
|
+
}
|
|
55
65
|
try {
|
|
56
66
|
coerceAndValidateFormValues({
|
|
57
67
|
...params,
|
|
@@ -71,92 +81,124 @@ async function collectValidField(ui, params, name, schema, current) {
|
|
|
71
81
|
}
|
|
72
82
|
async function collectField(ui, params, name, schema, current) {
|
|
73
83
|
const required = params.requestedSchema.required?.includes(name) === true;
|
|
74
|
-
const title = [
|
|
84
|
+
const title = [
|
|
85
|
+
schema.title ?? humanizeName(name),
|
|
86
|
+
required ? "(required)" : "",
|
|
87
|
+
schema.description,
|
|
88
|
+
]
|
|
75
89
|
.filter(Boolean)
|
|
76
90
|
.join(" ");
|
|
77
91
|
if (schema.type === "string" && ("enum" in schema || "oneOf" in schema)) {
|
|
78
92
|
const choices = "oneOf" in schema
|
|
79
|
-
? schema.oneOf.map(option => ({
|
|
93
|
+
? schema.oneOf.map((option) => ({
|
|
94
|
+
value: option.const,
|
|
95
|
+
display: formatChoice(option.const, option.title),
|
|
96
|
+
}))
|
|
80
97
|
: schema.enum.map((value, index) => ({
|
|
81
98
|
value,
|
|
82
99
|
display: formatChoice(value, "enumNames" in schema ? schema.enumNames?.[index] : undefined),
|
|
83
100
|
}));
|
|
84
|
-
const displays = uniqueLabels(choices.map(choice => choice.display));
|
|
101
|
+
const displays = uniqueLabels(choices.map((choice) => choice.display));
|
|
85
102
|
const actions = [...displays];
|
|
86
103
|
const useDefault = schema.default === undefined ? undefined : uniqueAction("Use default", actions);
|
|
87
|
-
if (useDefault)
|
|
104
|
+
if (useDefault) {
|
|
88
105
|
actions.push(useDefault);
|
|
106
|
+
}
|
|
89
107
|
const omit = required ? undefined : uniqueAction("Omit", actions);
|
|
90
|
-
if (omit)
|
|
108
|
+
if (omit) {
|
|
91
109
|
actions.push(omit);
|
|
110
|
+
}
|
|
92
111
|
const action = await ui.select(title, actions);
|
|
93
|
-
if (action === undefined)
|
|
112
|
+
if (action === undefined) {
|
|
94
113
|
return { cancelled: true };
|
|
95
|
-
|
|
114
|
+
}
|
|
115
|
+
if (action === useDefault) {
|
|
96
116
|
return { cancelled: false, value: schema.default };
|
|
97
|
-
|
|
117
|
+
}
|
|
118
|
+
if (action === omit) {
|
|
98
119
|
return { cancelled: false, value: undefined };
|
|
120
|
+
}
|
|
99
121
|
return { cancelled: false, value: choices[displays.indexOf(action)]?.value };
|
|
100
122
|
}
|
|
101
123
|
if (schema.type === "boolean") {
|
|
102
124
|
const actions = ["Yes", "No"];
|
|
103
|
-
if (schema.default !== undefined)
|
|
125
|
+
if (schema.default !== undefined) {
|
|
104
126
|
actions.push("Use default");
|
|
105
|
-
|
|
127
|
+
}
|
|
128
|
+
if (!required) {
|
|
106
129
|
actions.push("Omit");
|
|
130
|
+
}
|
|
107
131
|
const action = await ui.select(title, actions);
|
|
108
|
-
if (action === undefined)
|
|
132
|
+
if (action === undefined) {
|
|
109
133
|
return { cancelled: true };
|
|
110
|
-
|
|
134
|
+
}
|
|
135
|
+
if (action === "Use default") {
|
|
111
136
|
return { cancelled: false, value: schema.default };
|
|
112
|
-
|
|
137
|
+
}
|
|
138
|
+
if (action === "Omit") {
|
|
113
139
|
return { cancelled: false, value: undefined };
|
|
140
|
+
}
|
|
114
141
|
return { cancelled: false, value: action === "Yes" };
|
|
115
142
|
}
|
|
116
143
|
if (schema.type === "array") {
|
|
117
144
|
const actions = ["Choose values"];
|
|
118
|
-
if (schema.default !== undefined)
|
|
145
|
+
if (schema.default !== undefined) {
|
|
119
146
|
actions.push("Use default");
|
|
120
|
-
|
|
147
|
+
}
|
|
148
|
+
if (!required) {
|
|
121
149
|
actions.push("Omit");
|
|
150
|
+
}
|
|
122
151
|
const action = await ui.select(title, actions);
|
|
123
|
-
if (action === undefined)
|
|
152
|
+
if (action === undefined) {
|
|
124
153
|
return { cancelled: true };
|
|
125
|
-
|
|
154
|
+
}
|
|
155
|
+
if (action === "Use default") {
|
|
126
156
|
return { cancelled: false, value: schema.default };
|
|
127
|
-
|
|
157
|
+
}
|
|
158
|
+
if (action === "Omit") {
|
|
128
159
|
return { cancelled: false, value: undefined };
|
|
160
|
+
}
|
|
129
161
|
const choices = extractMultiSelectOptions(schema);
|
|
130
162
|
const selected = new Set(Array.isArray(current) ? current : []);
|
|
131
163
|
while (true) {
|
|
132
|
-
const displays = uniqueLabels(choices.map(choice => selected.has(choice.value) ? `✓ ${choice.display}` : choice.display));
|
|
164
|
+
const displays = uniqueLabels(choices.map((choice) => selected.has(choice.value) ? `✓ ${choice.display}` : choice.display));
|
|
133
165
|
const done = uniqueAction("Done", displays);
|
|
134
166
|
const picked = await ui.select(title, [...displays, done]);
|
|
135
|
-
if (picked === undefined)
|
|
167
|
+
if (picked === undefined) {
|
|
136
168
|
return { cancelled: true };
|
|
137
|
-
|
|
169
|
+
}
|
|
170
|
+
if (picked === done) {
|
|
138
171
|
return { cancelled: false, value: [...selected] };
|
|
172
|
+
}
|
|
139
173
|
const choice = choices[displays.indexOf(picked)];
|
|
140
|
-
if (!choice)
|
|
174
|
+
if (!choice) {
|
|
141
175
|
continue;
|
|
142
|
-
|
|
176
|
+
}
|
|
177
|
+
if (selected.has(choice.value)) {
|
|
143
178
|
selected.delete(choice.value);
|
|
144
|
-
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
145
181
|
selected.add(choice.value);
|
|
182
|
+
}
|
|
146
183
|
}
|
|
147
184
|
}
|
|
148
185
|
const actions = ["Enter value"];
|
|
149
|
-
if (schema.default !== undefined)
|
|
186
|
+
if (schema.default !== undefined) {
|
|
150
187
|
actions.push("Use default");
|
|
151
|
-
|
|
188
|
+
}
|
|
189
|
+
if (!required) {
|
|
152
190
|
actions.push("Omit");
|
|
191
|
+
}
|
|
153
192
|
const action = await ui.select(title, actions);
|
|
154
|
-
if (action === undefined)
|
|
193
|
+
if (action === undefined) {
|
|
155
194
|
return { cancelled: true };
|
|
156
|
-
|
|
195
|
+
}
|
|
196
|
+
if (action === "Use default") {
|
|
157
197
|
return { cancelled: false, value: schema.default };
|
|
158
|
-
|
|
198
|
+
}
|
|
199
|
+
if (action === "Omit") {
|
|
159
200
|
return { cancelled: false, value: undefined };
|
|
201
|
+
}
|
|
160
202
|
const entered = await ui.input(title, current === undefined ? undefined : String(current));
|
|
161
203
|
return entered === undefined ? { cancelled: true } : { cancelled: false, value: entered };
|
|
162
204
|
}
|
|
@@ -166,8 +208,9 @@ export function coerceAndValidateFormValues(params, values) {
|
|
|
166
208
|
for (const [name, schema] of Object.entries(params.requestedSchema.properties)) {
|
|
167
209
|
const value = values[name];
|
|
168
210
|
if (value === undefined) {
|
|
169
|
-
if (required.has(name))
|
|
211
|
+
if (required.has(name)) {
|
|
170
212
|
throw new Error(`Missing required elicitation field: ${name}`);
|
|
213
|
+
}
|
|
171
214
|
continue;
|
|
172
215
|
}
|
|
173
216
|
if (schema.type === "string") {
|
|
@@ -182,7 +225,7 @@ export function coerceAndValidateFormValues(params, values) {
|
|
|
182
225
|
if ("enum" in schema && !schema.enum.includes(stringValue)) {
|
|
183
226
|
throw new Error(`Elicitation field ${name} is not an allowed value`);
|
|
184
227
|
}
|
|
185
|
-
if ("oneOf" in schema && !schema.oneOf.some(option => option.const === stringValue)) {
|
|
228
|
+
if ("oneOf" in schema && !schema.oneOf.some((option) => option.const === stringValue)) {
|
|
186
229
|
throw new Error(`Elicitation field ${name} is not an allowed value`);
|
|
187
230
|
}
|
|
188
231
|
output[name] = stringValue;
|
|
@@ -193,8 +236,9 @@ export function coerceAndValidateFormValues(params, values) {
|
|
|
193
236
|
throw new Error(`Elicitation field ${name} must be a number`);
|
|
194
237
|
}
|
|
195
238
|
const numberValue = typeof value === "number" ? value : Number(value);
|
|
196
|
-
if (!Number.isFinite(numberValue))
|
|
239
|
+
if (!Number.isFinite(numberValue)) {
|
|
197
240
|
throw new Error(`Elicitation field ${name} must be a number`);
|
|
241
|
+
}
|
|
198
242
|
if (schema.type === "integer" && !Number.isInteger(numberValue)) {
|
|
199
243
|
throw new Error(`Elicitation field ${name} must be an integer`);
|
|
200
244
|
}
|
|
@@ -212,9 +256,10 @@ export function coerceAndValidateFormValues(params, values) {
|
|
|
212
256
|
continue;
|
|
213
257
|
}
|
|
214
258
|
if (schema.type === "array") {
|
|
215
|
-
if (!Array.isArray(value))
|
|
259
|
+
if (!Array.isArray(value)) {
|
|
216
260
|
throw new Error(`Elicitation field ${name} must be a list`);
|
|
217
|
-
|
|
261
|
+
}
|
|
262
|
+
const allowed = new Set(extractMultiSelectOptions(schema).map((option) => option.value));
|
|
218
263
|
const arrayValue = value.map(String);
|
|
219
264
|
if (schema.minItems !== undefined && arrayValue.length < schema.minItems) {
|
|
220
265
|
throw new Error(`Elicitation field ${name} has fewer than ${schema.minItems} selections`);
|
|
@@ -222,14 +267,13 @@ export function coerceAndValidateFormValues(params, values) {
|
|
|
222
267
|
if (schema.maxItems !== undefined && arrayValue.length > schema.maxItems) {
|
|
223
268
|
throw new Error(`Elicitation field ${name} has more than ${schema.maxItems} selections`);
|
|
224
269
|
}
|
|
225
|
-
if (arrayValue.some(item => !allowed.has(item))) {
|
|
270
|
+
if (arrayValue.some((item) => !allowed.has(item))) {
|
|
226
271
|
throw new Error(`Elicitation field ${name} contains an invalid selection`);
|
|
227
272
|
}
|
|
228
273
|
output[name] = arrayValue;
|
|
229
274
|
}
|
|
230
275
|
}
|
|
231
|
-
const validation = new AjvJsonSchemaValidator()
|
|
232
|
-
.getValidator(params.requestedSchema)(output);
|
|
276
|
+
const validation = new AjvJsonSchemaValidator().getValidator(params.requestedSchema)(output);
|
|
233
277
|
if (!validation.valid) {
|
|
234
278
|
throw new Error(`Invalid elicitation response: ${validation.errorMessage}`);
|
|
235
279
|
}
|
|
@@ -240,33 +284,39 @@ function formatChoice(value, title) {
|
|
|
240
284
|
}
|
|
241
285
|
function uniqueLabels(labels) {
|
|
242
286
|
const used = new Set();
|
|
243
|
-
return labels.map(label => {
|
|
287
|
+
return labels.map((label) => {
|
|
244
288
|
let unique = label;
|
|
245
|
-
while (used.has(unique))
|
|
289
|
+
while (used.has(unique)) {
|
|
246
290
|
unique += "…";
|
|
291
|
+
}
|
|
247
292
|
used.add(unique);
|
|
248
293
|
return unique;
|
|
249
294
|
});
|
|
250
295
|
}
|
|
251
296
|
function uniqueAction(label, choices) {
|
|
252
297
|
let unique = label;
|
|
253
|
-
while (choices.includes(unique))
|
|
298
|
+
while (choices.includes(unique)) {
|
|
254
299
|
unique += "…";
|
|
300
|
+
}
|
|
255
301
|
return unique;
|
|
256
302
|
}
|
|
257
303
|
function extractMultiSelectOptions(schema) {
|
|
258
304
|
const items = schema.items;
|
|
259
305
|
return items.anyOf
|
|
260
|
-
? items.anyOf.map(option => ({
|
|
261
|
-
|
|
306
|
+
? items.anyOf.map((option) => ({
|
|
307
|
+
value: option.const,
|
|
308
|
+
display: formatChoice(option.const, option.title),
|
|
309
|
+
}))
|
|
310
|
+
: (items.enum ?? []).map((value) => ({ value, display: value }));
|
|
262
311
|
}
|
|
263
312
|
function formatReview(serverName, properties, content) {
|
|
264
313
|
const rows = properties.map(([name, schema]) => `${schema.title ?? humanizeName(name)}: ${content[name] === undefined ? "(omitted)" : String(content[name])}`);
|
|
265
314
|
return [`Review input for ${serverName}`, "", ...rows].join("\n");
|
|
266
315
|
}
|
|
267
316
|
export async function handleUrlElicitation(options, params) {
|
|
268
|
-
if (!options.allowUrl)
|
|
317
|
+
if (!options.allowUrl) {
|
|
269
318
|
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "URL elicitation is not supported");
|
|
319
|
+
}
|
|
270
320
|
let parsed;
|
|
271
321
|
try {
|
|
272
322
|
parsed = new URL(params.url);
|
|
@@ -288,10 +338,12 @@ export async function handleUrlElicitation(options, params) {
|
|
|
288
338
|
"",
|
|
289
339
|
"Open this URL in your browser?",
|
|
290
340
|
].join("\n"), ["Open", "Decline"]);
|
|
291
|
-
if (decision === undefined)
|
|
341
|
+
if (decision === undefined) {
|
|
292
342
|
return { action: "cancel" };
|
|
293
|
-
|
|
343
|
+
}
|
|
344
|
+
if (decision === "Decline") {
|
|
294
345
|
return { action: "decline" };
|
|
346
|
+
}
|
|
295
347
|
try {
|
|
296
348
|
await open(params.url);
|
|
297
349
|
}
|
|
@@ -304,5 +356,8 @@ export async function handleUrlElicitation(options, params) {
|
|
|
304
356
|
return { action: "accept" };
|
|
305
357
|
}
|
|
306
358
|
function humanizeName(name) {
|
|
307
|
-
return name
|
|
359
|
+
return name
|
|
360
|
+
.replace(/[_-]+/g, " ")
|
|
361
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
362
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
308
363
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
declare const actions: readonly ["status", "search", "list", "describe", "call", "connect", "instructions", "resources", "read-resource", "read-result", "auth-start", "auth-complete", "ui-messages"];
|
|
3
3
|
export type GatewayArguments = {
|
|
4
|
-
action: typeof actions[number];
|
|
4
|
+
action: (typeof actions)[number];
|
|
5
5
|
server?: string;
|
|
6
6
|
tool?: string;
|
|
7
7
|
args?: Record<string, unknown>;
|