pi-mcp-adapter 2.24.0 → 2.25.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 CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.25.0] - 2026-08-13
11
+
12
+ ### Added
13
+ - Added `settings.notifyOnStartupConnect` to suppress successful MCP startup connection notices. Thanks @pierre-mgmt for issue #341.
14
+ - Added compact self-rendered MCP proxy and direct-tool result rows by default, with `settings.toolResultRendering: "boxed"` for the legacy boxed row and `settings.collapsedResultLines` for 1-3 collapsed result lines. Thanks @pierre-mgmt for issue #349.
15
+
16
+ ### Changed
17
+ - Removed unused TypeScript declarations found by stricter compiler checks.
18
+
19
+ ### Fixed
20
+ - Kept `mcpScript` tool-call replies flowing when Pi runs as a Bun-compiled binary by starting worker execution without module-level top-level await. Thanks @AndreiKopylov for issue #340 and @thesobercoder for the verified fix.
21
+ - Fixed direct-tool server prefixes to preserve provider-valid underscores and hyphens, including `codebase-memory-mcp`, while retaining safe escaping, collision handling, and exact proxy routing. Thanks @mightymatth for issue #342 and @JasonLandbridge for issue #343.
22
+
10
23
  ## [2.24.0] - 2026-08-13
11
24
 
12
25
  ### Added
package/README.md CHANGED
@@ -307,6 +307,9 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
307
307
  "requestTimeoutMs": 30000,
308
308
  "showStatusIcon": true,
309
309
  "mcpFooterStatus": "full",
310
+ "toolResultRendering": "compact",
311
+ "collapsedResultLines": 1,
312
+ "notifyOnStartupConnect": true,
310
313
  "hostConfigDiscovery": "off",
311
314
  "approveTools": ["github_delete_*", "notion_update_*"],
312
315
  "oauthDir": ".pi/mcp-oauth",
@@ -328,6 +331,9 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
328
331
  | `requestTimeoutMs` | Global request timeout in milliseconds for live MCP calls (if omitted or `<= 0`, the MCP SDK default timeout is used) |
329
332
  | `showStatusIcon` | Show the plug icon in MCP status and connection text (default: `true`). Set to `false` for plain `MCP: ...` text. |
330
333
  | `mcpFooterStatus` | MCP footer verbosity: `"full"` (default), `"compact"` for `MCP connected/enabled`, or `"off"` to clear the persistent footer status. `/mcp status` remains available. |
334
+ | `toolResultRendering` | MCP tool result row style: `"compact"` (default) uses self-rendered rows, or `"boxed"` restores the legacy Pi boxed tool row. |
335
+ | `collapsedResultLines` | Number of result text lines to show before expansion: `1`, `2`, or `3`. Defaults to `1` in compact mode and `3` in boxed mode. |
336
+ | `notifyOnStartupConnect` | Show successful startup connection notices (default: `true`). Set to `false` to suppress routine `MCP: N servers connected (M tools)` notices. Connection errors and authentication warnings remain visible. |
331
337
  | `hostConfigDiscovery` | Host-specific config policy: `"off"` (default), `"prompt"` (detect/report only), or `"on"` (explicitly load detected host configs as the lowest-precedence fallback) |
332
338
  | `agentPluginPaths` | Agent Plugins package directories to load MCP servers from. Relative paths resolve from the active project cwd. |
333
339
  | `approveTools` | `true` to require approval before every MCP tool call, or an array of glob patterns such as `["github_delete_*", "notion_update_*"]`. Per-server `approveTools` overrides this. |
@@ -649,7 +655,7 @@ Prefer `.mcp.json` for project-local shared MCP config. Use `.pi/mcp.json` only
649
655
 
650
656
  `mcp({ connect: "server-name" })` refreshes an already connected server, so new tools, resources, prompts, and instructions can load without restarting Pi.
651
657
 
