pi-mcp-adapter 3.0.0 → 3.2.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 +56 -0
- package/OAUTH.md +7 -7
- package/README.md +53 -5
- package/cli.js +17 -15
- package/commands.ts +15 -3
- package/config.ts +51 -44
- package/direct-tools.ts +4 -3
- package/dist/config.d.ts +1 -1
- package/dist/config.js +53 -47
- package/dist/config.js.map +1 -1
- package/dist/mcp-callback-server.js +1 -2
- package/dist/mcp-callback-server.js.map +1 -1
- package/dist/mcp-oauth-provider.d.ts +2 -1
- package/dist/mcp-oauth-provider.js +5 -2
- package/dist/mcp-oauth-provider.js.map +1 -1
- package/dist/metadata-cache.js +4 -1
- package/dist/metadata-cache.js.map +1 -1
- package/dist/state.d.ts +2 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +4 -1
- package/dist/utils.js.map +1 -1
- package/failure-backoff.ts +11 -0
- package/host-managed.ts +547 -0
- package/index.ts +24 -27
- package/init.ts +18 -10
- package/mcp-callback-server.ts +1 -1
- package/mcp-code.ts +2 -2
- package/mcp-oauth-provider.ts +5 -2
- package/mcp-panel.ts +7 -6
- package/mcp-script-wasm.ts +18 -1
- package/mcp-script-worker.mjs +9 -2
- package/mcp-setup-panel.ts +18 -4
- package/metadata-cache.ts +4 -1
- package/namespace-tools.ts +22 -2
- package/package.json +9 -2
- package/project-server-trust.ts +47 -8
- package/proxy-modes.ts +9 -10
- package/search-ranking.ts +1 -1
- package/semantic-search.ts +6 -5
- package/state.ts +2 -1
- package/tool-approval.ts +11 -6
- package/tool-parameters.ts +11 -0
- package/utils.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.2.0] - 2026-09-28
|
|
11
|
+
|
|
12
|
+
### Highlights
|
|
13
|
+
|
|
14
|
+
- Apps that embed Pi can now run the adapter on MCP connections they manage themselves, and keep control of approvals and results.
|
|
15
|
+
- OAuth sign-in now works with servers that reject `localhost` callbacks, such as Lovable.
|
|
16
|
+
- Direct tools no longer vanish from a session when another Pi session updates the shared tool cache.
|
|
17
|
+
- `mcpScript` now works in Bun-compiled builds of Pi.
|
|
18
|
+
- Trusted projects no longer show a false "project servers blocked" warning at startup.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- New `pi-mcp-adapter/host-managed` entry point for apps that embed Pi and manage their own MCP connections. The app supplies the connections, records each approved call before it is sent, sees the raw result first, and decides when the adapter starts and stops. The adapter never retries a call or reconnects, and never reads config, OAuth, or keyring state. See [Host-managed embedding](README.md#host-managed-embedding). Thanks to [@LeonEthan](https://github.com/LeonEthan) for the design in [issue #716](https://github.com/nicobailon/pi-mcp-adapter/issues/716).
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- OAuth callbacks now default to `http://127.0.0.1:<port>/callback` instead of `localhost`, as RFC 8252 recommends. Servers that reject `localhost`, such as Lovable, now work without extra setup. If a server only accepts `localhost`, set `oauth.redirectUri` to that address. Thanks to [@ebysofyan](https://github.com/ebysofyan) for [issue #711](https://github.com/nicobailon/pi-mcp-adapter/issues/711) and [PR #712](https://github.com/nicobailon/pi-mcp-adapter/pull/712).
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Pi no longer warns "MCP: Project servers blocked: … (blocked by project trust)" at startup in a project you already trust, including one trusted through a parent folder. `eager` and `keep-alive` project servers now wait for Pi's trust check, and the approval prompt still appears when the session starts. Thanks to [@pnym-ai](https://github.com/pnym-ai) for reporting [issue #713](https://github.com/nicobailon/pi-mcp-adapter/issues/713).
|
|
31
|
+
- Direct tools no longer disappear from a session when another Pi session writes different details for the same server to the shared cache, or when the cache entry expires, is deleted, or the server disconnects while idle. Each session now keeps the tools and resources it found. Changing the server's config still clears them. Thanks to [@NoahWTeng](https://github.com/NoahWTeng) for [PR #718](https://github.com/nicobailon/pi-mcp-adapter/pull/718).
|
|
32
|
+
- A server's `mcp__<server>` tool now comes back once the adapter can retry the server after a failed connection. Before, on Pi versions without `unregisterTool`, it stayed hidden for the rest of the session. A tool you turned off yourself stays off. Thanks to [@fer-git](https://github.com/fer-git) for [issue #717](https://github.com/nicobailon/pi-mcp-adapter/issues/717).
|
|
33
|
+
- `mcpScript` works when Pi runs as a Bun-compiled executable. Before, every call failed with `Cannot find package 'quickjs-wasi'` from the sandbox worker. Thanks to [@fmoda3](https://github.com/fmoda3) for [issue #720](https://github.com/nicobailon/pi-mcp-adapter/issues/720).
|
|
34
|
+
|
|
35
|
+
## [3.1.0] - 2026-09-27
|
|
36
|
+
|
|
37
|
+
### Highlights
|
|
38
|
+
|
|
39
|
+
- Approving a project's MCP servers once now covers every git worktree of that repository, so new worktrees stop asking again.
|
|
40
|
+
- The project-server approval prompt no longer disappears after `/reload` or when another extension takes over the editor. Before, that could leave MCP stuck starting and add up to 30 seconds to every prompt.
|
|
41
|
+
- When a server can't connect, the agent now sees the actual error instead of just "failed".
|
|
42
|
+
- Config files are safer: files saved with a Windows byte order mark now load, and a config file the adapter can't parse is no longer overwritten.
|
|
43
|
+
- A dependency update fixes a high-severity denial-of-service bug in TOML config parsing.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- Project MCP server approvals are now shared across a repository's git worktrees. Approving a server in one checkout covers the same folder in the others, so a new worktree no longer asks again unless the server definition differs. This works for regular, bare, and `--separate-git-dir` repositories; only the main checkout of a `--separate-git-dir` repository asks once on its own. Thanks to [@MauricioRobayo](https://github.com/MauricioRobayo) for [issue #708](https://github.com/nicobailon/pi-mcp-adapter/issues/708).
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- The project MCP server approval prompt now stays on screen until you answer it, including after `/reload` and panel saves. Before, `/reload` or another extension replacing the editor could hide it, leave MCP stuck starting, and make each prompt wait up to 30 seconds. Thanks to [@Gybra](https://github.com/Gybra) for [issue #690](https://github.com/nicobailon/pi-mcp-adapter/issues/690) and [PR #691](https://github.com/nicobailon/pi-mcp-adapter/pull/691), and to [@nazerim](https://github.com/nazerim) for [issue #695](https://github.com/nicobailon/pi-mcp-adapter/issues/695).
|
|
52
|
+
- Disabled project MCP servers no longer ask for approval or delay interactive startup. Thanks to [@ismailokta](https://github.com/ismailokta) for reporting [issue #685](https://github.com/nicobailon/pi-mcp-adapter/issues/685).
|
|
53
|
+
- The approval prompt now labels its path as the project config that needs approval, instead of calling it the server's source. A project file that only enables a server you defined globally no longer looks like it defines the command. Thanks to [@nazerim](https://github.com/nazerim) for reporting [issue #695](https://github.com/nicobailon/pi-mcp-adapter/issues/695).
|
|
54
|
+
- When a server fails to connect, the agent now sees why. `mcp({})` status, the `mcp` tool's "not available" replies, and direct tools show the connection error (for example `spawn demo ENOENT`) next to "failed 12s ago", shortened to one line. Before, only `/mcp-adapter status` and the panel showed it.
|
|
55
|
+
- MCP config files saved with a UTF-8 byte order mark, as some Windows editors do, now load instead of failing to parse. This covers adapter, shared, and imported JSON configs and imported TOML configs, and `pi-mcp-adapter init` keeps the servers in such a file. Thanks to [@quifox](https://github.com/quifox) for [PR #697](https://github.com/nicobailon/pi-mcp-adapter/pull/697).
|
|
56
|
+
- Adding servers, imports, or direct tools no longer overwrites an existing MCP config file that fails to parse or has the wrong shape. The adapter reports the error and leaves the file alone; empty files can still be set up. Thanks to [@quifox](https://github.com/quifox) for [PR #693](https://github.com/nicobailon/pi-mcp-adapter/pull/693).
|
|
57
|
+
- Repeating a word in an MCP tool search no longer hides tools that match it. Thanks to [@kojizada490-wq](https://github.com/kojizada490-wq) for [PR #678](https://github.com/nicobailon/pi-mcp-adapter/pull/678).
|
|
58
|
+
- Searching the `/mcp-adapter` panel by server name now finds disabled or unconnected servers, so you can select and enable them. Thanks to [@nazerim](https://github.com/nazerim) for reporting [issue #696](https://github.com/nicobailon/pi-mcp-adapter/issues/696).
|
|
59
|
+
- When Jev semantic search times out, is rate limited, or is unavailable, the lexical fallback now returns only tools from `settings.jev.allowedServers`, as the semantic results already did. Thanks to [@quifox](https://github.com/quifox) for [PR #698](https://github.com/nicobailon/pi-mcp-adapter/pull/698).
|
|
60
|
+
- Changing a stdio server's `inheritEnv` or `literalEnv` setting now refreshes its cached tools, so search and direct tools no longer show stale entries. Thanks to [@kojizada490-wq](https://github.com/kojizada490-wq) for [PR #683](https://github.com/nicobailon/pi-mcp-adapter/pull/683).
|
|
61
|
+
|
|
62
|
+
### Security
|
|
63
|
+
|
|
64
|
+
- `smol-toml` now requires 1.9.0 or later, which fixes a high-severity denial of service on malformed TOML. The lockfile also moves `hono`, which comes in through the MCP SDK, to a release with its moderate advisories fixed, so `npm audit` on a lockfile install reports no production vulnerabilities. Thanks to [@jvpacini-CW](https://github.com/jvpacini-CW) for [PR #694](https://github.com/nicobailon/pi-mcp-adapter/pull/694).
|
|
65
|
+
|
|
10
66
|
## [3.0.0] - 2026-09-26
|
|
11
67
|
|
|
12
68
|
### Highlights
|
package/OAUTH.md
CHANGED
|
@@ -88,7 +88,7 @@ You can optionally provide a pre-registered client:
|
|
|
88
88
|
- `oauth.authServerMetadataUrl` - HTTPS OAuth/OIDC authorization-server metadata document to use authoritatively when MCP protected-resource discovery is unavailable (optional; issuer validation remains enabled)
|
|
89
89
|
- `oauth.skipIssuerMetadataValidation` - Set `true` only for a known-misconfigured authorization server whose metadata issuer cannot be fixed immediately. This weakens OAuth issuer validation.
|
|
90
90
|
|
|
91
|
-
Dynamic fallback clients normally omit `oauth.redirectUri`; the adapter starts the callback server lazily on the default loopback host (`
|
|
91
|
+
Dynamic fallback clients normally omit `oauth.redirectUri`; the adapter starts the callback server lazily on the default loopback host (`127.0.0.1`, as RFC 8252 recommends) and asks the OS for an available local port when auth begins. Use `oauth.redirectUri` when the provider requires a pre-registered callback, such as Slack MCP's Claude-compatible `http://localhost:3118/callback`, or when a server only accepts a `localhost` redirect (for example `http://localhost:{port}/callback`). A loopback URI must use `http://` with `localhost`, `127.0.0.1`, or `[::1]`. It may contain an explicit port, which is bound exactly, or `{port}`, which is replaced with the OS-assigned port in the authorization and token requests.
|
|
92
92
|
|
|
93
93
|
### Non-Interactive `client_credentials`
|
|
94
94
|
|
|
@@ -142,13 +142,13 @@ When Pi runs over SSH or in a headless environment, use the proxy tool to retrie
|
|
|
142
142
|
mcp({ action: "auth-start", server: "my-oauth-server" })
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
Open the returned URL in your local browser. After approval, copy the full redirected
|
|
145
|
+
Open the returned URL in your local browser. After approval, copy the full redirected callback URL from the browser address bar (the page may fail to load locally) and complete the same pending auth flow:
|
|
146
146
|
|
|
147
147
|
```
|
|
148
148
|
mcp({
|
|
149
149
|
action: "auth-complete",
|
|
150
150
|
server: "my-oauth-server",
|
|
151
|
-
args: { redirectUrl: "http://
|
|
151
|
+
args: { redirectUrl: "http://127.0.0.1:19876/callback?code=...&state=..." }
|
|
152
152
|
})
|
|
153
153
|
```
|
|
154
154
|
|
|
@@ -220,7 +220,7 @@ The adapter does not provide a default URL or host the public document: operator
|
|
|
220
220
|
2. Registers a new client with:
|
|
221
221
|
- `client_name`: configured `oauth.clientName` or "Pi Coding Agent"
|
|
222
222
|
- `client_uri`: configured `oauth.clientUri` or the adapter repository URL
|
|
223
|
-
- `redirect_uris`: `["http://
|
|
223
|
+
- `redirect_uris`: `["http://127.0.0.1:<active-callback-port>/callback"]`, or the configured `oauth.redirectUri`
|
|
224
224
|
- `grant_types`: `["authorization_code", "refresh_token"]`
|
|
225
225
|
3. Stores the registered client credentials and the redirect URIs returned by the authorization server
|
|
226
226
|
|
|
@@ -230,8 +230,8 @@ When a fresh browser auth starts, cached dynamic fallback client info with token
|
|
|
230
230
|
|
|
231
231
|
A Node.js HTTP server runs on a loopback callback endpoint and handles the active callback path:
|
|
232
232
|
|
|
233
|
-
- Dynamic registration starts the callback server only when auth begins, binds the default host `
|
|
234
|
-
- Pre-registered clients (`oauth.clientId`) without `oauth.redirectUri` require the exact configured callback port from `MCP_OAUTH_CALLBACK_PORT` or the default `19876` on `
|
|
233
|
+
- Dynamic registration starts the callback server only when auth begins, binds the default host `127.0.0.1`, and asks the OS for an available local port
|
|
234
|
+
- Pre-registered clients (`oauth.clientId`) without `oauth.redirectUri` require the exact configured callback port from `MCP_OAUTH_CALLBACK_PORT` or the default `19876` on `127.0.0.1`
|
|
235
235
|
- `oauth.redirectUri` binds the exact loopback host and path. An explicit port is bound exactly; `{port}` asks the OS for a port and is replaced with that assigned value before the URI is advertised to the provider
|
|
236
236
|
|
|
237
237
|
- Handles `code`, `state`, and `error` parameters
|
|
@@ -320,7 +320,7 @@ Some servers require pre-registered clients. Obtain a client ID from your OAuth
|
|
|
320
320
|
|
|
321
321
|
### Callback server already in use
|
|
322
322
|
|
|
323
|
-
Dynamic fallback browser OAuth uses a lazy OS-assigned port on the default loopback host (`
|
|
323
|
+
Dynamic fallback browser OAuth uses a lazy OS-assigned port on the default loopback host (`127.0.0.1`), so the configured default port being busy should not block fallback registration.
|
|
324
324
|
|
|
325
325
|
For pre-registered OAuth clients (`oauth.clientId`), the callback redirect URI must match the provider registration policy. Set `oauth.redirectUri` to the full fixed callback, such as Slack MCP's Claude-compatible `http://localhost:3118/callback`, use a loopback `{port}` URI when the provider explicitly permits dynamic RFC 8252 ports, or free/set `MCP_OAUTH_CALLBACK_PORT` when you rely on the default `/callback` path without an explicit redirect URI.
|
|
326
326
|
|
package/README.md
CHANGED
|
@@ -263,6 +263,52 @@ await updateMcpOAuthTokensForUrl("jira", "https://jira.example.com/mcp", { acces
|
|
|
263
263
|
|
|
264
264
|
The public subpath exposes only token read/update helpers plus a status helper. The async read path uses the adapter's refresh logic before it returns tokens. For a service-protected endpoint or a pre-registered OAuth client, pass the explicit refresh configuration as `getMcpOAuthTokensForUrl(name, url, { definition: { headers, oauth } })`. This optional configuration is never loaded from ambient config or stored with the tokens; headers are bound to the supplied MCP URL's origin. The helpers keep secure-store storage, URL binding, refresh persistence, chunk handling, legacy import, and fail-closed credential-store errors. They do not expose client registration secrets, PKCE verifiers, or OAuth state.
|
|
265
265
|
|
|
266
|
+
### Host-managed embedding
|
|
267
|
+
|
|
268
|
+
Use `pi-mcp-adapter/host-managed` when an application embeds Pi and owns the MCP connection itself: it builds the transports, holds the credentials, records each call before it runs, and decides when the adapter starts and stops. For an ordinary integration that only supplies MCP configuration, use `createMcpAdapter` instead.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { createAgentSession, DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
|
|
272
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
273
|
+
import { createHostManagedMcpAdapter } from "pi-mcp-adapter/host-managed";
|
|
274
|
+
|
|
275
|
+
const adapter = createHostManagedMcpAdapter({
|
|
276
|
+
servers: {
|
|
277
|
+
github: {
|
|
278
|
+
createTransport: () => new StreamableHTTPClientTransport(new URL(url), { fetch: guardedFetch }),
|
|
279
|
+
tools: ["search_issues", "get_issue"], // omit to expose every listed tool
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
async onToolCall(call) {
|
|
283
|
+
await journal.record(call.toolCallId, call.server, call.tool, call.arguments);
|
|
284
|
+
return await call.dispatch();
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await adapter.ready(); // connects, lists tools, and freezes the catalog
|
|
289
|
+
const loader = new DefaultResourceLoader({ extensionFactories: [adapter.extensionFactory] });
|
|
290
|
+
await loader.reload();
|
|
291
|
+
const { session } = await createAgentSession({ resourceLoader: loader });
|
|
292
|
+
await adapter.close(); // after the session settles
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Nothing connects until `ready()`, which calls each `createTransport` once. It rejects and closes the adapter if a server fails to start, a requested tool is missing, or two tools map to the same Pi name (`<server>_<tool>`). `extensionFactory` registers only the selected tools and throws before `ready()` resolves or after `close()`. `close()` refuses new calls, aborts in-flight ones, and closes every transport within five seconds. A dropped connection is never reopened.
|
|
296
|
+
|
|
297
|
+
Every call goes through the approval broker (`pi-mcp-adapter:tool-approval-request` on `pi.events`, origin `direct`). Only `allow_once` or `allow_for_session` lets it run. A denial, an `abstain`, or no handler at all refuses the call before `onToolCall` sees it. `onToolCall` receives the frozen arguments and input schema, the Pi tool-call id, an opaque `connectionId`, and a `dispatch()` that sends `tools/call` at most once. The result it returns goes through the normal output guard, whether raw or changed (for example after importing images). An `isError: true` result reaches the model as a tool error. `dispatch()` rejects with `HostManagedMcpError`. The raw error is kept only as its `cause`, and the model sees fixed text.
|
|
298
|
+
|
|
299
|
+
| `delivery` | Meaning |
|
|
300
|
+
|---|---|
|
|
301
|
+
| `not_sent` | The request never left the adapter; the tool did not run. |
|
|
302
|
+
| `may_have_run` | The request was sent and the outcome is unknown (timeout, cancellation, lost connection, HTTP error). The agent is told not to retry automatically. |
|
|
303
|
+
| `server_error` | The server answered with a JSON-RPC error (`protocolCode`). |
|
|
304
|
+
| `invalid_result` | The server answered, but the result failed validation, including the tool's `outputSchema`. The tool may have run. |
|
|
305
|
+
|
|
306
|
+
On `notifications/tools/list_changed` the adapter lists the server's tools again. A selected tool that changed or disappeared is retired for the life of the adapter; if the re-list fails, every selected tool on that server is retired. Calls wait for a running re-list and are refused as `not_sent` both before `onToolCall` and inside `dispatch()`. The model is told to start a new session.
|
|
307
|
+
|
|
308
|
+
After `dispatch()` resolves, `call.linkedResource(uri)` prepares a read of a `resource_link` from that result. The read needs its own broker approval (origin `resource`, args `{ uri }`) and uses the same connection. It returns a `readId` and a single-use `dispatch()`, so you can record the read before it happens. A URI that is not linked throws, and a read is refused as `not_sent` once the parent call has settled.
|
|
309
|
+
|
|
310
|
+
Supplied transports must not carry an OAuth `authProvider` or any retry or replay behavior. The adapter sends each `tools/call` at most once at its own layer. It turns off the SDK's multi-round-trip auto-fulfilment, passes the tool definition so the SDK cannot resend on a header mismatch, and never recovers a session or reconnects. This mode does not read config files, use OAuth or the credential store, register commands, UI, or the `mcp` proxy tool, expose resource tools or prompts, or answer sampling or elicitation requests.
|
|
311
|
+
|
|
266
312
|
### Runtime status snapshots
|
|
267
313
|
|
|
268
314
|
Extensions can subscribe to the adapter's versioned shared event-bus channel instead of parsing `/mcp-adapter` or `mcp({})` output:
|
|
@@ -315,7 +361,7 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
|
|
|
315
361
|
| `oauth.clientSecret` | OAuth client secret for confidential clients; a value beginning with `!` runs a command when OAuth authenticates, while `!!` escapes a literal leading `!`. Combining it with `oauth.clientMetadataUrl` requires an explicit `oauth.clientId`. |
|
|
316
362
|
| `oauth.clientMetadataUrl` | Advanced opt-in for an operator-supplied public HTTPS Client ID Metadata Document (CIMD) URL with a non-root path. Used as the `client_id` when the authorization server advertises CIMD support; otherwise the adapter falls back to Dynamic Client Registration. The adapter does not provide or host a default document. |
|
|
317
363
|
| `oauth.scope` | Requested OAuth scopes |
|
|
318
|
-
| `oauth.redirectUri` | Redirect URI for browser OAuth. Dynamic clients normally omit it and use an OS-assigned
|
|
364
|
+
| `oauth.redirectUri` | Redirect URI for browser OAuth. Dynamic clients normally omit it and use an OS-assigned port on the default `http://127.0.0.1:<port>/callback`. Local `http://` loopback URIs accept an explicit port or `{port}` for an OS-assigned port (for example, `http://localhost:{port}/callback` for a server registered only for `localhost`). Pre-registered `https://` callbacks use manual completion by pasting the full callback URL. |
|
|
319
365
|
| `oauth.clientName` | Client display name advertised during Dynamic Client Registration fallback |
|
|
320
366
|
| `oauth.clientUri` | Client homepage URI advertised during Dynamic Client Registration fallback. Defaults to `piConfig.clientUri` from the host's manifest when set, and is omitted rather than guessed under a rebranded host |
|
|
321
367
|
| `oauth.logoUri` | Client logo URL advertised during Dynamic Client Registration fallback (RFC 7591 `logo_uri`). Must be an absolute `http(s)` URL — consent screens fetch it server-side, so local paths render nothing. Omitted from the registration request when unset |
|
|
@@ -437,7 +483,7 @@ Public servers are ready immediately. For OAuth servers, the same action opens t
|
|
|
437
483
|
|
|
438
484
|
### Remote/headless OAuth
|
|
439
485
|
|
|
440
|
-
If Pi is running on a remote server, `/mcp-auth <server>` shows a clickable authorization URL first. Open it in your local browser and approve access, then select **Yes** in Pi to open the callback input. The browser may fail to load the
|
|
486
|
+
If Pi is running on a remote server, `/mcp-auth <server>` shows a clickable authorization URL first. Open it in your local browser and approve access, then select **Yes** in Pi to open the callback input. The browser may fail to load the loopback callback page because `127.0.0.1` refers to your workstation; copy the full URL from its address bar and paste it into Pi. The authorization screen closes automatically instead when the browser can reach Pi's callback directly.
|
|
441
487
|
|
|
442
488
|
The same flow is available through the proxy tool for non-interactive clients. By default, persistent OAuth 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.
|
|
443
489
|
|
|
@@ -449,13 +495,13 @@ On Linux, if credential access fails because Pi inherited a revoked session keyr
|
|
|
449
495
|
mcp({ action: "auth-start", server: "linear-server" })
|
|
450
496
|
```
|
|
451
497
|
|
|
452
|
-
For a loopback redirect, the adapter attempts to open the returned authorization URL, watches the callback, completes token exchange, and sends an `mcp-oauth-status` event when authentication finishes. If Pi is remote or cannot open a browser, open the returned URL locally. If the browser cannot reach Pi's callback, copy the full
|
|
498
|
+
For a loopback redirect, the adapter attempts to open the returned authorization URL, watches the callback, completes token exchange, and sends an `mcp-oauth-status` event when authentication finishes. If Pi is remote or cannot open a browser, open the returned URL locally. If the browser cannot reach Pi's callback, copy the full callback URL from the address bar and complete the flow in the same Pi session:
|
|
453
499
|
|
|
454
500
|
```js
|
|
455
501
|
mcp({
|
|
456
502
|
action: "auth-complete",
|
|
457
503
|
server: "linear-server",
|
|
458
|
-
args: { redirectUrl: "http://
|
|
504
|
+
args: { redirectUrl: "http://127.0.0.1:19876/callback?code=...&state=..." }
|
|
459
505
|
})
|
|
460
506
|
```
|
|
461
507
|
|
|
@@ -463,7 +509,7 @@ You can also pass only the `code` query parameter with `args: { code: "..." }`.
|
|
|
463
509
|
|
|
464
510
|
### Project server trust
|
|
465
511
|
|
|
466
|
-
Servers defined or changed by project-scoped MCP files (`.mcp.json`, `.pi/mcp-adapter.json`, and opted-in ancestor project files) do not run merely because a repository was opened. This includes servers brought in through project `imports`, `claudePlugins`, `settings.agentPluginPaths`, repo-local host config files (even when imports or discovery are enabled globally), or Pi packages listed in project `.pi/settings.json`. If Pi reports the project as untrusted, the adapter blocks them. In a trusted interactive session, the adapter shows the source file and command or URL and asks once before the first connection. The approval is stored under the Pi agent directory and is tied to the canonical project path, server name, and complete effective definition; changing the definition requires a new approval. Blocked servers are identified in the panel, while MCP status includes the required trust or approval action.
|
|
512
|
+
Servers defined or changed by project-scoped MCP files (`.mcp.json`, `.pi/mcp-adapter.json`, and opted-in ancestor project files) do not run merely because a repository was opened. This includes servers brought in through project `imports`, `claudePlugins`, `settings.agentPluginPaths`, repo-local host config files (even when imports or discovery are enabled globally), or Pi packages listed in project `.pi/settings.json`. If Pi reports the project as untrusted, the adapter blocks them. In a trusted interactive session, the adapter shows the source file and command or URL and asks once before the first connection. The approval is stored under the Pi agent directory and is tied to the canonical project path, server name, and complete effective definition; changing the definition requires a new approval. Git worktrees of one repository share approvals for the same folder, so a new worktree only asks when its definition differs. This covers regular, bare, and `--separate-git-dir` repositories; the one exception is the main checkout of a `--separate-git-dir` repository, which git records no link to, so it asks once on its own. Blocked servers are identified in the panel, while MCP status includes the required trust or approval action.
|
|
467
513
|
|
|
468
514
|
In print, JSON, and RPC sessions, an unapproved project server is skipped. To intentionally allow project servers in trusted headless sessions, set `"projectServers": "allow"` in the **user-global** `settings` object. The default is `"ask"`; project files cannot change this policy. Explicit config files, programmatic configuration, global/import/plugin servers, and runtime registrations retain their existing behavior. Project servers are always excluded from extension-load initialization and are admitted only after `session_start` supplies Pi's trust context.
|
|
469
515
|
|
|
@@ -837,6 +883,8 @@ Direct tools register from the metadata cache in the Pi agent dir (`~/.pi/agent/
|
|
|
837
883
|
|
|
838
884
|
For faster startup, set `settings.deferWithMissingMetadata` to `true`. Servers with missing or invalid metadata (expired, mismatched, or non-cacheable) then contribute no tools, prompts, resources, or search entries until the first MCP operation starts the runtime and loads live metadata; the `mcp` gateway stays available. Because Pi cannot unregister slash commands, cached prompt commands also wait for live metadata under this setting. `eager`/`keep-alive` servers and cold `MCP_DIRECT_TOOLS` selections still start immediately.
|
|
839
885
|
|
|
886
|
+
After discovery, each runtime retains its own schema-bearing tool and resource catalogue. Another session overwriting the shared disk cache, disk-cache expiry (including `ttlMs: 0`), cache deletion, or an idle disconnect does not revoke that runtime's tools. Disk TTLs still govern reuse at startup; session catalogues are not persisted with extended TTLs. A failed resource discovery preserves that runtime's previously discovered resources for the same server configuration, whereas a successful empty resource list removes them. Current server identity, disabled state, tool filters and failure backoff still apply, and an authoritative empty discovery removes the tools. Replacing the runtime requires fresh discovery or valid disk metadata.
|
|
887
|
+
|
|
840
888
|
Models sometimes encode an object or array argument as a JSON string. Set `settings.strictDirectToolArguments` to `true` to recover one such layer for schema-declared object and array properties, then validate the complete input against the advertised schema before execution.
|
|
841
889
|
|
|
842
890
|
Set `settings.directToolResultDetails` to `"bounded"` when an extension needs structured MCP result fields in Pi's direct-tool result details. The same output guard limits apply. Small leading structured fields stay available, while large fields receive bounded summaries and the complete guarded result follows the output guard's spill-file policy. The default `"lean"` mode keeps the existing server and tool metadata only.
|
package/cli.js
CHANGED
|
@@ -86,29 +86,31 @@ function printHelp(log = console.log) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
function readJsonFile(filePath) {
|
|
89
|
-
|
|
89
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
90
|
+
const text = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
|
|
91
|
+
return text.trim() === "" ? {} : JSON.parse(stripJsonComments(text, { trailingCommas: true }));
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
function loadPiConfig() {
|
|
93
|
-
if (!fs.
|
|
95
|
+
if (!fs.lstatSync(PI_CONFIG_PATH, { throwIfNoEntry: false })) {
|
|
94
96
|
return { mcpServers: {} };
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
const raw = readJsonFile(PI_CONFIG_PATH);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
throw new Error(`Invalid MCP config at ${PI_CONFIG_PATH}: expected \"mcpServers\" to be an object`);
|
|
100
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
101
|
+
throw new Error(`Invalid MCP config at ${PI_CONFIG_PATH}: top-level value must be an object`);
|
|
101
102
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
};
|
|
103
|
+
for (const key of ["mcpServers", "mcp-servers", "settings"]) {
|
|
104
|
+
const value = raw[key];
|
|
105
|
+
if (value !== undefined && (!value || typeof value !== "object" || Array.isArray(value))) {
|
|
106
|
+
throw new Error(`Invalid MCP config at ${PI_CONFIG_PATH}: ${key} must be an object`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (raw.imports !== undefined && (!Array.isArray(raw.imports) || raw.imports.some((value) => typeof value !== "string"))) {
|
|
110
|
+
throw new Error(`Invalid MCP config at ${PI_CONFIG_PATH}: imports must be an array of strings`);
|
|
111
|
+
}
|
|
112
|
+
const { "mcp-servers": legacyServers, ...normalized } = raw;
|
|
113
|
+
return { ...normalized, mcpServers: raw.mcpServers ?? legacyServers ?? {} };
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
function findAvailableImports() {
|
package/commands.ts
CHANGED
|
@@ -771,6 +771,8 @@ export async function openMcpPanel(
|
|
|
771
771
|
ctx.ui.custom(
|
|
772
772
|
(tui, theme, keybindings, done) => {
|
|
773
773
|
return createMcpPanel(config, cache, provenanceMap, callbacks, tui, (result: McpPanelResult) => {
|
|
774
|
+
let directToolsSaved = false;
|
|
775
|
+
let directToolsFileWritten = false;
|
|
774
776
|
void (async () => {
|
|
775
777
|
if (!result.cancelled && result.disabledChanges.size > 0) {
|
|
776
778
|
for (const [serverName, disabled] of result.disabledChanges) {
|
|
@@ -785,7 +787,10 @@ export async function openMcpPanel(
|
|
|
785
787
|
}
|
|
786
788
|
}
|
|
787
789
|
if (!result.cancelled && result.changes.size > 0) {
|
|
788
|
-
writeDirectToolsConfig(result.changes, provenanceMap, config)
|
|
790
|
+
writeDirectToolsConfig(result.changes, provenanceMap, config, () => {
|
|
791
|
+
directToolsFileWritten = true;
|
|
792
|
+
});
|
|
793
|
+
directToolsSaved = true;
|
|
789
794
|
await onDirectToolsConfigChanged?.(result.changes);
|
|
790
795
|
ctx.ui.notify("Direct tools updated for this session.", "info");
|
|
791
796
|
}
|
|
@@ -793,8 +798,15 @@ export async function openMcpPanel(
|
|
|
793
798
|
resolve();
|
|
794
799
|
})().catch((error) => {
|
|
795
800
|
const message = error instanceof Error ? error.message : String(error);
|
|
796
|
-
|
|
797
|
-
|
|
801
|
+
if (directToolsSaved) {
|
|
802
|
+
ctx.ui.notify(`Direct tools updated, but live refresh failed: ${message}`, "error");
|
|
803
|
+
configChanged = true;
|
|
804
|
+
} else if (directToolsFileWritten) {
|
|
805
|
+
ctx.ui.notify(`Direct tools were partially saved before another write failed: ${message}`, "error");
|
|
806
|
+
configChanged = true;
|
|
807
|
+
} else {
|
|
808
|
+
ctx.ui.notify(`Failed to save direct tools: ${message}`, "error");
|
|
809
|
+
}
|
|
798
810
|
done(undefined);
|
|
799
811
|
resolve();
|
|
800
812
|
});
|
package/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// config.ts - Config loading with import support
|
|
2
|
-
import { chmodSync, existsSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { parse as parseToml } from "smol-toml";
|
|
@@ -11,7 +11,7 @@ import { loadClaudePluginBundles } from "./claude-plugin-loader.ts";
|
|
|
11
11
|
import { loadPackageMcpConfigs } from "./package-mcp-loader.ts";
|
|
12
12
|
import { validateJevSettings } from "./jev-client.ts";
|
|
13
13
|
import { formatServerNamespace, isServerDisabled, type ClaudePluginConfig, type HostConfigDiscovery, type McpConfig, type ServerEntry, type McpSettings, type ImportKind, type ServerProvenance } from "./types.ts";
|
|
14
|
-
import { parseJsonWithComments, toStringRecord } from "./utils.ts";
|
|
14
|
+
import { parseJsonWithComments, stripUtf8Bom, toStringRecord } from "./utils.ts";
|
|
15
15
|
|
|
16
16
|
const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
|
|
17
17
|
const AGENTS_GLOBAL_CONFIG_PATHS = [
|
|
@@ -932,7 +932,7 @@ function resolveImportCandidates(importKind: ImportKind, cwd: string): ImportedS
|
|
|
932
932
|
|
|
933
933
|
function readImportedConfig(path: string): unknown {
|
|
934
934
|
const raw = readFileSync(path, "utf-8");
|
|
935
|
-
return path.endsWith(".toml") ? parseToml(raw) : parseJsonWithComments(raw);
|
|
935
|
+
return path.endsWith(".toml") ? parseToml(stripUtf8Bom(raw)) : parseJsonWithComments(raw);
|
|
936
936
|
}
|
|
937
937
|
|
|
938
938
|
function loadImportedConfig(
|
|
@@ -1335,22 +1335,30 @@ function buildConfigWritePreview(filePath: string, nextRaw: Record<string, unkno
|
|
|
1335
1335
|
}
|
|
1336
1336
|
|
|
1337
1337
|
function readRawConfigObject(filePath: string): Record<string, unknown> {
|
|
1338
|
-
if (!
|
|
1338
|
+
if (!lstatSync(filePath, { throwIfNoEntry: false })) return {};
|
|
1339
1339
|
|
|
1340
1340
|
try {
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1341
|
+
const text = readFileSync(filePath, "utf-8");
|
|
1342
|
+
if (text.trim() === "") return {};
|
|
1343
|
+
const raw = parseJsonWithComments(text);
|
|
1344
|
+
if (!isRecord(raw)) throw new Error("top-level value must be an object");
|
|
1345
|
+
return raw;
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
throw new Error(`Failed to read MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1345
1348
|
}
|
|
1346
1349
|
}
|
|
1347
1350
|
|
|
1348
1351
|
function writeConfigText(writePath: string, text: string): void {
|
|
1352
|
+
const originalPath = writePath;
|
|
1349
1353
|
let mode: number | undefined;
|
|
1350
1354
|
try {
|
|
1351
1355
|
writePath = realpathSync(writePath);
|
|
1352
1356
|
mode = statSync(writePath).mode & 0o777;
|
|
1353
|
-
} catch {
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
if (lstatSync(originalPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
1359
|
+
throw new Error(`Cannot write MCP config at ${originalPath}: symbolic link target is unavailable`, { cause: error });
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1354
1362
|
mkdirSync(dirname(writePath), { recursive: true });
|
|
1355
1363
|
const tmpPath = `${writePath}.${process.pid}.tmp`;
|
|
1356
1364
|
rmSync(tmpPath, { force: true });
|
|
@@ -1380,16 +1388,7 @@ export function writeJevSemanticSearchConfig(
|
|
|
1380
1388
|
effectiveJev?: unknown,
|
|
1381
1389
|
): { path: string; changed: boolean } {
|
|
1382
1390
|
const filePath = overridePath ? getPiGlobalConfigPath(overridePath) : getProjectPiConfigPath(cwd);
|
|
1383
|
-
|
|
1384
|
-
if (existsSync(filePath)) {
|
|
1385
|
-
try {
|
|
1386
|
-
const parsed = parseJsonWithComments(readFileSync(filePath, "utf8"));
|
|
1387
|
-
if (!isRecord(parsed)) throw new Error("top-level value must be an object");
|
|
1388
|
-
raw = parsed;
|
|
1389
|
-
} catch (error) {
|
|
1390
|
-
throw new Error(`Failed to update Jev settings at ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1391
|
+
const raw = readRawConfigObject(filePath);
|
|
1393
1392
|
if (raw.settings !== undefined && !isRecord(raw.settings)) {
|
|
1394
1393
|
throw new Error(`Failed to update Jev settings at ${filePath}: settings must be an object`);
|
|
1395
1394
|
}
|
|
@@ -1408,12 +1407,21 @@ export function writeJevSemanticSearchConfig(
|
|
|
1408
1407
|
return { path: filePath, changed: true };
|
|
1409
1408
|
}
|
|
1410
1409
|
|
|
1411
|
-
function getServersObject(raw: Record<string, unknown
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
|
|
1410
|
+
function getServersObject(raw: Record<string, unknown>, filePath: string): Record<string, ServerEntry> {
|
|
1411
|
+
for (const key of ["mcpServers", "mcp-servers"]) {
|
|
1412
|
+
if (Object.hasOwn(raw, key) && !isRecord(raw[key])) {
|
|
1413
|
+
throw new Error(`Failed to update MCP config at ${filePath}: ${key} must be an object`);
|
|
1414
|
+
}
|
|
1415
1415
|
}
|
|
1416
|
-
return
|
|
1416
|
+
return (raw.mcpServers ?? raw["mcp-servers"] ?? {}) as Record<string, ServerEntry>;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function getConfigImports(raw: Record<string, unknown>, filePath: string): ImportKind[] {
|
|
1420
|
+
if (raw.imports === undefined) return [];
|
|
1421
|
+
if (!Array.isArray(raw.imports) || raw.imports.some((value) => typeof value !== "string")) {
|
|
1422
|
+
throw new Error(`Failed to update MCP config at ${filePath}: imports must be an array of strings`);
|
|
1423
|
+
}
|
|
1424
|
+
return raw.imports as ImportKind[];
|
|
1417
1425
|
}
|
|
1418
1426
|
|
|
1419
1427
|
function setServersObject(raw: Record<string, unknown>, servers: Record<string, ServerEntry>): void {
|
|
@@ -1438,18 +1446,7 @@ export function writeProjectServerDisabledOverride(
|
|
|
1438
1446
|
disabled: boolean,
|
|
1439
1447
|
): ServerDisabledOverrideResult {
|
|
1440
1448
|
const filePath = getProjectPiConfigPath(cwd);
|
|
1441
|
-
|
|
1442
|
-
if (existsSync(filePath)) {
|
|
1443
|
-
try {
|
|
1444
|
-
const parsed = parseJsonWithComments(readFileSync(filePath, "utf-8"));
|
|
1445
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1446
|
-
throw new Error("root value must be an object");
|
|
1447
|
-
}
|
|
1448
|
-
raw = parsed as Record<string, unknown>;
|
|
1449
|
-
} catch (error) {
|
|
1450
|
-
throw new Error(`Failed to read project MCP override at ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1449
|
+
const raw = readRawConfigObject(filePath);
|
|
1453
1450
|
|
|
1454
1451
|
const serverKey = raw.mcpServers !== undefined ? "mcpServers" : raw["mcp-servers"] !== undefined ? "mcp-servers" : "mcpServers";
|
|
1455
1452
|
const rawServers = raw[serverKey];
|
|
@@ -1565,17 +1562,17 @@ function detectRepoPrompt(summary: Omit<McpDiscoverySummary, "fingerprint" | "re
|
|
|
1565
1562
|
export function previewCompatibilityImports(importKinds: ImportKind[], overridePath?: string): ConfigWritePreview {
|
|
1566
1563
|
const targetPath = getPiGlobalConfigPath(overridePath);
|
|
1567
1564
|
const raw = readRawConfigObject(targetPath);
|
|
1568
|
-
const currentImports =
|
|
1565
|
+
const currentImports = getConfigImports(raw, targetPath);
|
|
1569
1566
|
const merged = [...new Set([...currentImports, ...importKinds])];
|
|
1570
1567
|
const nextRaw = { ...raw, imports: merged };
|
|
1571
|
-
setServersObject(nextRaw, getServersObject(nextRaw));
|
|
1568
|
+
setServersObject(nextRaw, getServersObject(nextRaw, targetPath));
|
|
1572
1569
|
return buildConfigWritePreview(targetPath, nextRaw);
|
|
1573
1570
|
}
|
|
1574
1571
|
|
|
1575
1572
|
export function ensureCompatibilityImports(importKinds: ImportKind[], overridePath?: string): { path: string; added: ImportKind[] } {
|
|
1576
1573
|
const targetPath = getPiGlobalConfigPath(overridePath);
|
|
1577
1574
|
const raw = readRawConfigObject(targetPath);
|
|
1578
|
-
const currentImports =
|
|
1575
|
+
const currentImports = getConfigImports(raw, targetPath);
|
|
1579
1576
|
const merged = [...new Set([...currentImports, ...importKinds])];
|
|
1580
1577
|
const added = merged.filter((kind) => !currentImports.includes(kind));
|
|
1581
1578
|
if (added.length === 0) {
|
|
@@ -1583,7 +1580,7 @@ export function ensureCompatibilityImports(importKinds: ImportKind[], overridePa
|
|
|
1583
1580
|
}
|
|
1584
1581
|
|
|
1585
1582
|
raw.imports = merged;
|
|
1586
|
-
const servers = getServersObject(raw);
|
|
1583
|
+
const servers = getServersObject(raw, targetPath);
|
|
1587
1584
|
setServersObject(raw, servers);
|
|
1588
1585
|
writeRawConfigObject(targetPath, raw);
|
|
1589
1586
|
return { path: targetPath, added };
|
|
@@ -1595,14 +1592,20 @@ export function buildStarterProjectConfig(): McpConfig {
|
|
|
1595
1592
|
};
|
|
1596
1593
|
}
|
|
1597
1594
|
|
|
1595
|
+
function assertScaffoldTargetAbsent(filePath: string): void {
|
|
1596
|
+
if (lstatSync(filePath, { throwIfNoEntry: false })) throw new Error(`Cannot scaffold MCP config at ${filePath}: file already exists`);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1598
1599
|
export function previewStarterSharedConfig(target: SharedConfigTarget, cwd = process.cwd()): ConfigWritePreview {
|
|
1599
1600
|
const targetPath = getSharedConfigPath(target, cwd);
|
|
1601
|
+
assertScaffoldTargetAbsent(targetPath);
|
|
1600
1602
|
const nextRaw = { mcpServers: buildStarterProjectConfig().mcpServers };
|
|
1601
1603
|
return buildConfigWritePreview(targetPath, nextRaw);
|
|
1602
1604
|
}
|
|
1603
1605
|
|
|
1604
1606
|
export function writeStarterSharedConfig(target: SharedConfigTarget, cwd = process.cwd()): string {
|
|
1605
1607
|
const targetPath = getSharedConfigPath(target, cwd);
|
|
1608
|
+
assertScaffoldTargetAbsent(targetPath);
|
|
1606
1609
|
const raw = { mcpServers: buildStarterProjectConfig().mcpServers };
|
|
1607
1610
|
writeRawConfigObject(targetPath, raw);
|
|
1608
1611
|
return targetPath;
|
|
@@ -1619,7 +1622,7 @@ export function writeStarterProjectConfig(cwd = process.cwd()): string {
|
|
|
1619
1622
|
export function previewSharedServerEntry(filePath: string, serverName: string, entry: ServerEntry): ConfigWritePreview {
|
|
1620
1623
|
const raw = readRawConfigObject(filePath);
|
|
1621
1624
|
const nextRaw = { ...raw };
|
|
1622
|
-
const servers = getServersObject(nextRaw);
|
|
1625
|
+
const servers = getServersObject(nextRaw, filePath);
|
|
1623
1626
|
servers[serverName] = entry;
|
|
1624
1627
|
setServersObject(nextRaw, servers);
|
|
1625
1628
|
return buildConfigWritePreview(filePath, nextRaw);
|
|
@@ -1627,7 +1630,7 @@ export function previewSharedServerEntry(filePath: string, serverName: string, e
|
|
|
1627
1630
|
|
|
1628
1631
|
export function writeSharedServerEntry(filePath: string, serverName: string, entry: ServerEntry): string {
|
|
1629
1632
|
const raw = readRawConfigObject(filePath);
|
|
1630
|
-
const servers = getServersObject(raw);
|
|
1633
|
+
const servers = getServersObject(raw, filePath);
|
|
1631
1634
|
servers[serverName] = entry;
|
|
1632
1635
|
setServersObject(raw, servers);
|
|
1633
1636
|
writeRawConfigObject(filePath, raw);
|
|
@@ -1684,6 +1687,7 @@ export function writeDirectToolsConfig(
|
|
|
1684
1687
|
changes: Map<string, true | string[] | false>,
|
|
1685
1688
|
provenance: Map<string, ServerProvenance>,
|
|
1686
1689
|
fullConfig: McpConfig,
|
|
1690
|
+
onFileWritten?: () => void,
|
|
1687
1691
|
): void {
|
|
1688
1692
|
const byPath = new Map<string, { name: string; value: true | string[] | false; prov: ServerProvenance }[]>();
|
|
1689
1693
|
|
|
@@ -1697,10 +1701,12 @@ export function writeDirectToolsConfig(
|
|
|
1697
1701
|
byPath.get(targetPath)!.push({ name: serverName, value, prov });
|
|
1698
1702
|
}
|
|
1699
1703
|
|
|
1704
|
+
// Validate every target before writing any, then re-read each one: two paths can alias one file.
|
|
1705
|
+
for (const filePath of byPath.keys()) getServersObject(readRawConfigObject(filePath), filePath);
|
|
1706
|
+
|
|
1700
1707
|
for (const [filePath, entries] of byPath) {
|
|
1701
1708
|
const raw = readRawConfigObject(filePath);
|
|
1702
|
-
const servers = getServersObject(raw);
|
|
1703
|
-
|
|
1709
|
+
const servers = getServersObject(raw, filePath);
|
|
1704
1710
|
for (const { name, value, prov } of entries) {
|
|
1705
1711
|
if (prov.kind === "import") {
|
|
1706
1712
|
const fullDef = fullConfig.mcpServers[name];
|
|
@@ -1714,6 +1720,7 @@ export function writeDirectToolsConfig(
|
|
|
1714
1720
|
|
|
1715
1721
|
setServersObject(raw, servers);
|
|
1716
1722
|
writeRawConfigObject(filePath, raw);
|
|
1723
|
+
onFileWritten?.();
|
|
1717
1724
|
}
|
|
1718
1725
|
}
|
|
1719
1726
|
|
package/direct-tools.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { UrlElicitationRequiredError, type Client } from "@modelcontextprotocol/
|
|
|
3
3
|
import type { McpExtensionState } from "./state.ts";
|
|
4
4
|
import { disabledServerReason } from "./project-server-trust.ts";
|
|
5
5
|
import type { DirectToolSpec, McpContent } from "./types.ts";
|
|
6
|
-
import { lazyConnect,
|
|
6
|
+
import { lazyConnect, clearFailure } from "./init.ts";
|
|
7
|
+
import { describeFailure } from "./failure-backoff.ts";
|
|
7
8
|
import { abortable, throwIfAborted } from "./abort.ts";
|
|
8
9
|
import { formatSchema } from "./tool-metadata.ts";
|
|
9
10
|
import { resolveMcpResultContent, transformMcpResourceContents } from "./tool-registrar.ts";
|
|
@@ -190,9 +191,9 @@ export function createDirectToolExecutor(
|
|
|
190
191
|
details: { error: "auth_required", server: spec.serverName, message, autoAuthAttempted },
|
|
191
192
|
};
|
|
192
193
|
}
|
|
193
|
-
const
|
|
194
|
+
const failure = describeFailure(state, spec.serverName);
|
|
194
195
|
return {
|
|
195
|
-
content: [{ type: "text" as const, text: `MCP server "${spec.serverName}" not available${
|
|
196
|
+
content: [{ type: "text" as const, text: `MCP server "${spec.serverName}" not available${failure !== null ? ` (${failure})` : ""}` }],
|
|
196
197
|
details: { error: "server_unavailable", server: spec.serverName },
|
|
197
198
|
};
|
|
198
199
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -142,6 +142,6 @@ export declare function writeStarterProjectConfig(cwd?: string): string;
|
|
|
142
142
|
export declare function previewSharedServerEntry(filePath: string, serverName: string, entry: ServerEntry): ConfigWritePreview;
|
|
143
143
|
export declare function writeSharedServerEntry(filePath: string, serverName: string, entry: ServerEntry): string;
|
|
144
144
|
export declare function getServerProvenance(overridePath?: string, cwd?: string): Map<string, ServerProvenance>;
|
|
145
|
-
export declare function writeDirectToolsConfig(changes: Map<string, true | string[] | false>, provenance: Map<string, ServerProvenance>, fullConfig: McpConfig): void;
|
|
145
|
+
export declare function writeDirectToolsConfig(changes: Map<string, true | string[] | false>, provenance: Map<string, ServerProvenance>, fullConfig: McpConfig, onFileWritten?: () => void): void;
|
|
146
146
|
export declare function resolveConfiguredOAuthDir(raw: unknown, cwd?: string): string | undefined;
|
|
147
147
|
export {};
|