pi-mcp-adapter 2.25.0 → 2.26.1
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 +22 -0
- package/README.md +10 -5
- package/agent-dir.ts +12 -4
- package/cli.js +25 -4
- package/commands.ts +26 -0
- package/config.ts +4 -4
- package/direct-tools.ts +18 -15
- package/index.ts +71 -27
- package/init.ts +26 -2
- package/lifecycle.ts +257 -24
- package/mcp-setup-panel.ts +2 -1
- package/metadata-cache.ts +18 -8
- package/package.json +2 -1
- package/request-headers-command.ts +336 -0
- package/server-manager.ts +126 -1
- package/session-recovery.ts +7 -0
- package/tool-approval.ts +15 -2
- package/tool-metadata.ts +38 -18
- package/tool-result-renderer.ts +44 -4
- package/types.ts +93 -10
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.26.1] - 2026-08-18
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Scoped “Allow for session” tool approvals to the approved arguments, so one approval no longer applies to later calls with different inputs. Thanks to [@spaceshipmike](https://github.com/spaceshipmike) for #367.
|
|
14
|
+
- Stopped MCP panel commands from hanging in RPC, JSON, and print modes when terminal-only UI is unavailable. Thanks to [@shixin-guo](https://github.com/shixin-guo) for PR #365.
|
|
15
|
+
- Kept compact MCP result rows useful by showing a short input preview and skipping leading blank output in collapsed previews.
|
|
16
|
+
- Recovered MCP gateway requests nested inside proxy `args` instead of showing status, and now rejects invalid nested gateway requests with guidance. Thanks to [@ibrmora](https://github.com/ibrmora) for #363.
|
|
17
|
+
- Kept remote keep-alive tool catalogs fresh across server restarts, so long-lived Pi sessions can discover replacement tools without restarting. Thanks to [@dmorn](https://github.com/dmorn) for #369 and PR #370.
|
|
18
|
+
|
|
19
|
+
## [2.26.0] - 2026-08-14
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- 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.
|
|
23
|
+
- Added `settings.warnOnLargeDirectTools` to suppress the advisory for 75 or more resolved direct tools. Thanks @Roshvan for issue #358.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Refined request-header command result handling types without changing runtime behavior.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- 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.
|
|
30
|
+
- 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.
|
|
31
|
+
|
|
10
32
|
## [2.25.0] - 2026-08-13
|
|
11
33
|
|
|
12
34
|
### 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. |
|
|
@@ -292,8 +293,10 @@ You can also pass only the `code` query parameter with `args: { code: "..." }`.
|
|
|
292
293
|
|
|
293
294
|
- **`lazy`** (default) — Don't connect at startup. Connect on first tool call. Disconnect after idle timeout. Cached metadata keeps search/list working without connections.
|
|
294
295
|
- **`eager`** — Connect at startup but don't auto-reconnect if the connection drops. No idle timeout by default (set `idleTimeout` explicitly to enable).
|
|
295
|
-
- **`keep-alive`** — Connect at startup.
|
|
296
|
-
- **`lazy-keep-alive`** — Don't connect at startup. Connect on first tool call (like `lazy`). Once spawned, never idle-shut down and
|
|
296
|
+
- **`keep-alive`** — Connect at startup. Remote HTTP servers refresh their tool catalog during health checks, before user input, and before adapter-triggered turns, reconnecting when the server reports that the session expired. No idle timeout. Use for servers you always need available.
|
|
297
|
+
- **`lazy-keep-alive`** — Don't connect at startup. Connect on first tool call (like `lazy`). Once spawned, never idle-shut down and use the same catalog refresh and reconnect checks as `keep-alive`. Use for servers that are expensive to start but should stay resident after their first use.
|
|
298
|
+
|
|
299
|
+
For remote HTTP keep-alive servers, the authoritative `tools/list` refresh is also the fallback when `list_changed` notifications are unavailable or their stream is lost. Each `tools/list` or `ping` request is capped at 5 seconds, up to 10 servers are checked concurrently, and transient failures use bounded backoff. A successful refresh updates metadata without reconnecting; a response proving that the HTTP session expired triggers a full reconnect and reinstalls the notification handlers. Dynamic direct-tool registration follows the refreshed metadata unless `freezeDirectTools` is enabled.
|
|
297
300
|
|
|
298
301
|
When any enabled server uses `eager` or `keep-alive`, initialization also starts when the extension loads. This supports hosts that embed Pi programmatically and never emit `session_start`; if a session does start later, the session-owned runtime supersedes the load-time runtime.
|
|
299
302
|
|
|
@@ -310,6 +313,7 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
310
313
|
"toolResultRendering": "compact",
|
|
311
314
|
"collapsedResultLines": 1,
|
|
312
315
|
"notifyOnStartupConnect": true,
|
|
316
|
+
"warnOnLargeDirectTools": true,
|
|
313
317
|
"hostConfigDiscovery": "off",
|
|
314
318
|
"approveTools": ["github_delete_*", "notion_update_*"],
|
|
315
319
|
"oauthDir": ".pi/mcp-oauth",
|
|
@@ -340,6 +344,7 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
340
344
|
| `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. |
|
|
341
345
|
| `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. |
|
|
342
346
|
| `directTools` | Global default for all servers (default: false). Per-server overrides this. |
|
|
347
|
+
| `warnOnLargeDirectTools` | Show the advisory when 75 or more direct tools resolve (default: `true`). Set to `false` to suppress only this advisory. |
|
|
343
348
|
| `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. |
|
|
344
349
|
| `scriptMode` | Register the MCP-only `mcpScript` plain-JavaScript tool (default: true). Set to `false` to hide it. |
|
|
345
350
|
| `disableProxyTool` | Hide the `mcp` proxy tool once configured direct tools are fully available from cache. |
|
|
@@ -544,7 +549,7 @@ To hide specific tools while still using `directTools: true`, add `excludeTools`
|
|
|
544
549
|
|
|
545
550
|
`includeTools` and `excludeTools` filter direct tools, proxy search/list/describe, and the `/mcp` panel view.
|
|
546
551
|
|
|
547
|
-
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
|
|
552
|
+
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.
|
|
548
553
|
|
|
549
554
|
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>`.
|
|
550
555
|
|
|
@@ -655,7 +660,7 @@ Prefer `.mcp.json` for project-local shared MCP config. Use `.pi/mcp.json` only
|
|
|
655
660
|
|
|
656
661
|
`mcp({ connect: "server-name" })` refreshes an already connected server, so new tools, resources, prompts, and instructions can load without restarting Pi.
|
|
657
662
|
|
|
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.
|
|
663
|
+
MCP proxy and direct-tool results use compact self-rendered rows by default. Collapsed success output shows the call title, a bounded one-line input preview when arguments exist, 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.
|
|
659
664
|
|
|
660
665
|
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.
|
|
661
666
|
|
|
@@ -722,7 +727,7 @@ Advertised tool `outputSchema` values support JSON Schema draft-07 and 2020-12.
|
|
|
722
727
|
- Idle servers disconnect after 10 minutes (configurable), reconnect automatically on next use
|
|
723
728
|
- npx-based servers resolve to direct binary paths, skipping the ~143 MB npm parent process
|
|
724
729
|
- MCP server validates arguments, not the adapter
|
|
725
|
-
-
|
|
730
|
+
- Remote keep-alive servers force-refresh their tool catalog during health checks, before user input, and before adapter-triggered turns, with bounded reconnect backoff
|
|
726
731
|
- Specific tools can be promoted from the proxy to first-class Pi tools via `directTools` config, so the LLM sees them directly instead of having to search
|
|
727
732
|
|
|
728
733
|
## Limitations
|
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
|
|
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(),
|
|
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 {
|
package/cli.js
CHANGED
|
@@ -14,15 +14,36 @@ function expandHome(input) {
|
|
|
14
14
|
return path.resolve(input);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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(),
|
|
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
|
@@ -28,6 +28,19 @@ function terminalHyperlink(label: string, url: string): string {
|
|
|
28
28
|
return `\u001B]8;;${sanitizeTerminalText(url)}\u001B\\${sanitizeTerminalText(label)}\u001B]8;;\u001B\\`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* True when this run mode can display a `ctx.ui.custom()` overlay.
|
|
33
|
+
*
|
|
34
|
+
* `ctx.hasUI` only reports that *some* UI context is bound. In rpc/print mode
|
|
35
|
+
* that context is a headless stub whose `custom()` returns immediately without
|
|
36
|
+
* ever invoking the factory or the `done` callback, so a panel awaited through
|
|
37
|
+
* `new Promise(resolve => ctx.ui.custom(...))` never settles and the command
|
|
38
|
+
* hangs. Overlay panels therefore additionally require `ctx.mode === "tui"`.
|
|
39
|
+
*/
|
|
40
|
+
function canRenderPanel(ctx: ExtensionContext): boolean {
|
|
41
|
+
return ctx.hasUI && ctx.mode === "tui";
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
export async function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {
|
|
32
45
|
if (!ctx.hasUI) return;
|
|
33
46
|
|
|
@@ -399,6 +412,10 @@ export async function openMcpSetup(
|
|
|
399
412
|
options: { includeHostConfigs?: boolean } = {},
|
|
400
413
|
): Promise<PanelFlowResult> {
|
|
401
414
|
if (!ctx.hasUI) return { configChanged: false };
|
|
415
|
+
if (!canRenderPanel(ctx)) {
|
|
416
|
+
ctx.ui.notify(`The interactive MCP setup panel is only available in the terminal UI (current mode: ${ctx.mode}). Edit .mcp.json directly, or run /mcp status to review servers.`, "info");
|
|
417
|
+
return { configChanged: false };
|
|
418
|
+
}
|
|
402
419
|
if (state.programmaticConfig) {
|
|
403
420
|
ctx.ui.notify("MCP setup is unavailable when config is supplied by createMcpAdapter().", "info");
|
|
404
421
|
return { configChanged: false };
|
|
@@ -533,6 +550,11 @@ export async function openMcpPanel(
|
|
|
533
550
|
}
|
|
534
551
|
return { configChanged: false };
|
|
535
552
|
}
|
|
553
|
+
if (!canRenderPanel(ctx)) {
|
|
554
|
+
// No overlay here, but the same information is available as text.
|
|
555
|
+
await showStatus(state, ctx);
|
|
556
|
+
return { configChanged: false };
|
|
557
|
+
}
|
|
536
558
|
if (Object.keys(state.config.mcpServers).length === 0) {
|
|
537
559
|
return openMcpSetup(state, pi, ctx, configOverridePath, "empty", { includeHostConfigs: false });
|
|
538
560
|
}
|
|
@@ -587,6 +609,10 @@ export async function openMcpAuthPanel(
|
|
|
587
609
|
configOverridePath?: string,
|
|
588
610
|
): Promise<PanelFlowResult> {
|
|
589
611
|
if (!ctx.hasUI) return { configChanged: false };
|
|
612
|
+
if (!canRenderPanel(ctx)) {
|
|
613
|
+
ctx.ui.notify(`The interactive MCP auth panel is only available in the terminal UI (current mode: ${ctx.mode}). Use /mcp-auth <server> to authenticate a specific server.`, "info");
|
|
614
|
+
return { configChanged: false };
|
|
615
|
+
}
|
|
590
616
|
if (state.programmaticConfig) {
|
|
591
617
|
ctx.ui.notify("Use /mcp-auth <server> to authenticate a server from the in-memory SDK config.", "info");
|
|
592
618
|
return { configChanged: false };
|
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 = "
|
|
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,7 @@ 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, getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.ts";
|
|
14
|
+
import { createToolSelectorCandidateIndex, formatToolName, getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.ts";
|
|
15
15
|
import { isUiToolVisibleToModel } from "./ui-tool-visibility.ts";
|
|
16
16
|
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
17
17
|
import { authenticate, supportsOAuth } from "./mcp-auth-flow.ts";
|
|
@@ -150,7 +150,10 @@ export function resolveDirectTools(
|
|
|
150
150
|
if (!toolFilter) continue;
|
|
151
151
|
|
|
152
152
|
const effectivePrefix = resolveToolPrefix(definition, prefix);
|
|
153
|
-
const
|
|
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 ? (() => {
|
|
154
157
|
const candidates = new Set<string>();
|
|
155
158
|
for (const [otherServerName, otherDefinition] of Object.entries(config.mcpServers)) {
|
|
156
159
|
const otherCache = cache.servers[otherServerName];
|
|
@@ -167,14 +170,13 @@ export function resolveDirectTools(
|
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
172
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
};
|
|
173
|
+
return createToolSelectorCandidateIndex(candidates);
|
|
174
|
+
})() : undefined;
|
|
173
175
|
|
|
174
176
|
for (const tool of serverCache.tools ?? []) {
|
|
175
177
|
if (!isUiToolVisibleToModel(tool.uiVisibility)) continue;
|
|
176
178
|
if (toolFilter !== true && !toolFilter.includes(tool.name)) continue;
|
|
177
|
-
if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools,
|
|
179
|
+
if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) continue;
|
|
178
180
|
const prefixedName = formatToolName(tool.name, serverName, effectivePrefix);
|
|
179
181
|
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
180
182
|
console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
|
|
@@ -200,7 +202,7 @@ export function resolveDirectTools(
|
|
|
200
202
|
for (const resource of serverCache.resources ?? []) {
|
|
201
203
|
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
202
204
|
if (toolFilter !== true && !toolFilter.includes(baseName)) continue;
|
|
203
|
-
if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools,
|
|
205
|
+
if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) continue;
|
|
204
206
|
const prefixedName = formatToolName(baseName, serverName, effectivePrefix);
|
|
205
207
|
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
206
208
|
console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
|
|
@@ -222,7 +224,7 @@ export function resolveDirectTools(
|
|
|
222
224
|
}
|
|
223
225
|
}
|
|
224
226
|
|
|
225
|
-
if (specs.length >= DIRECT_TOOLS_ADVISORY_THRESHOLD) {
|
|
227
|
+
if (config.settings?.warnOnLargeDirectTools !== false && specs.length >= DIRECT_TOOLS_ADVISORY_THRESHOLD) {
|
|
226
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.`);
|
|
227
229
|
}
|
|
228
230
|
|
|
@@ -255,8 +257,10 @@ export function buildProxyDescription(
|
|
|
255
257
|
const cachedEntry = cache?.servers?.[serverName];
|
|
256
258
|
const entry = cachedEntry && isServerCacheValid(cachedEntry, definition) ? cachedEntry : undefined;
|
|
257
259
|
const effectivePrefix = resolveToolPrefix(definition, prefix);
|
|
258
|
-
const
|
|
259
|
-
|
|
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 ? (() => {
|
|
260
264
|
const candidates = new Set<string>();
|
|
261
265
|
for (const [otherServerName, otherDefinition] of Object.entries(config.mcpServers)) {
|
|
262
266
|
const otherEntry = cache.servers[otherServerName];
|
|
@@ -273,17 +277,16 @@ export function buildProxyDescription(
|
|
|
273
277
|
}
|
|
274
278
|
}
|
|
275
279
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
};
|
|
280
|
+
return createToolSelectorCandidateIndex(candidates);
|
|
281
|
+
})() : undefined;
|
|
279
282
|
const toolCount = (entry?.tools ?? []).filter(
|
|
280
283
|
(tool) => isUiToolVisibleToModel(tool.uiVisibility)
|
|
281
|
-
&& isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools,
|
|
284
|
+
&& isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex),
|
|
282
285
|
).length;
|
|
283
286
|
const resourceCount = definition?.exposeResources !== false
|
|
284
287
|
? (entry?.resources ?? []).filter((resource) => {
|
|
285
288
|
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
286
|
-
return isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools,
|
|
289
|
+
return isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex);
|
|
287
290
|
}).length
|
|
288
291
|
: 0;
|
|
289
292
|
const totalItems = toolCount + resourceCount;
|
package/index.ts
CHANGED
|
@@ -417,6 +417,29 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
417
417
|
}
|
|
418
418
|
});
|
|
419
419
|
|
|
420
|
+
pi.on("input", async () => {
|
|
421
|
+
const inputOwner = currentOwner;
|
|
422
|
+
if (!inputOwner?.isActive()) return;
|
|
423
|
+
|
|
424
|
+
if (!state && initPromise) {
|
|
425
|
+
try {
|
|
426
|
+
await awaitWithTimeout(initPromise, INIT_WAIT_TIMEOUT_MS);
|
|
427
|
+
} catch {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const inputState = state;
|
|
433
|
+
if (!inputState || !inputOwner.isActive()) return;
|
|
434
|
+
try {
|
|
435
|
+
await inputState.lifecycle.ensureConverged(inputOwner.signal);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (!isAbortError(error, inputOwner.signal)) {
|
|
438
|
+
logger.debug(`MCP: keep-alive convergence failed before input: ${formatTerminalError(error)}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
420
443
|
pi.on("session_shutdown", async () => {
|
|
421
444
|
++lifecycleGeneration;
|
|
422
445
|
const currentState = state;
|
|
@@ -737,12 +760,12 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
737
760
|
action?: string;
|
|
738
761
|
}, signal: AbortSignal | undefined, _onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined, _ctx: ExtensionContext) {
|
|
739
762
|
const executeOwner = currentOwner;
|
|
740
|
-
|
|
741
|
-
|
|
763
|
+
const parseArgs = (value: string | Record<string, unknown> | undefined): Record<string, unknown> | undefined => {
|
|
764
|
+
if (value === undefined || value === "") return undefined;
|
|
742
765
|
let args: unknown;
|
|
743
|
-
if (typeof
|
|
766
|
+
if (typeof value === "string") {
|
|
744
767
|
try {
|
|
745
|
-
args = JSON.parse(
|
|
768
|
+
args = JSON.parse(value);
|
|
746
769
|
} catch (error) {
|
|
747
770
|
if (error instanceof SyntaxError) {
|
|
748
771
|
throw new Error(`Invalid args JSON: ${error.message}`, { cause: error });
|
|
@@ -750,14 +773,35 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
750
773
|
throw error;
|
|
751
774
|
}
|
|
752
775
|
} else {
|
|
753
|
-
args =
|
|
776
|
+
args = value;
|
|
754
777
|
}
|
|
755
778
|
|
|
756
779
|
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
757
780
|
const gotType = Array.isArray(args) ? "array" : args === null ? "null" : typeof args;
|
|
758
781
|
throw new Error(`Invalid args: expected a JSON object, got ${gotType}`);
|
|
759
782
|
}
|
|
760
|
-
|
|
783
|
+
return args as Record<string, unknown>;
|
|
784
|
+
};
|
|
785
|
+
let parsedArgs = parseArgs(params.args);
|
|
786
|
+
let dispatchParams = params;
|
|
787
|
+
const hasGatewayMode = (value: typeof params): boolean =>
|
|
788
|
+
value.tool !== undefined
|
|
789
|
+
|| value.connect !== undefined
|
|
790
|
+
|| value.describe !== undefined
|
|
791
|
+
|| value.instructions !== undefined
|
|
792
|
+
|| value.search !== undefined
|
|
793
|
+
|| value.server !== undefined
|
|
794
|
+
|| value.action !== undefined;
|
|
795
|
+
if (!hasGatewayMode(params) && parsedArgs) {
|
|
796
|
+
const nestedParams = parsedArgs as typeof params;
|
|
797
|
+
if (hasGatewayMode(nestedParams)) {
|
|
798
|
+
dispatchParams = nestedParams;
|
|
799
|
+
parsedArgs = parseArgs(nestedParams.args);
|
|
800
|
+
} else {
|
|
801
|
+
throw new Error("Gateway params were nested inside `args`; pass them top-level (for example, mcp({ search: \"...\" }) or mcp({ tool: \"...\", args: {} })).");
|
|
802
|
+
}
|
|
803
|
+
} else if (!hasGatewayMode(params) && params.args !== undefined) {
|
|
804
|
+
throw new Error("Gateway params were nested inside `args`; pass them top-level (for example, mcp({ search: \"...\" }) or mcp({ tool: \"...\", args: {} })).");
|
|
761
805
|
}
|
|
762
806
|
|
|
763
807
|
if (!state && initPromise) {
|
|
@@ -788,22 +832,22 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
788
832
|
}
|
|
789
833
|
executeOwner?.throwIfInactive();
|
|
790
834
|
|
|
791
|
-
if (
|
|
835
|
+
if (dispatchParams.action === "ui-messages") {
|
|
792
836
|
return executeUiMessages(state);
|
|
793
837
|
}
|
|
794
|
-
if (
|
|
795
|
-
if (!
|
|
838
|
+
if (dispatchParams.action === "auth-start") {
|
|
839
|
+
if (!dispatchParams.server) {
|
|
796
840
|
return {
|
|
797
841
|
content: [{ type: "text" as const, text: "auth-start requires `server`. Example: mcp({ action: \"auth-start\", server: \"linear-server\" })" }],
|
|
798
842
|
details: { mode: "auth-start", error: "missing_server" },
|
|
799
843
|
};
|
|
800
844
|
}
|
|
801
845
|
return signal
|
|
802
|
-
? executeAuthStart(state,
|
|
803
|
-
: executeAuthStart(state,
|
|
846
|
+
? executeAuthStart(state, dispatchParams.server, signal)
|
|
847
|
+
: executeAuthStart(state, dispatchParams.server);
|
|
804
848
|
}
|
|
805
|
-
if (
|
|
806
|
-
if (!
|
|
849
|
+
if (dispatchParams.action === "auth-complete") {
|
|
850
|
+
if (!dispatchParams.server) {
|
|
807
851
|
return {
|
|
808
852
|
content: [{ type: "text" as const, text: "auth-complete requires `server`." }],
|
|
809
853
|
details: { mode: "auth-complete", error: "missing_server" },
|
|
@@ -817,28 +861,28 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
817
861
|
};
|
|
818
862
|
}
|
|
819
863
|
return signal
|
|
820
|
-
? executeAuthComplete(state,
|
|
821
|
-
: executeAuthComplete(state,
|
|
864
|
+
? executeAuthComplete(state, dispatchParams.server, input, signal)
|
|
865
|
+
: executeAuthComplete(state, dispatchParams.server, input);
|
|
822
866
|
}
|
|
823
|
-
if (
|
|
824
|
-
return executeCall(state,
|
|
867
|
+
if (dispatchParams.tool) {
|
|
868
|
+
return executeCall(state, dispatchParams.tool, parsedArgs, dispatchParams.server, getPiTools, signal);
|
|
825
869
|
}
|
|
826
|
-
if (
|
|
827
|
-
const result = await executeConnect(state,
|
|
870
|
+
if (dispatchParams.connect) {
|
|
871
|
+
const result = await executeConnect(state, dispatchParams.connect, signal);
|
|
828
872
|
syncToolSurface(_ctx as ExtensionContext);
|
|
829
873
|
return result;
|
|
830
874
|
}
|
|
831
|
-
if (
|
|
832
|
-
return executeDescribe(state,
|
|
875
|
+
if (dispatchParams.describe) {
|
|
876
|
+
return executeDescribe(state, dispatchParams.describe);
|
|
833
877
|
}
|
|
834
|
-
if (
|
|
835
|
-
return executeInstructions(state,
|
|
878
|
+
if (dispatchParams.instructions) {
|
|
879
|
+
return executeInstructions(state, dispatchParams.instructions);
|
|
836
880
|
}
|
|
837
|
-
if (
|
|
838
|
-
return executeSearch(state,
|
|
881
|
+
if (dispatchParams.search !== undefined) {
|
|
882
|
+
return executeSearch(state, dispatchParams.search, dispatchParams.regex, dispatchParams.server, dispatchParams.includeSchemas, dispatchParams.limit, dispatchParams.offset);
|
|
839
883
|
}
|
|
840
|
-
if (
|
|
841
|
-
return executeList(state,
|
|
884
|
+
if (dispatchParams.server) {
|
|
885
|
+
return executeList(state, dispatchParams.server);
|
|
842
886
|
}
|
|
843
887
|
return executeStatus(state);
|
|
844
888
|
},
|
package/init.ts
CHANGED
|
@@ -179,8 +179,20 @@ export async function initializeMcp(
|
|
|
179
179
|
},
|
|
180
180
|
...(ui !== undefined ? { ui } : {}),
|
|
181
181
|
sendMessage: (message, options) => {
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
const deliver = () => {
|
|
183
|
+
if (!owner.isActive()) return;
|
|
184
|
+
pi.sendMessage(message as unknown as Parameters<typeof pi.sendMessage>[0], options);
|
|
185
|
+
};
|
|
186
|
+
if (!options?.triggerTurn) {
|
|
187
|
+
deliver();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
void lifecycle.ensureConverged(owner.signal).then(deliver, error => {
|
|
191
|
+
if (!owner.isActive() || isAbortError(error, owner.signal)) return;
|
|
192
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
193
|
+
logger.debug(`MCP: pre-turn keep-alive convergence failed: ${sanitizeTerminalText(detail)}`);
|
|
194
|
+
deliver();
|
|
195
|
+
});
|
|
184
196
|
},
|
|
185
197
|
...(options.statusEvents !== undefined ? { statusEvents: options.statusEvents } : {}),
|
|
186
198
|
};
|
|
@@ -419,6 +431,18 @@ export async function initializeMcp(
|
|
|
419
431
|
updateStatusBar(state);
|
|
420
432
|
});
|
|
421
433
|
|
|
434
|
+
lifecycle.setHealthRestoredCallback((serverName) => {
|
|
435
|
+
if (!owner.isActive()) return;
|
|
436
|
+
clearFailure(state, serverName);
|
|
437
|
+
updateStatusBar(state);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
lifecycle.setAuthRequiredCallback((serverName) => {
|
|
441
|
+
if (!owner.isActive()) return;
|
|
442
|
+
clearFailure(state, serverName);
|
|
443
|
+
updateStatusBar(state);
|
|
444
|
+
});
|
|
445
|
+
|
|
422
446
|
lifecycle.setIdleShutdownCallback((serverName) => {
|
|
423
447
|
if (!owner.isActive()) return;
|
|
424
448
|
const idleMinutes = getEffectiveIdleTimeoutMinutes(state, serverName);
|