652
- MCP proxy and direct-tool results render compactly by default: long text shows the first three terminal-wrapped lines plus a `Ctrl+O to expand` hint, while the full result remains available when expanded and is still returned unchanged to the model.
658
+ MCP proxy and direct-tool results use compact self-rendered rows by default. Collapsed success output shows the call title and the first result line, with a `Ctrl+O to expand` hint when more text is hidden. The full result remains available when expanded and is still returned unchanged to the model. Set `settings.toolResultRendering` to `"boxed"` to restore the legacy boxed Pi row, or set `settings.collapsedResultLines` to `2` or `3` when you want more collapsed text.
653
659
 
654
660
  Search includes both MCP tools and Pi tools (from extensions). Pi tools appear first with `[pi tool]` prefix. Space-separated words are ranked by weighted matches across name, server, description, and any configured `searchKeywords`, then returned one page at a time (`limit` defaults to 12). Use `details.nextOffset` for the next page. Regex search is still available with `regex: true`, but regex results are paginated without ranking.
655
661
 
@@ -723,4 +729,5 @@ Advertised tool `outputSchema` values support JSON Schema draft-07 and 2020-12.
723
729
 
724
730
  - Cross-session server sharing not yet implemented (each Pi session runs its own server processes)
725
731
  - Compact MCP result rendering summarizes text, but inline images are still controlled by Pi's image display settings and may render below the compact text summary.
732
+ - Pi still owns one separator row before self-rendered tool output, so compact mode reduces adapter rendering height but cannot promise true zero-gap rows.
726
733
  - MCP sampling support is text-only; context inclusion, tools, stop sequences, audio, and image content are rejected with explicit errors.
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { isAbsolute, relative, resolve, sep } from "node:path";
3
3
  import { getAgentPath } from "./agent-dir.ts";
4
4
  import type { McpConfig, ServerEntry } from "./types.ts";
package/commands.ts CHANGED
@@ -12,7 +12,6 @@ import {
12
12
  previewSharedServerEntry,
13
13
  previewStarterProjectConfig,
14
14
  writeDirectToolsConfig,
15
- writeProjectServerDisabledOverride,
16
15
  writeSharedServerEntry,
17
16
  writeStarterProjectConfig,
18
17
  } from "./config.ts";
@@ -170,7 +169,7 @@ export async function reconnectServer(
170
169
  }
171
170
 
172
171
  const prefix = state.config.settings?.toolPrefix ?? "server";
173
- const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
172
+ const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix, state.config.mcpServers, state.toolMetadata);
174
173
  state.toolMetadata.set(name, metadata);
