pi-mcp-adapter 2.4.0 → 2.4.2

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/config.ts CHANGED
@@ -1,110 +1,350 @@
1
1
  // config.ts - Config loading with import support
2
2
  import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { join, resolve, dirname } from "node:path";
4
+ import { dirname, join, resolve } from "node:path";
5
5
  import type { McpConfig, ServerEntry, McpSettings, ImportKind, ServerProvenance } from "./types.js";
6
6
 
7
- const DEFAULT_CONFIG_PATH = join(homedir(), ".pi", "agent", "mcp.json");
8
- const PROJECT_CONFIG_NAME = ".pi/mcp.json";
9
-
10
- // Import source paths for other tools
11
- const IMPORT_PATHS: Record<ImportKind, string> = {
12
- "cursor": join(homedir(), ".cursor", "mcp.json"),
13
- "claude-code": join(homedir(), ".claude", "claude_desktop_config.json"),
14
- "claude-desktop": join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
15
- "codex": join(homedir(), ".codex", "config.json"),
16
- "windsurf": join(homedir(), ".windsurf", "mcp.json"),
17
- "vscode": ".vscode/mcp.json", // Relative to project
7
+ const PI_GLOBAL_CONFIG_PATH = join(homedir(), ".pi", "agent", "mcp.json");
8
+ const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
9
+ const PROJECT_CONFIG_NAME = ".mcp.json";
10
+ const PROJECT_PI_CONFIG_NAME = ".pi/mcp.json";
11
+ const REPOPROMPT_BINARY_CANDIDATES = [
12
+ join(homedir(), "RepoPrompt", "repoprompt_cli"),
13
+ "/Applications/Repo Prompt.app/Contents/MacOS/repoprompt-mcp",
14
+ ];
15
+
16
+ const IMPORT_PATHS: Record<ImportKind, string[]> = {
17
+ cursor: [join(homedir(), ".cursor", "mcp.json")],
18
+ "claude-code": [
19
+ join(homedir(), ".claude", "mcp.json"),
20
+ join(homedir(), ".claude.json"),
21
+ join(homedir(), ".claude", "claude_desktop_config.json"),
22
+ ],
23
+ "claude-desktop": [join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json")],
24
+ codex: [join(homedir(), ".codex", "config.json")],
25
+ windsurf: [join(homedir(), ".windsurf", "mcp.json")],
26
+ vscode: [".vscode/mcp.json"],
18
27
  };
19
28
 
29
+ interface ConfigSourceSpec {
30
+ id: "shared-global" | "pi-global" | "shared-project" | "pi-project";
31
+ label: string;
32
+ readPath: string;
33
+ writePath: string;
34
+ kind: "user" | "project" | "import";
35
+ importKind?: string;
36
+ shared: boolean;
37
+ scope: "global" | "project";
38
+ }
39
+
40
+ export interface ConfigDiscoveryPath {
41
+ label: string;
42
+ path: string;
43
+ exists: boolean;
44
+ }
45
+
46
+ export interface DiscoveredImportConfig {
47
+ kind: ImportKind;
48
+ path: string;
49
+ }
50
+
51
+ export interface ConfigDiscoverySource extends ConfigDiscoveryPath {
52
+ id: ConfigSourceSpec["id"];
53
+ scope: ConfigSourceSpec["scope"];
54
+ kind: "shared" | "pi";
55
+ serverCount: number;
56
+ }
57
+
58
+ export interface ImportConfigSummary extends DiscoveredImportConfig {
59
+ serverCount: number;
60
+ }
61
+
62
+ export interface RepoPromptDiscovery {
63
+ configured: boolean;
64
+ configuredPath?: string;
65
+ executablePath?: string;
66
+ targetPath?: string;
67
+ serverName?: string;
68
+ entry?: ServerEntry;
69
+ }
70
+
71
+ export interface McpDiscoverySummary {
72
+ sources: ConfigDiscoverySource[];
73
+ imports: ImportConfigSummary[];
74
+ hasAnyConfig: boolean;
75
+ hasAnyDetectedPaths: boolean;
76
+ hasSharedServers: boolean;
77
+ hasPiOwnedServers: boolean;
78
+ totalServerCount: number;
79
+ fingerprint: string;
80
+ repoPrompt: RepoPromptDiscovery;
81
+ }
82
+
83
+ export interface ConfigWritePreview {
84
+ path: string;
85
+ existed: boolean;
86
+ changed: boolean;
87
+ beforeText: string;
88
+ afterText: string;
89
+ diffText: string;
90
+ }
91
+
92
+ export function getPiGlobalConfigPath(overridePath?: string): string {
93
+ return overridePath ? resolve(overridePath) : PI_GLOBAL_CONFIG_PATH;
94
+ }
95
+
96
+ export function getGenericGlobalConfigPath(): string {
97
+ return GENERIC_GLOBAL_CONFIG_PATH;
98
+ }
99
+
100
+ export function getProjectConfigPath(cwd = process.cwd()): string {
101
+ return resolve(cwd, PROJECT_CONFIG_NAME);
102
+ }
103
+
104
+ export function getProjectPiConfigPath(cwd = process.cwd()): string {
105
+ return resolve(cwd, PROJECT_PI_CONFIG_NAME);
106
+ }
107
+
108
+ export function getConfigDiscoveryPaths(overridePath?: string): ConfigDiscoveryPath[] {
109
+ return getConfigSources(overridePath).map((source) => ({
110
+ label: source.label,
111
+ path: source.readPath,
112
+ exists: existsSync(source.readPath),
113
+ }));
114
+ }
115
+
116
+ export function findAvailableImportConfigs(cwd = process.cwd()): DiscoveredImportConfig[] {
117
+ const discovered: DiscoveredImportConfig[] = [];
118
+
119
+ for (const importKind of Object.keys(IMPORT_PATHS) as ImportKind[]) {
120
+ const importPath = resolveImportPath(importKind, cwd);
121
+ if (importPath) {
122
+ discovered.push({ kind: importKind, path: importPath });
123
+ }
124
+ }
125
+
126
+ return discovered;
127
+ }
128
+
129
+ export function getMcpDiscoverySummary(overridePath?: string, cwd = process.cwd()): McpDiscoverySummary {
130
+ const sources = getConfigSources(overridePath, cwd).map((source) => {
131
+ const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
132
+ return {
133
+ id: source.id,
134
+ label: source.label,
135
+ path: source.readPath,
136
+ exists: existsSync(source.readPath),
137
+ scope: source.scope,
138
+ kind: source.shared ? "shared" : "pi",
139
+ serverCount: loaded ? Object.keys(loaded.mcpServers).length : 0,
140
+ } satisfies ConfigDiscoverySource;
141
+ });
142
+
143
+ const imports = (Object.keys(IMPORT_PATHS) as ImportKind[])
144
+ .map((kind) => {
145
+ const path = resolveImportPath(kind, cwd);
146
+ if (!path) return null;
147
+ return {
148
+ kind,
149
+ path,
150
+ serverCount: getImportServerCount(kind, path),
151
+ } satisfies ImportConfigSummary;
152
+ })
153
+ .filter((value): value is ImportConfigSummary => value !== null);
154
+
155
+ const totalServerCount = sources.reduce((sum, source) => sum + source.serverCount, 0);
156
+ const hasSharedServers = sources.some((source) => source.kind === "shared" && source.serverCount > 0);
157
+ const hasPiOwnedServers = sources.some((source) => source.kind === "pi" && source.serverCount > 0);
158
+ const hasAnyDetectedPaths = sources.some((source) => source.exists) || imports.length > 0;
159
+ const hasAnyConfig = totalServerCount > 0 || imports.some((entry) => entry.serverCount > 0) || hasAnyDetectedPaths;
160
+
161
+ const summaryWithoutRepoPrompt = {
162
+ sources,
163
+ imports,
164
+ hasAnyConfig,
165
+ hasAnyDetectedPaths,
166
+ hasSharedServers,
167
+ hasPiOwnedServers,
168
+ totalServerCount,
169
+ };
170
+
171
+ const fingerprint = JSON.stringify({
172
+ sources: sources.map((source) => [source.id, source.exists, source.serverCount]),
173
+ imports: imports.map((entry) => [entry.kind, entry.path, entry.serverCount]),
174
+ });
175
+
176
+ return {
177
+ ...summaryWithoutRepoPrompt,
178
+ fingerprint,
179
+ repoPrompt: detectRepoPrompt(summaryWithoutRepoPrompt, cwd),
180
+ };
181
+ }
182
+
20
183
  export function loadMcpConfig(overridePath?: string): McpConfig {
21
- const configPath = overridePath ? resolve(overridePath) : DEFAULT_CONFIG_PATH;
22
-
23
- // Load base config
24
184
  let config: McpConfig = { mcpServers: {} };
25
-
26
- if (existsSync(configPath)) {
27
- try {
28
- const raw = JSON.parse(readFileSync(configPath, "utf-8"));
29
- config = validateConfig(raw);
30
- } catch (error) {
31
- console.warn(`Failed to load MCP config from ${configPath}:`, error);
32
- }
185
+
186
+ for (const source of getConfigSources(overridePath)) {
187
+ const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
188
+ if (!loaded) continue;
189
+ config = mergeConfigs(config, expandImports(loaded));
33
190
  }
34
-
35
- // Process imports from other tools
36
- if (config.imports?.length) {
37
- for (const importKind of config.imports) {
38
- const importPath = IMPORT_PATHS[importKind];
39
- if (!importPath) continue;
40
-
41
- const fullPath = importPath.startsWith(".")
42
- ? resolve(process.cwd(), importPath)
43
- : importPath;
44
-
45
- if (!existsSync(fullPath)) continue;
46
-
47
- try {
48
- const imported = JSON.parse(readFileSync(fullPath, "utf-8"));
49
- const servers = extractServers(imported, importKind);
50
-
51
- // Merge - local config takes precedence over imports
52
- for (const [name, def] of Object.entries(servers)) {
53
- if (!config.mcpServers[name]) {
54
- config.mcpServers[name] = def;
55
- }
56
- }
57
- } catch (error) {
58
- console.warn(`Failed to import MCP config from ${importKind}:`, error);
59
- }
60
- }
191
+
192
+ return config;
193
+ }
194
+
195
+ function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSourceSpec[] {
196
+ const userPath = getPiGlobalConfigPath(overridePath);
197
+ const projectPath = getProjectConfigPath(cwd);
198
+ const projectPiPath = getProjectPiConfigPath(cwd);
199
+ const sources: ConfigSourceSpec[] = [];
200
+
201
+ if (GENERIC_GLOBAL_CONFIG_PATH !== userPath) {
202
+ sources.push({
203
+ id: "shared-global",
204
+ label: "user-global standard MCP",
205
+ readPath: GENERIC_GLOBAL_CONFIG_PATH,
206
+ writePath: userPath,
207
+ kind: "import",
208
+ importKind: "global MCP config",
209
+ shared: true,
210
+ scope: "global",
211
+ });
61
212
  }
62
-
63
- // Check for project-local config (skip if it's the same as the main config)
64
- const projectPath = resolve(process.cwd(), PROJECT_CONFIG_NAME);
65
- if (existsSync(projectPath) && projectPath !== configPath) {
213
+
214
+ sources.push({
215
+ id: "pi-global",
216
+ label: "Pi global override",
217
+ readPath: userPath,
218
+ writePath: userPath,
219
+ kind: "user",
220
+ shared: false,
221
+ scope: "global",
222
+ });
223
+
224
+ if (projectPath !== userPath) {
225
+ sources.push({
226
+ id: "shared-project",
227
+ label: "project standard MCP",
228
+ readPath: projectPath,
229
+ writePath: projectPath,
230
+ kind: "project",
231
+ shared: true,
232
+ scope: "project",
233
+ });
234
+ }
235
+
236
+ if (projectPiPath !== userPath && projectPiPath !== projectPath) {
237
+ sources.push({
238
+ id: "pi-project",
239
+ label: "project Pi override",
240
+ readPath: projectPiPath,
241
+ writePath: projectPiPath,
242
+ kind: "project",
243
+ shared: false,
244
+ scope: "project",
245
+ });
246
+ }
247
+
248
+ return sources;
249
+ }
250
+
251
+ function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
252
+ return {
253
+ mcpServers: { ...base.mcpServers, ...next.mcpServers },
254
+ imports: mergeImports(base.imports, next.imports),
255
+ settings: next.settings ? { ...base.settings, ...next.settings } : base.settings,
256
+ };
257
+ }
258
+
259
+ function mergeImports(left: ImportKind[] | undefined, right: ImportKind[] | undefined): ImportKind[] | undefined {
260
+ const merged = [...(left ?? []), ...(right ?? [])];
261
+ if (merged.length === 0) return undefined;
262
+ return [...new Set(merged)];
263
+ }
264
+
265
+ function expandImports(config: McpConfig): McpConfig {
266
+ if (!config.imports?.length) return config;
267
+
268
+ const importedServers: Record<string, ServerEntry> = {};
269
+ for (const importKind of config.imports) {
270
+ const importPath = resolveImportPath(importKind);
271
+ if (!importPath) continue;
272
+
66
273
  try {
67
- const projectConfig = JSON.parse(readFileSync(projectPath, "utf-8"));
68
- const validated = validateConfig(projectConfig);
69
-
70
- // Project config overrides everything
71
- config.mcpServers = { ...config.mcpServers, ...validated.mcpServers };
72
- if (validated.settings) {
73
- config.settings = { ...config.settings, ...validated.settings };
274
+ const imported = JSON.parse(readFileSync(importPath, "utf-8"));
275
+ const servers = extractServers(imported, importKind);
276
+ for (const [name, definition] of Object.entries(servers)) {
277
+ if (!importedServers[name]) {
278
+ importedServers[name] = definition;
279
+ }
74
280
  }
75
281
  } catch (error) {
76
- console.warn(`Failed to load project MCP config:`, error);
282
+ console.warn(`Failed to import MCP config from ${importKind}:`, error);
77
283
  }
78
284
  }
79
-
80
- return config;
285
+
286
+ return {
287
+ imports: config.imports,
288
+ settings: config.settings,
289
+ mcpServers: { ...importedServers, ...config.mcpServers },
290
+ };
291
+ }
292
+
293
+ function resolveImportPath(importKind: ImportKind, cwd = process.cwd()): string | null {
294
+ const candidates = IMPORT_PATHS[importKind] ?? [];
295
+ for (const candidate of candidates) {
296
+ const fullPath = candidate.startsWith(".") ? resolve(cwd, candidate) : candidate;
297
+ if (existsSync(fullPath)) {
298
+ return fullPath;
299
+ }
300
+ }
301
+ return null;
302
+ }
303
+
304
+ function getImportServerCount(importKind: ImportKind, path: string): number {
305
+ try {
306
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
307
+ return Object.keys(extractServers(raw, importKind)).length;
308
+ } catch {
309
+ return 0;
310
+ }
311
+ }
312
+
313
+ function readValidatedConfig(path: string, label: string): McpConfig | null {
314
+ if (!existsSync(path)) return null;
315
+
316
+ try {
317
+ return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
318
+ } catch (error) {
319
+ console.warn(`Failed to load ${label}:`, error);
320
+ return null;
321
+ }
81
322
  }
82
323
 
83
324
  function validateConfig(raw: unknown): McpConfig {
84
325
  if (!raw || typeof raw !== "object") {
85
326
  return { mcpServers: {} };
86
327
  }
87
-
328
+
88
329
  const obj = raw as Record<string, unknown>;
89
330
  const servers = obj.mcpServers ?? obj["mcp-servers"] ?? {};
90
-
91
- // Must be a plain object, not an array or null
331
+
92
332
  if (typeof servers !== "object" || servers === null || Array.isArray(servers)) {
93
333
  return { mcpServers: {} };
94
334
  }
95
-
335
+
96
336
  return {
97
337
  mcpServers: servers as Record<string, ServerEntry>,
98
- imports: Array.isArray(obj.imports) ? obj.imports as ImportKind[] : undefined,
338
+ imports: Array.isArray(obj.imports) ? (obj.imports as ImportKind[]) : undefined,
99
339
  settings: obj.settings as McpSettings | undefined,
100
340
  };
101
341
  }
102
342
 
103
343
  function extractServers(config: unknown, kind: ImportKind): Record<string, ServerEntry> {
104
344
  if (!config || typeof config !== "object") return {};
105
-
345
+
106
346
  const obj = config as Record<string, unknown>;
107
-
347
+
108
348
  let servers: unknown;
109
349
  switch (kind) {
110
350
  case "claude-desktop":
@@ -120,56 +360,269 @@ function extractServers(config: unknown, kind: ImportKind): Record<string, Serve
120
360
  default:
121
361
  return {};
122
362
  }
123
-
363
+
124
364
  if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
125
365
  return {};
126
366
  }
127
-
367
+
128
368
  return servers as Record<string, ServerEntry>;
129
369
  }
130
370
 
371
+ function serializeRawConfig(raw: Record<string, unknown>): string {
372
+ return `${JSON.stringify(raw, null, 2)}\n`;
373
+ }
374
+
375
+ function buildUnifiedDiff(beforeText: string, afterText: string): string {
376
+ if (beforeText === afterText) return "(no changes)";
377
+
378
+ const before = beforeText.split("\n");
379
+ const after = afterText.split("\n");
380
+ const rows = before.length;
381
+ const cols = after.length;
382
+ const lcs = Array.from({ length: rows + 1 }, () => Array<number>(cols + 1).fill(0));
383
+
384
+ for (let i = rows - 1; i >= 0; i--) {
385
+ for (let j = cols - 1; j >= 0; j--) {
386
+ lcs[i][j] = before[i] === after[j]
387
+ ? lcs[i + 1][j + 1] + 1
388
+ : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
389
+ }
390
+ }
391
+
392
+ const lines: string[] = ["--- before", "+++ after"];
393
+ let i = 0;
394
+ let j = 0;
395
+ while (i < rows || j < cols) {
396
+ if (i < rows && j < cols && before[i] === after[j]) {
397
+ lines.push(` ${before[i]}`);
398
+ i++;
399
+ j++;
400
+ continue;
401
+ }
402
+ if (j < cols && (i === rows || lcs[i][j + 1] >= lcs[i + 1][j])) {
403
+ lines.push(`+ ${after[j]}`);
404
+ j++;
405
+ continue;
406
+ }
407
+ if (i < rows) {
408
+ lines.push(`- ${before[i]}`);
409
+ i++;
410
+ }
411
+ }
412
+
413
+ return lines.join("\n");
414
+ }
415
+
416
+ function buildConfigWritePreview(filePath: string, nextRaw: Record<string, unknown>): ConfigWritePreview {
417
+ const existed = existsSync(filePath);
418
+ const beforeRaw = readRawConfigObject(filePath);
419
+ const beforeText = existed ? serializeRawConfig(beforeRaw) : "";
420
+ const afterText = serializeRawConfig(nextRaw);
421
+ return {
422
+ path: filePath,
423
+ existed,
424
+ changed: beforeText !== afterText,
425
+ beforeText,
426
+ afterText,
427
+ diffText: buildUnifiedDiff(beforeText, afterText),
428
+ };
429
+ }
430
+
431
+ function readRawConfigObject(filePath: string): Record<string, unknown> {
432
+ if (!existsSync(filePath)) return {};
433
+
434
+ try {
435
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
436
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
437
+ } catch {
438
+ return {};
439
+ }
440
+ }
441
+
442
+ function writeRawConfigObject(filePath: string, raw: Record<string, unknown>): void {
443
+ mkdirSync(dirname(filePath), { recursive: true });
444
+ const tmpPath = `${filePath}.${process.pid}.tmp`;
445
+ writeFileSync(tmpPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
446
+ renameSync(tmpPath, filePath);
447
+ }
448
+
449
+ function getServersObject(raw: Record<string, unknown>): Record<string, ServerEntry> {
450
+ const existing = raw.mcpServers ?? raw["mcp-servers"] ?? {};
451
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
452
+ return {};
453
+ }
454
+ return existing as Record<string, ServerEntry>;
455
+ }
456
+
457
+ function setServersObject(raw: Record<string, unknown>, servers: Record<string, ServerEntry>): void {
458
+ delete raw["mcp-servers"];
459
+ raw.mcpServers = servers;
460
+ }
461
+
462
+ function isRepoPromptServer(name: string, entry: ServerEntry): boolean {
463
+ const normalizedName = name.toLowerCase();
464
+ if (normalizedName.includes("repoprompt") || normalizedName === "rp") {
465
+ return true;
466
+ }
467
+
468
+ const command = entry.command?.toLowerCase() ?? "";
469
+ if (command.includes("repoprompt") || command.includes("rp-mcp") || command.endsWith("repoprompt_cli")) {
470
+ return true;
471
+ }
472
+
473
+ return (entry.args ?? []).some((arg) => typeof arg === "string" && arg.toLowerCase().includes("repoprompt"));
474
+ }
475
+
476
+ function findProjectRoot(cwd = process.cwd()): string | null {
477
+ let current = resolve(cwd);
478
+ while (true) {
479
+ if (
480
+ existsSync(join(current, ".git"))
481
+ || existsSync(join(current, "package.json"))
482
+ || existsSync(join(current, PROJECT_CONFIG_NAME))
483
+ || existsSync(join(current, ".pi"))
484
+ ) {
485
+ return current;
486
+ }
487
+
488
+ const parent = dirname(current);
489
+ if (parent === current) return null;
490
+ current = parent;
491
+ }
492
+ }
493
+
494
+ function buildRepoPromptEntry(executablePath: string): ServerEntry {
495
+ return {
496
+ command: executablePath,
497
+ args: [],
498
+ lifecycle: "lazy",
499
+ };
500
+ }
501
+
502
+ function detectRepoPrompt(summary: Omit<McpDiscoverySummary, "fingerprint" | "repoPrompt">, cwd = process.cwd()): RepoPromptDiscovery {
503
+ for (const source of summary.sources) {
504
+ if (source.kind !== "shared" || source.serverCount === 0) continue;
505
+ const config = readValidatedConfig(source.path, `MCP config from ${source.path}`);
506
+ if (!config) continue;
507
+ for (const [name, entry] of Object.entries(config.mcpServers)) {
508
+ if (isRepoPromptServer(name, entry)) {
509
+ return { configured: true, configuredPath: source.path };
510
+ }
511
+ }
512
+ }
513
+
514
+ const executablePath = REPOPROMPT_BINARY_CANDIDATES.find((candidate) => existsSync(candidate));
515
+ if (!executablePath) {
516
+ return { configured: false };
517
+ }
518
+
519
+ const projectRoot = findProjectRoot(cwd);
520
+ const targetPath = projectRoot ? join(projectRoot, PROJECT_CONFIG_NAME) : GENERIC_GLOBAL_CONFIG_PATH;
521
+ return {
522
+ configured: false,
523
+ executablePath,
524
+ targetPath,
525
+ serverName: "repoprompt",
526
+ entry: buildRepoPromptEntry(executablePath),
527
+ };
528
+ }
529
+
530
+ export function previewCompatibilityImports(importKinds: ImportKind[], overridePath?: string): ConfigWritePreview {
531
+ const targetPath = getPiGlobalConfigPath(overridePath);
532
+ const raw = readRawConfigObject(targetPath);
533
+ const currentImports = Array.isArray(raw.imports) ? raw.imports.filter((value): value is ImportKind => typeof value === "string") : [];
534
+ const merged = [...new Set([...currentImports, ...importKinds])];
535
+ const nextRaw = { ...raw, imports: merged };
536
+ setServersObject(nextRaw, getServersObject(nextRaw));
537
+ return buildConfigWritePreview(targetPath, nextRaw);
538
+ }
539
+
540
+ export function ensureCompatibilityImports(importKinds: ImportKind[], overridePath?: string): { path: string; added: ImportKind[] } {
541
+ const targetPath = getPiGlobalConfigPath(overridePath);
542
+ const raw = readRawConfigObject(targetPath);
543
+ const currentImports = Array.isArray(raw.imports) ? raw.imports.filter((value): value is ImportKind => typeof value === "string") : [];
544
+ const merged = [...new Set([...currentImports, ...importKinds])];
545
+ const added = merged.filter((kind) => !currentImports.includes(kind));
546
+ if (added.length === 0) {
547
+ return { path: targetPath, added: [] };
548
+ }
549
+
550
+ raw.imports = merged;
551
+ const servers = getServersObject(raw);
552
+ setServersObject(raw, servers);
553
+ writeRawConfigObject(targetPath, raw);
554
+ return { path: targetPath, added };
555
+ }
556
+
557
+ export function buildStarterProjectConfig(): McpConfig {
558
+ return {
559
+ mcpServers: {},
560
+ };
561
+ }
562
+
563
+ export function previewStarterProjectConfig(cwd = process.cwd()): ConfigWritePreview {
564
+ const targetPath = getProjectConfigPath(cwd);
565
+ const nextRaw = { mcpServers: buildStarterProjectConfig().mcpServers };
566
+ return buildConfigWritePreview(targetPath, nextRaw);
567
+ }
568
+
569
+ export function writeStarterProjectConfig(cwd = process.cwd()): string {
570
+ const targetPath = getProjectConfigPath(cwd);
571
+ const raw = { mcpServers: buildStarterProjectConfig().mcpServers };
572
+ writeRawConfigObject(targetPath, raw);
573
+ return targetPath;
574
+ }
575
+
576
+ export function previewSharedServerEntry(filePath: string, serverName: string, entry: ServerEntry): ConfigWritePreview {
577
+ const raw = readRawConfigObject(filePath);
578
+ const nextRaw = { ...raw };
579
+ const servers = getServersObject(nextRaw);
580
+ servers[serverName] = entry;
581
+ setServersObject(nextRaw, servers);
582
+ return buildConfigWritePreview(filePath, nextRaw);
583
+ }
584
+
585
+ export function writeSharedServerEntry(filePath: string, serverName: string, entry: ServerEntry): string {
586
+ const raw = readRawConfigObject(filePath);
587
+ const servers = getServersObject(raw);
588
+ servers[serverName] = entry;
589
+ setServersObject(raw, servers);
590
+ writeRawConfigObject(filePath, raw);
591
+ return filePath;
592
+ }
593
+
131
594
  export function getServerProvenance(overridePath?: string): Map<string, ServerProvenance> {
132
595
  const provenance = new Map<string, ServerProvenance>();
133
- const userPath = overridePath ? resolve(overridePath) : DEFAULT_CONFIG_PATH;
596
+ const userPath = getPiGlobalConfigPath(overridePath);
134
597
 
135
- let userConfig: McpConfig = { mcpServers: {} };
136
- if (existsSync(userPath)) {
137
- try {
138
- userConfig = validateConfig(JSON.parse(readFileSync(userPath, "utf-8")));
139
- } catch {}
140
- }
141
- for (const name of Object.keys(userConfig.mcpServers)) {
142
- provenance.set(name, { path: userPath, kind: "user" });
143
- }
144
-
145
- if (userConfig.imports?.length) {
146
- for (const importKind of userConfig.imports) {
147
- const importPath = IMPORT_PATHS[importKind];
148
- if (!importPath) continue;
149
- const fullPath = importPath.startsWith(".")
150
- ? resolve(process.cwd(), importPath)
151
- : importPath;
152
- if (!existsSync(fullPath)) continue;
153
- try {
154
- const imported = JSON.parse(readFileSync(fullPath, "utf-8"));
155
- const servers = extractServers(imported, importKind);
156
- for (const name of Object.keys(servers)) {
157
- if (!provenance.has(name)) {
158
- provenance.set(name, { path: userPath, kind: "import", importKind });
598
+ for (const source of getConfigSources(overridePath)) {
599
+ const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
600
+ if (!loaded) continue;
601
+
602
+ if (loaded.imports?.length) {
603
+ for (const importKind of loaded.imports) {
604
+ const importPath = resolveImportPath(importKind);
605
+ if (!importPath) continue;
606
+
607
+ try {
608
+ const imported = JSON.parse(readFileSync(importPath, "utf-8"));
609
+ const servers = extractServers(imported, importKind);
610
+ for (const name of Object.keys(servers)) {
611
+ if (!provenance.has(name)) {
612
+ provenance.set(name, { path: userPath, kind: "import", importKind });
613
+ }
159
614
  }
160
- }
161
- } catch {}
615
+ } catch {}
616
+ }
162
617
  }
163
- }
164
618
 
165
- const projectPath = resolve(process.cwd(), PROJECT_CONFIG_NAME);
166
- if (existsSync(projectPath) && projectPath !== userPath) {
167
- try {
168
- const projectConfig = validateConfig(JSON.parse(readFileSync(projectPath, "utf-8")));
169
- for (const name of Object.keys(projectConfig.mcpServers)) {
170
- provenance.set(name, { path: projectPath, kind: "project" });
171
- }
172
- } catch {}
619
+ for (const name of Object.keys(loaded.mcpServers)) {
620
+ provenance.set(name, {
621
+ path: source.writePath,
622
+ kind: source.kind,
623
+ importKind: source.importKind,
624
+ });
625
+ }
173
626
  }
174
627
 
175
628
  return provenance;
@@ -193,16 +646,8 @@ export function writeDirectToolsConfig(
193
646
  }
194
647
 
195
648
  for (const [filePath, entries] of byPath) {
196
- let raw: Record<string, unknown> = {};
197
- if (existsSync(filePath)) {
198
- try {
199
- raw = JSON.parse(readFileSync(filePath, "utf-8"));
200
- } catch {}
201
- }
202
- if (!raw || typeof raw !== "object") raw = {};
203
-
204
- const servers = (raw.mcpServers ?? raw["mcp-servers"] ?? {}) as Record<string, ServerEntry>;
205
- if (typeof servers !== "object" || Array.isArray(servers)) continue;
649
+ const raw = readRawConfigObject(filePath);
650
+ const servers = getServersObject(raw);
206
651
 
207
652
  for (const { name, value, prov } of entries) {
208
653
  if (prov.kind === "import") {
@@ -215,12 +660,7 @@ export function writeDirectToolsConfig(
215
660
  }
216
661
  }
217
662
 
218
- const key = raw["mcp-servers"] && !raw.mcpServers ? "mcp-servers" : "mcpServers";
219
- raw[key] = servers;
220
-
221
- mkdirSync(dirname(filePath), { recursive: true });
222
- const tmpPath = `${filePath}.${process.pid}.tmp`;
223
- writeFileSync(tmpPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
224
- renameSync(tmpPath, filePath);
663
+ setServersObject(raw, servers);
664
+ writeRawConfigObject(filePath, raw);
225
665
  }
226
666
  }