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 CHANGED
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.26.0] - 2026-08-14
11
+
12
+ ### Added
13
+ - Added per-server `requestHeadersCommand` support for deriving fail-closed HTTP headers from the exact outbound request on every Streamable HTTP or SSE call. Thanks @kgreen18 for PR #353.
14
+ - Added `settings.warnOnLargeDirectTools` to suppress the advisory for 75 or more resolved direct tools. Thanks @Roshvan for issue #358.
15
+
16
+ ### Changed
17
+ - Refined request-header command result handling types without changing runtime behavior.
18
+
19
+ ### Fixed
20
+ - Matched adapter-owned config and state paths to the host agent directory when Pi is rebranded, including its environment override and config directory. Thanks @mindplay-dk for issue #356.
21
+ - Avoided O(tools²) cross-server tool-name collision scans at startup by skipping collision candidates when selectors are absent and sharing one indexed candidate set when `includeTools` or `excludeTools` is configured. Thanks @mjlbach for PR #357 and @cataldoc for issue #354.
22
+
23
+ ## [2.25.0] - 2026-08-13
24
+
25
+ ### Added
26
+ - Added `settings.notifyOnStartupConnect` to suppress successful MCP startup connection notices. Thanks @pierre-mgmt for issue #341.
27
+ - 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.
28
+
29
+ ### Changed
30
+ - Removed unused TypeScript declarations found by stricter compiler checks.
31
+
32
+ ### Fixed
33
+ - 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.
34
+ - 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.
35
+
10
36
  ## [2.24.0] - 2026-08-13
11
37
 
12
38
  ### Added
package/README.md CHANGED
@@ -207,6 +207,7 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
207
207
  | `cwd` | Working directory; supports `${VAR}`, `$env:VAR`, and `~` expansion |
208
208
  | `url` | HTTP endpoint (StreamableHTTP with SSE fallback); supports raw `${VAR}` and `$env:VAR` interpolation, and missing URL variables fail before any request is sent |
209
209
  | `headers` | HTTP headers; supports `${VAR}` and `$env:VAR` interpolation. A value beginning with `!` runs a command when the HTTP server connects or OAuth authenticates; use `!!` for a literal leading `!`. |
210
+ | `requestHeadersCommand` | Trusted executable run for every HTTP request. It receives a versioned JSON envelope containing `method`, `url`, and the exact `bodyBase64` on stdin, and must return a JSON object of headers on stdout. `command`, `args`, and `env` support environment interpolation. Use for caller-bound request signatures; failures stop the request. |
210
211
  | `auth` | `"bearer"` or `"oauth"` |
211
212
  | `oauth.grantType` | `"authorization_code"` (default) or `"client_credentials"` for non-interactive machine auth |
212
213
  | `oauth.clientId` | Pre-registered OAuth client ID. MCP 2026 prefers pre-registered clients or Client ID Metadata Documents; this adapter falls back to Dynamic Client Registration when the ID is omitted and the server supports it. |
@@ -307,6 +308,10 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
307
308
  "requestTimeoutMs": 30000,
308
309
  "showStatusIcon": true,
309
310
  "mcpFooterStatus": "full",
311
+ "toolResultRendering": "compact",
312
+ "collapsedResultLines": 1,
313
+ "notifyOnStartupConnect": true,
314
+ "warnOnLargeDirectTools": true,
310
315
  "hostConfigDiscovery": "off",
311
316
  "approveTools": ["github_delete_*", "notion_update_*"],
312
317
  "oauthDir": ".pi/mcp-oauth",
@@ -328,12 +333,16 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
328
333
  | `requestTimeoutMs` | Global request timeout in milliseconds for live MCP calls (if omitted or `<= 0`, the MCP SDK default timeout is used) |
329
334
  | `showStatusIcon` | Show the plug icon in MCP status and connection text (default: `true`). Set to `false` for plain `MCP: ...` text. |
330
335
  | `mcpFooterStatus` | MCP footer verbosity: `"full"` (default), `"compact"` for `MCP connected/enabled`, or `"off"` to clear the persistent footer status. `/mcp status` remains available. |
336
+ | `toolResultRendering` | MCP tool result row style: `"compact"` (default) uses self-rendered rows, or `"boxed"` restores the legacy Pi boxed tool row. |
337
+ | `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. |
338
+ | `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
339
  | `hostConfigDiscovery` | Host-specific config policy: `"off"` (default), `"prompt"` (detect/report only), or `"on"` (explicitly load detected host configs as the lowest-precedence fallback) |
