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/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 { MetadataCache, ServerCacheEntry, CachedTool } from "./metadata-cache.ts";
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 serverCache = cache?.servers?.[serverName];
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 (!isToolAllowed(tool.name, serverName, this.prefix, definition.includeTools, definition.excludeTools)) {
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 (!isToolAllowed(tool.name, server.name, this.prefix, server.includeTools, server.excludeTools)) {
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
 
@@ -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
- try {
110
- const context = vm.createContext(Object.assign(Object.create(null), {
111
- tools,
112
- emit,
113
- console: capturedConsole,
114
- }), {
115
- codeGeneration: { strings: false, wasm: false },
116
- name: "mcpScript",
117
- });
118
- const script = new vm.Script(`(async () => {\n${workerData.code}\n})()`, { filename: "mcpScript.js" });
119
- const returnValue = await Promise.resolve(script.runInContext(context));
120
- parentPort.postMessage(returnValue === undefined
121
- ? { type: "done" }
122
- : { type: "done", returnBlock: toContentBlock(returnValue) });
123
- } catch (error) {
124
- parentPort.postMessage({
125
- type: "error",
126
- message: error instanceof Error ? error.message : String(error),
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
+ })();
@@ -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
- "6. .pi/mcp.json",
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.24.0",
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
- let serverName: string | undefined;
408
- let toolMeta: ToolMetadata | undefined;
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
- for (const [server, metadata] of state.toolMetadata.entries()) {
412
- const found = findToolByName(metadata, toolName);
413
- if (!found) continue;
414
- if (isServerDisabled(state.config.mcpServers[server])) {
415
- disabledMatch ??= server;
416
- continue;
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
- toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
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 = findToolByName(metadata, toolName);
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
- toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
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
- toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
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
- toolMeta = findToolByName(state.toolMetadata.get(configuredServer), toolName);
917
- if (toolMeta) {
918
- serverName = configuredServer;
919
- break;
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
- toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
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