@fitchmultz/pi-mcp-adapter 5.0.0 → 7.0.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/CHANGELOG.md +50 -0
- package/OAUTH.md +3 -3
- package/README.md +160 -88
- package/dist/app-bridge.bundle.js +75 -33
- package/dist/commands.js +9 -6
- package/dist/config.js +41 -16
- package/dist/direct-tools.d.ts +2 -4
- package/dist/direct-tools.js +21 -319
- package/dist/gateway-arguments.d.ts +33 -0
- package/dist/gateway-arguments.js +63 -0
- package/dist/index.js +181 -276
- package/dist/init.js +6 -20
- package/dist/lifecycle.d.ts +2 -2
- package/dist/lifecycle.js +2 -2
- package/dist/mcp-auth-flow.js +2 -2
- package/dist/mcp-auth.d.ts +1 -1
- package/dist/mcp-auth.js +3 -3
- package/dist/mcp-code.d.ts +7 -6
- package/dist/mcp-code.js +98 -51
- package/dist/mcp-oauth-provider.d.ts +5 -5
- package/dist/mcp-oauth-provider.js +5 -5
- package/dist/mcp-output-guard.d.ts +57 -2
- package/dist/mcp-output-guard.js +129 -24
- package/dist/mcp-panel.d.ts +3 -3
- package/dist/mcp-panel.js +36 -99
- package/dist/mcp-script-worker.mjs +5 -2
- package/dist/mcp-setup-panel.d.ts +5 -4
- package/dist/mcp-setup-panel.js +15 -7
- package/dist/mcp-status.js +2 -1
- package/dist/mcp-trace.d.ts +1 -1
- package/dist/mcp-trace.js +1 -1
- package/dist/metadata-cache.d.ts +1 -1
- package/dist/metadata-cache.js +25 -88
- package/dist/npx-resolver.js +8 -4
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.js +1 -1
- package/dist/proxy-modes.d.ts +41 -7
- package/dist/proxy-modes.js +275 -490
- package/dist/resource-tools.d.ts +15 -0
- package/dist/resource-tools.js +26 -1
- package/dist/sampling-handler.js +5 -1
- package/dist/search-ranking.d.ts +0 -2
- package/dist/search-ranking.js +70 -137
- package/dist/server-manager.d.ts +2 -2
- package/dist/server-manager.js +44 -27
- package/dist/session-recovery.d.ts +7 -4
- package/dist/session-recovery.js +76 -10
- package/dist/state.d.ts +1 -1
- package/dist/tool-loader.d.ts +32 -0
- package/dist/tool-loader.js +152 -0
- package/dist/tool-metadata.d.ts +67 -0
- package/dist/tool-metadata.js +23 -17
- package/dist/tool-registrar.d.ts +4 -4
- package/dist/tool-registrar.js +18 -13
- package/dist/tool-result-renderer.d.ts +6 -3
- package/dist/tool-result-renderer.js +11 -6
- package/dist/types.d.ts +11 -36
- package/dist/ui-resource-handler.d.ts +2 -2
- package/dist/ui-resource-handler.js +2 -2
- package/dist/ui-server.js +4 -3
- package/dist/ui-stream-types.d.ts +8 -8
- package/dist/unix-socket-transport.d.ts +1 -1
- package/dist/unix-socket-transport.js +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +16 -4
- package/package.json +37 -96
- package/skills/mcp-scripting/SKILL.md +67 -20
- package/abort.ts +0 -34
- package/agent-dir.ts +0 -24
- package/app-bridge.bundle.js +0 -67
- package/banner.png +0 -0
- package/checkpoint.ts +0 -55
- package/commands.ts +0 -619
- package/config.ts +0 -1204
- package/consent-manager.ts +0 -25
- package/direct-tools.ts +0 -408
- package/dist/ts-shape.d.ts +0 -2
- package/dist/ts-shape.js +0 -147
- package/elicitation-handler.ts +0 -349
- package/error-signal.ts +0 -21
- package/glimpse-ui.ts +0 -80
- package/host-html-template.ts +0 -456
- package/index.ts +0 -900
- package/init.ts +0 -627
- package/legacy-migration.ts +0 -47
- package/lifecycle.ts +0 -165
- package/logger.ts +0 -127
- package/mcp-auth-flow.ts +0 -975
- package/mcp-auth.ts +0 -793
- package/mcp-callback-server.ts +0 -450
- package/mcp-code.ts +0 -386
- package/mcp-keyring-helper.cjs +0 -89
- package/mcp-oauth-provider.ts +0 -774
- package/mcp-output-guard.ts +0 -412
- package/mcp-panel.ts +0 -978
- package/mcp-script-worker.mjs +0 -128
- package/mcp-setup-panel.ts +0 -666
- package/mcp-status.ts +0 -109
- package/mcp-trace.ts +0 -295
- package/metadata-cache.ts +0 -333
- package/npx-resolver.ts +0 -488
- package/onboarding-state.ts +0 -58
- package/panel-keys.ts +0 -37
- package/prompts.ts +0 -337
- package/proxy-modes.ts +0 -1361
- package/resource-tools.ts +0 -17
- package/runtime-owner.ts +0 -123
- package/sampling-handler.ts +0 -273
- package/scripts/build.mjs +0 -131
- package/scripts/prepare.mjs +0 -102
- package/search-ranking.ts +0 -196
- package/server-manager.ts +0 -1320
- package/session-recovery.ts +0 -316
- package/state.ts +0 -64
- package/tool-approval.ts +0 -78
- package/tool-metadata.ts +0 -235
- package/tool-registrar.ts +0 -69
- package/tool-result-renderer.ts +0 -286
- package/ts-shape.ts +0 -145
- package/tsconfig.build.json +0 -13
- package/tsconfig.json +0 -16
- package/types.ts +0 -810
- package/ui-resource-handler.ts +0 -249
- package/ui-server.ts +0 -808
- package/ui-session.ts +0 -568
- package/ui-stream-types.ts +0 -89
- package/ui-tool-visibility.ts +0 -25
- package/unix-socket-transport.ts +0 -85
- package/utils.ts +0 -309
package/consent-manager.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { logger } from "./logger.ts";
|
|
2
|
-
|
|
3
|
-
export class ConsentManager {
|
|
4
|
-
private decisions = new Map<string, boolean>();
|
|
5
|
-
private log = logger.child({ component: "ConsentManager" });
|
|
6
|
-
|
|
7
|
-
hasDecisions(): boolean { return this.decisions.size > 0; }
|
|
8
|
-
|
|
9
|
-
requiresPrompt(serverName: string): boolean {
|
|
10
|
-
return this.decisions.get(serverName) !== true;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
registerDecision(serverName: string, approved: boolean): void {
|
|
14
|
-
this.decisions.set(serverName, approved);
|
|
15
|
-
this.log.debug(`Consent ${approved ? "granted" : "denied"}`, { server: serverName });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
ensureApproved(serverName: string): void {
|
|
19
|
-
const decision = this.decisions.get(serverName);
|
|
20
|
-
if (decision === true) return;
|
|
21
|
-
throw new Error(decision === false
|
|
22
|
-
? `Tool calls for "${serverName}" were denied for this session`
|
|
23
|
-
: `Tool call approval required for "${serverName}"`);
|
|
24
|
-
}
|
|
25
|
-
}
|
package/direct-tools.ts
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { McpExtensionState } from "./state.ts";
|
|
3
|
-
import type { DirectToolSpec, McpAdapterOptions, McpConfig, ToolPrefix } from "./types.ts";
|
|
4
|
-
import type { MetadataCache } from "./metadata-cache.ts";
|
|
5
|
-
import { lazyConnect, getFailureAgeSeconds, clearFailure, recordFailure, updateStatusBar } from "./init.ts";
|
|
6
|
-
import { abortable, throwIfAborted } from "./abort.ts";
|
|
7
|
-
import { isServerCacheValid, parseDirectToolSelectors } from "./metadata-cache.ts";
|
|
8
|
-
export { getMissingConfiguredDirectToolServers } from "./metadata-cache.ts";
|
|
9
|
-
import { runToolCall } from "./proxy-modes.ts";
|
|
10
|
-
import { formatToolName, isServerDisabled, isNonInteractiveOAuth, isToolAllowed, resolveToolPrefix } from "./types.ts";
|
|
11
|
-
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
12
|
-
import { authenticate, supportsOAuth } from "./mcp-auth-flow.ts";
|
|
13
|
-
import { formatAuthRequiredMessage, resolveServerUrl } from "./utils.ts";
|
|
14
|
-
import { SessionRecoveryAuthRequiredError, type SessionRecoveryDeps } from "./session-recovery.ts";
|
|
15
|
-
import { combineAbortSignals, isAbortError } from "./runtime-owner.ts";
|
|
16
|
-
import { ensureToolCallApproved } from "./tool-approval.ts";
|
|
17
|
-
|
|
18
|
-
const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
19
|
-
export const DIRECT_TOOLS_ADVISORY_THRESHOLD = 75;
|
|
20
|
-
const advisedDirectToolSets = new Set<string>();
|
|
21
|
-
|
|
22
|
-
type DirectAutoAuthResult =
|
|
23
|
-
| { status: "skipped" }
|
|
24
|
-
| { status: "success" }
|
|
25
|
-
| { status: "failed"; message: string };
|
|
26
|
-
|
|
27
|
-
function getDirectAuthRequiredMessage(
|
|
28
|
-
state: McpExtensionState,
|
|
29
|
-
serverName: string,
|
|
30
|
-
defaultMessage = `MCP server "${serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`,
|
|
31
|
-
): string {
|
|
32
|
-
const oauth = state.config.mcpServers[serverName]?.oauth;
|
|
33
|
-
if (oauth && oauth.crossAppAccess) defaultMessage = `MCP server "${serverName}" requires enterprise authorization. Check oauth.crossAppAccess IdP configuration and ID-token source, then retry.`;
|
|
34
|
-
return formatAuthRequiredMessage(state.config, serverName, defaultMessage);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function getDirectAuthFailedMessage(state: McpExtensionState, serverName: string, message: string): string {
|
|
38
|
-
const customGuidance = state.config.settings?.authRequiredMessage;
|
|
39
|
-
const oauth = state.config.mcpServers[serverName]?.oauth;
|
|
40
|
-
if (customGuidance || (oauth && oauth.crossAppAccess)) {
|
|
41
|
-
return `OAuth authentication failed for "${serverName}": ${message}. ${getDirectAuthRequiredMessage(state, serverName)}`;
|
|
42
|
-
}
|
|
43
|
-
return `OAuth authentication failed for "${serverName}": ${message}. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function attemptDirectAutoAuth(
|
|
47
|
-
state: McpExtensionState,
|
|
48
|
-
serverName: string,
|
|
49
|
-
signal?: AbortSignal,
|
|
50
|
-
): Promise<DirectAutoAuthResult> {
|
|
51
|
-
if (state.config.settings?.autoAuth !== true) {
|
|
52
|
-
return { status: "skipped" };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const definition = state.config.mcpServers[serverName];
|
|
56
|
-
if (!definition || isServerDisabled(definition) || !supportsOAuth(definition)) {
|
|
57
|
-
return { status: "skipped" };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
let serverUrl: string | undefined;
|
|
61
|
-
try {
|
|
62
|
-
serverUrl = resolveServerUrl(definition);
|
|
63
|
-
} catch (error) {
|
|
64
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
-
return { status: "failed", message: getDirectAuthFailedMessage(state, serverName, message) };
|
|
66
|
-
}
|
|
67
|
-
if (!serverUrl) {
|
|
68
|
-
return { status: "skipped" };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (!state.ui && !isNonInteractiveOAuth(definition.oauth)) {
|
|
72
|
-
return {
|
|
73
|
-
status: "failed",
|
|
74
|
-
message: getDirectAuthRequiredMessage(
|
|
75
|
-
state,
|
|
76
|
-
serverName,
|
|
77
|
-
`MCP server "${serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`,
|
|
78
|
-
),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
if (state.authStorageOptions) {
|
|
84
|
-
await authenticate(
|
|
85
|
-
serverName,
|
|
86
|
-
serverUrl,
|
|
87
|
-
definition,
|
|
88
|
-
signal
|
|
89
|
-
? { authStorageOptions: state.authStorageOptions, signal, runtime: state.oauthRuntime }
|
|
90
|
-
: { authStorageOptions: state.authStorageOptions, runtime: state.oauthRuntime },
|
|
91
|
-
);
|
|
92
|
-
} else {
|
|
93
|
-
await authenticate(serverName, serverUrl, definition, {
|
|
94
|
-
...(signal ? { signal } : {}),
|
|
95
|
-
runtime: state.oauthRuntime,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
return { status: "success" };
|
|
99
|
-
} catch (error) {
|
|
100
|
-
if (isAbortError(error, signal)) throw error;
|
|
101
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
102
|
-
return {
|
|
103
|
-
status: "failed",
|
|
104
|
-
message: getDirectAuthFailedMessage(state, serverName, message),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function resolveDirectTools(
|
|
110
|
-
config: McpConfig,
|
|
111
|
-
cache: MetadataCache | null,
|
|
112
|
-
prefix: ToolPrefix,
|
|
113
|
-
envOverride?: string[],
|
|
114
|
-
): DirectToolSpec[] {
|
|
115
|
-
const specs: DirectToolSpec[] = [];
|
|
116
|
-
if (!cache) return specs;
|
|
117
|
-
|
|
118
|
-
const seenNames = new Set<string>();
|
|
119
|
-
|
|
120
|
-
const envSelection = envOverride ? parseDirectToolSelectors(envOverride) : null;
|
|
121
|
-
const globalDirect = config.settings?.directTools;
|
|
122
|
-
|
|
123
|
-
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
124
|
-
if (isServerDisabled(definition)) continue;
|
|
125
|
-
const serverCache = cache.servers[serverName];
|
|
126
|
-
if (!serverCache || !isServerCacheValid(serverCache, definition)) continue;
|
|
127
|
-
|
|
128
|
-
let toolFilter: true | string[] | false = false;
|
|
129
|
-
|
|
130
|
-
if (envSelection) {
|
|
131
|
-
if (envSelection.servers.has(serverName)) {
|
|
132
|
-
toolFilter = true;
|
|
133
|
-
} else if (envSelection.tools.has(serverName)) {
|
|
134
|
-
toolFilter = [...envSelection.tools.get(serverName)!];
|
|
135
|
-
}
|
|
136
|
-
} else {
|
|
137
|
-
if (definition.directTools !== undefined) {
|
|
138
|
-
toolFilter = definition.directTools;
|
|
139
|
-
} else if (globalDirect) {
|
|
140
|
-
toolFilter = globalDirect;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!toolFilter) continue;
|
|
145
|
-
|
|
146
|
-
const effectivePrefix = resolveToolPrefix(definition, prefix);
|
|
147
|
-
|
|
148
|
-
for (const tool of serverCache.tools ?? []) {
|
|
149
|
-
if (toolFilter !== true && !toolFilter.includes(tool.name)) continue;
|
|
150
|
-
if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) continue;
|
|
151
|
-
const prefixedName = formatToolName(tool.name, serverName, effectivePrefix);
|
|
152
|
-
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
153
|
-
console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (seenNames.has(prefixedName)) {
|
|
157
|
-
console.warn(`MCP: skipping duplicate direct tool "${prefixedName}" from "${serverName}"`);
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
seenNames.add(prefixedName);
|
|
161
|
-
specs.push({
|
|
162
|
-
serverName,
|
|
163
|
-
originalName: tool.name,
|
|
164
|
-
prefixedName,
|
|
165
|
-
description: tool.description ?? "",
|
|
166
|
-
...(tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}),
|
|
167
|
-
...(tool.annotations !== undefined ? { annotations: tool.annotations } : {}),
|
|
168
|
-
...(tool.uiResourceUri !== undefined ? { uiResourceUri: tool.uiResourceUri } : {}),
|
|
169
|
-
...(tool.uiStreamMode !== undefined ? { uiStreamMode: tool.uiStreamMode } : {}),
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (definition.exposeResources !== false) {
|
|
174
|
-
for (const resource of serverCache.resources ?? []) {
|
|
175
|
-
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
176
|
-
if (toolFilter !== true && !toolFilter.includes(baseName)) continue;
|
|
177
|
-
if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) continue;
|
|
178
|
-
const prefixedName = formatToolName(baseName, serverName, effectivePrefix);
|
|
179
|
-
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
180
|
-
console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
if (seenNames.has(prefixedName)) {
|
|
184
|
-
console.warn(`MCP: skipping duplicate direct resource tool "${prefixedName}" from "${serverName}"`);
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
seenNames.add(prefixedName);
|
|
188
|
-
specs.push({
|
|
189
|
-
serverName,
|
|
190
|
-
originalName: baseName,
|
|
191
|
-
prefixedName,
|
|
192
|
-
description: resource.description ?? `Read resource: ${resource.uri}`,
|
|
193
|
-
resourceUri: resource.uri,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const advisoryKey = specs.map(spec => spec.prefixedName).sort().join("\0");
|
|
200
|
-
if (specs.length >= DIRECT_TOOLS_ADVISORY_THRESHOLD && !advisedDirectToolSets.has(advisoryKey)) {
|
|
201
|
-
advisedDirectToolSets.add(advisoryKey);
|
|
202
|
-
console.warn(`MCP: ${specs.length} direct tools resolved. Each direct tool adds prompt context; README guidance recommends targeted sets of 5-20 tools and using the proxy or an explicit string[] when 75+ direct tools would be registered.`);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return specs;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function buildProxyDescription(config: McpConfig): string {
|
|
209
|
-
let desc = `MCP gateway — server status, tool search/describe, auth, and single MCP tool calls. When one request needs several MCP calls with logic between them, use mcp_script. Non-MCP Pi tools should be called directly, not through mcp.\n`;
|
|
210
|
-
|
|
211
|
-
// Catalog counts and instruction previews change on connect and break prompt caching.
|
|
212
|
-
// Keep this directory config-only; discovery and instructions belong in tool results.
|
|
213
|
-
const enabledServers = Object.entries(config.mcpServers)
|
|
214
|
-
.filter(([, definition]) => !isServerDisabled(definition))
|
|
215
|
-
.map(([serverName]) => serverName)
|
|
216
|
-
.sort();
|
|
217
|
-
if (enabledServers.length > 0) {
|
|
218
|
-
desc += `\nConfigured servers (call mcp({}) for live status): ${enabledServers.join(", ")}\n`;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const disabledServers = Object.entries(config.mcpServers)
|
|
222
|
-
.filter(([, definition]) => isServerDisabled(definition))
|
|
223
|
-
.map(([serverName]) => serverName);
|
|
224
|
-
if (disabledServers.length > 0) {
|
|
225
|
-
desc += `\nDisabled servers (enable with /mcp enable <server> and /reload): ${disabledServers.join(", ")}\n`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
desc += `\nUsage:\n`;
|
|
229
|
-
desc += ` mcp({ }) → Show server status\n`;
|
|
230
|
-
desc += ` mcp({ server: "name", limit: 12 }) → Browse a server's tools (use offset for more)\n`;
|
|
231
|
-
desc += ` mcp({ search: "query" }) → Search MCP tools by name/description\n`;
|
|
232
|
-
desc += ` mcp({ describe: "tool_name" }) → Show tool details and parameters\n`;
|
|
233
|
-
desc += ` mcp({ instructions: "name" }) → Show full server usage instructions\n`;
|
|
234
|
-
desc += ` mcp({ connect: "server-name" }) → Connect to a server and refresh metadata\n`;
|
|
235
|
-
desc += ` mcp({ tool: "name", args: { key: "value" } }) → Call a tool (object args; JSON string also accepted)\n`;
|
|
236
|
-
desc += ` mcp({ action: "ui-messages" }) → Retrieve accumulated messages from completed UI sessions\n`;
|
|
237
|
-
desc += ` mcp({ action: "auth-start", server: "name" }) → Start manual OAuth and get a browser URL\n`;
|
|
238
|
-
desc += ` mcp({ action: "auth-complete", server: "name", args: { redirectUrl: "..." } }) → Complete manual OAuth\n`;
|
|
239
|
-
desc += `\nMode: action > tool (call) > connect > describe > instructions > search > server (list) > nothing (status)`;
|
|
240
|
-
|
|
241
|
-
return desc;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
type DirectToolExecute = (
|
|
245
|
-
toolCallId: string,
|
|
246
|
-
params: Record<string, unknown>,
|
|
247
|
-
signal: AbortSignal | undefined,
|
|
248
|
-
onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined,
|
|
249
|
-
ctx: ExtensionContext,
|
|
250
|
-
) => Promise<AgentToolResult<Record<string, unknown>>>;
|
|
251
|
-
|
|
252
|
-
export function createDirectToolExecutor(
|
|
253
|
-
getState: () => McpExtensionState | null,
|
|
254
|
-
getInitPromise: () => Promise<McpExtensionState> | null,
|
|
255
|
-
spec: DirectToolSpec,
|
|
256
|
-
beforeExecute?: McpAdapterOptions["beforeExecute"],
|
|
257
|
-
): DirectToolExecute {
|
|
258
|
-
return async function execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
259
|
-
throwIfAborted(signal);
|
|
260
|
-
let state = getState();
|
|
261
|
-
const initPromise = getInitPromise();
|
|
262
|
-
|
|
263
|
-
if (!state && initPromise) {
|
|
264
|
-
try {
|
|
265
|
-
state = await abortable(initPromise, signal);
|
|
266
|
-
} catch (error) {
|
|
267
|
-
throwIfAborted(signal);
|
|
268
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
269
|
-
return {
|
|
270
|
-
content: [{ type: "text" as const, text: `MCP initialization failed: ${message}` }],
|
|
271
|
-
details: { error: "init_failed", message },
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
if (!state) {
|
|
276
|
-
return {
|
|
277
|
-
content: [{ type: "text" as const, text: "MCP not initialized" }],
|
|
278
|
-
details: { error: "not_initialized" },
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const definition = state.config.mcpServers[spec.serverName];
|
|
283
|
-
if (isServerDisabled(definition)) {
|
|
284
|
-
const message = `MCP server "${spec.serverName}" is disabled. Run /mcp enable ${spec.serverName} and /reload to enable it.`;
|
|
285
|
-
return {
|
|
286
|
-
content: [{ type: "text" as const, text: message }],
|
|
287
|
-
details: { error: "server_disabled", server: spec.serverName, message },
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
292
|
-
throwIfAborted(ownedSignal);
|
|
293
|
-
let connected = await lazyConnect(state, spec.serverName, ownedSignal);
|
|
294
|
-
let autoAuthAttempted = false;
|
|
295
|
-
|
|
296
|
-
const needsAuthConnection = state.manager.getConnection(spec.serverName);
|
|
297
|
-
if (!connected && needsAuthConnection?.status === "needs-auth") {
|
|
298
|
-
autoAuthAttempted = true;
|
|
299
|
-
const autoAuth = await attemptDirectAutoAuth(state, spec.serverName, ownedSignal);
|
|
300
|
-
if (autoAuth.status === "failed") {
|
|
301
|
-
return {
|
|
302
|
-
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
303
|
-
details: { error: "auth_required", server: spec.serverName, message: autoAuth.message },
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
if (autoAuth.status === "success") {
|
|
307
|
-
clearFailure(state, spec.serverName);
|
|
308
|
-
try {
|
|
309
|
-
const liveDefinition = state.config.mcpServers[spec.serverName];
|
|
310
|
-
if (liveDefinition) {
|
|
311
|
-
await state.manager.reconnect(spec.serverName, liveDefinition, needsAuthConnection, ownedSignal);
|
|
312
|
-
connected = true;
|
|
313
|
-
}
|
|
314
|
-
} catch (error) {
|
|
315
|
-
if (isAbortError(error, ownedSignal)) throwIfAborted(ownedSignal);
|
|
316
|
-
recordFailure(state, spec.serverName, error instanceof Error ? error.message : String(error));
|
|
317
|
-
updateStatusBar(state);
|
|
318
|
-
}
|
|
319
|
-
if (connected) connected = await lazyConnect(state, spec.serverName, ownedSignal);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (!connected) {
|
|
324
|
-
const authConnection = state.manager.getConnection(spec.serverName);
|
|
325
|
-
if (authConnection?.status === "needs-auth") {
|
|
326
|
-
const message = getDirectAuthRequiredMessage(state, spec.serverName);
|
|
327
|
-
return {
|
|
328
|
-
content: [{ type: "text" as const, text: message }],
|
|
329
|
-
details: { error: "auth_required", server: spec.serverName, message, autoAuthAttempted },
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
const failedAgo = getFailureAgeSeconds(state, spec.serverName);
|
|
333
|
-
return {
|
|
334
|
-
content: [{ type: "text" as const, text: `MCP server "${spec.serverName}" not available${failedAgo !== null ? ` (failed ${failedAgo}s ago)` : ""}` }],
|
|
335
|
-
details: { error: "server_unavailable", server: spec.serverName },
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const connection = state.manager.getConnection(spec.serverName);
|
|
340
|
-
if (!connection || connection.status !== "connected") {
|
|
341
|
-
return {
|
|
342
|
-
content: [{ type: "text" as const, text: `MCP server "${spec.serverName}" not connected` }],
|
|
343
|
-
details: { error: "not_connected", server: spec.serverName },
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const approval = await ensureToolCallApproved(state, spec.serverName, {
|
|
348
|
-
name: spec.prefixedName,
|
|
349
|
-
originalName: spec.originalName,
|
|
350
|
-
description: spec.description,
|
|
351
|
-
...(spec.inputSchema !== undefined ? { inputSchema: spec.inputSchema } : {}),
|
|
352
|
-
...(spec.resourceUri !== undefined ? { resourceUri: spec.resourceUri } : {}),
|
|
353
|
-
...(spec.uiResourceUri !== undefined ? { uiResourceUri: spec.uiResourceUri } : {}),
|
|
354
|
-
...(spec.uiStreamMode !== undefined ? { uiStreamMode: spec.uiStreamMode } : {}),
|
|
355
|
-
}, params, ownedSignal);
|
|
356
|
-
if (approval.ok === false) {
|
|
357
|
-
const denied = approval.reason === "denied";
|
|
358
|
-
const message = denied
|
|
359
|
-
? `The user declined approval to run MCP tool "${spec.originalName}" on server "${spec.serverName}".`
|
|
360
|
-
: `MCP tool "${spec.originalName}" on server "${spec.serverName}" is approval-gated and requires an interactive session.`;
|
|
361
|
-
return {
|
|
362
|
-
content: [{ type: "text" as const, text: message }],
|
|
363
|
-
details: {
|
|
364
|
-
error: denied ? "approval_denied" : "approval_required",
|
|
365
|
-
server: spec.serverName,
|
|
366
|
-
tool: spec.originalName,
|
|
367
|
-
},
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const recoverAuthConnection: NonNullable<SessionRecoveryDeps["onNeedsAuth"]> = async (_serverName, recoverySignal = ownedSignal, challenge) => {
|
|
372
|
-
throwIfAborted(recoverySignal);
|
|
373
|
-
const current = state.manager.getConnection(spec.serverName);
|
|
374
|
-
if (current?.status === "connected" && current !== challenge?.connection) return current;
|
|
375
|
-
|
|
376
|
-
if (!autoAuthAttempted) {
|
|
377
|
-
autoAuthAttempted = true;
|
|
378
|
-
const autoAuth = await attemptDirectAutoAuth(state, spec.serverName, recoverySignal);
|
|
379
|
-
throwIfAborted(recoverySignal);
|
|
380
|
-
if (autoAuth.status === "failed") {
|
|
381
|
-
throw new SessionRecoveryAuthRequiredError(spec.serverName, autoAuth.message);
|
|
382
|
-
}
|
|
383
|
-
if (autoAuth.status === "success") {
|
|
384
|
-
const liveDefinition = state.config.mcpServers[spec.serverName];
|
|
385
|
-
if (!current || !liveDefinition) return undefined;
|
|
386
|
-
await state.manager.reconnect(spec.serverName, liveDefinition, current, recoverySignal);
|
|
387
|
-
clearFailure(state, spec.serverName);
|
|
388
|
-
const reconnected = await lazyConnect(state, spec.serverName, recoverySignal);
|
|
389
|
-
return reconnected ? state.manager.getConnection(spec.serverName) : undefined;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return challenge ? undefined : state.manager.getConnection(spec.serverName);
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
return runToolCall(state, spec.serverName, spec, params, {
|
|
396
|
-
toolCallId,
|
|
397
|
-
...(beforeExecute ? { beforeDispatch: (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation) } : {}),
|
|
398
|
-
detailsBase: spec.resourceUri
|
|
399
|
-
? { server: spec.serverName, resourceUri: spec.resourceUri }
|
|
400
|
-
: { server: spec.serverName, tool: spec.originalName },
|
|
401
|
-
ownedSignal,
|
|
402
|
-
signal,
|
|
403
|
-
recoverAuthConnection,
|
|
404
|
-
authRequiredMessage: () => getDirectAuthRequiredMessage(state, spec.serverName),
|
|
405
|
-
autoAuthAttempted: () => autoAuthAttempted,
|
|
406
|
-
});
|
|
407
|
-
};
|
|
408
|
-
}
|
package/dist/ts-shape.d.ts
DELETED
package/dist/ts-shape.js
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
const UNSUPPORTED_KEYWORDS = ["if", "then", "else", "allOf", "not", "patternProperties", "additionalProperties"];
|
|
2
|
-
/** Renders the useful JSON Schema subset as TypeScript, or null for unsupported schemas. */
|
|
3
|
-
export function renderTsShape(inputSchema) {
|
|
4
|
-
try {
|
|
5
|
-
if (!isSchema(inputSchema))
|
|
6
|
-
return null;
|
|
7
|
-
const definitions = new Map();
|
|
8
|
-
for (const key of ["$defs", "definitions"]) {
|
|
9
|
-
const rawDefinitions = inputSchema[key];
|
|
10
|
-
if (rawDefinitions === undefined)
|
|
11
|
-
continue;
|
|
12
|
-
if (!isSchema(rawDefinitions))
|
|
13
|
-
return null;
|
|
14
|
-
for (const [name, definition] of Object.entries(rawDefinitions)) {
|
|
15
|
-
if (!isSchema(definition))
|
|
16
|
-
return null;
|
|
17
|
-
definitions.set(`${key}/${decodePointerToken(name)}`, definition);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
const aliases = new Map();
|
|
21
|
-
const usedAliases = new Set();
|
|
22
|
-
let aliasIndex = 0;
|
|
23
|
-
const aliasFor = (definitionKey) => {
|
|
24
|
-
let alias = aliases.get(definitionKey);
|
|
25
|
-
if (alias)
|
|
26
|
-
return alias;
|
|
27
|
-
const name = definitionKey.slice(definitionKey.indexOf("/") + 1);
|
|
28
|
-
alias = /^[A-Za-z_$][\w$]*$/.test(name) && !usedAliases.has(name) ? name : `Definition${++aliasIndex}`;
|
|
29
|
-
while (usedAliases.has(alias))
|
|
30
|
-
alias = `Definition${++aliasIndex}`;
|
|
31
|
-
aliases.set(definitionKey, alias);
|
|
32
|
-
usedAliases.add(alias);
|
|
33
|
-
return alias;
|
|
34
|
-
};
|
|
35
|
-
const render = (schema) => {
|
|
36
|
-
if (!isSchema(schema) || hasUnsupportedKeyword(schema))
|
|
37
|
-
return null;
|
|
38
|
-
if ("$ref" in schema) {
|
|
39
|
-
if (typeof schema.$ref !== "string")
|
|
40
|
-
return null;
|
|
41
|
-
const match = schema.$ref.match(/^#\/(\$defs|definitions)\/([^/]+)$/);
|
|
42
|
-
if (!match)
|
|
43
|
-
return null;
|
|
44
|
-
const definitionGroup = match[1];
|
|
45
|
-
const definitionName = match[2];
|
|
46
|
-
if (definitionGroup === undefined || definitionName === undefined)
|
|
47
|
-
return null;
|
|
48
|
-
const definitionKey = `${definitionGroup}/${decodePointerToken(definitionName)}`;
|
|
49
|
-
if (!definitions.has(definitionKey))
|
|
50
|
-
return null;
|
|
51
|
-
return aliasFor(definitionKey);
|
|
52
|
-
}
|
|
53
|
-
if (Array.isArray(schema.enum)) {
|
|
54
|
-
const values = schema.enum.map(renderLiteral);
|
|
55
|
-
return values.every((value) => value !== null) ? values.join(" | ") : null;
|
|
56
|
-
}
|
|
57
|
-
if (Object.hasOwn(schema, "const"))
|
|
58
|
-
return renderLiteral(schema.const);
|
|
59
|
-
if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {
|
|
60
|
-
const variants = (schema.anyOf ?? schema.oneOf);
|
|
61
|
-
if (variants.length === 0)
|
|
62
|
-
return null;
|
|
63
|
-
const rendered = variants.map(render);
|
|
64
|
-
return rendered.every((value) => value !== null) ? rendered.join(" | ") : null;
|
|
65
|
-
}
|
|
66
|
-
if (schema.type === "object" || schema.properties !== undefined) {
|
|
67
|
-
if (schema.properties === undefined)
|
|
68
|
-
return "{}";
|
|
69
|
-
if (!isSchema(schema.properties))
|
|
70
|
-
return null;
|
|
71
|
-
const required = new Set(Array.isArray(schema.required)
|
|
72
|
-
? schema.required.filter((name) => typeof name === "string")
|
|
73
|
-
: []);
|
|
74
|
-
const properties = [];
|
|
75
|
-
for (const [name, property] of Object.entries(schema.properties)) {
|
|
76
|
-
const rendered = render(property);
|
|
77
|
-
if (rendered === null)
|
|
78
|
-
return null;
|
|
79
|
-
properties.push(`${formatPropertyName(name)}${required.has(name) ? "" : "?"}: ${rendered};`);
|
|
80
|
-
}
|
|
81
|
-
return properties.length === 0 ? "{}" : `{ ${properties.join(" ")} }`;
|
|
82
|
-
}
|
|
83
|
-
if (schema.type === "array") {
|
|
84
|
-
if (schema.items === undefined)
|
|
85
|
-
return "unknown[]";
|
|
86
|
-
const item = render(schema.items);
|
|
87
|
-
return item === null ? null : `${needsParentheses(item) ? `(${item})` : item}[]`;
|
|
88
|
-
}
|
|
89
|
-
if (Array.isArray(schema.type)) {
|
|
90
|
-
const types = schema.type.map(renderType);
|
|
91
|
-
return types.every((type) => type !== null) ? types.join(" | ") : null;
|
|
92
|
-
}
|
|
93
|
-
if (typeof schema.type === "string")
|
|
94
|
-
return renderType(schema.type);
|
|
95
|
-
return "unknown";
|
|
96
|
-
};
|
|
97
|
-
const root = render(inputSchema);
|
|
98
|
-
if (root === null)
|
|
99
|
-
return null;
|
|
100
|
-
const definitionsText = [];
|
|
101
|
-
for (const [key, alias] of aliases) {
|
|
102
|
-
const definition = definitions.get(key);
|
|
103
|
-
if (!definition)
|
|
104
|
-
return null;
|
|
105
|
-
const rendered = render(definition);
|
|
106
|
-
if (rendered === null)
|
|
107
|
-
return null;
|
|
108
|
-
definitionsText.push(`type ${alias} = ${rendered};`);
|
|
109
|
-
}
|
|
110
|
-
return definitionsText.length > 0 ? `${definitionsText.join("\n")}\n\n${root}` : root;
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
function isSchema(value) {
|
|
117
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
118
|
-
}
|
|
119
|
-
function hasUnsupportedKeyword(schema) {
|
|
120
|
-
return UNSUPPORTED_KEYWORDS.some(keyword => Object.hasOwn(schema, keyword));
|
|
121
|
-
}
|
|
122
|
-
function decodePointerToken(token) {
|
|
123
|
-
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
124
|
-
}
|
|
125
|
-
function renderType(type) {
|
|
126
|
-
switch (type) {
|
|
127
|
-
case "string": return "string";
|
|
128
|
-
case "number":
|
|
129
|
-
case "integer": return "number";
|
|
130
|
-
case "boolean": return "boolean";
|
|
131
|
-
case "null": return "null";
|
|
132
|
-
case "object": return "{}";
|
|
133
|
-
case "array": return "unknown[]";
|
|
134
|
-
default: return null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function renderLiteral(value) {
|
|
138
|
-
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
139
|
-
return JSON.stringify(value);
|
|
140
|
-
return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
|
|
141
|
-
}
|
|
142
|
-
function formatPropertyName(name) {
|
|
143
|
-
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
|
|
144
|
-
}
|
|
145
|
-
function needsParentheses(type) {
|
|
146
|
-
return type.includes(" | ");
|
|
147
|
-
}
|