332
340
  | `agentPluginPaths` | Agent Plugins package directories to load MCP servers from. Relative paths resolve from the active project cwd. |
333
341
  | `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. |
334
342
  | `oauthDir` | Legacy OAuth `tokens.json` import directory for this MCP config. Relative paths resolve from the active project cwd. `MCP_OAUTH_DIR` still wins when set. Persistent OAuth credentials are stored in the OS credential store, not this directory. |
335
343
  | `mcpServers.<name>.oauth.authorizationParams` | Extra authorization URL parameters for provider-specific OAuth extensions. Flow-owned parameters such as `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge`, `response_type`, and `resource` cannot be overridden. |
336
344
  | `directTools` | Global default for all servers (default: false). Per-server overrides this. |
345
+ | `warnOnLargeDirectTools` | Show the advisory when 75 or more direct tools resolve (default: `true`). Set to `false` to suppress only this advisory. |
337
346
  | `freezeDirectTools` | Keep direct-tool registration stable after the initial sync so automatic reconnects and list-change notifications do not rebuild the system prompt. Use `mcp({ connect: "server" })` or `/mcp reconnect <server>` to refresh deliberately. Default: false. |
338
347
  | `scriptMode` | Register the MCP-only `mcpScript` plain-JavaScript tool (default: true). Set to `false` to hide it. |
339
348
  | `disableProxyTool` | Hide the `mcp` proxy tool once configured direct tools are fully available from cache. |
@@ -538,7 +547,7 @@ To hide specific tools while still using `directTools: true`, add `excludeTools`
538
547
 
539
548
  `includeTools` and `excludeTools` filter direct tools, proxy search/list/describe, and the `/mcp` panel view.
540
549
 
541
- Each direct tool costs ~150-300 tokens in the system prompt (name + description + schema). Good for targeted sets of 5-20 tools. For servers with 75+ tools, stick with the proxy or pick specific tools with a `string[]`. If 75+ direct tools resolve, the adapter prints a warning but still registers the tools you configured.
550
+ Each direct tool costs ~150-300 tokens in the system prompt (name + description + schema). Good for targeted sets of 5-20 tools. For servers with 75+ tools, stick with the proxy or pick specific tools with a `string[]`. If 75+ direct tools resolve, the adapter prints an advisory but still registers the tools you configured. Set `settings.warnOnLargeDirectTools` to `false` to suppress this advisory.
542
551
 
543
552
  Direct tools register from the metadata cache in the Pi agent dir (`~/.pi/agent/mcp-cache.json` by default, or `$PI_CODING_AGENT_DIR/mcp-cache.json` when set), so no server connections are needed at startup. On the first session after adding `directTools` to a new server, the cache won't exist yet — tools fall back to proxy-only while the cache populates, then the extension hot-loads the refreshed direct tools into the current session. Servers that advertise MCP list-change notifications refresh the current session when their tool or resource list changes. On Pi versions that expose `pi.unregisterTool()`, stale direct tools are removed from the registry during refresh; older Pi versions still deactivate them from the active tool set. To force a refresh: `/mcp reconnect <server>`.
544
553
 
@@ -649,7 +658,7 @@ Prefer `.mcp.json` for project-local shared MCP config. Use `.pi/mcp.json` only
649
658
 
650
659
  `mcp({ connect: "server-name" })` refreshes an already connected server, so new tools, resources, prompts, and instructions can load without restarting Pi.
651
660
 
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.
661
+ 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
662
 
654
663
  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
664
 
@@ -723,4 +732,5 @@ Advertised tool `outputSchema` values support JSON Schema draft-07 and 2020-12.
723
732
 
724
733
  - Cross-session server sharing not yet implemented (each Pi session runs its own server processes)
725
734
  - 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.
735
+ - 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
736
  - MCP sampling support is text-only; context inclusion, tools, stop sequences, audio, and image content are rejected with explicit errors.
package/agent-dir.ts CHANGED
@@ -2,10 +2,18 @@ import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
 
5
+ export function getConfigDirName(): string {
6
+ const configDir = readPiConfig()?.configDir;
7
+ return typeof configDir === "string" && configDir.trim() ? configDir.trim() : ".pi";
8
+ }
9
+
5
10
  export function getAgentDir(): string {
6
- const configured = process.env.PI_CODING_AGENT_DIR?.trim();
11
+ const piConfig = readPiConfig();
12
+ const name = piConfig?.name;
13
+ const appName = typeof name === "string" && name.trim() ? name.trim() : "pi";
14
+ const configured = process.env[`${appName.toUpperCase()}_CODING_AGENT_DIR`]?.trim();
7
15
  if (!configured) {
8
- return join(homedir(), ".pi", "agent");
16
+ return join(homedir(), getConfigDirName(), "agent");
9
17
  }
10
18
  if (configured === "~") {
11
19
  return homedir();
@@ -31,12 +39,12 @@ export function getAgentPath(...segments: string[]): string {
31
39
  *
32
40
  * Falls back to "pi", which is what pi's own APP_NAME resolves to.
33
41
  */
34
- function readPiConfig(): { name?: unknown; clientUri?: unknown } | undefined {
42
+ function readPiConfig(): { name?: unknown; configDir?: unknown; clientUri?: unknown } | undefined {
35
43
  const dir = process.env.PI_PACKAGE_DIR?.trim()
36
44
  if (!dir) return undefined
37
45
  try {
38
46
  const manifest = JSON.parse(readFileSync(join(resolve(dir), "package.json"), "utf8")) as {
39
- piConfig?: { name?: unknown; clientUri?: unknown }
47
+ piConfig?: { name?: unknown; configDir?: unknown; clientUri?: unknown }
40
48
  }
41
49
  return manifest.piConfig
42
50
  } catch {
@@ -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/cli.js CHANGED
@@ -14,15 +14,36 @@ function expandHome(input) {
14
14
  return path.resolve(input);
15
15
  }
16
16
 
17
- const AGENT_DIR = process.env.PI_CODING_AGENT_DIR?.trim()
18
- ? expandHome(process.env.PI_CODING_AGENT_DIR.trim())
19
- : path.join(HOME, ".pi", "agent");
17
+ function readPiConfig() {
18
+ const dir = process.env.PI_PACKAGE_DIR?.trim();
19
+ if (!dir) return undefined;
20
+ try {
21
+ return JSON.parse(fs.readFileSync(path.join(path.resolve(dir), "package.json"), "utf8")).piConfig;
22
+ } catch {
23
+ return undefined;
24
+ }
25
+ }
26
+
27
+ function getConfigDirName() {
28
+ const configDir = readPiConfig()?.configDir;
29
+ return typeof configDir === "string" && configDir.trim() ? configDir.trim() : ".pi";
30
+ }
31
+
32
+ function getAgentDir() {
33
+ const piConfig = readPiConfig();
34
+ const appName = typeof piConfig?.name === "string" && piConfig.name.trim() ? piConfig.name.trim() : "pi";
35
+ const configured = process.env[`${appName.toUpperCase()}_CODING_AGENT_DIR`]?.trim();
36
+ if (configured) return expandHome(configured);
37
+ return path.join(HOME, getConfigDirName(), "agent");
38
+ }
39
+
40
+ const AGENT_DIR = getAgentDir();
20
41
  const PI_CONFIG_PATH = path.join(AGENT_DIR, "mcp.json");
21
42
  const GENERIC_GLOBAL_CONFIG_PATH = path.join(HOME, ".config", "mcp", "mcp.json");
22
43
  const AGENTS_GLOBAL_CONFIG_PATH = path.join(HOME, ".agents", "mcp.json");
23
44
  const AGENTS_NESTED_GLOBAL_CONFIG_PATH = path.join(HOME, ".agents", "mcp", "mcp.json");
24
45
  const PROJECT_CONFIG_PATH = path.resolve(process.cwd(), ".mcp.json");
25
- const PROJECT_PI_CONFIG_PATH = path.resolve(process.cwd(), ".pi", "mcp.json");
46
+ const PROJECT_PI_CONFIG_PATH = path.resolve(process.cwd(), getConfigDirName(), "mcp.json");
26
47
 
27
48
  const IMPORT_PATHS = {
28
49
  cursor: [path.join(HOME, ".cursor", "mcp.json")],
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/config.ts CHANGED
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { parse as parseToml } from "smol-toml";
6
6
  import stripJsonComments from "strip-json-comments";
7
- import { getAgentPath } from "./agent-dir.ts";
7
+ import { getAgentPath, getConfigDirName } from "./agent-dir.ts";
8
8
  import { getAgentPluginSummaries, loadAgentPluginConfigs, type AgentPluginSummary } from "./agent-plugin-loader.ts";
9
9
  import { isServerDisabled, type HostConfigDiscovery, type McpConfig, type ServerEntry, type McpSettings, type ImportKind, type ServerProvenance } from "./types.ts";
10
10
  import { toStringRecord } from "./utils.ts";
@@ -15,7 +15,7 @@ const AGENTS_GLOBAL_CONFIG_PATHS = [
15
15
  join(homedir(), ".agents", "mcp", "mcp.json"),
16
16
  ] as const;
17
17
  const PROJECT_CONFIG_NAME = ".mcp.json";
18
- const PROJECT_PI_CONFIG_NAME = ".pi/mcp.json";
18
+ const PROJECT_PI_CONFIG_NAME = "mcp.json";
19
19
  const REPOPROMPT_BINARY_CANDIDATES = [
20
20
  join(homedir(), "RepoPrompt", "repoprompt_cli"),
21
21
  "/Applications/Repo Prompt.app/Contents/MacOS/repoprompt-mcp",
@@ -177,7 +177,7 @@ export function getProjectConfigPath(cwd = process.cwd()): string {
177
177
  }
178
178
 
179
179
  export function getProjectPiConfigPath(cwd = process.cwd()): string {
180
- return resolve(cwd, PROJECT_PI_CONFIG_NAME);
180
+ return resolve(cwd, getConfigDirName(), PROJECT_PI_CONFIG_NAME);
181
181
  }
182
182
 
183
183
  export function getConfigDiscoveryPaths(overridePath?: string, cwd = process.cwd()): ConfigDiscoveryPath[] {
@@ -471,7 +471,7 @@ function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
471
471
  // different url, these MUST NOT be inherited from the lower-precedence entry —
472
472
  // otherwise the original endpoint's credentials would be shipped to the new
473
473
  // url. See the SECURITY note in mergeServerMaps.
474
- const URL_BOUND_AUTH_FIELDS = ["headers", "bearerToken", "bearerTokenEnv"] as const;
474
+ const URL_BOUND_AUTH_FIELDS = ["headers", "bearerToken", "bearerTokenEnv", "requestHeadersCommand"] as const;
475
475
 
476
476
  function mergeServerMaps(
477
477
  base: Record<string, ServerEntry>,
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 { createToolSelectorCandidateIndex, 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,33 @@ export function resolveDirectTools(
149
150
  if (!toolFilter) continue;
150
151
 
151
152
  const effectivePrefix = resolveToolPrefix(definition, prefix);
153
+ const hasToolFilters =
154
+ (Array.isArray(definition.includeTools) && definition.includeTools.length > 0) ||
155
+ (Array.isArray(definition.excludeTools) && definition.excludeTools.length > 0);
156
+ const selectorCandidateIndex = hasToolFilters ? (() => {
157
+ const candidates = new Set<string>();
158
+ for (const [otherServerName, otherDefinition] of Object.entries(config.mcpServers)) {
159
+ const otherCache = cache.servers[otherServerName];
160
+ if (!otherCache || !isServerCacheValid(otherCache, otherDefinition) || isServerDisabled(otherDefinition)) continue;
161
+ const otherPrefix = resolveToolPrefix(otherDefinition, prefix);
162
+ for (const otherTool of otherCache.tools ?? []) {
163
+ if (!isUiToolVisibleToModel(otherTool.uiVisibility)) continue;
164
+ for (const candidate of getToolNameCandidates(otherTool.name, otherServerName, otherPrefix, false)) candidates.add(candidate);
165
+ }
166
+ if (otherDefinition.exposeResources !== false) {
167
+ for (const resource of otherCache.resources ?? []) {
168
+ const baseName = `read_${resourceNameToToolName(resource.name)}`;
169
+ for (const candidate of getToolNameCandidates(baseName, otherServerName, otherPrefix, false)) candidates.add(candidate);
170
+ }
171
+ }
172
+ }
173
+ return createToolSelectorCandidateIndex(candidates);
174
+ })() : undefined;
152
175
 
153
176
  for (const tool of serverCache.tools ?? []) {
177
+ if (!isUiToolVisibleToModel(tool.uiVisibility)) continue;
154
178
  if (toolFilter !== true && !toolFilter.includes(tool.name)) continue;
155
- if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) continue;
179
+ if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) continue;
156
180
  const prefixedName = formatToolName(tool.name, serverName, effectivePrefix);
157
181
  if (BUILTIN_NAMES.has(prefixedName)) {
158
182
  console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
@@ -178,7 +202,7 @@ export function resolveDirectTools(
178
202
  for (const resource of serverCache.resources ?? []) {
179
203
  const baseName = `read_${resourceNameToToolName(resource.name)}`;
180
204
  if (toolFilter !== true && !toolFilter.includes(baseName)) continue;
181
- if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) continue;
205
+ if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) continue;
182
206
  const prefixedName = formatToolName(baseName, serverName, effectivePrefix);
183
207
  if (BUILTIN_NAMES.has(prefixedName)) {
184
208
  console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
@@ -200,7 +224,7 @@ export function resolveDirectTools(
200
224
  }
201
225
  }
202
226
 
203
- if (specs.length >= DIRECT_TOOLS_ADVISORY_THRESHOLD) {
227
+ if (config.settings?.warnOnLargeDirectTools !== false && specs.length >= DIRECT_TOOLS_ADVISORY_THRESHOLD) {
204
228
  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.`);
