pi-mcp-adapter 3.0.0 → 3.1.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 +31 -0
- package/README.md +1 -1
- 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/metadata-cache.js +4 -1
- package/dist/metadata-cache.js.map +1 -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/index.ts +5 -5
- package/init.ts +3 -1
- package/mcp-panel.ts +7 -6
- package/mcp-setup-panel.ts +18 -4
- package/metadata-cache.ts +4 -1
- package/package.json +2 -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/utils.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.1.0] - 2026-09-27
|
|
11
|
+
|
|
12
|
+
### Highlights
|
|
13
|
+
|
|
14
|
+
- Approving a project's MCP servers once now covers every git worktree of that repository, so new worktrees stop asking again.
|
|
15
|
+
- 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.
|
|
16
|
+
- When a server can't connect, the agent now sees the actual error instead of just "failed".
|
|
17
|
+
- 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.
|
|
18
|
+
- A dependency update fixes a high-severity denial-of-service bug in TOML config parsing.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- 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).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- 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).
|
|
27
|
+
- 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).
|
|
28
|
+
- 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).
|
|
29
|
+
- 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.
|
|
30
|
+
- 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).
|
|
31
|
+
- 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).
|
|
32
|
+
- 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).
|
|
33
|
+
- 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).
|
|
34
|
+
- 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).
|
|
35
|
+
- 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).
|
|
36
|
+
|
|
37
|
+
### Security
|
|
38
|
+
|
|
39
|
+
- `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).
|
|
40
|
+
|
|
10
41
|
## [3.0.0] - 2026-09-26
|
|
11
42
|
|
|
12
43
|
### Highlights
|
package/README.md
CHANGED
|
@@ -463,7 +463,7 @@ You can also pass only the `code` query parameter with `args: { code: "..." }`.
|
|
|
463
463
|
|
|
464
464
|
### Project server trust
|
|
465
465
|
|
|
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.
|
|
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. 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
467
|
|
|
468
468
|
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
469
|
|
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 {};
|
package/dist/config.js
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.js";
|
|
|
11
11
|
import { loadPackageMcpConfigs } from "./package-mcp-loader.js";
|
|
12
12
|
import { validateJevSettings } from "./jev-client.js";
|
|
13
13
|
import { formatServerNamespace, isServerDisabled } from "./types.js";
|
|
14
|
-
import { parseJsonWithComments, toStringRecord } from "./utils.js";
|
|
14
|
+
import { parseJsonWithComments, stripUtf8Bom, toStringRecord } from "./utils.js";
|
|
15
15
|
const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
|
|
16
16
|
const AGENTS_GLOBAL_CONFIG_PATHS = [
|
|
17
17
|
join(homedir(), ".agents", "mcp.json"),
|
|
@@ -776,7 +776,7 @@ function resolveImportCandidates(importKind, cwd) {
|
|
|
776
776
|
}
|
|
777
777
|
function readImportedConfig(path) {
|
|
778
778
|
const raw = readFileSync(path, "utf-8");
|
|
779
|
-
return path.endsWith(".toml") ? parseToml(raw) : parseJsonWithComments(raw);
|
|
779
|
+
return path.endsWith(".toml") ? parseToml(stripUtf8Bom(raw)) : parseJsonWithComments(raw);
|
|
780
780
|
}
|
|
781
781
|
function loadImportedConfig(importKind, cwd, warningPrefix) {
|
|
782
782
|
if (importKind === "opencode") {
|
|
@@ -1155,23 +1155,33 @@ function buildConfigWritePreview(filePath, nextRaw) {
|
|
|
1155
1155
|
};
|
|
1156
1156
|
}
|
|
1157
1157
|
function readRawConfigObject(filePath) {
|
|
1158
|
-
if (!
|
|
1158
|
+
if (!lstatSync(filePath, { throwIfNoEntry: false }))
|
|
1159
1159
|
return {};
|
|
1160
1160
|
try {
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1161
|
+
const text = readFileSync(filePath, "utf-8");
|
|
1162
|
+
if (text.trim() === "")
|
|
1163
|
+
return {};
|
|
1164
|
+
const raw = parseJsonWithComments(text);
|
|
1165
|
+
if (!isRecord(raw))
|
|
1166
|
+
throw new Error("top-level value must be an object");
|
|
1167
|
+
return raw;
|
|
1163
1168
|
}
|
|
1164
|
-
catch {
|
|
1165
|
-
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
throw new Error(`Failed to read MCP config at ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1166
1171
|
}
|
|
1167
1172
|
}
|
|
1168
1173
|
function writeConfigText(writePath, text) {
|
|
1174
|
+
const originalPath = writePath;
|
|
1169
1175
|
let mode;
|
|
1170
1176
|
try {
|
|
1171
1177
|
writePath = realpathSync(writePath);
|
|
1172
1178
|
mode = statSync(writePath).mode & 0o777;
|
|
1173
1179
|
}
|
|
1174
|
-
catch {
|
|
1180
|
+
catch (error) {
|
|
1181
|
+
if (lstatSync(originalPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
1182
|
+
throw new Error(`Cannot write MCP config at ${originalPath}: symbolic link target is unavailable`, { cause: error });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1175
1185
|
mkdirSync(dirname(writePath), { recursive: true });
|
|
1176
1186
|
const tmpPath = `${writePath}.${process.pid}.tmp`;
|
|
1177
1187
|
rmSync(tmpPath, { force: true });
|
|
@@ -1199,18 +1209,7 @@ export function writeSharedConfigText(filePath, text) {
|
|
|
1199
1209
|
}
|
|
1200
1210
|
export function writeJevSemanticSearchConfig(overridePath, cwd, allowedServers, effectiveJev) {
|
|
1201
1211
|
const filePath = overridePath ? getPiGlobalConfigPath(overridePath) : getProjectPiConfigPath(cwd);
|
|
1202
|
-
|
|
1203
|
-
if (existsSync(filePath)) {
|
|
1204
|
-
try {
|
|
1205
|
-
const parsed = parseJsonWithComments(readFileSync(filePath, "utf8"));
|
|
1206
|
-
if (!isRecord(parsed))
|
|
1207
|
-
throw new Error("top-level value must be an object");
|
|
1208
|
-
raw = parsed;
|
|
1209
|
-
}
|
|
1210
|
-
catch (error) {
|
|
1211
|
-
throw new Error(`Failed to update Jev settings at ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1212
|
+
const raw = readRawConfigObject(filePath);
|
|
1214
1213
|
if (raw.settings !== undefined && !isRecord(raw.settings)) {
|
|
1215
1214
|
throw new Error(`Failed to update Jev settings at ${filePath}: settings must be an object`);
|
|
1216
1215
|
}
|
|
@@ -1229,12 +1228,21 @@ export function writeJevSemanticSearchConfig(overridePath, cwd, allowedServers,
|
|
|
1229
1228
|
writeRawConfigObject(filePath, raw);
|
|
1230
1229
|
return { path: filePath, changed: true };
|
|
1231
1230
|
}
|
|
1232
|
-
function getServersObject(raw) {
|
|
1233
|
-
const
|
|
1234
|
-
|
|
1235
|
-
|
|
1231
|
+
function getServersObject(raw, filePath) {
|
|
1232
|
+
for (const key of ["mcpServers", "mcp-servers"]) {
|
|
1233
|
+
if (Object.hasOwn(raw, key) && !isRecord(raw[key])) {
|
|
1234
|
+
throw new Error(`Failed to update MCP config at ${filePath}: ${key} must be an object`);
|
|
1235
|
+
}
|
|
1236
1236
|
}
|
|
1237
|
-
return
|
|
1237
|
+
return (raw.mcpServers ?? raw["mcp-servers"] ?? {});
|
|
1238
|
+
}
|
|
1239
|
+
function getConfigImports(raw, filePath) {
|
|
1240
|
+
if (raw.imports === undefined)
|
|
1241
|
+
return [];
|
|
1242
|
+
if (!Array.isArray(raw.imports) || raw.imports.some((value) => typeof value !== "string")) {
|
|
1243
|
+
throw new Error(`Failed to update MCP config at ${filePath}: imports must be an array of strings`);
|
|
1244
|
+
}
|
|
1245
|
+
return raw.imports;
|
|
1238
1246
|
}
|
|
1239
1247
|
function setServersObject(raw, servers) {
|
|
1240
1248
|
delete raw["mcp-servers"];
|
|
@@ -1247,19 +1255,7 @@ function setServersObject(raw, servers) {
|
|
|
1247
1255
|
*/
|
|
1248
1256
|
export function writeProjectServerDisabledOverride(overridePath, cwd, serverName, disabled) {
|
|
1249
1257
|
const filePath = getProjectPiConfigPath(cwd);
|
|
1250
|
-
|
|
1251
|
-
if (existsSync(filePath)) {
|
|
1252
|
-
try {
|
|
1253
|
-
const parsed = parseJsonWithComments(readFileSync(filePath, "utf-8"));
|
|
1254
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1255
|
-
throw new Error("root value must be an object");
|
|
1256
|
-
}
|
|
1257
|
-
raw = parsed;
|
|
1258
|
-
}
|
|
1259
|
-
catch (error) {
|
|
1260
|
-
throw new Error(`Failed to read project MCP override at ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1258
|
+
const raw = readRawConfigObject(filePath);
|
|
1263
1259
|
const serverKey = raw.mcpServers !== undefined ? "mcpServers" : raw["mcp-servers"] !== undefined ? "mcp-servers" : "mcpServers";
|
|
1264
1260
|
const rawServers = raw[serverKey];
|
|
1265
1261
|
if (rawServers !== undefined && (!rawServers || typeof rawServers !== "object" || Array.isArray(rawServers))) {
|
|
@@ -1368,23 +1364,23 @@ function detectRepoPrompt(summary, cwd = process.cwd()) {
|
|
|
1368
1364
|
export function previewCompatibilityImports(importKinds, overridePath) {
|
|
1369
1365
|
const targetPath = getPiGlobalConfigPath(overridePath);
|
|
1370
1366
|
const raw = readRawConfigObject(targetPath);
|
|
1371
|
-
const currentImports =
|
|
1367
|
+
const currentImports = getConfigImports(raw, targetPath);
|
|
1372
1368
|
const merged = [...new Set([...currentImports, ...importKinds])];
|
|
1373
1369
|
const nextRaw = { ...raw, imports: merged };
|
|
1374
|
-
setServersObject(nextRaw, getServersObject(nextRaw));
|
|
1370
|
+
setServersObject(nextRaw, getServersObject(nextRaw, targetPath));
|
|
1375
1371
|
return buildConfigWritePreview(targetPath, nextRaw);
|
|
1376
1372
|
}
|
|
1377
1373
|
export function ensureCompatibilityImports(importKinds, overridePath) {
|
|
1378
1374
|
const targetPath = getPiGlobalConfigPath(overridePath);
|
|
1379
1375
|
const raw = readRawConfigObject(targetPath);
|
|
1380
|
-
const currentImports =
|
|
1376
|
+
const currentImports = getConfigImports(raw, targetPath);
|
|
1381
1377
|
const merged = [...new Set([...currentImports, ...importKinds])];
|
|
1382
1378
|
const added = merged.filter((kind) => !currentImports.includes(kind));
|
|
1383
1379
|
if (added.length === 0) {
|
|
1384
1380
|
return { path: targetPath, added: [] };
|
|
1385
1381
|
}
|
|
1386
1382
|
raw.imports = merged;
|
|
1387
|
-
const servers = getServersObject(raw);
|
|
1383
|
+
const servers = getServersObject(raw, targetPath);
|
|
1388
1384
|
setServersObject(raw, servers);
|
|
1389
1385
|
writeRawConfigObject(targetPath, raw);
|
|
1390
1386
|
return { path: targetPath, added };
|
|
@@ -1394,13 +1390,19 @@ export function buildStarterProjectConfig() {
|
|
|
1394
1390
|
mcpServers: {},
|
|
1395
1391
|
};
|
|
1396
1392
|
}
|
|
1393
|
+
function assertScaffoldTargetAbsent(filePath) {
|
|
1394
|
+
if (lstatSync(filePath, { throwIfNoEntry: false }))
|
|
1395
|
+
throw new Error(`Cannot scaffold MCP config at ${filePath}: file already exists`);
|
|
1396
|
+
}
|
|
1397
1397
|
export function previewStarterSharedConfig(target, cwd = process.cwd()) {
|
|
1398
1398
|
const targetPath = getSharedConfigPath(target, cwd);
|
|
1399
|
+
assertScaffoldTargetAbsent(targetPath);
|
|
1399
1400
|
const nextRaw = { mcpServers: buildStarterProjectConfig().mcpServers };
|
|
1400
1401
|
return buildConfigWritePreview(targetPath, nextRaw);
|
|
1401
1402
|
}
|
|
1402
1403
|
export function writeStarterSharedConfig(target, cwd = process.cwd()) {
|
|
1403
1404
|
const targetPath = getSharedConfigPath(target, cwd);
|
|
1405
|
+
assertScaffoldTargetAbsent(targetPath);
|
|
1404
1406
|
const raw = { mcpServers: buildStarterProjectConfig().mcpServers };
|
|
1405
1407
|
writeRawConfigObject(targetPath, raw);
|
|
1406
1408
|
return targetPath;
|
|
@@ -1414,14 +1416,14 @@ export function writeStarterProjectConfig(cwd = process.cwd()) {
|
|
|
1414
1416
|
export function previewSharedServerEntry(filePath, serverName, entry) {
|
|
1415
1417
|
const raw = readRawConfigObject(filePath);
|
|
1416
1418
|
const nextRaw = { ...raw };
|
|
1417
|
-
const servers = getServersObject(nextRaw);
|
|
1419
|
+
const servers = getServersObject(nextRaw, filePath);
|
|
1418
1420
|
servers[serverName] = entry;
|
|
1419
1421
|
setServersObject(nextRaw, servers);
|
|
1420
1422
|
return buildConfigWritePreview(filePath, nextRaw);
|
|
1421
1423
|
}
|
|
1422
1424
|
export function writeSharedServerEntry(filePath, serverName, entry) {
|
|
1423
1425
|
const raw = readRawConfigObject(filePath);
|
|
1424
|
-
const servers = getServersObject(raw);
|
|
1426
|
+
const servers = getServersObject(raw, filePath);
|
|
1425
1427
|
servers[serverName] = entry;
|
|
1426
1428
|
setServersObject(raw, servers);
|
|
1427
1429
|
writeRawConfigObject(filePath, raw);
|
|
@@ -1469,7 +1471,7 @@ export function getServerProvenance(overridePath, cwd = process.cwd()) {
|
|
|
1469
1471
|
}
|
|
1470
1472
|
return provenance;
|
|
1471
1473
|
}
|
|
1472
|
-
export function writeDirectToolsConfig(changes, provenance, fullConfig) {
|
|
1474
|
+
export function writeDirectToolsConfig(changes, provenance, fullConfig, onFileWritten) {
|
|
1473
1475
|
const byPath = new Map();
|
|
1474
1476
|
for (const [serverName, value] of changes) {
|
|
1475
1477
|
const prov = provenance.get(serverName);
|
|
@@ -1480,9 +1482,12 @@ export function writeDirectToolsConfig(changes, provenance, fullConfig) {
|
|
|
1480
1482
|
byPath.set(targetPath, []);
|
|
1481
1483
|
byPath.get(targetPath).push({ name: serverName, value, prov });
|
|
1482
1484
|
}
|
|
1485
|
+
// Validate every target before writing any, then re-read each one: two paths can alias one file.
|
|
1486
|
+
for (const filePath of byPath.keys())
|
|
1487
|
+
getServersObject(readRawConfigObject(filePath), filePath);
|
|
1483
1488
|
for (const [filePath, entries] of byPath) {
|
|
1484
1489
|
const raw = readRawConfigObject(filePath);
|
|
1485
|
-
const servers = getServersObject(raw);
|
|
1490
|
+
const servers = getServersObject(raw, filePath);
|
|
1486
1491
|
for (const { name, value, prov } of entries) {
|
|
1487
1492
|
if (prov.kind === "import") {
|
|
1488
1493
|
const fullDef = fullConfig.mcpServers[name];
|
|
@@ -1496,6 +1501,7 @@ export function writeDirectToolsConfig(changes, provenance, fullConfig) {
|
|
|
1496
1501
|
}
|
|
1497
1502
|
setServersObject(raw, servers);
|
|
1498
1503
|
writeRawConfigObject(filePath, raw);
|
|
1504
|
+
onFileWritten?.();
|
|
1499
1505
|
}
|
|
1500
1506
|
}
|
|
1501
1507
|
export function resolveConfiguredOAuthDir(raw, cwd = process.cwd()) {
|