pi-mcp-adapter 2.12.0 → 2.13.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 +30 -0
- package/README.md +62 -13
- package/cli.js +25 -4
- package/config.ts +142 -11
- package/direct-tools.ts +8 -31
- package/index.ts +15 -1
- package/init.ts +28 -5
- package/mcp-auth.ts +161 -54
- package/mcp-output-guard.ts +2 -1
- package/mcp-panel.ts +9 -7
- package/mcp-setup-panel.ts +21 -8
- package/mcp-status.ts +109 -0
- package/mcp-trace.ts +310 -0
- package/metadata-cache.ts +46 -40
- package/oauth-handler.ts +21 -48
- package/package.json +8 -3
- package/proxy-modes.ts +56 -36
- package/server-manager.ts +43 -6
- package/state.ts +4 -0
- package/tool-metadata.ts +4 -4
- package/tool-result-renderer.ts +20 -2
- package/types.ts +143 -17
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.13.0] - 2026-07-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Added a versioned, sanitized MCP runtime status snapshot on Pi's shared event bus for extensions, without connecting lazy servers or exposing SDK internals. Thanks Ludev (@ludevdot) for issue #110.
|
|
14
|
+
- Added opt-in host-specific MCP config discovery with source/provenance and conflict reporting. Standard shared and Pi-owned config precedence remains unchanged, and external host files are never written or silently executed. Thanks @lsmir2 for issue #169.
|
|
15
|
+
- Added opt-in metadata-only JSONL MCP protocol tracing with bounded per-session files and redaction. Thanks @66-firebat for issue #45.
|
|
16
|
+
- Added per-server `includeTools` allowlists with exact-name and glob matching for proxy, direct-tool, and `/mcp` panel surfaces. Thanks Finn (@finnvyrn) for issue #136.
|
|
17
|
+
- Made `mcp({ connect: "server" })` refresh an already connected server instead of reusing stale tool metadata. Thanks Sebastiano Poggi (@rock3r) for issue #28 and @theflysurfer for the refresh analysis.
|
|
18
|
+
- Added a plug icon prefix to MCP footer status text. Thanks Felipe Cadal (@cadal-cw) for issue #145.
|
|
19
|
+
- Discovered user-global MCP configs from `~/.agents/mcp.json` and `~/.agents/mcp/mcp.json`. Thanks David Jadczyk (@davidjadczyk) for issue #117.
|
|
20
|
+
- Accepted JSONC-style comments and trailing commas in MCP JSON config files. Thanks @GoCoder7 for issue #124.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Measured and spilled oversized raw MCP result details as compact JSON instead of pretty-printed JSON, reducing hot-path allocation and event-loop work. Thanks José Maia (@glitch-ux) for issue #214 and PR #215.
|
|
24
|
+
- Renamed generated MCP resource tools from `get_<resource>` to `read_<resource>` to match the MCP `resources/read` operation. Thanks @vdom-1 for issue #185.
|
|
25
|
+
|
|
26
|
+
### Security
|
|
27
|
+
- Moved persistent OAuth credentials from plaintext `tokens.json` files into the operating system credential store, with one-way legacy import and fail-closed behavior when secure storage is unavailable. Thanks Sam Atkins (@atkinsam) for issue #180.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Skipped `resources/list` for MCP servers that do not advertise the `resources` capability, matching the existing prompt discovery gate and silencing the SDK v2 debug line that tools-only servers printed on every connect. Thanks Aleksandr Davydenko (@kotuke) for PR #213.
|
|
31
|
+
- Made the MCP footer show enabled configured servers as the primary count, with active connections as secondary state, so lazy servers no longer look broken before first use or after idle shutdown. Thanks blumlaut (@Blumlaut) for issue #93.
|
|
32
|
+
- Show the actual proxied MCP server/tool name in `mcp` tool results. Thanks Finn (@finnvyrn) and @dillontkh for issue #68.
|
|
33
|
+
- Removed the remaining TypeScript import cycles reported by `madge`. Thanks @av1155 for issue #101.
|
|
34
|
+
|
|
35
|
+
## [2.12.1] - 2026-07-24
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- Restored the SDK v1 dependency required by the MCP Apps bridge during Pi managed installs, where peer dependencies are intentionally not auto-installed. Thanks Nikolai Ugelvik (@NikolaiUgelvik), @warmwaffles, and @max-miller1204 for issue #212.
|
|
39
|
+
|
|
10
40
|
## [2.12.0] - 2026-07-24
|
|
11
41
|
|
|
12
42
|
### Added
|
package/README.md
CHANGED
|
@@ -51,19 +51,23 @@ Preferred project config: `.mcp.json`
|
|
|
51
51
|
}
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
Preferred user-global shared config: `~/.config/mcp/mcp.json`
|
|
54
|
+
Preferred user-global shared config: `~/.config/mcp/mcp.json`. Pi also reads the tool-agnostic global paths `~/.agents/mcp.json` and `~/.agents/mcp/mcp.json`.
|
|
55
55
|
|
|
56
56
|
Pi also reads Pi-owned override files for settings and host-specific compatibility:
|
|
57
57
|
|
|
58
58
|
- `<Pi agent dir>/mcp.json` — Pi global override (`~/.pi/agent/mcp.json` by default)
|
|
59
59
|
- `.pi/mcp.json` — Pi project override
|
|
60
60
|
|
|
61
|
+
Host-specific configs are detected and shown by `/mcp setup` and `pi-mcp-adapter init`, but they are not loaded automatically. To explicitly opt in to host-config fallback discovery, set `settings.hostConfigDiscovery` to `"on"` or run `pi-mcp-adapter init --discover-host-configs`. The default is `"off"`; `"prompt"` is available for integrations that want detection without activation. Host configs are lower precedence than every shared and Pi-owned source, and `/mcp setup` continues to offer explicit import adoption. Discovery reports source paths, provenance, and same-name conflicts; it never writes to external host files or silently launches commands from them.
|
|
62
|
+
|
|
61
63
|
Precedence is:
|
|
62
64
|
|
|
63
65
|
1. `~/.config/mcp/mcp.json`
|
|
64
|
-
2.
|
|
65
|
-
3.
|
|
66
|
-
4.
|
|
66
|
+
2. `~/.agents/mcp.json`
|
|
67
|
+
3. `~/.agents/mcp/mcp.json`
|
|
68
|
+
4. `<Pi agent dir>/mcp.json`
|
|
69
|
+
5. `.mcp.json`
|
|
70
|
+
6. `.pi/mcp.json`
|
|
67
71
|
|
|
68
72
|
`/mcp disable <server>` and `/mcp enable <server>` persist only the `disabled` field in the project-local `.pi/mcp.json`, which is the highest-precedence Pi layer. Enabling removes the project flag when lower layers are enabled, or writes `false` when needed to override a disabled lower source. This applies even when the effective server came from a shared global/project file, an imported host config, or `configPath`; the source file is never rewritten and credentials are never copied. Run `/reload` after changing the flag so registered tool surfaces are refreshed. The manual equivalent is to add `{ "disabled": true }` to a server in any normal MCP config. Supplied in-memory `createMcpAdapter({ config })` configurations are isolated and do not read or write this project override; the commands are unavailable in that mode.
|
|
69
73
|
|
|
@@ -97,6 +101,8 @@ Use the shared MCP files when you want one setup to work across hosts, and Pi-ow
|
|
|
97
101
|
| File | Purpose |
|
|
98
102
|
|------|---------|
|
|
99
103
|
| `~/.config/mcp/mcp.json` | User-global shared MCP config |
|
|
104
|
+
| `~/.agents/mcp.json` | User-global tool-agnostic MCP config |
|
|
105
|
+
| `~/.agents/mcp/mcp.json` | User-global tool-agnostic MCP config |
|
|
100
106
|
| `.mcp.json` | Project-local shared MCP config |
|
|
101
107
|
| `<Pi agent dir>/mcp.json` | Pi global override and compatibility imports (`~/.pi/agent/mcp.json` by default) |
|
|
102
108
|
| `.pi/mcp.json` | Pi project override |
|
|
@@ -128,7 +134,23 @@ The package ships TypeScript source for Pi's source-loader and SDK integrations.
|
|
|
128
134
|
|
|
129
135
|
A supplied `config` is a complete, isolated snapshot. It is not merged with files, imports, global config, project config, or `--mcp-config`, and it is never mutated. Each adapter factory and session receives its own clone, so separate integrations can use different servers and settings safely. In this mode, server status, reconnect, explicit `/mcp-auth <server>`, proxy calls, and direct tools continue to work; setup and no-argument auth/status panels report the limitation instead of discovering or writing ambient config.
|
|
130
136
|
|
|
131
|
-
With `configPath` and no `config`, the adapter keeps normal file merge behavior, and that path takes precedence over argv and `--mcp-config`. The default export keeps the normal file-based behavior. OAuth credentials
|
|
137
|
+
With `configPath` and no `config`, the adapter keeps normal file merge behavior, and that path takes precedence over argv and `--mcp-config`. The default export keeps the normal file-based behavior. OAuth credentials are stored in the operating system credential store and keyed by the configured server name; URL binding prevents credentials from being accepted for a different server URL. `settings.oauthDir` and `MCP_OAUTH_DIR` are used only as legacy plaintext import locations for older `tokens.json` files, not as credential namespaces. CSRF state and PKCE verifiers are flow-local, so concurrent authorization flows do not share transient secrets.
|
|
138
|
+
|
|
139
|
+
### Runtime status snapshots
|
|
140
|
+
|
|
141
|
+
Extensions can subscribe to the adapter's versioned shared event-bus channel instead of parsing `/mcp` or `mcp({})` output:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { MCP_STATUS_EVENT, type McpStatusSnapshot } from "pi-mcp-adapter";
|
|
145
|
+
|
|
146
|
+
pi.events.on(MCP_STATUS_EVENT, (snapshot) => {
|
|
147
|
+
const status = snapshot as McpStatusSnapshot;
|
|
148
|
+
// status.servers contains connected, cached, failed, needs-auth,
|
|
149
|
+
// not-connected, or disabled entries.
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The snapshot is read-only machine-readable data with copied per-server entries. It includes `totalTools`, `totalResources`, `connectedCount`, and `disabledCount`; each server includes `name`, `status`, `toolCount`, and `disabled`, with `resourceCount` when known and `failedAgoSeconds` only for an active failure. Reading status never connects a lazy server, starts authentication, or exposes SDK clients, transports, credentials, or server definitions. An initial snapshot is emitted after initialization, updates are emitted for status and metadata changes, and an empty snapshot is emitted when the session shuts down.
|
|
132
154
|
|
|
133
155
|
In the configuration examples below, `30000` is illustrative only. If `requestTimeoutMs` is omitted or set to `<= 0`, the MCP SDK default timeout is used.
|
|
134
156
|
|
|
@@ -170,15 +192,17 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
|
|
|
170
192
|
| `requestTimeoutMs` | Request timeout in milliseconds for live MCP calls (overrides global; if omitted or `<= 0`, the MCP SDK default timeout is used) |
|
|
171
193
|
| `exposeResources` | Expose MCP resources as tools (default: true) |
|
|
172
194
|
| `directTools` | `true`, `string[]`, or `false` — register tools individually instead of through proxy |
|
|
173
|
-
| `
|
|
195
|
+
| `includeTools` | `string[]` of tool names or glob patterns to expose (matches original names like `get_screenshot`, generated resource names like `read_figjam`, and prefixed names like `figma_get_screenshot`) |
|
|
196
|
+
| `excludeTools` | `string[]` of tool names or glob patterns to hide (applied after `includeTools`) |
|
|
174
197
|
| `debug` | Show server stderr (default: false) |
|
|
198
|
+
| `trace` | Enable metadata-only JSONL protocol tracing for this server; payloads, prompts, tool arguments/results, authorization data, and URLs are never persisted |
|
|
175
199
|
| `disabled` | Keep the server visible in config and status, but prevent connections, authentication, tools, and resource calls (only literal `true` disables it) |
|
|
176
200
|
|
|
177
201
|
For pre-registered browser OAuth clients, set `oauth.redirectUri` to the exact callback registered with the provider, for example `"http://localhost:3118/callback"`. Dynamic clients normally omit it and use a lazy OS-assigned localhost callback port.
|
|
178
202
|
|
|
179
203
|
### Remote/headless OAuth
|
|
180
204
|
|
|
181
|
-
If Pi is running on a remote server and cannot open a local browser, start OAuth through the proxy tool:
|
|
205
|
+
If Pi is running on a remote server and cannot open a local browser, start OAuth through the proxy tool. Persistent OAuth still requires an available OS credential store; on headless Linux that usually means an unlocked Secret Service/libsecret keyring. The adapter fails closed instead of falling back to plaintext credentials when the secure store is unavailable:
|
|
182
206
|
|
|
183
207
|
```js
|
|
184
208
|
mcp({ action: "auth-start", server: "linear-server" })
|
|
@@ -213,7 +237,14 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
213
237
|
"toolPrefix": "server",
|
|
214
238
|
"idleTimeout": 10,
|
|
215
239
|
"requestTimeoutMs": 30000,
|
|
216
|
-
"
|
|
240
|
+
"hostConfigDiscovery": "off",
|
|
241
|
+
"oauthDir": ".pi/mcp-oauth",
|
|
242
|
+
"trace": {
|
|
243
|
+
"enabled": true,
|
|
244
|
+
"file": ".pi/mcp-traces/mcp.jsonl",
|
|
245
|
+
"maxBytes": 262144,
|
|
246
|
+
"maxEvents": 10000
|
|
247
|
+
}
|
|
217
248
|
},
|
|
218
249
|
"mcpServers": { }
|
|
219
250
|
}
|
|
@@ -224,7 +255,8 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
224
255
|
| `toolPrefix` | `"server"` (default), `"short"` (strips `-mcp` suffix), `"none"`, or `"mcp"` (prefixes with `mcp__`, using server-mode normalization) |
|
|
225
256
|
| `idleTimeout` | Global idle timeout in minutes (default: 10, 0 to disable) |
|
|
226
257
|
| `requestTimeoutMs` | Global request timeout in milliseconds for live MCP calls (if omitted or `<= 0`, the MCP SDK default timeout is used) |
|
|
227
|
-
| `
|
|
258
|
+
| `hostConfigDiscovery` | Host-specific config policy: `"off"` (default), `"prompt"` (detect/report only), or `"on"` (explicitly load detected host configs as the lowest-precedence fallback) |
|
|
259
|
+
| `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. |
|
|
228
260
|
| `directTools` | Global default for all servers (default: false). Per-server overrides this. |
|
|
229
261
|
| `disableProxyTool` | Hide the `mcp` proxy tool once configured direct tools are fully available from cache. |
|
|
230
262
|
| `autoAuth` | Auto-run OAuth on `connect`/tool calls when a server needs auth, then retry once (default: false). |
|
|
@@ -232,8 +264,9 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
232
264
|
| `samplingAutoApprove` | Skip sampling confirmation prompts. Required for sampling in non-UI sessions (default: false). |
|
|
233
265
|
| `elicitation` | Allow MCP servers to request user input through Pi dialogs (default: true when Pi UI is available). |
|
|
234
266
|
| `outputGuard` | Guard oversized MCP output: `true` (default), `false`, or `{ maxBytes, maxLines, detailsMaxBytes }`. See [Output Guard](#output-guard). |
|
|
267
|
+
| `trace` | Opt-in metadata-only protocol tracing. Set `{ enabled: true }` globally or `trace: true` on a server. The per-session JSONL file defaults to `.pi/mcp-traces/`; `file`, `maxBytes` (default 262144), and `maxEvents` (default 10000) can be set. Raw MCP payloads, prompts, tool arguments/results, auth data, and URLs are never persisted. |
|
|
235
268
|
|
|
236
|
-
Per-server `idleTimeout` and `requestTimeoutMs` override the global settings.
|
|
269
|
+
Per-server `idleTimeout` and `requestTimeoutMs` override the global settings. `debug` remains stderr display and is unrelated to protocol tracing.
|
|
237
270
|
|
|
238
271
|
### Output Guard
|
|
239
272
|
|
|
@@ -323,7 +356,21 @@ To set a global default for all servers:
|
|
|
323
356
|
|
|
324
357
|
Per-server `directTools` overrides the global setting. The example above registers direct tools for every server except `huge-server`.
|
|
325
358
|
|
|
326
|
-
To
|
|
359
|
+
To expose only a subset of a noisy server, add `includeTools` on the server. Values can be exact original names, generated resource names such as `read_<resource>`, prefixed names, or simple glob patterns:
|
|
360
|
+
|
|
361
|
+
```json
|
|
362
|
+
{
|
|
363
|
+
"mcpServers": {
|
|
364
|
+
"dokploy": {
|
|
365
|
+
"url": "http://localhost:3845/mcp",
|
|
366
|
+
"directTools": true,
|
|
367
|
+
"includeTools": ["get_*", "dokploy_list_apps"]
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
To hide specific tools while still using `directTools: true`, add `excludeTools` on the server. `excludeTools` is applied after `includeTools`:
|
|
327
374
|
|
|
328
375
|
```json
|
|
329
376
|
{
|
|
@@ -331,13 +378,13 @@ To exclude specific tools while still using `directTools: true`, add `excludeToo
|
|
|
331
378
|
"figma": {
|
|
332
379
|
"url": "http://localhost:3845/mcp",
|
|
333
380
|
"directTools": true,
|
|
334
|
-
"excludeTools": ["
|
|
381
|
+
"excludeTools": ["read_figjam", "figma_get_code_connect_map"]
|
|
335
382
|
}
|
|
336
383
|
}
|
|
337
384
|
}
|
|
338
385
|
```
|
|
339
386
|
|
|
340
|
-
`excludeTools`
|
|
387
|
+
`includeTools` and `excludeTools` filter direct tools, proxy search/list/describe, and the `/mcp` panel view.
|
|
341
388
|
|
|
342
389
|
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[]`.
|
|
343
390
|
|
|
@@ -445,6 +492,8 @@ Prefer `.mcp.json` for project-local shared MCP config. Use `.pi/mcp.json` only
|
|
|
445
492
|
| Auth start | `mcp({ action: "auth-start", server: "name" })` |
|
|
446
493
|
| Auth complete | `mcp({ action: "auth-complete", server: "name", args: { redirectUrl: "..." } })` |
|
|
447
494
|
|
|
495
|
+
`mcp({ connect: "server-name" })` refreshes an already connected server, so new tools, resources, prompts, and instructions can load without restarting Pi.
|
|
496
|
+
|
|
448
497
|
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.
|
|
449
498
|
|
|
450
499
|
Search includes both MCP tools and Pi tools (from extensions). Pi tools appear first with `[pi tool]` prefix. Space-separated words are OR'd.
|
package/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
import stripJsonComments from "strip-json-comments";
|
|
7
8
|
|
|
8
9
|
const HOME = os.homedir();
|
|
9
10
|
|
|
@@ -18,6 +19,8 @@ const AGENT_DIR = process.env.PI_CODING_AGENT_DIR?.trim()
|
|
|
18
19
|
: path.join(HOME, ".pi", "agent");
|
|
19
20
|
const PI_CONFIG_PATH = path.join(AGENT_DIR, "mcp.json");
|
|
20
21
|
const GENERIC_GLOBAL_CONFIG_PATH = path.join(HOME, ".config", "mcp", "mcp.json");
|
|
22
|
+
const AGENTS_GLOBAL_CONFIG_PATH = path.join(HOME, ".agents", "mcp.json");
|
|
23
|
+
const AGENTS_NESTED_GLOBAL_CONFIG_PATH = path.join(HOME, ".agents", "mcp", "mcp.json");
|
|
21
24
|
const PROJECT_CONFIG_PATH = path.resolve(process.cwd(), ".mcp.json");
|
|
22
25
|
const PROJECT_PI_CONFIG_PATH = path.resolve(process.cwd(), ".pi", "mcp.json");
|
|
23
26
|
|
|
@@ -33,6 +36,10 @@ const IMPORT_PATHS = {
|
|
|
33
36
|
path.join(HOME, ".codex", "config.toml"),
|
|
34
37
|
path.join(HOME, ".codex", "config.json"),
|
|
35
38
|
],
|
|
39
|
+
opencode: [
|
|
40
|
+
path.join(HOME, ".config", "opencode", "opencode.json"),
|
|
41
|
+
path.resolve(process.cwd(), "opencode.json"),
|
|
42
|
+
],
|
|
36
43
|
windsurf: [path.join(HOME, ".windsurf", "mcp.json")],
|
|
37
44
|
vscode: [path.resolve(process.cwd(), ".vscode", "mcp.json")],
|
|
38
45
|
};
|
|
@@ -44,10 +51,11 @@ function printHelp(log = console.log) {
|
|
|
44
51
|
log("Then optionally run:");
|
|
45
52
|
log(" pi-mcp-adapter init Detect host configs and scaffold Pi imports");
|
|
46
53
|
log(" pi-mcp-adapter init --dry-run");
|
|
54
|
+
log(" pi-mcp-adapter init --discover-host-configs Opt in to host config fallback discovery");
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
function readJsonFile(filePath) {
|
|
50
|
-
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
58
|
+
return JSON.parse(stripJsonComments(fs.readFileSync(filePath, "utf-8"), { trailingCommas: true }));
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
function loadPiConfig() {
|
|
@@ -90,6 +98,8 @@ function printDiscovery(log, imports) {
|
|
|
90
98
|
|
|
91
99
|
const paths = [
|
|
92
100
|
["User-global standard MCP", GENERIC_GLOBAL_CONFIG_PATH],
|
|
101
|
+
["User-global .agents MCP", AGENTS_GLOBAL_CONFIG_PATH],
|
|
102
|
+
["User-global .agents nested MCP", AGENTS_NESTED_GLOBAL_CONFIG_PATH],
|
|
93
103
|
["Pi global override", PI_CONFIG_PATH],
|
|
94
104
|
["Project standard MCP", PROJECT_CONFIG_PATH],
|
|
95
105
|
["Project Pi override", PROJECT_PI_CONFIG_PATH],
|
|
@@ -118,6 +128,7 @@ function writePiConfig(config) {
|
|
|
118
128
|
|
|
119
129
|
async function runInit(argv, log = console.log) {
|
|
120
130
|
const dryRun = argv.includes("--dry-run");
|
|
131
|
+
const discoverHostConfigs = argv.includes("--discover-host-configs");
|
|
121
132
|
const foundImports = findAvailableImports();
|
|
122
133
|
const existingConfig = loadPiConfig();
|
|
123
134
|
const existingImports = new Set(existingConfig.imports ?? []);
|
|
@@ -127,7 +138,8 @@ async function runInit(argv, log = console.log) {
|
|
|
127
138
|
|
|
128
139
|
printDiscovery(log, foundImports);
|
|
129
140
|
|
|
130
|
-
|
|
141
|
+
const discoverySettingChanged = discoverHostConfigs && existingConfig.settings?.hostConfigDiscovery !== "on";
|
|
142
|
+
if (importsToAdd.length === 0 && !discoverySettingChanged) {
|
|
131
143
|
log("\nNo Pi config changes needed.");
|
|
132
144
|
log("Standard MCP configs are discovered automatically, and host-specific imports are already configured or unavailable.");
|
|
133
145
|
return 0;
|
|
@@ -135,11 +147,17 @@ async function runInit(argv, log = console.log) {
|
|
|
135
147
|
|
|
136
148
|
const nextConfig = {
|
|
137
149
|
...existingConfig,
|
|
138
|
-
|
|
150
|
+
...(discoverySettingChanged ? { settings: { ...existingConfig.settings, hostConfigDiscovery: "on" } } : {}),
|
|
151
|
+
...(importsToAdd.length > 0 ? { imports: [...existingImports, ...importsToAdd] } : {}),
|
|
139
152
|
mcpServers: existingConfig.mcpServers ?? {},
|
|
140
153
|
};
|
|
141
154
|
|
|
142
|
-
|
|
155
|
+
if (importsToAdd.length > 0) {
|
|
156
|
+
log(`\nDetected host configs to import into Pi: ${importsToAdd.join(", ")}`);
|
|
157
|
+
}
|
|
158
|
+
if (discoverySettingChanged) {
|
|
159
|
+
log("Opting in to host-specific fallback discovery (standard and Pi-owned configs still take precedence).");
|
|
160
|
+
}
|
|
143
161
|
|
|
144
162
|
if (dryRun) {
|
|
145
163
|
log(`Dry run: would update ${PI_CONFIG_PATH}`);
|
|
@@ -149,6 +167,9 @@ async function runInit(argv, log = console.log) {
|
|
|
149
167
|
writePiConfig(nextConfig);
|
|
150
168
|
log(`Updated ${PI_CONFIG_PATH}`);
|
|
151
169
|
log("Pi will now keep reading standard MCP configs automatically, while these imports cover host-specific config formats.");
|
|
170
|
+
if (discoverySettingChanged) {
|
|
171
|
+
log("Host config discovery is explicit and does not write to or execute commands from external host files.");
|
|
172
|
+
}
|
|
152
173
|
return 0;
|
|
153
174
|
}
|
|
154
175
|
|
package/config.ts
CHANGED
|
@@ -3,11 +3,16 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { parse as parseToml } from "smol-toml";
|
|
6
|
+
import stripJsonComments from "strip-json-comments";
|
|
6
7
|
import { getAgentPath } from "./agent-dir.ts";
|
|
7
|
-
import { isServerDisabled, type McpConfig, type ServerEntry, type McpSettings, type ImportKind, type ServerProvenance } from "./types.ts";
|
|
8
|
+
import { isServerDisabled, type HostConfigDiscovery, type McpConfig, type ServerEntry, type McpSettings, type ImportKind, type ServerProvenance } from "./types.ts";
|
|
8
9
|
import { toStringRecord } from "./utils.ts";
|
|
9
10
|
|
|
10
11
|
const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
|
|
12
|
+
const AGENTS_GLOBAL_CONFIG_PATHS = [
|
|
13
|
+
join(homedir(), ".agents", "mcp.json"),
|
|
14
|
+
join(homedir(), ".agents", "mcp", "mcp.json"),
|
|
15
|
+
] as const;
|
|
11
16
|
const PROJECT_CONFIG_NAME = ".mcp.json";
|
|
12
17
|
const PROJECT_PI_CONFIG_NAME = ".pi/mcp.json";
|
|
13
18
|
const REPOPROMPT_BINARY_CANDIDATES = [
|
|
@@ -36,7 +41,7 @@ const IMPORT_PATHS: Record<ImportKind, string[]> = {
|
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
interface ConfigSourceSpec {
|
|
39
|
-
id: "shared-global" | "pi-global" | "shared-project" | "pi-project";
|
|
44
|
+
id: "shared-global" | "agents-global" | "agents-nested-global" | "pi-global" | "shared-project" | "pi-project";
|
|
40
45
|
label: string;
|
|
41
46
|
readPath: string;
|
|
42
47
|
writePath: string;
|
|
@@ -68,6 +73,16 @@ export interface ImportConfigSummary extends DiscoveredImportConfig {
|
|
|
68
73
|
serverCount: number;
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
export interface HostConfigSummary extends ImportConfigSummary {
|
|
77
|
+
active: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface McpConfigConflict {
|
|
81
|
+
serverName: string;
|
|
82
|
+
sources: Array<{ kind: "shared" | "pi" | "host"; path: string }>;
|
|
83
|
+
winner: { kind: "shared" | "pi" | "host"; path: string };
|
|
84
|
+
}
|
|
85
|
+
|
|
71
86
|
export interface RepoPromptDiscovery {
|
|
72
87
|
configured: boolean;
|
|
73
88
|
configuredPath?: string;
|
|
@@ -80,6 +95,9 @@ export interface RepoPromptDiscovery {
|
|
|
80
95
|
export interface McpDiscoverySummary {
|
|
81
96
|
sources: ConfigDiscoverySource[];
|
|
82
97
|
imports: ImportConfigSummary[];
|
|
98
|
+
hostConfigs: HostConfigSummary[];
|
|
99
|
+
hostConfigDiscovery: HostConfigDiscovery;
|
|
100
|
+
conflicts: McpConfigConflict[];
|
|
83
101
|
hasAnyConfig: boolean;
|
|
84
102
|
hasAnyDetectedPaths: boolean;
|
|
85
103
|
hasSharedServers: boolean;
|
|
@@ -136,7 +154,8 @@ export function findAvailableImportConfigs(cwd = process.cwd()): DiscoveredImpor
|
|
|
136
154
|
}
|
|
137
155
|
|
|
138
156
|
export function getMcpDiscoverySummary(overridePath?: string, cwd = process.cwd()): McpDiscoverySummary {
|
|
139
|
-
const
|
|
157
|
+
const sourceSpecs = getConfigSources(overridePath, cwd);
|
|
158
|
+
const sources = sourceSpecs.map((source) => {
|
|
140
159
|
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
141
160
|
return {
|
|
142
161
|
id: source.id,
|
|
@@ -160,7 +179,8 @@ export function getMcpDiscoverySummary(overridePath?: string, cwd = process.cwd(
|
|
|
160
179
|
} satisfies ImportConfigSummary;
|
|
161
180
|
})
|
|
162
181
|
.filter((value): value is ImportConfigSummary => value !== null);
|
|
163
|
-
|
|
182
|
+
const hostConfigDiscovery = getConfiguredHostConfigDiscovery(overridePath, cwd);
|
|
183
|
+
const hostConfigs = imports.map((entry) => ({ ...entry, active: hostConfigDiscovery === "on" }));
|
|
164
184
|
const totalServerCount = sources.reduce((sum, source) => sum + source.serverCount, 0);
|
|
165
185
|
const hasSharedServers = sources.some((source) => source.kind === "shared" && source.serverCount > 0);
|
|
166
186
|
const hasPiOwnedServers = sources.some((source) => source.kind === "pi" && source.serverCount > 0);
|
|
@@ -170,6 +190,9 @@ export function getMcpDiscoverySummary(overridePath?: string, cwd = process.cwd(
|
|
|
170
190
|
const summaryWithoutRepoPrompt = {
|
|
171
191
|
sources,
|
|
172
192
|
imports,
|
|
193
|
+
hostConfigs,
|
|
194
|
+
hostConfigDiscovery,
|
|
195
|
+
conflicts: getConfigConflicts(sourceSpecs, imports, cwd),
|
|
173
196
|
hasAnyConfig,
|
|
174
197
|
hasAnyDetectedPaths,
|
|
175
198
|
hasSharedServers,
|
|
@@ -180,6 +203,8 @@ export function getMcpDiscoverySummary(overridePath?: string, cwd = process.cwd(
|
|
|
180
203
|
const fingerprint = JSON.stringify({
|
|
181
204
|
sources: sources.map((source) => [source.id, source.exists, source.serverCount]),
|
|
182
205
|
imports: imports.map((entry) => [entry.kind, entry.path, entry.serverCount]),
|
|
206
|
+
hostConfigDiscovery,
|
|
207
|
+
conflicts: summaryWithoutRepoPrompt.conflicts,
|
|
183
208
|
});
|
|
184
209
|
|
|
185
210
|
return {
|
|
@@ -194,9 +219,16 @@ export function cloneMcpConfig(config: McpConfig): McpConfig {
|
|
|
194
219
|
}
|
|
195
220
|
|
|
196
221
|
export function loadMcpConfig(overridePath?: string, cwd = process.cwd()): McpConfig {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
222
|
+
const sourceSpecs = getConfigSources(overridePath, cwd);
|
|
223
|
+
const hostConfigDiscovery = getConfiguredHostConfigDiscovery(overridePath, cwd);
|
|
224
|
+
// Host files are a lower-precedence fallback. This ordering means an opt-in
|
|
225
|
+
// discovery cannot override a shared or Pi-owned definition, and all normal
|
|
226
|
+
// URL-bound credential stripping remains in mergeServerMaps.
|
|
227
|
+
let config: McpConfig = hostConfigDiscovery === "on"
|
|
228
|
+
? loadDiscoveredHostConfigs(cwd)
|
|
229
|
+
: { mcpServers: {} };
|
|
230
|
+
|
|
231
|
+
for (const source of sourceSpecs) {
|
|
200
232
|
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
201
233
|
if (!loaded) continue;
|
|
202
234
|
config = mergeConfigs(config, expandImports(loaded, cwd));
|
|
@@ -205,6 +237,75 @@ export function loadMcpConfig(overridePath?: string, cwd = process.cwd()): McpCo
|
|
|
205
237
|
return config;
|
|
206
238
|
}
|
|
207
239
|
|
|
240
|
+
function getConfiguredHostConfigDiscovery(overridePath?: string, cwd = process.cwd()): HostConfigDiscovery {
|
|
241
|
+
let configured: HostConfigDiscovery = "off";
|
|
242
|
+
for (const source of getConfigSources(overridePath, cwd)) {
|
|
243
|
+
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
244
|
+
const value = loaded?.settings?.hostConfigDiscovery;
|
|
245
|
+
if (value === "off" || value === "prompt" || value === "on") configured = value;
|
|
246
|
+
}
|
|
247
|
+
return configured;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function loadDiscoveredHostConfigs(cwd: string): McpConfig {
|
|
251
|
+
let config: McpConfig = { mcpServers: {} };
|
|
252
|
+
for (const importKind of Object.keys(IMPORT_PATHS) as ImportKind[]) {
|
|
253
|
+
const imported = loadImportedConfig(importKind, cwd, `Failed to discover imported MCP config from ${importKind}:`);
|
|
254
|
+
if (!imported) continue;
|
|
255
|
+
config = mergeConfigs(config, {
|
|
256
|
+
mcpServers: extractServers(imported.value, importKind),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return config;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getConfigConflicts(
|
|
263
|
+
sourceSpecs: ConfigSourceSpec[],
|
|
264
|
+
imports: ImportConfigSummary[],
|
|
265
|
+
cwd: string,
|
|
266
|
+
): McpConfigConflict[] {
|
|
267
|
+
const seen = new Map<string, Array<{ kind: "shared" | "pi" | "host"; path: string }>>();
|
|
268
|
+
const record = (name: string, source: { kind: "shared" | "pi" | "host"; path: string }): void => {
|
|
269
|
+
const entries = seen.get(name) ?? [];
|
|
270
|
+
if (!entries.some((entry) => entry.kind === source.kind && entry.path === source.path)) entries.push(source);
|
|
271
|
+
seen.set(name, entries);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Host candidates are listed first because, when enabled, they are the
|
|
275
|
+
// lowest-precedence fallback. The fixed IMPORT_PATHS order is deterministic.
|
|
276
|
+
for (const entry of imports) {
|
|
277
|
+
const imported = loadImportedConfig(entry.kind, cwd, `Failed to inspect imported MCP config from ${entry.kind}:`);
|
|
278
|
+
if (!imported) continue;
|
|
279
|
+
for (const name of Object.keys(extractServers(imported.value, entry.kind))) {
|
|
280
|
+
record(name, { kind: "host", path: imported.path });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const source of sourceSpecs) {
|
|
284
|
+
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
285
|
+
if (!loaded) continue;
|
|
286
|
+
if (loaded.imports?.length) {
|
|
287
|
+
for (const importKind of loaded.imports) {
|
|
288
|
+
const imported = loadImportedConfig(importKind, cwd, `Failed to inspect imported MCP config from ${importKind}:`);
|
|
289
|
+
if (!imported) continue;
|
|
290
|
+
for (const name of Object.keys(extractServers(imported.value, importKind))) {
|
|
291
|
+
record(name, { kind: "host", path: imported.path });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const name of Object.keys(loaded.mcpServers)) {
|
|
296
|
+
record(name, {
|
|
297
|
+
kind: source.shared ? "shared" : "pi",
|
|
298
|
+
path: source.readPath,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return [...seen.entries()]
|
|
304
|
+
.filter(([, sources]) => sources.length > 1)
|
|
305
|
+
.map(([serverName, sources]) => ({ serverName, sources, winner: sources[sources.length - 1]! }))
|
|
306
|
+
.sort((left, right) => left.serverName.localeCompare(right.serverName));
|
|
307
|
+
}
|
|
308
|
+
|
|
208
309
|
function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSourceSpec[] {
|
|
209
310
|
const userPath = getPiGlobalConfigPath(overridePath);
|
|
210
311
|
const projectPath = getProjectConfigPath(cwd);
|
|
@@ -224,6 +325,20 @@ function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSou
|
|
|
224
325
|
});
|
|
225
326
|
}
|
|
226
327
|
|
|
328
|
+
for (const [index, agentsPath] of AGENTS_GLOBAL_CONFIG_PATHS.entries()) {
|
|
329
|
+
if (agentsPath === userPath || agentsPath === GENERIC_GLOBAL_CONFIG_PATH) continue;
|
|
330
|
+
sources.push({
|
|
331
|
+
id: index === 0 ? "agents-global" : "agents-nested-global",
|
|
332
|
+
label: index === 0 ? "user-global .agents MCP" : "user-global .agents nested MCP",
|
|
333
|
+
readPath: agentsPath,
|
|
334
|
+
writePath: userPath,
|
|
335
|
+
kind: "import",
|
|
336
|
+
importKind: index === 0 ? ".agents MCP config" : ".agents/mcp MCP config",
|
|
337
|
+
shared: true,
|
|
338
|
+
scope: "global",
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
227
342
|
sources.push({
|
|
228
343
|
id: "pi-global",
|
|
229
344
|
label: "Pi global override",
|
|
@@ -365,9 +480,13 @@ function resolveImportCandidates(importKind: ImportKind, cwd: string): string[]
|
|
|
365
480
|
});
|
|
366
481
|
}
|
|
367
482
|
|
|
483
|
+
function parseJsonConfig(raw: string): unknown {
|
|
484
|
+
return JSON.parse(stripJsonComments(raw, { trailingCommas: true }));
|
|
485
|
+
}
|
|
486
|
+
|
|
368
487
|
function readImportedConfig(path: string): unknown {
|
|
369
488
|
const raw = readFileSync(path, "utf-8");
|
|
370
|
-
return path.endsWith(".toml") ? parseToml(raw) :
|
|
489
|
+
return path.endsWith(".toml") ? parseToml(raw) : parseJsonConfig(raw);
|
|
371
490
|
}
|
|
372
491
|
|
|
373
492
|
function loadImportedConfig(
|
|
@@ -417,7 +536,7 @@ function readValidatedConfig(path: string, label: string): McpConfig | null {
|
|
|
417
536
|
if (!existsSync(path)) return null;
|
|
418
537
|
|
|
419
538
|
try {
|
|
420
|
-
return validateConfig(
|
|
539
|
+
return validateConfig(parseJsonConfig(readFileSync(path, "utf-8")));
|
|
421
540
|
} catch (error) {
|
|
422
541
|
console.warn(`Failed to load ${label}:`, error);
|
|
423
542
|
return null;
|
|
@@ -667,7 +786,7 @@ function readRawConfigObject(filePath: string): Record<string, unknown> {
|
|
|
667
786
|
if (!existsSync(filePath)) return {};
|
|
668
787
|
|
|
669
788
|
try {
|
|
670
|
-
const raw =
|
|
789
|
+
const raw = parseJsonConfig(readFileSync(filePath, "utf-8"));
|
|
671
790
|
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
|
|
672
791
|
} catch {
|
|
673
792
|
return {};
|
|
@@ -714,7 +833,7 @@ export function writeProjectServerDisabledOverride(
|
|
|
714
833
|
let raw: Record<string, unknown> = {};
|
|
715
834
|
if (existsSync(filePath)) {
|
|
716
835
|
try {
|
|
717
|
-
const parsed =
|
|
836
|
+
const parsed = parseJsonConfig(readFileSync(filePath, "utf-8"));
|
|
718
837
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
719
838
|
throw new Error("root value must be an object");
|
|
720
839
|
}
|
|
@@ -903,6 +1022,18 @@ export function getServerProvenance(overridePath?: string, cwd = process.cwd()):
|
|
|
903
1022
|
const provenance = new Map<string, ServerProvenance>();
|
|
904
1023
|
const userPath = getPiGlobalConfigPath(overridePath);
|
|
905
1024
|
|
|
1025
|
+
if (getConfiguredHostConfigDiscovery(overridePath, cwd) === "on") {
|
|
1026
|
+
for (const importKind of Object.keys(IMPORT_PATHS) as ImportKind[]) {
|
|
1027
|
+
const imported = loadImportedConfig(importKind, cwd, `Failed to inspect imported MCP config from ${importKind}:`);
|
|
1028
|
+
if (!imported) continue;
|
|
1029
|
+
for (const name of Object.keys(extractServers(imported.value, importKind))) {
|
|
1030
|
+
// Keep writes inside Pi-owned storage even though the source is external.
|
|
1031
|
+
// Later import kinds win in the same deterministic order as loadDiscoveredHostConfigs.
|
|
1032
|
+
provenance.set(name, { path: userPath, kind: "import", importKind });
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
906
1037
|
for (const source of getConfigSources(overridePath, cwd)) {
|
|
907
1038
|
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
908
1039
|
if (!loaded) continue;
|
package/direct-tools.ts
CHANGED
|
@@ -6,11 +6,12 @@ import type { MetadataCache } from "./metadata-cache.ts";
|
|
|
6
6
|
import { lazyConnect, getFailureAgeSeconds, clearFailure } from "./init.ts";
|
|
7
7
|
import { abortable, throwIfAborted } from "./abort.ts";
|
|
8
8
|
import { isServerCacheValid } from "./metadata-cache.ts";
|
|
9
|
+
export { getMissingConfiguredDirectToolServers } from "./metadata-cache.ts";
|
|
9
10
|
import { formatSchema } from "./tool-metadata.ts";
|
|
10
11
|
import { resolveMcpResultContent, transformMcpContent } from "./tool-registrar.ts";
|
|
11
12
|
import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.ts";
|
|
12
13
|
import { maybeStartUiSession, summarizeUiSessionResult, type UiSessionRuntime } from "./ui-session.ts";
|
|
13
|
-
import { formatToolName, isServerDisabled,
|
|
14
|
+
import { formatToolName, isServerDisabled, isToolAllowed } from "./types.ts";
|
|
14
15
|
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
15
16
|
import { authenticate, supportsOAuth } from "./mcp-auth-flow.ts";
|
|
16
17
|
import { formatAuthRequiredMessage, resolveServerUrl, truncateAtWord } from "./utils.ts";
|
|
@@ -159,7 +160,7 @@ export function resolveDirectTools(
|
|
|
159
160
|
|
|
160
161
|
for (const tool of serverCache.tools ?? []) {
|
|
161
162
|
if (toolFilter !== true && !toolFilter.includes(tool.name)) continue;
|
|
162
|
-
if (
|
|
163
|
+
if (!isToolAllowed(tool.name, serverName, prefix, definition.includeTools, definition.excludeTools)) continue;
|
|
163
164
|
const prefixedName = formatToolName(tool.name, serverName, prefix);
|
|
164
165
|
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
165
166
|
console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
|
|
@@ -183,9 +184,9 @@ export function resolveDirectTools(
|
|
|
183
184
|
|
|
184
185
|
if (definition.exposeResources !== false) {
|
|
185
186
|
for (const resource of serverCache.resources ?? []) {
|
|
186
|
-
const baseName = `
|
|
187
|
+
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
187
188
|
if (toolFilter !== true && !toolFilter.includes(baseName)) continue;
|
|
188
|
-
if (
|
|
189
|
+
if (!isToolAllowed(baseName, serverName, prefix, definition.includeTools, definition.excludeTools)) continue;
|
|
189
190
|
const prefixedName = formatToolName(baseName, serverName, prefix);
|
|
190
191
|
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
191
192
|
console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
|
|
@@ -210,30 +211,6 @@ export function resolveDirectTools(
|
|
|
210
211
|
return specs;
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
export function getMissingConfiguredDirectToolServers(
|
|
214
|
-
config: McpConfig,
|
|
215
|
-
cache: MetadataCache | null,
|
|
216
|
-
): string[] {
|
|
217
|
-
const missing: string[] = [];
|
|
218
|
-
const globalDirect = config.settings?.directTools;
|
|
219
|
-
|
|
220
|
-
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
221
|
-
if (isServerDisabled(definition)) continue;
|
|
222
|
-
const hasDirectTools = definition.directTools !== undefined
|
|
223
|
-
? !!definition.directTools
|
|
224
|
-
: !!globalDirect;
|
|
225
|
-
|
|
226
|
-
if (!hasDirectTools) continue;
|
|
227
|
-
|
|
228
|
-
const serverCache = cache?.servers?.[serverName];
|
|
229
|
-
if (!serverCache || !isServerCacheValid(serverCache, definition)) {
|
|
230
|
-
missing.push(serverName);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return missing;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
214
|
export function buildProxyDescription(
|
|
238
215
|
config: McpConfig,
|
|
239
216
|
cache: MetadataCache | null,
|
|
@@ -259,12 +236,12 @@ export function buildProxyDescription(
|
|
|
259
236
|
if (isServerDisabled(definition)) continue;
|
|
260
237
|
const entry = cache?.servers?.[serverName];
|
|
261
238
|
const toolCount = (entry?.tools ?? []).filter(
|
|
262
|
-
(tool) =>
|
|
239
|
+
(tool) => isToolAllowed(tool.name, serverName, prefix, definition.includeTools, definition.excludeTools),
|
|
263
240
|
).length;
|
|
264
241
|
const resourceCount = definition?.exposeResources !== false
|
|
265
242
|
? (entry?.resources ?? []).filter((resource) => {
|
|
266
|
-
const baseName = `
|
|
267
|
-
return
|
|
243
|
+
const baseName = `read_${resourceNameToToolName(resource.name)}`;
|
|
244
|
+
return isToolAllowed(baseName, serverName, prefix, definition.includeTools, definition.excludeTools);
|
|
268
245
|
}).length
|
|
269
246
|
: 0;
|
|
270
247
|
const totalItems = toolCount + resourceCount;
|