205
229
  }
206
230
 
@@ -230,15 +254,39 @@ export function buildProxyDescription(
230
254
  for (const serverName of Object.keys(config.mcpServers)) {
231
255
  const definition = config.mcpServers[serverName];
232
256
  if (!definition || isServerDisabled(definition)) continue;
233
- const entry = cache?.servers?.[serverName];
257
+ const cachedEntry = cache?.servers?.[serverName];
258
+ const entry = cachedEntry && isServerCacheValid(cachedEntry, definition) ? cachedEntry : undefined;
234
259
  const effectivePrefix = resolveToolPrefix(definition, prefix);
260
+ const hasToolFilters =
261
+ (Array.isArray(definition.includeTools) && definition.includeTools.length > 0) ||
262
+ (Array.isArray(definition.excludeTools) && definition.excludeTools.length > 0);
263
+ const selectorCandidateIndex = hasToolFilters && cache ? (() => {
264
+ const candidates = new Set<string>();
265
+ for (const [otherServerName, otherDefinition] of Object.entries(config.mcpServers)) {
266
+ const otherEntry = cache.servers[otherServerName];
267
+ if (!otherEntry || !isServerCacheValid(otherEntry, otherDefinition) || isServerDisabled(otherDefinition)) continue;
268
+ const otherPrefix = resolveToolPrefix(otherDefinition, prefix);
269
+ for (const otherTool of otherEntry.tools ?? []) {
270
+ if (!isUiToolVisibleToModel(otherTool.uiVisibility)) continue;
271
+ for (const candidate of getToolNameCandidates(otherTool.name, otherServerName, otherPrefix, false)) candidates.add(candidate);
272
+ }
273
+ if (otherDefinition.exposeResources !== false) {
274
+ for (const resource of otherEntry.resources ?? []) {
275
+ const baseName = `read_${resourceNameToToolName(resource.name)}`;
276
+ for (const candidate of getToolNameCandidates(baseName, otherServerName, otherPrefix, false)) candidates.add(candidate);
277
+ }
278
+ }
279
+ }
280
+ return createToolSelectorCandidateIndex(candidates);
281
+ })() : undefined;
235
282
  const toolCount = (entry?.tools ?? []).filter(
236
- (tool) => isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools),
283
+ (tool) => isUiToolVisibleToModel(tool.uiVisibility)
284
+ && isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex),
237
285
  ).length;
238
286
  const resourceCount = definition?.exposeResources !== false
239
287
  ? (entry?.resources ?? []).filter((resource) => {
240
288
  const baseName = `read_${resourceNameToToolName(resource.name)}`;
241
- return isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools);
289
+ return isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex);
242
290
  }).length
243
291
  : 0;
244
292
  const totalItems = toolCount + resourceCount;
@@ -264,7 +312,9 @@ export function buildProxyDescription(
264
312
  const instructionSummaries: string[] = [];
265
313
  for (const serverName of Object.keys(config.mcpServers)) {
266
314
  if (isServerDisabled(config.mcpServers[serverName])) continue;
267
- const instructions = cache?.servers?.[serverName]?.instructions;
315
+ const definition = config.mcpServers[serverName];
316
+ const entry = definition && cache?.servers?.[serverName];
317
+ const instructions = entry && definition && isServerCacheValid(entry, definition) ? entry.instructions : undefined;
268
318
  if (!instructions) continue;
269
319
  const snippet = truncateAtWord(instructions.replace(/\s+/g, " ").trim(), INSTRUCTIONS_SNIPPET_LENGTH);
270
320
  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"