pi-mcp-adapter 2.24.0 → 2.26.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 +26 -0
- package/README.md +12 -2
- package/agent-dir.ts +12 -4
- package/agent-plugin-loader.ts +1 -1
- package/cli.js +25 -4
- package/commands.ts +1 -2
- package/config.ts +4 -4
- package/direct-tools.ts +58 -8
- package/host-html-template.ts +0 -8
- package/index.ts +8 -3
- package/init.ts +29 -5
- package/lifecycle.ts +0 -4
- package/mcp-auth-flow.ts +1 -2
- package/mcp-panel.ts +45 -9
- package/mcp-script-worker.mjs +22 -20
- package/mcp-setup-panel.ts +2 -1
- package/metadata-cache.ts +37 -4
- package/package.json +2 -1
- package/proxy-modes.ts +101 -23
- package/request-headers-command.ts +336 -0
- package/server-manager.ts +5 -0
- package/tool-approval.ts +48 -7
- package/tool-metadata.ts +62 -4
- package/tool-result-renderer.ts +138 -12
- package/types.ts +158 -20
- package/ui-server.ts +20 -1
package/mcp-panel.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { copyToClipboard } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { createPanelKeys, type PanelKeybindings, type PanelKeys } from "./panel-keys.ts";
|
|
4
|
-
import { isServerDisabled, isToolAllowed } from "./types.ts";
|
|
4
|
+
import { getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.ts";
|
|
5
5
|
import type { McpConfig, McpPanelCallbacks, McpPanelResult, ServerProvenance, ToolPrefix } from "./types.ts";
|
|
6
6
|
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
7
7
|
import { sanitizeTerminalText, stripOscSequences } from "./utils.ts";
|
|
8
|
-
import type
|
|
8
|
+
import { isServerCacheValid, type MetadataCache, type ServerCacheEntry, type CachedTool } from "./metadata-cache.ts";
|
|
9
|
+
import { isUiToolVisibleToModel } from "./ui-tool-visibility.ts";
|
|
9
10
|
|
|
10
11
|
interface PanelTheme {
|
|
11
12
|
border: string;
|
|
@@ -170,8 +171,8 @@ class McpPanel {
|
|
|
170
171
|
private static readonly INACTIVITY_MS = 60_000;
|
|
171
172
|
|
|
172
173
|
constructor(
|
|
173
|
-
config: McpConfig,
|
|
174
|
-
cache: MetadataCache | null,
|
|
174
|
+
private config: McpConfig,
|
|
175
|
+
private cache: MetadataCache | null,
|
|
175
176
|
provenance: Map<string, ServerProvenance>,
|
|
176
177
|
private callbacks: McpPanelCallbacks,
|
|
177
178
|
tui: { requestRender(): void },
|
|
@@ -187,7 +188,8 @@ class McpPanel {
|
|
|
187
188
|
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
188
189
|
if (this.authOnly && !callbacks.canAuthenticate(serverName)) continue;
|
|
189
190
|
const prov = provenance.get(serverName);
|
|
190
|
-
const
|
|
191
|
+
const cachedEntry = this.cache?.servers?.[serverName];
|
|
192
|
+
const serverCache = cachedEntry && isServerCacheValid(cachedEntry, definition) ? cachedEntry : undefined;
|
|
191
193
|
|
|
192
194
|
const globalDirect = config.settings?.directTools;
|
|
193
195
|
let toolFilter: true | string[] | false = false;
|
|
@@ -200,7 +202,8 @@ class McpPanel {
|
|
|
200
202
|
const tools: ToolState[] = [];
|
|
201
203
|
if (serverCache && !this.authOnly && !isServerDisabled(definition)) {
|
|
202
204
|
for (const tool of serverCache.tools ?? []) {
|
|
203
|
-
if (!
|
|
205
|
+
if (!isUiToolVisibleToModel(tool.uiVisibility)) continue;
|
|
206
|
+
if (!isToolAllowed(tool.name, serverName, this.prefix, definition.includeTools, definition.excludeTools, this.getOtherCurrentCandidates(serverName, definition, serverCache, tool.name))) {
|
|
204
207
|
continue;
|
|
205
208
|
}
|
|
206
209
|
|
|
@@ -216,7 +219,7 @@ class McpPanel {
|
|
|
216
219
|
if (definition.exposeResources !== false) {
|
|
217
220
|
for (const resource of serverCache.resources ?? []) {
|
|
218
221
|
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
219
|
-
if (!isToolAllowed(baseName, serverName, this.prefix, definition.includeTools, definition.excludeTools)) {
|
|
222
|
+
if (!isToolAllowed(baseName, serverName, this.prefix, definition.includeTools, definition.excludeTools, this.getOtherCurrentCandidates(serverName, definition, serverCache, baseName))) {
|
|
220
223
|
continue;
|
|
221
224
|
}
|
|
222
225
|
|
|
@@ -563,6 +566,8 @@ class McpPanel {
|
|
|
563
566
|
if (server.connectionStatus === "connected") {
|
|
564
567
|
const entry = this.callbacks.refreshCacheAfterReconnect(server.name);
|
|
565
568
|
if (entry) {
|
|
569
|
+
this.cache ??= { version: 1, servers: {} };
|
|
570
|
+
this.cache.servers[server.name] = entry;
|
|
566
571
|
this.rebuildServerTools(server, entry);
|
|
567
572
|
}
|
|
568
573
|
server.hasCachedData = true;
|
|
@@ -636,13 +641,44 @@ class McpPanel {
|
|
|
636
641
|
this.cursorIndex = Math.max(0, Math.min(this.visibleItems.length - 1, this.cursorIndex + delta));
|
|
637
642
|
}
|
|
638
643
|
|
|
644
|
+
private getOtherCurrentCandidates(
|
|
645
|
+
serverName: string,
|
|
646
|
+
definition: McpConfig["mcpServers"][string],
|
|
647
|
+
currentEntry: ServerCacheEntry,
|
|
648
|
+
toolName: string,
|
|
649
|
+
): Set<string> {
|
|
650
|
+
const candidates = new Set<string>();
|
|
651
|
+
for (const [otherServerName, otherDefinition] of Object.entries(this.config.mcpServers)) {
|
|
652
|
+
if (isServerDisabled(otherDefinition)) continue;
|
|
653
|
+
const cachedEntry = this.cache?.servers?.[otherServerName];
|
|
654
|
+
const entry = otherServerName === serverName
|
|
655
|
+
? currentEntry
|
|
656
|
+
: cachedEntry && isServerCacheValid(cachedEntry, otherDefinition) ? cachedEntry : undefined;
|
|
657
|
+
if (!entry) continue;
|
|
658
|
+
const otherPrefix = resolveToolPrefix(otherDefinition, this.prefix);
|
|
659
|
+
for (const tool of entry.tools ?? []) {
|
|
660
|
+
if (!isUiToolVisibleToModel(tool.uiVisibility)) continue;
|
|
661
|
+
for (const candidate of getToolNameCandidates(tool.name, otherServerName, otherPrefix, false)) candidates.add(candidate);
|
|
662
|
+
}
|
|
663
|
+
if (otherDefinition.exposeResources !== false) {
|
|
664
|
+
for (const resource of entry.resources ?? []) {
|
|
665
|
+
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
666
|
+
for (const candidate of getToolNameCandidates(baseName, otherServerName, otherPrefix, false)) candidates.add(candidate);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
for (const candidate of getToolNameCandidates(toolName, serverName, resolveToolPrefix(definition, this.prefix), false)) candidates.delete(candidate);
|
|
671
|
+
return candidates;
|
|
672
|
+
}
|
|
673
|
+
|
|
639
674
|
private rebuildServerTools(server: ServerState, entry: ServerCacheEntry): void {
|
|
640
675
|
const existingState = new Map<string, boolean>();
|
|
641
676
|
for (const t of server.tools) existingState.set(t.name, t.isDirect);
|
|
642
677
|
|
|
643
678
|
const newTools: ToolState[] = [];
|
|
644
679
|
for (const tool of entry.tools ?? []) {
|
|
645
|
-
if (!
|
|
680
|
+
if (!isUiToolVisibleToModel(tool.uiVisibility)) continue;
|
|
681
|
+
if (!isToolAllowed(tool.name, server.name, this.prefix, server.includeTools, server.excludeTools, this.getOtherCurrentCandidates(server.name, server, entry, tool.name))) {
|
|
646
682
|
continue;
|
|
647
683
|
}
|
|
648
684
|
|
|
@@ -660,7 +696,7 @@ class McpPanel {
|
|
|
660
696
|
if (server.exposeResources) {
|
|
661
697
|
for (const resource of entry.resources ?? []) {
|
|
662
698
|
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
663
|
-
if (!isToolAllowed(baseName, server.name, this.prefix, server.includeTools, server.excludeTools)) {
|
|
699
|
+
if (!isToolAllowed(baseName, server.name, this.prefix, server.includeTools, server.excludeTools, this.getOtherCurrentCandidates(server.name, server, entry, baseName))) {
|
|
664
700
|
continue;
|
|
665
701
|
}
|
|
666
702
|
|
package/mcp-script-worker.mjs
CHANGED
|
@@ -106,23 +106,25 @@ const capturedConsole = Object.freeze({
|
|
|
106
106
|
debug: (...args) => emit(`[console.debug] ${formatWithOptions({ colors: false, depth: 4 }, ...args)}`),
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
109
|
+
void (async () => {
|
|
110
|
+
try {
|
|
111
|
+
const context = vm.createContext(Object.assign(Object.create(null), {
|
|
112
|
+
tools,
|
|
113
|
+
emit,
|
|
114
|
+
console: capturedConsole,
|
|
115
|
+
}), {
|
|
116
|
+
codeGeneration: { strings: false, wasm: false },
|
|
117
|
+
name: "mcpScript",
|
|
118
|
+
});
|
|
119
|
+
const script = new vm.Script(`(async () => {\n${workerData.code}\n})()`, { filename: "mcpScript.js" });
|
|
120
|
+
const returnValue = await Promise.resolve(script.runInContext(context));
|
|
121
|
+
parentPort.postMessage(returnValue === undefined
|
|
122
|
+
? { type: "done" }
|
|
123
|
+
: { type: "done", returnBlock: toContentBlock(returnValue) });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
parentPort.postMessage({
|
|
126
|
+
type: "error",
|
|
127
|
+
message: error instanceof Error ? error.message : String(error),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
})();
|
package/mcp-setup-panel.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { createPanelKeys, type PanelKeybindings, type PanelKeys } from "./panel-keys.ts";
|
|
3
3
|
import type { ImportKind } from "./types.ts";
|
|
4
|
+
import { getConfigDirName } from "./agent-dir.ts";
|
|
4
5
|
import { KNOWN_SERVER_PRESETS, type ConfigWritePreview, type KnownServerPreset, type McpDiscoverySummary } from "./config.ts";
|
|
5
6
|
import type { McpOnboardingState } from "./onboarding-state.ts";
|
|
6
7
|
|
|
@@ -556,7 +557,7 @@ export class McpSetupPanel {
|
|
|
556
557
|
"3. ~/.agents/mcp/mcp.json",
|
|
557
558
|
"4. <Pi agent dir>/mcp.json",
|
|
558
559
|
"5. .mcp.json",
|
|
559
|
-
|
|
560
|
+
`6. ${getConfigDirName()}/mcp.json`,
|
|
560
561
|
`Host discovery: ${this.discovery.hostConfigDiscovery}. Conflicts reported: ${this.discovery.conflicts.length}.`,
|
|
561
562
|
...this.discovery.conflicts.slice(0, 8).map((conflict) =>
|
|
562
563
|
`${conflict.serverName}: ${conflict.sources.map((source) => source.path).join(" -> ")} (winner: ${conflict.winner.path})`,
|
package/metadata-cache.ts
CHANGED
|
@@ -19,11 +19,12 @@ import type {
|
|
|
19
19
|
ToolMetadata,
|
|
20
20
|
PromptMetadata,
|
|
21
21
|
} from "./types.ts";
|
|
22
|
-
import { formatPromptCommandName, formatToolName, isServerDisabled, isToolAllowed, resolveToolPrefix, type ToolPrefix } from "./types.ts";
|
|
22
|
+
import { createToolSelectorCandidateIndex, formatPromptCommandName, formatToolName, getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix, type ToolPrefix } from "./types.ts";
|
|
23
23
|
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
24
24
|
import {
|
|
25
25
|
extractToolUiStreamMode,
|
|
26
26
|
interpolateEnvRecord,
|
|
27
|
+
interpolateEnvVars,
|
|
27
28
|
resolveBearerToken,
|
|
28
29
|
resolveConfigPath,
|
|
29
30
|
resolveServerUrl,
|
|
@@ -90,6 +91,14 @@ export function computeServerHash(definition: ServerEntry): string {
|
|
|
90
91
|
cwd: resolveConfigPath(definition.cwd),
|
|
91
92
|
url: resolveServerUrl(definition),
|
|
92
93
|
headers: interpolateEnvRecord(definition.headers),
|
|
94
|
+
requestHeadersCommand: definition.requestHeadersCommand
|
|
95
|
+
? {
|
|
96
|
+
command: interpolateEnvVars(definition.requestHeadersCommand.command),
|
|
97
|
+
args: definition.requestHeadersCommand.args?.map(interpolateEnvVars),
|
|
98
|
+
env: interpolateEnvRecord(definition.requestHeadersCommand.env),
|
|
99
|
+
timeoutMs: definition.requestHeadersCommand.timeoutMs,
|
|
100
|
+
}
|
|
101
|
+
: undefined,
|
|
93
102
|
auth: definition.auth,
|
|
94
103
|
protocolVersion: definition.protocolVersion,
|
|
95
104
|
bearerToken: resolveBearerToken(definition),
|
|
@@ -177,18 +186,42 @@ export function reconstructToolMetadata(
|
|
|
177
186
|
serverName: string,
|
|
178
187
|
entry: ServerCacheEntry,
|
|
179
188
|
prefix: ToolPrefix,
|
|
180
|
-
definition: Pick<ServerEntry, "exposeResources" | "includeTools" | "excludeTools" | "toolPrefix"
|
|
189
|
+
definition: Pick<ServerEntry, "exposeResources" | "includeTools" | "excludeTools" | "toolPrefix">,
|
|
190
|
+
configuredServers?: Record<string, ServerEntry>,
|
|
191
|
+
cache?: MetadataCache,
|
|
181
192
|
): ToolMetadata[] {
|
|
182
193
|
const metadata: ToolMetadata[] = [];
|
|
183
194
|
const seenNames = new Set<string>();
|
|
184
195
|
const effectivePrefix = resolveToolPrefix(definition, prefix);
|
|
196
|
+
const hasToolFilters =
|
|
197
|
+
(Array.isArray(definition.includeTools) && definition.includeTools.length > 0) ||
|
|
198
|
+
(Array.isArray(definition.excludeTools) && definition.excludeTools.length > 0);
|
|
199
|
+
const selectorCandidateIndex = hasToolFilters && configuredServers && cache ? (() => {
|
|
200
|
+
const candidates = new Set<string>();
|
|
201
|
+
for (const [otherServerName, otherDefinition] of Object.entries(configuredServers)) {
|
|
202
|
+
const otherEntry = cache.servers[otherServerName];
|
|
203
|
+
if (!otherEntry || !isServerCacheValid(otherEntry, otherDefinition) || isServerDisabled(otherDefinition)) continue;
|
|
204
|
+
const otherPrefix = resolveToolPrefix(otherDefinition, prefix);
|
|
205
|
+
for (const otherTool of otherEntry.tools ?? []) {
|
|
206
|
+
if (!isUiToolVisibleToModel(otherTool.uiVisibility)) continue;
|
|
207
|
+
for (const candidate of getToolNameCandidates(otherTool.name, otherServerName, otherPrefix, false)) candidates.add(candidate);
|
|
208
|
+
}
|
|
209
|
+
if (otherDefinition.exposeResources !== false) {
|
|
210
|
+
for (const resource of otherEntry.resources ?? []) {
|
|
211
|
+
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
212
|
+
for (const candidate of getToolNameCandidates(baseName, otherServerName, otherPrefix, false)) candidates.add(candidate);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return createToolSelectorCandidateIndex(candidates);
|
|
217
|
+
})() : undefined;
|
|
185
218
|
|
|
186
219
|
for (const tool of entry.tools ?? []) {
|
|
187
220
|
if (!tool?.name) continue;
|
|
188
221
|
if (!isUiToolVisibleToModel(tool.uiVisibility)) {
|
|
189
222
|
continue;
|
|
190
223
|
}
|
|
191
|
-
if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) {
|
|
224
|
+
if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) {
|
|
192
225
|
continue;
|
|
193
226
|
}
|
|
194
227
|
|
|
@@ -213,7 +246,7 @@ export function reconstructToolMetadata(
|
|
|
213
246
|
for (const resource of entry.resources ?? []) {
|
|
214
247
|
if (!resource?.name || !resource?.uri) continue;
|
|
215
248
|
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
216
|
-
if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) {
|
|
249
|
+
if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) {
|
|
217
250
|
continue;
|
|
218
251
|
}
|
|
219
252
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mcp-adapter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.26.0",
|
|
4
4
|
"description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.ts",
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"ui-app-bridge-helpers.ts",
|
|
90
90
|
"config.ts",
|
|
91
91
|
"server-manager.ts",
|
|
92
|
+
"request-headers-command.ts",
|
|
92
93
|
"unix-socket-transport.ts",
|
|
93
94
|
"json-schema-validator.ts",
|
|
94
95
|
"session-recovery.ts",
|
package/proxy-modes.ts
CHANGED
|
@@ -37,6 +37,36 @@ type AutoAuthResult =
|
|
|
37
37
|
| { status: "success" }
|
|
38
38
|
| { status: "failed"; message: string };
|
|
39
39
|
|
|
40
|
+
function getToolMatches(metadata: ToolMetadata[] | undefined, toolName: string, exact: boolean): ToolMetadata[] {
|
|
41
|
+
if (!metadata) return [];
|
|
42
|
+
if (exact) return metadata.filter(tool => tool.name === toolName);
|
|
43
|
+
const normalizedName = toolName.replace(/-/g, "_");
|
|
44
|
+
return metadata.filter(tool => tool.name.replace(/-/g, "_") === normalizedName);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getEnabledToolMatches(state: McpExtensionState, toolName: string, exact: boolean): { server: string; tool: ToolMetadata }[] {
|
|
48
|
+
const matches: { server: string; tool: ToolMetadata }[] = [];
|
|
49
|
+
for (const [server, metadata] of state.toolMetadata) {
|
|
50
|
+
if (isServerDisabled(state.config.mcpServers[server])) continue;
|
|
51
|
+
for (const tool of getToolMatches(metadata, toolName, exact)) matches.push({ server, tool });
|
|
52
|
+
}
|
|
53
|
+
return matches;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getSingleToolMatch(metadata: ToolMetadata[] | undefined, toolName: string): ToolMetadata | "ambiguous" | undefined {
|
|
57
|
+
const exactMatches = getToolMatches(metadata, toolName, true);
|
|
58
|
+
const matches = exactMatches.length > 0 ? exactMatches : getToolMatches(metadata, toolName, false);
|
|
59
|
+
return matches.length > 1 ? "ambiguous" : matches[0];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ambiguousToolResult(mode: "call" | "describe", toolName: string): ProxyToolResult {
|
|
63
|
+
const message = `Tool "${toolName}" matches multiple servers. Specify a server.`;
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text" as const, text: message }],
|
|
66
|
+
details: { mode, error: "ambiguous_tool", requestedTool: toolName, message },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
function disabledResult(mode: string, serverName: string): ProxyToolResult {
|
|
41
71
|
const message = `Server "${serverName}" is disabled. Run /mcp enable ${serverName} and /reload to enable it.`;
|
|
42
72
|
return {
|
|
@@ -404,20 +434,28 @@ export async function executeAuthComplete(state: McpExtensionState, serverName:
|
|
|
404
434
|
}
|
|
405
435
|
|
|
406
436
|
export function executeDescribe(state: McpExtensionState, toolName: string): ProxyToolResult {
|
|
407
|
-
|
|
408
|
-
|
|
437
|
+
const exactMatches = getEnabledToolMatches(state, toolName, true);
|
|
438
|
+
if (exactMatches.length > 1) return ambiguousToolResult("describe", toolName);
|
|
439
|
+
if (exactMatches.length === 0 && getEnabledToolMatches(state, toolName, false).length > 1) {
|
|
440
|
+
return ambiguousToolResult("describe", toolName);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let serverName = exactMatches[0]?.server;
|
|
444
|
+
let toolMeta = exactMatches[0]?.tool;
|
|
409
445
|
let disabledMatch: string | undefined;
|
|
410
446
|
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
447
|
+
if (!toolMeta) {
|
|
448
|
+
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
449
|
+
const found = findToolByName(metadata, toolName);
|
|
450
|
+
if (!found) continue;
|
|
451
|
+
if (isServerDisabled(state.config.mcpServers[server])) {
|
|
452
|
+
disabledMatch ??= server;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
serverName = server;
|
|
456
|
+
toolMeta = found;
|
|
457
|
+
break;
|
|
417
458
|
}
|
|
418
|
-
serverName = server;
|
|
419
|
-
toolMeta = found;
|
|
420
|
-
break;
|
|
421
459
|
}
|
|
422
460
|
|
|
423
461
|
if (!serverName || !toolMeta) {
|
|
@@ -430,7 +468,7 @@ export function executeDescribe(state: McpExtensionState, toolName: string): Pro
|
|
|
430
468
|
};
|
|
431
469
|
}
|
|
432
470
|
|
|
433
|
-
const approvalMarker = isToolCallApprovalRequired(state.config, serverName, toolMeta)
|
|
471
|
+
const approvalMarker = isToolCallApprovalRequired(state.config, serverName, toolMeta, state.toolMetadata)
|
|
434
472
|
? " (requires approval)"
|
|
435
473
|
: "";
|
|
436
474
|
let text = `${toolMeta.name}${approvalMarker}\n`;
|
|
@@ -557,7 +595,7 @@ export function executeSearch(
|
|
|
557
595
|
|
|
558
596
|
let text = `Found ${page.total} tool${page.total === 1 ? "" : "s"} matching "${query}":\n\n`;
|
|
559
597
|
for (const match of page.items) {
|
|
560
|
-
const approvalMarker = isToolCallApprovalRequired(state.config, match.server, match.tool)
|
|
598
|
+
const approvalMarker = isToolCallApprovalRequired(state.config, match.server, match.tool, state.toolMetadata)
|
|
561
599
|
? " (requires approval)"
|
|
562
600
|
: "";
|
|
563
601
|
if (showSchemas) {
|
|
@@ -737,7 +775,7 @@ export async function executeConnect(state: McpExtensionState, serverName: strin
|
|
|
737
775
|
}
|
|
738
776
|
}
|
|
739
777
|
const prefix = state.config.settings?.toolPrefix ?? "server";
|
|
740
|
-
const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, serverName, prefix);
|
|
778
|
+
const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, serverName, prefix, state.config.mcpServers, state.toolMetadata);
|
|
741
779
|
state.toolMetadata.set(serverName, metadata);
|
|
742
780
|
if (!connection.promptDiscoveryFailed) {
|
|
743
781
|
state.promptMetadata?.set(serverName, reconstructPromptMetadata(serverName, connection.prompts ?? [], prefix, definition));
|
|
@@ -805,14 +843,22 @@ export async function executeCall(
|
|
|
805
843
|
};
|
|
806
844
|
}
|
|
807
845
|
if (serverName) {
|
|
808
|
-
|
|
846
|
+
const match = getSingleToolMatch(state.toolMetadata.get(serverName), toolName);
|
|
847
|
+
if (match === "ambiguous") return ambiguousToolResult("call", toolName);
|
|
848
|
+
toolMeta = match;
|
|
809
849
|
if (isServerDisabled(state.config.mcpServers[serverName])) {
|
|
810
850
|
return disabledCallResult(serverName, toolMeta);
|
|
811
851
|
}
|
|
812
852
|
} else {
|
|
853
|
+
const exactMatches = getEnabledToolMatches(state, toolName, true);
|
|
854
|
+
if (exactMatches.length > 1) return ambiguousToolResult("call", toolName);
|
|
855
|
+
if (exactMatches.length === 0 && getEnabledToolMatches(state, toolName, false).length > 1) {
|
|
856
|
+
return ambiguousToolResult("call", toolName);
|
|
857
|
+
}
|
|
858
|
+
|
|
813
859
|
let disabledMatch: { serverName: string; toolMeta: ToolMetadata } | undefined;
|
|
814
860
|
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
815
|
-
const found =
|
|
861
|
+
const found = metadata.find(tool => tool.name === toolName);
|
|
816
862
|
if (!found) continue;
|
|
817
863
|
if (isServerDisabled(state.config.mcpServers[server])) {
|
|
818
864
|
disabledMatch ??= { serverName: server, toolMeta: found };
|
|
@@ -822,13 +868,28 @@ export async function executeCall(
|
|
|
822
868
|
toolMeta = found;
|
|
823
869
|
break;
|
|
824
870
|
}
|
|
871
|
+
if (!toolMeta && !disabledMatch) {
|
|
872
|
+
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
873
|
+
const found = findToolByName(metadata, toolName);
|
|
874
|
+
if (!found) continue;
|
|
875
|
+
if (isServerDisabled(state.config.mcpServers[server])) {
|
|
876
|
+
disabledMatch ??= { serverName: server, toolMeta: found };
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
serverName = server;
|
|
880
|
+
toolMeta = found;
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
825
884
|
if (!toolMeta && disabledMatch) return disabledCallResult(disabledMatch.serverName, disabledMatch.toolMeta);
|
|
826
885
|
}
|
|
827
886
|
|
|
828
887
|
if (serverName && !toolMeta) {
|
|
829
888
|
const connected = await lazyConnect(state, serverName, ownedSignal);
|
|
830
889
|
if (connected) {
|
|
831
|
-
|
|
890
|
+
const match = getSingleToolMatch(state.toolMetadata.get(serverName), toolName);
|
|
891
|
+
if (match === "ambiguous") return ambiguousToolResult("call", toolName);
|
|
892
|
+
toolMeta = match;
|
|
832
893
|
} else {
|
|
833
894
|
const needsAuthConnection = state.manager.getConnection(serverName);
|
|
834
895
|
if (needsAuthConnection?.status === "needs-auth") {
|
|
@@ -846,7 +907,9 @@ export async function executeCall(
|
|
|
846
907
|
clearFailure(state, serverName);
|
|
847
908
|
const connectedAfterAuth = await lazyConnect(state, serverName, ownedSignal);
|
|
848
909
|
if (connectedAfterAuth) {
|
|
849
|
-
|
|
910
|
+
const match = getSingleToolMatch(state.toolMetadata.get(serverName), toolName);
|
|
911
|
+
if (match === "ambiguous") return ambiguousToolResult("call", toolName);
|
|
912
|
+
toolMeta = match;
|
|
850
913
|
if (!toolMeta) {
|
|
851
914
|
const suggestions = rankSuggestions(state, toolName, 5);
|
|
852
915
|
const suggestionText = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}` : "";
|
|
@@ -883,6 +946,8 @@ export async function executeCall(
|
|
|
883
946
|
let prefixMatchedServer: string | undefined;
|
|
884
947
|
|
|
885
948
|
if (!serverName && !toolMeta && prefixMode !== "none") {
|
|
949
|
+
const lazyExactMatches: { serverName: string; toolMeta: ToolMetadata }[] = [];
|
|
950
|
+
const lazyFallbackMatches: { serverName: string; toolMeta: ToolMetadata }[] = [];
|
|
886
951
|
const candidates = Object.keys(state.config.mcpServers)
|
|
887
952
|
.filter(name => !isServerDisabled(state.config.mcpServers[name]))
|
|
888
953
|
.map(name => ({ name, prefix: getServerPrefix(name, prefixMode) }))
|
|
@@ -913,11 +978,22 @@ export async function executeCall(
|
|
|
913
978
|
|
|
914
979
|
if (!connected) continue;
|
|
915
980
|
if (!prefixMatchedServer) prefixMatchedServer = configuredServer;
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
981
|
+
const metadata = state.toolMetadata.get(configuredServer);
|
|
982
|
+
const exactMatches = getToolMatches(metadata, toolName, true);
|
|
983
|
+
if (exactMatches.length > 1) return ambiguousToolResult("call", toolName);
|
|
984
|
+
if (exactMatches.length === 1) {
|
|
985
|
+
lazyExactMatches.push({ serverName: configuredServer, toolMeta: exactMatches[0]! });
|
|
986
|
+
continue;
|
|
920
987
|
}
|
|
988
|
+
const fallbackMatches = getToolMatches(metadata, toolName, false);
|
|
989
|
+
if (fallbackMatches.length > 1) return ambiguousToolResult("call", toolName);
|
|
990
|
+
if (fallbackMatches.length === 1) lazyFallbackMatches.push({ serverName: configuredServer, toolMeta: fallbackMatches[0]! });
|
|
991
|
+
}
|
|
992
|
+
const lazyMatches = lazyExactMatches.length > 0 ? lazyExactMatches : lazyFallbackMatches;
|
|
993
|
+
if (lazyMatches.length > 1) return ambiguousToolResult("call", toolName);
|
|
994
|
+
if (lazyMatches.length === 1) {
|
|
995
|
+
serverName = lazyMatches[0]!.serverName;
|
|
996
|
+
toolMeta = lazyMatches[0]!.toolMeta;
|
|
921
997
|
}
|
|
922
998
|
}
|
|
923
999
|
|
|
@@ -1030,7 +1106,9 @@ export async function executeCall(
|
|
|
1030
1106
|
notifyToolMetadataUpdated(state, serverName, "proxy-call-reconnect");
|
|
1031
1107
|
markKeepAliveAfterConnect(state, serverName);
|
|
1032
1108
|
updateStatusBar(state);
|
|
1033
|
-
|
|
1109
|
+
const match = getSingleToolMatch(state.toolMetadata.get(serverName), toolName);
|
|
1110
|
+
if (match === "ambiguous") return ambiguousToolResult("call", toolName);
|
|
1111
|
+
toolMeta = match;
|
|
1034
1112
|
if (!toolMeta) {
|
|
1035
1113
|
const available = getToolNames(state, serverName);
|
|
1036
1114
|
const hint = available.length > 0
|