@fitchmultz/pi-mcp-adapter 8.0.6 → 8.0.7
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 +128 -8
- package/OAUTH.md +13 -16
- package/README.md +8 -8
- package/cli.js +67 -28
- package/dist/abort.js +18 -9
- package/dist/client-operations.js +4 -2
- package/dist/commands.js +77 -43
- package/dist/config.js +194 -105
- package/dist/connection-runtime.d.ts +2 -2
- package/dist/connection-runtime.js +29 -18
- package/dist/consent-manager.d.ts +2 -2
- package/dist/consent-manager.js +5 -2
- package/dist/direct-tools.d.ts +1 -1
- package/dist/direct-tools.js +13 -7
- package/dist/elicitation-handler.d.ts +2 -3
- package/dist/elicitation-handler.js +113 -58
- package/dist/gateway-arguments.d.ts +1 -1
- package/dist/gateway-arguments.js +90 -23
- package/dist/glimpse-ui.js +10 -5
- package/dist/host-html-template.d.ts +3 -1
- package/dist/host-html-template.js +9 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +264 -119
- package/dist/init.d.ts +1 -3
- package/dist/init.js +91 -51
- package/dist/legacy-migration.d.ts +1 -1
- package/dist/legacy-migration.js +17 -12
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +33 -17
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +4 -2
- package/dist/mcp-auth-flow.js +150 -58
- package/dist/mcp-auth.d.ts +7 -7
- package/dist/mcp-auth.js +189 -138
- package/dist/mcp-callback-server.js +19 -8
- package/dist/mcp-code.js +168 -54
- package/dist/mcp-keyring-helper.cjs +57 -23
- package/dist/mcp-oauth-provider.d.ts +6 -7
- package/dist/mcp-oauth-provider.js +146 -72
- package/dist/mcp-oauth-runtime.js +29 -16
- package/dist/mcp-output-guard.js +193 -65
- package/dist/mcp-output-preview.js +41 -20
- package/dist/mcp-panel.d.ts +9 -9
- package/dist/mcp-panel.js +141 -70
- package/dist/mcp-script-worker.mjs +135 -43
- package/dist/mcp-setup-panel.d.ts +8 -8
- package/dist/mcp-setup-panel.js +130 -44
- package/dist/mcp-status.js +14 -7
- package/dist/mcp-trace.d.ts +10 -5
- package/dist/mcp-trace.js +127 -61
- package/dist/metadata-cache.d.ts +3 -4
- package/dist/metadata-cache.js +83 -43
- package/dist/npx-resolver.js +101 -60
- package/dist/onboarding-state.js +4 -2
- package/dist/plain-result-reader.js +78 -31
- package/dist/prompts.js +36 -20
- package/dist/proxy-modes.js +670 -193
- package/dist/resource-tools.js +30 -9
- package/dist/runtime-owner.js +36 -18
- package/dist/sampling-handler.d.ts +1 -2
- package/dist/sampling-handler.js +41 -21
- package/dist/search-ranking.d.ts +1 -1
- package/dist/search-ranking.js +67 -34
- package/dist/server-manager.d.ts +11 -11
- package/dist/server-manager.js +283 -166
- package/dist/session-recovery.js +113 -61
- package/dist/tool-approval.js +9 -3
- package/dist/tool-loader.js +154 -55
- package/dist/tool-metadata.d.ts +1 -1
- package/dist/tool-metadata.js +51 -23
- package/dist/tool-registrar.js +32 -10
- package/dist/tool-result-renderer.js +58 -33
- package/dist/types.d.ts +2 -2
- package/dist/types.js +39 -18
- package/dist/ui-message-history.js +33 -13
- package/dist/ui-resource-handler.d.ts +3 -3
- package/dist/ui-resource-handler.js +37 -21
- package/dist/ui-server.js +101 -43
- package/dist/ui-session.js +66 -29
- package/dist/ui-stream-types.js +14 -4
- package/dist/ui-tool-visibility.js +12 -6
- package/dist/unix-socket-transport.d.ts +1 -2
- package/dist/unix-socket-transport.js +19 -12
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +60 -35
- package/package.json +26 -2
- package/skills/mcp-scripting/SKILL.md +6 -6
package/dist/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// config.ts - Config loading with import support
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync, renameSync, statSync, chmodSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync, renameSync, statSync, chmodSync, } from "node:fs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -7,7 +7,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
7
7
|
import { parse as parseToml } from "smol-toml";
|
|
8
8
|
import stripJsonComments from "strip-json-comments";
|
|
9
9
|
import { getAdapterPath } from "./agent-dir.js";
|
|
10
|
-
import { isServerDisabled, validateServerProtocolConfig } from "./types.js";
|
|
10
|
+
import { isServerDisabled, validateServerProtocolConfig, } from "./types.js";
|
|
11
11
|
import { toStringRecord } from "./utils.js";
|
|
12
12
|
const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
|
|
13
13
|
const AGENTS_GLOBAL_CONFIG_PATHS = [
|
|
@@ -59,15 +59,11 @@ const IMPORT_PATHS = {
|
|
|
59
59
|
join(homedir(), ".claude.json"),
|
|
60
60
|
join(homedir(), ".claude", "claude_desktop_config.json"),
|
|
61
61
|
],
|
|
62
|
-
"claude-desktop": [
|
|
63
|
-
|
|
64
|
-
join(homedir(), ".codex", "config.toml"),
|
|
65
|
-
join(homedir(), ".codex", "config.json"),
|
|
66
|
-
],
|
|
67
|
-
opencode: [
|
|
68
|
-
join(homedir(), ".config", "opencode", "opencode.json"),
|
|
69
|
-
"./opencode.json",
|
|
62
|
+
"claude-desktop": [
|
|
63
|
+
join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
70
64
|
],
|
|
65
|
+
codex: [join(homedir(), ".codex", "config.toml"), join(homedir(), ".codex", "config.json")],
|
|
66
|
+
opencode: [join(homedir(), ".config", "opencode", "opencode.json"), "./opencode.json"],
|
|
71
67
|
windsurf: [join(homedir(), ".windsurf", "mcp.json")],
|
|
72
68
|
vscode: [".vscode/mcp.json"],
|
|
73
69
|
};
|
|
@@ -97,8 +93,9 @@ export function getMcpDiscoverySummary(overridePath, cwd = process.cwd()) {
|
|
|
97
93
|
const imports = Object.keys(IMPORT_PATHS)
|
|
98
94
|
.map((kind) => {
|
|
99
95
|
const imported = loadImportedConfig(kind, cwd, `Failed to inspect imported MCP config from ${kind}:`);
|
|
100
|
-
if (!imported)
|
|
96
|
+
if (!imported) {
|
|
101
97
|
return null;
|
|
98
|
+
}
|
|
102
99
|
return {
|
|
103
100
|
kind,
|
|
104
101
|
path: imported.path,
|
|
@@ -135,8 +132,7 @@ export function cloneMcpConfig(config) {
|
|
|
135
132
|
}
|
|
136
133
|
export function loadMcpConfig(overridePath, cwd = process.cwd(), options = {}) {
|
|
137
134
|
const includeProject = options.includeProject !== false;
|
|
138
|
-
const sourceSpecs = getConfigSources(overridePath, cwd)
|
|
139
|
-
.filter((source) => includeProject || !isProjectConfigSource(source, cwd));
|
|
135
|
+
const sourceSpecs = getConfigSources(overridePath, cwd).filter((source) => includeProject || !isProjectConfigSource(source, cwd));
|
|
140
136
|
const hostConfigDiscovery = getConfiguredHostConfigDiscovery(overridePath, cwd, includeProject);
|
|
141
137
|
// Host files are a lower-precedence fallback. This ordering means an opt-in
|
|
142
138
|
// discovery cannot override a shared or Pi-owned definition, and all normal
|
|
@@ -146,8 +142,9 @@ export function loadMcpConfig(overridePath, cwd = process.cwd(), options = {}) {
|
|
|
146
142
|
: { mcpServers: {} };
|
|
147
143
|
for (const source of sourceSpecs) {
|
|
148
144
|
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
149
|
-
if (!loaded)
|
|
145
|
+
if (!loaded) {
|
|
150
146
|
continue;
|
|
147
|
+
}
|
|
151
148
|
config = mergeConfigs(config, expandImports(loaded, cwd, includeProject));
|
|
152
149
|
}
|
|
153
150
|
return config;
|
|
@@ -155,12 +152,14 @@ export function loadMcpConfig(overridePath, cwd = process.cwd(), options = {}) {
|
|
|
155
152
|
function getConfiguredHostConfigDiscovery(overridePath, cwd = process.cwd(), includeProject = true) {
|
|
156
153
|
let configured = "off";
|
|
157
154
|
for (const source of getConfigSources(overridePath, cwd)) {
|
|
158
|
-
if (!includeProject && isProjectConfigSource(source, cwd))
|
|
155
|
+
if (!includeProject && isProjectConfigSource(source, cwd)) {
|
|
159
156
|
continue;
|
|
157
|
+
}
|
|
160
158
|
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
161
159
|
const value = loaded?.settings?.hostConfigDiscovery;
|
|
162
|
-
if (value === "off" || value === "on")
|
|
160
|
+
if (value === "off" || value === "on") {
|
|
163
161
|
configured = value;
|
|
162
|
+
}
|
|
164
163
|
}
|
|
165
164
|
return configured;
|
|
166
165
|
}
|
|
@@ -168,8 +167,9 @@ function loadDiscoveredHostConfigs(cwd, includeProject) {
|
|
|
168
167
|
let config = { mcpServers: {} };
|
|
169
168
|
for (const importKind of Object.keys(IMPORT_PATHS)) {
|
|
170
169
|
const imported = loadImportedConfig(importKind, cwd, `Failed to discover imported MCP config from ${importKind}:`, includeProject);
|
|
171
|
-
if (!imported)
|
|
170
|
+
if (!imported) {
|
|
172
171
|
continue;
|
|
172
|
+
}
|
|
173
173
|
config = mergeConfigs(config, {
|
|
174
174
|
mcpServers: extractServers(imported.value, importKind),
|
|
175
175
|
});
|
|
@@ -180,29 +180,33 @@ function getConfigConflicts(sourceSpecs, imports, cwd) {
|
|
|
180
180
|
const seen = new Map();
|
|
181
181
|
const record = (name, source) => {
|
|
182
182
|
const entries = seen.get(name) ?? [];
|
|
183
|
-
if (!entries.some((entry) => entry.kind === source.kind && entry.path === source.path))
|
|
183
|
+
if (!entries.some((entry) => entry.kind === source.kind && entry.path === source.path)) {
|
|
184
184
|
entries.push(source);
|
|
185
|
+
}
|
|
185
186
|
seen.set(name, entries);
|
|
186
187
|
};
|
|
187
188
|
// Host candidates are listed first because, when enabled, they are the
|
|
188
189
|
// lowest-precedence fallback. The fixed IMPORT_PATHS order is deterministic.
|
|
189
190
|
for (const entry of imports) {
|
|
190
191
|
const imported = loadImportedConfig(entry.kind, cwd, `Failed to inspect imported MCP config from ${entry.kind}:`);
|
|
191
|
-
if (!imported)
|
|
192
|
+
if (!imported) {
|
|
192
193
|
continue;
|
|
194
|
+
}
|
|
193
195
|
for (const name of Object.keys(extractServers(imported.value, entry.kind))) {
|
|
194
196
|
record(name, { kind: "host", path: imported.path });
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
199
|
for (const source of sourceSpecs) {
|
|
198
200
|
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
199
|
-
if (!loaded)
|
|
201
|
+
if (!loaded) {
|
|
200
202
|
continue;
|
|
203
|
+
}
|
|
201
204
|
if (loaded.imports?.length) {
|
|
202
205
|
for (const importKind of loaded.imports) {
|
|
203
206
|
const imported = loadImportedConfig(importKind, cwd, `Failed to inspect imported MCP config from ${importKind}:`);
|
|
204
|
-
if (!imported)
|
|
207
|
+
if (!imported) {
|
|
205
208
|
continue;
|
|
209
|
+
}
|
|
206
210
|
for (const name of Object.keys(extractServers(imported.value, importKind))) {
|
|
207
211
|
record(name, { kind: "host", path: imported.path });
|
|
208
212
|
}
|
|
@@ -225,8 +229,9 @@ function canonicalPath(path) {
|
|
|
225
229
|
let existingPath = resolvedPath;
|
|
226
230
|
while (!existsSync(existingPath)) {
|
|
227
231
|
const parent = dirname(existingPath);
|
|
228
|
-
if (parent === existingPath)
|
|
232
|
+
if (parent === existingPath) {
|
|
229
233
|
return resolvedPath;
|
|
234
|
+
}
|
|
230
235
|
existingPath = parent;
|
|
231
236
|
}
|
|
232
237
|
try {
|
|
@@ -238,16 +243,13 @@ function canonicalPath(path) {
|
|
|
238
243
|
}
|
|
239
244
|
export function isPathInsideProject(path, cwd) {
|
|
240
245
|
const projectRoots = findProjectRoots(cwd);
|
|
241
|
-
const roots = projectRoots.length > 0
|
|
242
|
-
? projectRoots
|
|
243
|
-
: isHomePath(cwd) ? [] : [cwd];
|
|
246
|
+
const roots = projectRoots.length > 0 ? projectRoots : isHomePath(cwd) ? [] : [cwd];
|
|
244
247
|
const contains = (root, candidate) => {
|
|
245
248
|
const pathFromRoot = relative(root, candidate);
|
|
246
|
-
return pathFromRoot === ""
|
|
247
|
-
|
|
249
|
+
return (pathFromRoot === "" ||
|
|
250
|
+
(pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep}`) && !isAbsolute(pathFromRoot)));
|
|
248
251
|
};
|
|
249
|
-
return roots.some((root) => contains(resolve(root), resolve(path))
|
|
250
|
-
|| contains(canonicalPath(root), canonicalPath(path)));
|
|
252
|
+
return roots.some((root) => contains(resolve(root), resolve(path)) || contains(canonicalPath(root), canonicalPath(path)));
|
|
251
253
|
}
|
|
252
254
|
function isProjectConfigSource(source, cwd) {
|
|
253
255
|
return source.scope === "project" || isPathInsideProject(source.readPath, cwd);
|
|
@@ -270,8 +272,9 @@ function getConfigSources(overridePath, cwd = process.cwd()) {
|
|
|
270
272
|
});
|
|
271
273
|
}
|
|
272
274
|
for (const [index, agentsPath] of AGENTS_GLOBAL_CONFIG_PATHS.entries()) {
|
|
273
|
-
if (agentsPath === userPath || agentsPath === GENERIC_GLOBAL_CONFIG_PATH)
|
|
275
|
+
if (agentsPath === userPath || agentsPath === GENERIC_GLOBAL_CONFIG_PATH) {
|
|
274
276
|
continue;
|
|
277
|
+
}
|
|
275
278
|
sources.push({
|
|
276
279
|
id: index === 0 ? "agents-global" : "agents-nested-global",
|
|
277
280
|
label: index === 0 ? "user-global .agents MCP" : "user-global .agents nested MCP",
|
|
@@ -349,19 +352,29 @@ function mergeServerMaps(base, next) {
|
|
|
349
352
|
if (existing && typeof definition.socket === "string") {
|
|
350
353
|
baseEntry = { ...existing };
|
|
351
354
|
for (const field of [
|
|
352
|
-
"command",
|
|
353
|
-
"
|
|
355
|
+
"command",
|
|
356
|
+
"args",
|
|
357
|
+
"env",
|
|
358
|
+
"cwd",
|
|
359
|
+
"url",
|
|
360
|
+
"headers",
|
|
361
|
+
"auth",
|
|
362
|
+
"bearerToken",
|
|
363
|
+
"bearerTokenEnv",
|
|
364
|
+
"oauth",
|
|
354
365
|
]) {
|
|
355
366
|
delete baseEntry[field];
|
|
356
367
|
}
|
|
357
368
|
}
|
|
358
|
-
else if (existing?.socket &&
|
|
369
|
+
else if (existing?.socket &&
|
|
370
|
+
(typeof definition.command === "string" || typeof definition.url === "string")) {
|
|
359
371
|
baseEntry = { ...existing };
|
|
360
372
|
delete baseEntry.socket;
|
|
361
373
|
}
|
|
362
374
|
if (existing && typeof definition.url === "string" && definition.url !== existing.url) {
|
|
363
|
-
if (baseEntry === existing)
|
|
375
|
+
if (baseEntry === existing) {
|
|
364
376
|
baseEntry = { ...existing };
|
|
377
|
+
}
|
|
365
378
|
for (const field of URL_BOUND_AUTH_FIELDS) {
|
|
366
379
|
delete baseEntry[field];
|
|
367
380
|
}
|
|
@@ -370,11 +383,14 @@ function mergeServerMaps(base, next) {
|
|
|
370
383
|
}
|
|
371
384
|
}
|
|
372
385
|
// Stdio env belongs to the program that received it, not just the server name.
|
|
373
|
-
if (existing?.command &&
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
386
|
+
if (existing?.command &&
|
|
387
|
+
((definition.command !== undefined && definition.command !== existing.command) ||
|
|
388
|
+
(definition.args !== undefined &&
|
|
389
|
+
!isDeepStrictEqual(definition.args, existing.args ?? [])) ||
|
|
390
|
+
(definition.cwd !== undefined && definition.cwd !== existing.cwd))) {
|
|
391
|
+
if (baseEntry === existing) {
|
|
377
392
|
baseEntry = { ...existing };
|
|
393
|
+
}
|
|
378
394
|
delete baseEntry.env;
|
|
379
395
|
}
|
|
380
396
|
merged[name] = { ...baseEntry, ...definition };
|
|
@@ -383,18 +399,21 @@ function mergeServerMaps(base, next) {
|
|
|
383
399
|
}
|
|
384
400
|
function mergeImports(left, right) {
|
|
385
401
|
const merged = [...(left ?? []), ...(right ?? [])];
|
|
386
|
-
if (merged.length === 0)
|
|
402
|
+
if (merged.length === 0) {
|
|
387
403
|
return undefined;
|
|
404
|
+
}
|
|
388
405
|
return [...new Set(merged)];
|
|
389
406
|
}
|
|
390
407
|
function expandImports(config, cwd, includeProject) {
|
|
391
|
-
if (!config.imports?.length)
|
|
408
|
+
if (!config.imports?.length) {
|
|
392
409
|
return config;
|
|
410
|
+
}
|
|
393
411
|
const importedServers = {};
|
|
394
412
|
for (const importKind of config.imports) {
|
|
395
413
|
const imported = loadImportedConfig(importKind, cwd, `Failed to import MCP config from ${importKind}:`, includeProject);
|
|
396
|
-
if (!imported)
|
|
414
|
+
if (!imported) {
|
|
397
415
|
continue;
|
|
416
|
+
}
|
|
398
417
|
const servers = extractServers(imported.value, importKind);
|
|
399
418
|
for (const [name, definition] of Object.entries(servers)) {
|
|
400
419
|
if (!importedServers[name]) {
|
|
@@ -421,17 +440,20 @@ function resolveImportCandidates(importKind, cwd, includeProject) {
|
|
|
421
440
|
break;
|
|
422
441
|
}
|
|
423
442
|
const parent = dirname(current);
|
|
424
|
-
if (parent === current)
|
|
443
|
+
if (parent === current) {
|
|
425
444
|
break;
|
|
445
|
+
}
|
|
426
446
|
current = parent;
|
|
427
447
|
}
|
|
428
|
-
if (!gitRoot)
|
|
448
|
+
if (!gitRoot) {
|
|
429
449
|
return join(start, "opencode.json");
|
|
450
|
+
}
|
|
430
451
|
current = start;
|
|
431
452
|
while (true) {
|
|
432
453
|
const projectConfig = join(current, "opencode.json");
|
|
433
|
-
if (existsSync(projectConfig) || current === gitRoot)
|
|
454
|
+
if (existsSync(projectConfig) || current === gitRoot) {
|
|
434
455
|
return projectConfig;
|
|
456
|
+
}
|
|
435
457
|
current = dirname(current);
|
|
436
458
|
}
|
|
437
459
|
}
|
|
@@ -451,8 +473,9 @@ function loadImportedConfig(importKind, cwd, warningPrefix, includeProject = tru
|
|
|
451
473
|
let merged = {};
|
|
452
474
|
let highestPrecedencePath;
|
|
453
475
|
for (const path of resolveImportCandidates(importKind, cwd, includeProject)) {
|
|
454
|
-
if (!existsSync(path))
|
|
476
|
+
if (!existsSync(path)) {
|
|
455
477
|
continue;
|
|
478
|
+
}
|
|
456
479
|
try {
|
|
457
480
|
const value = readImportedConfig(path);
|
|
458
481
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
@@ -467,8 +490,9 @@ function loadImportedConfig(importKind, cwd, warningPrefix, includeProject = tru
|
|
|
467
490
|
return highestPrecedencePath ? { path: highestPrecedencePath, value: merged } : null;
|
|
468
491
|
}
|
|
469
492
|
for (const path of resolveImportCandidates(importKind, cwd, includeProject)) {
|
|
470
|
-
if (!existsSync(path))
|
|
493
|
+
if (!existsSync(path)) {
|
|
471
494
|
continue;
|
|
495
|
+
}
|
|
472
496
|
try {
|
|
473
497
|
return { path, value: readImportedConfig(path) };
|
|
474
498
|
}
|
|
@@ -479,8 +503,9 @@ function loadImportedConfig(importKind, cwd, warningPrefix, includeProject = tru
|
|
|
479
503
|
return null;
|
|
480
504
|
}
|
|
481
505
|
function readValidatedConfig(path, label) {
|
|
482
|
-
if (!existsSync(path))
|
|
506
|
+
if (!existsSync(path)) {
|
|
483
507
|
return null;
|
|
508
|
+
}
|
|
484
509
|
try {
|
|
485
510
|
return validateConfig(parseJsonConfig(readFileSync(path, "utf-8")));
|
|
486
511
|
}
|
|
@@ -503,8 +528,9 @@ function validateConfig(raw) {
|
|
|
503
528
|
return { mcpServers: {} };
|
|
504
529
|
}
|
|
505
530
|
for (const entry of Object.values(servers)) {
|
|
506
|
-
if (entry && typeof entry === "object")
|
|
531
|
+
if (entry && typeof entry === "object") {
|
|
507
532
|
validateServerProtocolConfig(entry);
|
|
533
|
+
}
|
|
508
534
|
}
|
|
509
535
|
return {
|
|
510
536
|
mcpServers: servers,
|
|
@@ -521,13 +547,18 @@ function mergeOpenCodeConfigs(base, next) {
|
|
|
521
547
|
if (nextMcp && typeof nextMcp === "object" && !Array.isArray(nextMcp)) {
|
|
522
548
|
for (const [name, nextEntry] of Object.entries(nextMcp)) {
|
|
523
549
|
const baseEntry = mergedMcp[name];
|
|
524
|
-
if (baseEntry &&
|
|
525
|
-
|
|
550
|
+
if (baseEntry &&
|
|
551
|
+
typeof baseEntry === "object" &&
|
|
552
|
+
!Array.isArray(baseEntry) &&
|
|
553
|
+
nextEntry &&
|
|
554
|
+
typeof nextEntry === "object" &&
|
|
555
|
+
!Array.isArray(nextEntry)) {
|
|
526
556
|
const safeBase = { ...baseEntry };
|
|
527
557
|
const override = nextEntry;
|
|
528
558
|
if (typeof override.type === "string" && override.type !== safeBase.type) {
|
|
529
|
-
for (const field of ["command", "environment", "cwd", "url", "headers", "oauth"])
|
|
559
|
+
for (const field of ["command", "environment", "cwd", "url", "headers", "oauth"]) {
|
|
530
560
|
delete safeBase[field];
|
|
561
|
+
}
|
|
531
562
|
}
|
|
532
563
|
if (typeof override.url === "string" && override.url !== safeBase.url) {
|
|
533
564
|
delete safeBase.headers;
|
|
@@ -535,23 +566,31 @@ function mergeOpenCodeConfigs(base, next) {
|
|
|
535
566
|
}
|
|
536
567
|
if (Array.isArray(override.command)) {
|
|
537
568
|
const baseCommand = safeBase.command;
|
|
538
|
-
const commandChanged = !Array.isArray(baseCommand)
|
|
539
|
-
|
|
540
|
-
|
|
569
|
+
const commandChanged = !Array.isArray(baseCommand) ||
|
|
570
|
+
override.command.length !== baseCommand.length ||
|
|
571
|
+
override.command.some((value, index) => value !== baseCommand[index]);
|
|
541
572
|
if (commandChanged) {
|
|
542
573
|
delete safeBase.environment;
|
|
543
574
|
delete safeBase.cwd;
|
|
544
575
|
}
|
|
545
576
|
}
|
|
546
|
-
if (Object.hasOwn(override, "cwd") && override.cwd !== safeBase.cwd)
|
|
577
|
+
if (Object.hasOwn(override, "cwd") && override.cwd !== safeBase.cwd) {
|
|
547
578
|
delete safeBase.environment;
|
|
579
|
+
}
|
|
548
580
|
const mergedEntry = { ...safeBase, ...override };
|
|
549
581
|
for (const field of ["headers", "oauth"]) {
|
|
550
582
|
const baseField = safeBase[field];
|
|
551
583
|
const nextField = override[field];
|
|
552
|
-
if (baseField &&
|
|
553
|
-
|
|
554
|
-
|
|
584
|
+
if (baseField &&
|
|
585
|
+
typeof baseField === "object" &&
|
|
586
|
+
!Array.isArray(baseField) &&
|
|
587
|
+
nextField &&
|
|
588
|
+
typeof nextField === "object" &&
|
|
589
|
+
!Array.isArray(nextField)) {
|
|
590
|
+
mergedEntry[field] = {
|
|
591
|
+
...baseField,
|
|
592
|
+
...nextField,
|
|
593
|
+
};
|
|
555
594
|
}
|
|
556
595
|
}
|
|
557
596
|
mergedMcp[name] = mergedEntry;
|
|
@@ -564,8 +603,9 @@ function mergeOpenCodeConfigs(base, next) {
|
|
|
564
603
|
return { ...base, ...next, mcp: mergedMcp };
|
|
565
604
|
}
|
|
566
605
|
function extractServers(config, kind) {
|
|
567
|
-
if (!config || typeof config !== "object")
|
|
606
|
+
if (!config || typeof config !== "object") {
|
|
568
607
|
return {};
|
|
608
|
+
}
|
|
569
609
|
const obj = config;
|
|
570
610
|
let servers;
|
|
571
611
|
switch (kind) {
|
|
@@ -593,16 +633,22 @@ function extractServers(config, kind) {
|
|
|
593
633
|
const mappedServers = {};
|
|
594
634
|
for (const [name, entry] of Object.entries(servers)) {
|
|
595
635
|
if (kind === "opencode") {
|
|
596
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
636
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
597
637
|
continue;
|
|
638
|
+
}
|
|
598
639
|
const raw = entry;
|
|
599
|
-
if (raw.enabled === false)
|
|
640
|
+
if (raw.enabled === false) {
|
|
600
641
|
continue;
|
|
601
|
-
|
|
642
|
+
}
|
|
643
|
+
if (raw.type === "local" &&
|
|
644
|
+
Array.isArray(raw.command) &&
|
|
645
|
+
raw.command.length > 0 &&
|
|
646
|
+
raw.command.every((value) => typeof value === "string")) {
|
|
602
647
|
const env = toStringRecord(raw.environment);
|
|
603
648
|
const command = raw.command[0];
|
|
604
|
-
if (command === undefined)
|
|
649
|
+
if (command === undefined) {
|
|
605
650
|
continue;
|
|
651
|
+
}
|
|
606
652
|
const mapped = {
|
|
607
653
|
command,
|
|
608
654
|
args: raw.command.slice(1),
|
|
@@ -644,17 +690,22 @@ function extractServers(config, kind) {
|
|
|
644
690
|
const envHttpHeaders = mapped.env_http_headers;
|
|
645
691
|
if (typeof bearerTokenEnv === "string") {
|
|
646
692
|
mapped.bearerTokenEnv = bearerTokenEnv;
|
|
647
|
-
if (mapped.auth === undefined)
|
|
693
|
+
if (mapped.auth === undefined) {
|
|
648
694
|
mapped.auth = "bearer";
|
|
695
|
+
}
|
|
649
696
|
}
|
|
650
697
|
if (httpHeaders && typeof httpHeaders === "object" && !Array.isArray(httpHeaders)) {
|
|
651
|
-
mapped.headers = {
|
|
698
|
+
mapped.headers = {
|
|
699
|
+
...mapped.headers,
|
|
700
|
+
...httpHeaders,
|
|
701
|
+
};
|
|
652
702
|
}
|
|
653
703
|
if (envHttpHeaders && typeof envHttpHeaders === "object" && !Array.isArray(envHttpHeaders)) {
|
|
654
704
|
const headers = { ...mapped.headers };
|
|
655
705
|
for (const [header, envVar] of Object.entries(envHttpHeaders)) {
|
|
656
|
-
if (typeof envVar === "string" && headers[header] === undefined)
|
|
706
|
+
if (typeof envVar === "string" && headers[header] === undefined) {
|
|
657
707
|
headers[header] = `$env:${envVar}`;
|
|
708
|
+
}
|
|
658
709
|
}
|
|
659
710
|
mapped.headers = headers;
|
|
660
711
|
}
|
|
@@ -669,8 +720,9 @@ function serializeRawConfig(raw) {
|
|
|
669
720
|
return `${JSON.stringify(raw, null, 2)}\n`;
|
|
670
721
|
}
|
|
671
722
|
function buildUnifiedDiff(beforeText, afterText) {
|
|
672
|
-
if (beforeText === afterText)
|
|
723
|
+
if (beforeText === afterText) {
|
|
673
724
|
return "(no changes)";
|
|
725
|
+
}
|
|
674
726
|
const before = beforeText.split("\n");
|
|
675
727
|
const after = afterText.split("\n");
|
|
676
728
|
const rows = before.length;
|
|
@@ -680,11 +732,13 @@ function buildUnifiedDiff(beforeText, afterText) {
|
|
|
680
732
|
for (let j = cols - 1; j >= 0; j--) {
|
|
681
733
|
const row = lcs[i];
|
|
682
734
|
const nextRow = lcs[i + 1];
|
|
683
|
-
if (!row || !nextRow)
|
|
735
|
+
if (!row || !nextRow) {
|
|
684
736
|
continue;
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
737
|
+
}
|
|
738
|
+
row[j] =
|
|
739
|
+
before[i] === after[j]
|
|
740
|
+
? (nextRow[j + 1] ?? 0) + 1
|
|
741
|
+
: Math.max(nextRow[j] ?? 0, row[j + 1] ?? 0);
|
|
688
742
|
}
|
|
689
743
|
}
|
|
690
744
|
const lines = ["--- before", "+++ after"];
|
|
@@ -724,8 +778,9 @@ function buildConfigWritePreview(filePath, nextRaw) {
|
|
|
724
778
|
};
|
|
725
779
|
}
|
|
726
780
|
function readRawConfigObject(filePath) {
|
|
727
|
-
if (!existsSync(filePath))
|
|
781
|
+
if (!existsSync(filePath)) {
|
|
728
782
|
return {};
|
|
783
|
+
}
|
|
729
784
|
let raw;
|
|
730
785
|
try {
|
|
731
786
|
raw = parseJsonConfig(readFileSync(filePath, "utf-8"));
|
|
@@ -747,13 +802,16 @@ function writeRawConfigObject(filePath, raw) {
|
|
|
747
802
|
mode: existingMode ?? 0o666,
|
|
748
803
|
flag: "wx",
|
|
749
804
|
});
|
|
750
|
-
if (existingMode !== undefined)
|
|
805
|
+
if (existingMode !== undefined) {
|
|
751
806
|
chmodSync(tmpPath, existingMode);
|
|
807
|
+
}
|
|
752
808
|
renameSync(tmpPath, filePath);
|
|
753
809
|
}
|
|
754
810
|
function getServersObject(raw, filePath) {
|
|
755
|
-
const existing = raw.mcpServers !== undefined
|
|
756
|
-
|
|
811
|
+
const existing = raw.mcpServers !== undefined
|
|
812
|
+
? raw.mcpServers
|
|
813
|
+
: raw["mcp-servers"] !== undefined
|
|
814
|
+
? raw["mcp-servers"]
|
|
757
815
|
: {};
|
|
758
816
|
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
|
|
759
817
|
throw new Error(`Invalid MCP config at ${filePath}: mcpServers must be an object`);
|
|
@@ -784,14 +842,20 @@ export function writeProjectServerDisabledOverride(overridePath, cwd, serverName
|
|
|
784
842
|
throw new Error(`Failed to read project MCP override at ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
785
843
|
}
|
|
786
844
|
}
|
|
787
|
-
const serverKey = raw.mcpServers !== undefined
|
|
845
|
+
const serverKey = raw.mcpServers !== undefined
|
|
846
|
+
? "mcpServers"
|
|
847
|
+
: raw["mcp-servers"] !== undefined
|
|
848
|
+
? "mcp-servers"
|
|
849
|
+
: "mcpServers";
|
|
788
850
|
const rawServers = raw[serverKey];
|
|
789
|
-
if (rawServers !== undefined &&
|
|
851
|
+
if (rawServers !== undefined &&
|
|
852
|
+
(!rawServers || typeof rawServers !== "object" || Array.isArray(rawServers))) {
|
|
790
853
|
throw new Error(`Failed to update project MCP override at ${filePath}: ${serverKey} must be an object`);
|
|
791
854
|
}
|
|
792
855
|
const servers = (rawServers ?? {});
|
|
793
856
|
const previous = servers[serverName];
|
|
794
|
-
if (previous !== undefined &&
|
|
857
|
+
if (previous !== undefined &&
|
|
858
|
+
(!previous || typeof previous !== "object" || Array.isArray(previous))) {
|
|
795
859
|
throw new Error(`Failed to update project MCP override at ${filePath}: server "${serverName}" must be an object`);
|
|
796
860
|
}
|
|
797
861
|
const existing = previous;
|
|
@@ -803,28 +867,35 @@ export function writeProjectServerDisabledOverride(overridePath, cwd, serverName
|
|
|
803
867
|
next = Object.fromEntries(Object.entries(existing ?? {}).filter(([key]) => key !== "disabled"));
|
|
804
868
|
let lowerConfig = { mcpServers: {} };
|
|
805
869
|
for (const source of getConfigSources(overridePath, cwd)) {
|
|
806
|
-
if (source.readPath === filePath)
|
|
870
|
+
if (source.readPath === filePath) {
|
|
807
871
|
continue;
|
|
872
|
+
}
|
|
808
873
|
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
809
|
-
if (loaded)
|
|
874
|
+
if (loaded) {
|
|
810
875
|
lowerConfig = mergeConfigs(lowerConfig, expandImports(loaded, cwd, true));
|
|
876
|
+
}
|
|
811
877
|
}
|
|
812
878
|
if (raw.imports !== undefined) {
|
|
813
|
-
if (!Array.isArray(raw.imports) ||
|
|
879
|
+
if (!Array.isArray(raw.imports) ||
|
|
880
|
+
raw.imports.some((kind) => typeof kind !== "string" || !Object.hasOwn(IMPORT_PATHS, kind))) {
|
|
814
881
|
throw new Error(`Failed to update project MCP override at ${filePath}: imports contains an unsupported config kind`);
|
|
815
882
|
}
|
|
816
883
|
lowerConfig = mergeConfigs(lowerConfig, expandImports({ mcpServers: {}, imports: raw.imports }, cwd, true));
|
|
817
884
|
}
|
|
818
|
-
if (isServerDisabled(lowerConfig.mcpServers[serverName]))
|
|
885
|
+
if (isServerDisabled(lowerConfig.mcpServers[serverName])) {
|
|
819
886
|
next.disabled = false;
|
|
887
|
+
}
|
|
820
888
|
}
|
|
821
|
-
if ((!existing && Object.keys(next).length === 0) ||
|
|
889
|
+
if ((!existing && Object.keys(next).length === 0) ||
|
|
890
|
+
JSON.stringify(existing) === JSON.stringify(next)) {
|
|
822
891
|
return { path: filePath, changed: false };
|
|
823
892
|
}
|
|
824
|
-
if (Object.keys(next).length === 0)
|
|
893
|
+
if (Object.keys(next).length === 0) {
|
|
825
894
|
delete servers[serverName];
|
|
826
|
-
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
827
897
|
servers[serverName] = next;
|
|
898
|
+
}
|
|
828
899
|
raw[serverKey] = servers;
|
|
829
900
|
writeRawConfigObject(filePath, raw);
|
|
830
901
|
return { path: filePath, changed: true };
|
|
@@ -835,7 +906,9 @@ function isRepoPromptServer(name, entry) {
|
|
|
835
906
|
return true;
|
|
836
907
|
}
|
|
837
908
|
const command = entry.command?.toLowerCase() ?? "";
|
|
838
|
-
if (command.includes("repoprompt") ||
|
|
909
|
+
if (command.includes("repoprompt") ||
|
|
910
|
+
command.includes("rp-mcp") ||
|
|
911
|
+
command.endsWith("repoprompt_cli")) {
|
|
839
912
|
return true;
|
|
840
913
|
}
|
|
841
914
|
return (entry.args ?? []).some((arg) => typeof arg === "string" && arg.toLowerCase().includes("repoprompt"));
|
|
@@ -851,18 +924,19 @@ function findProjectRoots(cwd = process.cwd()) {
|
|
|
851
924
|
for (const startPath of startPaths) {
|
|
852
925
|
let current = startPath;
|
|
853
926
|
while (true) {
|
|
854
|
-
if (!isHomePath(current)
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
927
|
+
if (!isHomePath(current) &&
|
|
928
|
+
(existsSync(join(current, ".git")) ||
|
|
929
|
+
existsSync(join(current, "package.json")) ||
|
|
930
|
+
existsSync(join(current, PROJECT_CONFIG_NAME)) ||
|
|
931
|
+
existsSync(join(current, ".pi"))) &&
|
|
932
|
+
!seen.has(current)) {
|
|
860
933
|
roots.push(current);
|
|
861
934
|
seen.add(current);
|
|
862
935
|
}
|
|
863
936
|
const parent = dirname(current);
|
|
864
|
-
if (parent === current)
|
|
937
|
+
if (parent === current) {
|
|
865
938
|
break;
|
|
939
|
+
}
|
|
866
940
|
current = parent;
|
|
867
941
|
}
|
|
868
942
|
}
|
|
@@ -880,11 +954,13 @@ function buildRepoPromptEntry(executablePath) {
|
|
|
880
954
|
}
|
|
881
955
|
function detectRepoPrompt(summary, cwd = process.cwd()) {
|
|
882
956
|
for (const source of summary.sources) {
|
|
883
|
-
if (source.kind !== "shared" || source.serverCount === 0)
|
|
957
|
+
if (source.kind !== "shared" || source.serverCount === 0) {
|
|
884
958
|
continue;
|
|
959
|
+
}
|
|
885
960
|
const config = readValidatedConfig(source.path, `MCP config from ${source.path}`);
|
|
886
|
-
if (!config)
|
|
961
|
+
if (!config) {
|
|
887
962
|
continue;
|
|
963
|
+
}
|
|
888
964
|
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
889
965
|
if (isRepoPromptServer(name, entry)) {
|
|
890
966
|
return { configured: true, configuredPath: source.path };
|
|
@@ -896,7 +972,9 @@ function detectRepoPrompt(summary, cwd = process.cwd()) {
|
|
|
896
972
|
return { configured: false };
|
|
897
973
|
}
|
|
898
974
|
const projectRoot = findProjectRoot(cwd);
|
|
899
|
-
const targetPath = projectRoot
|
|
975
|
+
const targetPath = projectRoot
|
|
976
|
+
? join(projectRoot, PROJECT_CONFIG_NAME)
|
|
977
|
+
: GENERIC_GLOBAL_CONFIG_PATH;
|
|
900
978
|
return {
|
|
901
979
|
configured: false,
|
|
902
980
|
executablePath,
|
|
@@ -908,7 +986,9 @@ function detectRepoPrompt(summary, cwd = process.cwd()) {
|
|
|
908
986
|
export function previewCompatibilityImports(importKinds, overridePath) {
|
|
909
987
|
const targetPath = getPiGlobalConfigPath(overridePath);
|
|
910
988
|
const raw = readRawConfigObject(targetPath);
|
|
911
|
-
const currentImports = Array.isArray(raw.imports)
|
|
989
|
+
const currentImports = Array.isArray(raw.imports)
|
|
990
|
+
? raw.imports.filter((value) => typeof value === "string")
|
|
991
|
+
: [];
|
|
912
992
|
const merged = [...new Set([...currentImports, ...importKinds])];
|
|
913
993
|
const nextRaw = { ...raw, imports: merged };
|
|
914
994
|
setServersObject(nextRaw, getServersObject(nextRaw, targetPath));
|
|
@@ -917,7 +997,9 @@ export function previewCompatibilityImports(importKinds, overridePath) {
|
|
|
917
997
|
export function ensureCompatibilityImports(importKinds, overridePath) {
|
|
918
998
|
const targetPath = getPiGlobalConfigPath(overridePath);
|
|
919
999
|
const raw = readRawConfigObject(targetPath);
|
|
920
|
-
const currentImports = Array.isArray(raw.imports)
|
|
1000
|
+
const currentImports = Array.isArray(raw.imports)
|
|
1001
|
+
? raw.imports.filter((value) => typeof value === "string")
|
|
1002
|
+
: [];
|
|
921
1003
|
const merged = [...new Set([...currentImports, ...importKinds])];
|
|
922
1004
|
const added = merged.filter((kind) => !currentImports.includes(kind));
|
|
923
1005
|
if (added.length === 0) {
|
|
@@ -967,8 +1049,9 @@ export function getServerProvenance(overridePath, cwd = process.cwd()) {
|
|
|
967
1049
|
if (getConfiguredHostConfigDiscovery(overridePath, cwd) === "on") {
|
|
968
1050
|
for (const importKind of Object.keys(IMPORT_PATHS)) {
|
|
969
1051
|
const imported = loadImportedConfig(importKind, cwd, `Failed to inspect imported MCP config from ${importKind}:`);
|
|
970
|
-
if (!imported)
|
|
1052
|
+
if (!imported) {
|
|
971
1053
|
continue;
|
|
1054
|
+
}
|
|
972
1055
|
for (const name of Object.keys(extractServers(imported.value, importKind))) {
|
|
973
1056
|
// Keep writes inside Pi-owned storage even though the source is external.
|
|
974
1057
|
// Later import kinds win in the same deterministic order as loadDiscoveredHostConfigs.
|
|
@@ -978,13 +1061,15 @@ export function getServerProvenance(overridePath, cwd = process.cwd()) {
|
|
|
978
1061
|
}
|
|
979
1062
|
for (const source of getConfigSources(overridePath, cwd)) {
|
|
980
1063
|
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
981
|
-
if (!loaded)
|
|
1064
|
+
if (!loaded) {
|
|
982
1065
|
continue;
|
|
1066
|
+
}
|
|
983
1067
|
if (loaded.imports?.length) {
|
|
984
1068
|
for (const importKind of loaded.imports) {
|
|
985
1069
|
const imported = loadImportedConfig(importKind, cwd, `Failed to inspect imported MCP config from ${importKind}:`);
|
|
986
|
-
if (!imported)
|
|
1070
|
+
if (!imported) {
|
|
987
1071
|
continue;
|
|
1072
|
+
}
|
|
988
1073
|
const servers = extractServers(imported.value, importKind);
|
|
989
1074
|
for (const name of Object.keys(servers)) {
|
|
990
1075
|
if (!provenance.has(name)) {
|
|
@@ -1007,11 +1092,13 @@ export function writeDirectToolsConfig(changes, provenance) {
|
|
|
1007
1092
|
const byPath = new Map();
|
|
1008
1093
|
for (const [serverName, value] of changes) {
|
|
1009
1094
|
const prov = provenance.get(serverName);
|
|
1010
|
-
if (!prov)
|
|
1095
|
+
if (!prov) {
|
|
1011
1096
|
continue;
|
|
1097
|
+
}
|
|
1012
1098
|
const targetPath = prov.path;
|
|
1013
|
-
if (!byPath.has(targetPath))
|
|
1099
|
+
if (!byPath.has(targetPath)) {
|
|
1014
1100
|
byPath.set(targetPath, []);
|
|
1101
|
+
}
|
|
1015
1102
|
byPath.get(targetPath).push({ name: serverName, value });
|
|
1016
1103
|
}
|
|
1017
1104
|
for (const [filePath, entries] of byPath) {
|
|
@@ -1026,13 +1113,15 @@ export function writeDirectToolsConfig(changes, provenance) {
|
|
|
1026
1113
|
}
|
|
1027
1114
|
}
|
|
1028
1115
|
export function resolveConfiguredOAuthDir(raw, cwd = process.cwd()) {
|
|
1029
|
-
if (raw === undefined || raw === null)
|
|
1116
|
+
if (raw === undefined || raw === null) {
|
|
1030
1117
|
return undefined;
|
|
1118
|
+
}
|
|
1031
1119
|
if (typeof raw !== "string") {
|
|
1032
1120
|
throw new Error("settings.oauthDir must be a string");
|
|
1033
1121
|
}
|
|
1034
1122
|
const trimmed = raw.trim();
|
|
1035
|
-
if (!trimmed)
|
|
1123
|
+
if (!trimmed) {
|
|
1036
1124
|
return undefined;
|
|
1125
|
+
}
|
|
1037
1126
|
return resolve(cwd, trimmed);
|
|
1038
1127
|
}
|