175
174
  if (!connection.promptDiscoveryFailed) {
176
175
  state.promptMetadata?.set(name, reconstructPromptMetadata(name, connection.prompts ?? [], prefix, definition));
package/direct-tools.ts CHANGED
@@ -11,7 +11,8 @@ import { formatSchema } from "./tool-metadata.ts";
11
11
  import { resolveMcpResultContent, transformMcpContent, transformMcpResourceContents } from "./tool-registrar.ts";
12
12
  import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.ts";
13
13
  import { maybeStartUiSession, summarizeUiSessionResult, type UiSessionRuntime } from "./ui-session.ts";
14
- import { formatToolName, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.ts";
14
+ import { formatToolName, getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.ts";
15
+ import { isUiToolVisibleToModel } from "./ui-tool-visibility.ts";
15
16
  import { resourceNameToToolName } from "./resource-tools.ts";
16
17
  import { authenticate, supportsOAuth } from "./mcp-auth-flow.ts";
17
18
  import { formatAuthRequiredMessage, resolveServerUrl, truncateAtWord } from "./utils.ts";
@@ -149,10 +150,31 @@ export function resolveDirectTools(
149
150
  if (!toolFilter) continue;
150
151
 
151
152
  const effectivePrefix = resolveToolPrefix(definition, prefix);
153
+ const getOtherCurrentCandidates = (toolName: string): Set<string> => {
154
+ const candidates = new Set<string>();
155
+ for (const [otherServerName, otherDefinition] of Object.entries(config.mcpServers)) {
156
+ const otherCache = cache.servers[otherServerName];
157
+ if (!otherCache || !isServerCacheValid(otherCache, otherDefinition) || isServerDisabled(otherDefinition)) continue;
158
+ const otherPrefix = resolveToolPrefix(otherDefinition, prefix);
159
+ for (const otherTool of otherCache.tools ?? []) {
160
+ if (!isUiToolVisibleToModel(otherTool.uiVisibility)) continue;
161
+ for (const candidate of getToolNameCandidates(otherTool.name, otherServerName, otherPrefix, false)) candidates.add(candidate);
162
+ }
163
+ if (otherDefinition.exposeResources !== false) {
164
+ for (const resource of otherCache.resources ?? []) {
165
+ const baseName = `read_${resourceNameToToolName(resource.name)}`;
166
+ for (const candidate of getToolNameCandidates(baseName, otherServerName, otherPrefix, false)) candidates.add(candidate);
167
+ }
168
+ }
169
+ }
170
+ for (const candidate of getToolNameCandidates(toolName, serverName, effectivePrefix, false)) candidates.delete(candidate);
171
+ return candidates;
172
+ };
152
173
 
153
174
  for (const tool of serverCache.tools ?? []) {
175
+ if (!isUiToolVisibleToModel(tool.uiVisibility)) continue;
154
176
  if (toolFilter !== true && !toolFilter.includes(tool.name)) continue;
155
- if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) continue;
177
+ if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(tool.name))) continue;
156
178
  const prefixedName = formatToolName(tool.name, serverName, effectivePrefix);
157
179
  if (BUILTIN_NAMES.has(prefixedName)) {
158
180
  console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
@@ -178,7 +200,7 @@ export function resolveDirectTools(
178
200
  for (const resource of serverCache.resources ?? []) {
179
201
  const baseName = `read_${resourceNameToToolName(resource.name)}`;
180
202
  if (toolFilter !== true && !toolFilter.includes(baseName)) continue;
181
- if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) continue;
203
+ if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(baseName))) continue;
182
204
  const prefixedName = formatToolName(baseName, serverName, effectivePrefix);
183
205
  if (BUILTIN_NAMES.has(prefixedName)) {
184
206
  console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
@@ -230,15 +252,38 @@ export function buildProxyDescription(
230
252
  for (const serverName of Object.keys(config.mcpServers)) {
231
253
  const definition = config.mcpServers[serverName];
232
254
  if (!definition || isServerDisabled(definition)) continue;
233
- const entry = cache?.servers?.[serverName];
255
+ const cachedEntry = cache?.servers?.[serverName];
256
+ const entry = cachedEntry && isServerCacheValid(cachedEntry, definition) ? cachedEntry : undefined;
234
257
  const effectivePrefix = resolveToolPrefix(definition, prefix);
258
+ const getOtherCurrentCandidates = (toolName: string): Set<string> | undefined => {
259
+ if (!cache) return undefined;
260
+ const candidates = new Set<string>();
261
+ for (const [otherServerName, otherDefinition] of Object.entries(config.mcpServers)) {
262
+ const otherEntry = cache.servers[otherServerName];
263
+ if (!otherEntry || !isServerCacheValid(otherEntry, otherDefinition) || isServerDisabled(otherDefinition)) continue;
264
+ const otherPrefix = resolveToolPrefix(otherDefinition, prefix);
265
+ for (const otherTool of otherEntry.tools ?? []) {
266
+ if (!isUiToolVisibleToModel(otherTool.uiVisibility)) continue;
267
+ for (const candidate of getToolNameCandidates(otherTool.name, otherServerName, otherPrefix, false)) candidates.add(candidate);
268
+ }
269
+ if (otherDefinition.exposeResources !== false) {
270
+ for (const resource of otherEntry.resources ?? []) {
271
+ const baseName = `read_${resourceNameToToolName(resource.name)}`;
272
+ for (const candidate of getToolNameCandidates(baseName, otherServerName, otherPrefix, false)) candidates.add(candidate);
273
+ }
274
+ }
275
+ }
276
+ for (const candidate of getToolNameCandidates(toolName, serverName, effectivePrefix, false)) candidates.delete(candidate);
277
+ return candidates;
278
+ };
235
279
  const toolCount = (entry?.tools ?? []).filter(
236
- (tool) => isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools),
280
+ (tool) => isUiToolVisibleToModel(tool.uiVisibility)
281
+ && isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(tool.name)),
237
282
  ).length;
238
283
  const resourceCount = definition?.exposeResources !== false
239
284
  ? (entry?.resources ?? []).filter((resource) => {
240
285
  const baseName = `read_${resourceNameToToolName(resource.name)}`;
241
- return isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools);
286
+ return isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(baseName));
242
287
  }).length
243
288
  : 0;
244
289
  const totalItems = toolCount + resourceCount;
@@ -264,7 +309,9 @@ export function buildProxyDescription(
264
309
  const instructionSummaries: string[] = [];
265
310
  for (const serverName of Object.keys(config.mcpServers)) {
266
311
  if (isServerDisabled(config.mcpServers[serverName])) continue;
267
- const instructions = cache?.servers?.[serverName]?.instructions;
312
+ const definition = config.mcpServers[serverName];
313
+ const entry = definition && cache?.servers?.[serverName];
314
+ const instructions = entry && definition && isServerCacheValid(entry, definition) ? entry.instructions : undefined;
268
315
  if (!instructions) continue;
269
316
  const snippet = truncateAtWord(instructions.replace(/\s+/g, " ").trim(), INSTRUCTIONS_SNIPPET_LENGTH);
270
317
  instructionSummaries.push(` ${serverName}: ${snippet}`);
@@ -459,11 +459,3 @@ function escapeHtml(value: string): string {
459
459
  .replace(/"/g, "&quot;")
460
460
  .replace(/'/g, "&#39;");
461
461
  }
462
-
463
- function escapeHtmlAttribute(value: string): string {
464
- return value
465
- .replace(/&/g, "&amp;")
466
- .replace(/"/g, "&quot;")
467
- .replace(/</g, "&lt;")
468
- .replace(/>/g, "&gt;");
469
- }
package/index.ts CHANGED
@@ -14,7 +14,7 @@ import { logger } from "./logger.ts";
14
14
  import { executeAuthComplete, executeAuthStart, executeCall, executeConnect, executeDescribe, executeInstructions, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.ts";
15
15
  import { formatTerminalError, getConfigPathFromArgv, normalizeDirectToolInputSchema, truncateAtWord } from "./utils.ts";
16
16
  import { createOAuthRuntime, shutdownOAuth } from "./mcp-auth-flow.ts";
17
- import { createMcpDirectToolCallRenderer, renderMcpProxyToolCall, renderMcpToolResult } from "./tool-result-renderer.ts";
17
+ import { createMcpDirectToolCallRenderer, createMcpProxyToolCallRenderer, createMcpToolResultRenderer, resolveMcpToolRenderOptions } from "./tool-result-renderer.ts";
18
18
  import { toolErrorOverride } from "./error-signal.ts";
19
19
  import { createMcpRuntimeOwner, createOwnedUi, isAbortError, type McpRuntimeOwner } from "./runtime-owner.ts";
20
20
  import { publishMcpStatusShutdown } from "./mcp-status.ts";
@@ -124,6 +124,9 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
124
124
  const envDirectToolOverride = envRaw?.split(",").map(s => s.trim()).filter(Boolean);
125
125
  const registeredDirectTools = new Map<string, string>();
126
126
  const fallbackDeactivatedTools = new Set<string>();
127
+ const toolRenderOptions = resolveMcpToolRenderOptions(earlyConfig.settings);
128
+ const toolRenderShell = toolRenderOptions.resultRendering === "compact" ? "self" : "default";
129
+ const renderMcpToolResult = createMcpToolResultRenderer(toolRenderOptions);
127
130
  let proxyToolRegistered = false;
128
131
  let proxyToolDescription: string | null = null;
129
132
  let directToolsFrozen = false;
@@ -158,7 +161,8 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
158
161
  promptSnippet: truncateAtWord(spec.description, 100) || `MCP tool from ${spec.serverName}`,
159
162
  parameters: toToolParameters(normalizeDirectToolInputSchema(spec.inputSchema)),
160
163
  execute: createDirectToolExecutor(() => state, () => initPromise, spec),
161
- renderCall: createMcpDirectToolCallRenderer(spec.prefixedName),
164
+ renderShell: toolRenderShell,
165
+ renderCall: createMcpDirectToolCallRenderer(spec.prefixedName, toolRenderOptions),
162
166
  renderResult: renderMcpToolResult,
163
167
  });
164
168
  }
@@ -695,7 +699,8 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
695
699
  label: "MCP",
696
700
  description,
697
701
  promptSnippet: "MCP gateway — status, search, describe, auth, and single MCP tool calls",
698
- renderCall: renderMcpProxyToolCall,
702
+ renderShell: toolRenderShell,
703
+ renderCall: createMcpProxyToolCallRenderer(toolRenderOptions),
699
704
  parameters: Type.Object({
700
705
  tool: Type.Optional(Type.String({ description: "Tool name to call (e.g., 'xcodebuild_list_sims')" })),
701
706
  args: Type.Optional(Type.Union([
package/init.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { McpExtensionState } from "./state.ts";
3
- import { isServerDisabled, type McpAdapterOptions, type PromptMetadata, type ToolMetadata } from "./types.ts";
3
+ import { formatToolName, isServerDisabled, resolveToolPrefix, type McpAdapterOptions, type PromptMetadata, type ToolMetadata } from "./types.ts";
4
4
  import { existsSync } from "node:fs";
5
5
  import { cloneMcpConfig, loadMcpConfig } from "./config.ts";
6
6
  import { ConsentManager } from "./consent-manager.ts";
@@ -21,6 +21,7 @@ import {
21
21
  } from "./metadata-cache.ts";
22
22
  import { McpServerManager } from "./server-manager.ts";
23
23
  import { buildToolMetadata, totalToolCount } from "./tool-metadata.ts";
24
+ import { resourceNameToToolName } from "./resource-tools.ts";
24
25
  import { UiResourceHandler } from "./ui-resource-handler.ts";
25
26
  import { formatMcpStatus, openUrl, parallelLimit, sanitizeTerminalText } from "./utils.ts";
26
27
  import { logger } from "./logger.ts";
@@ -242,7 +243,7 @@ export async function initializeMcp(
242
243
 
243
244
  const cachedEntry = cache?.servers?.[name];
244
245
  if (cachedEntry && isServerCacheValid(cachedEntry, definition)) {
245
- const metadata = reconstructToolMetadata(name, cachedEntry, prefix, definition);
246
+ const metadata = reconstructToolMetadata(name, cachedEntry, prefix, definition, config.mcpServers, cache ?? undefined);
246
247
  toolMetadata.set(name, metadata);
247
248
  if (Array.isArray(cachedEntry.resources)) {
248
249
  resourceCounts.set(name, cachedEntry.resources.length);
@@ -288,6 +289,29 @@ export async function initializeMcp(
288
289
  if (initialSignal?.aborted) return state;
289
290
  owner.throwIfInactive();
290
291
 
292
+ const startupKnownMetadata = new Map<string, ToolMetadata[]>();
293
+ for (const { name, definition, connection } of results) {
294
+ if (!connection) continue;
295
+ const effectivePrefix = resolveToolPrefix(definition, prefix);
296
+ const metadata: ToolMetadata[] = [
297
+ ...connection.tools.filter(tool => tool?.name).map(tool => ({
298
+ name: formatToolName(tool.name, name, effectivePrefix),
299
+ originalName: tool.name,
300
+ description: tool.description ?? "",
301
+ })),
302
+ ...(definition.exposeResources !== false ? connection.resources.filter(resource => resource?.name && resource?.uri).map(resource => {
303
+ const originalName = `read_${resourceNameToToolName(resource.name)}`;
304
+ return {
305
+ name: formatToolName(originalName, name, effectivePrefix),
306
+ originalName,
307
+ description: resource.description ?? `Read resource: ${resource.uri}`,
308
+ resourceUri: resource.uri,
309
+ };
310
+ }) : []),
311
+ ];
312
+ startupKnownMetadata.set(name, metadata);
313
+ }
314
+
291
315
  for (const { name, definition, connection, error } of results) {
292
316
  owner.throwIfInactive();
293
317
  if (error || !connection) {
@@ -301,7 +325,7 @@ export async function initializeMcp(
301
325
  continue;
302
326
  }
303
327
 
304
- const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
328
+ const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix, config.mcpServers, startupKnownMetadata, true);
305
329
  toolMetadata.set(name, metadata);
306
330
  resourceCounts.set(name, connection.resources.length);
307
331
  if (!connection.promptDiscoveryFailed) {
@@ -327,7 +351,7 @@ export async function initializeMcp(
327
351
 
328
352
  const connectedCount = results.filter(r => r.connection).length;
329
353
  const failedCount = results.filter(r => r.error).length;
330
- if (ui && connectedCount > 0) {
354
+ if (ui && connectedCount > 0 && config.settings?.notifyOnStartupConnect !== false) {
331
355
  const totalTools = totalToolCount(state);
332
356
  const msg = failedCount > 0
333
357
  ? `MCP: ${connectedCount}/${startupServers.length} servers connected (${totalTools} tools)`
@@ -437,7 +461,7 @@ export function updateServerMetadata(state: McpExtensionState, serverName: strin
437
461
 
438
462
  const prefix = state.config.settings?.toolPrefix ?? "server";
439
463
 
440
- const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, serverName, prefix);
464
+ const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, serverName, prefix, state.config.mcpServers, state.toolMetadata);
441
465
  state.toolMetadata.set(serverName, metadata);
442
466
  state.resourceCounts?.set(serverName, connection.resources.length);
443
467
  if (!connection.promptDiscoveryFailed) {
package/lifecycle.ts CHANGED
@@ -19,7 +19,6 @@ export class McpLifecycleManager {
19
19
  private activeHealthCheck: Promise<void> | undefined;
20
20
  private shutdownPromise: Promise<void> | undefined;
21
21
  private stopped = false;
22
- private healthSignal: AbortSignal | undefined;
23
22
  private removeHealthAbortListener: (() => void) | undefined;
24
23
 
25
24
  constructor(
@@ -58,10 +57,8 @@ export class McpLifecycleManager {
58
57
  const signal = typeof signalOrInterval === "number" ? undefined : signalOrInterval;
59
58
  const intervalMs = typeof signalOrInterval === "number" ? signalOrInterval : maybeIntervalMs;
60
59
  this.stopped = false;
61
- this.healthSignal = signal;
62
60
  if (signal?.aborted) {
63
61
  this.stopped = true;
64
- this.healthSignal = undefined;
65
62
  return;
66
63
  }
67
64
  const stop = () => {
@@ -138,7 +135,6 @@ export class McpLifecycleManager {
138
135
  this.healthCheckInterval = undefined;
139
136
  this.removeHealthAbortListener?.();
140
137
  this.removeHealthAbortListener = undefined;
141
- this.healthSignal = undefined;
142
138
  await this.activeHealthCheck;
143
139
  this.activeHealthCheck = undefined;
144
140
  this.onReconnect = undefined;
package/mcp-auth-flow.ts CHANGED
@@ -633,7 +633,6 @@ export async function completeAuthFromInput(
633
633
  const signal = combineAbortSignals(runtime.signal, options.signal)
634
634
  throwIfAborted(signal)
635
635
  const key = getPendingAuthKey(serverName, fallbackAuthStorageOptions)
636
- const authStorageOptions = runtimeState.pendingAuths.get(key)?.authStorageOptions ?? fallbackAuthStorageOptions
637
636
  const oauthState = runtimeState.pendingAuthStates.get(key)
638
637
  throwIfAborted(signal)
639
638
  const parsed = parseAuthorizationRedirectInput(input, oauthState)
@@ -899,7 +898,7 @@ export async function getValidToken(
899
898
  * @returns The current auth status
900
899
  */
901
900
  export async function getAuthStatus(serverName: string, options: AuthenticateOptions = {}): Promise<AuthStatus> {
902
- const runtime = getRuntime(options)
901
+ getRuntime(options)
903
902
  const authStorageOptions = options.authStorageOptions ?? {}
904
903
  const hasTokens = await hasStoredTokens(serverName, authStorageOptions)
905
904
  if (!hasTokens) return "not_authenticated"
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
+ })();
package/metadata-cache.ts CHANGED
@@ -19,7 +19,7 @@ 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 { 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,
@@ -177,18 +177,41 @@ export function reconstructToolMetadata(
177
177
  serverName: string,
178
178
  entry: ServerCacheEntry,
179
179
  prefix: ToolPrefix,
180
- definition: Pick<ServerEntry, "exposeResources" | "includeTools" | "excludeTools" | "toolPrefix">
180
+ definition: Pick<ServerEntry, "exposeResources" | "includeTools" | "excludeTools" | "toolPrefix">,
181
+ configuredServers?: Record<string, ServerEntry>,
182
+ cache?: MetadataCache,
181
183
  ): ToolMetadata[] {
182
184
  const metadata: ToolMetadata[] = [];
183
185
  const seenNames = new Set<string>();
184
186
  const effectivePrefix = resolveToolPrefix(definition, prefix);
187
+ const getOtherCurrentCandidates = (toolName: string): Set<string> | undefined => {
188
+ if (!configuredServers || !cache) return undefined;
189
+ const candidates = new Set<string>();
190
+ for (const [otherServerName, otherDefinition] of Object.entries(configuredServers)) {
191
+ const otherEntry = cache.servers[otherServerName];
192
+ if (!otherEntry || !isServerCacheValid(otherEntry, otherDefinition) || isServerDisabled(otherDefinition)) continue;
193
+ const otherPrefix = resolveToolPrefix(otherDefinition, prefix);
194
+ for (const otherTool of otherEntry.tools ?? []) {
195
+ if (!isUiToolVisibleToModel(otherTool.uiVisibility)) continue;
196
+ for (const candidate of getToolNameCandidates(otherTool.name, otherServerName, otherPrefix, false)) candidates.add(candidate);
197
+ }
198
+ if (otherDefinition.exposeResources !== false) {
199
+ for (const resource of otherEntry.resources ?? []) {
200
+ const baseName = `read_${resourceNameToToolName(resource.name)}`;
201
+ for (const candidate of getToolNameCandidates(baseName, otherServerName, otherPrefix, false)) candidates.add(candidate);
202
+ }
203
+ }
204
+ }
205
+ for (const candidate of getToolNameCandidates(toolName, serverName, effectivePrefix, false)) candidates.delete(candidate);
206
+ return candidates;
207
+ };
185
208
 
186
209
  for (const tool of entry.tools ?? []) {
187
210
  if (!tool?.name) continue;
188
211
  if (!isUiToolVisibleToModel(tool.uiVisibility)) {
189
212
  continue;
190
213
  }
191
- if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) {
214
+ if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(tool.name))) {
192
215
  continue;
193
216
  }
194
217
 
@@ -213,7 +236,7 @@ export function reconstructToolMetadata(
213
236
  for (const resource of entry.resources ?? []) {
214
237
  if (!resource?.name || !resource?.uri) continue;
215
238
  const baseName = `read_${resourceNameToToolName(resource.name)}`;
216
- if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) {
239
+ if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(baseName))) {
217
240
  continue;
218
241
  }
219
242
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-adapter",
3
- "version": "2.24.0",
3
+ "version": "2.25.0",
4
4
  "description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
5
5
  "type": "module",
6
6
  "types": "./index.ts",