@aexol/spectral 0.2.5 → 0.2.6
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/dist/cli.js +10 -47
- package/dist/mcp/agent-dir.js +18 -0
- package/dist/mcp/app-bridge.bundle.js +67 -0
- package/dist/mcp/commands.js +263 -0
- package/dist/mcp/config.js +532 -0
- package/dist/mcp/consent-manager.js +59 -0
- package/dist/mcp/direct-tools.js +354 -0
- package/dist/mcp/errors.js +165 -0
- package/dist/mcp/glimpse-ui.js +67 -0
- package/dist/mcp/host-html-template.js +412 -0
- package/dist/mcp/index.js +291 -0
- package/dist/mcp/init.js +280 -0
- package/dist/mcp/lifecycle.js +79 -0
- package/dist/mcp/logger.js +130 -0
- package/dist/mcp/mcp-auth-flow.js +283 -0
- package/dist/mcp/mcp-auth.js +226 -0
- package/dist/mcp/mcp-callback-server.js +225 -0
- package/dist/mcp/mcp-oauth-provider.js +243 -0
- package/dist/mcp/mcp-panel.js +646 -0
- package/dist/mcp/mcp-setup-panel.js +485 -0
- package/dist/mcp/metadata-cache.js +158 -0
- package/dist/mcp/npx-resolver.js +385 -0
- package/dist/mcp/oauth-handler.js +54 -0
- package/dist/mcp/onboarding-state.js +56 -0
- package/dist/mcp/proxy-modes.js +714 -0
- package/dist/mcp/resource-tools.js +14 -0
- package/dist/mcp/sampling-handler.js +206 -0
- package/dist/mcp/server-manager.js +301 -0
- package/dist/mcp/state.js +1 -0
- package/dist/mcp/tool-metadata.js +128 -0
- package/dist/mcp/tool-registrar.js +43 -0
- package/dist/mcp/types.js +93 -0
- package/dist/mcp/ui-resource-handler.js +113 -0
- package/dist/mcp/ui-server.js +522 -0
- package/dist/mcp/ui-session.js +306 -0
- package/dist/mcp/ui-stream-types.js +58 -0
- package/dist/mcp/utils.js +104 -0
- package/dist/mcp/vitest.config.js +13 -0
- package/package.json +6 -3
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
// config.ts - Config loading with import support
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { getAgentPath } from "./agent-dir.js";
|
|
6
|
+
const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
|
|
7
|
+
const PROJECT_CONFIG_NAME = ".mcp.json";
|
|
8
|
+
const PROJECT_PI_CONFIG_NAME = ".pi/mcp.json";
|
|
9
|
+
const REPOPROMPT_BINARY_CANDIDATES = [
|
|
10
|
+
join(homedir(), "RepoPrompt", "repoprompt_cli"),
|
|
11
|
+
"/Applications/Repo Prompt.app/Contents/MacOS/repoprompt-mcp",
|
|
12
|
+
];
|
|
13
|
+
const IMPORT_PATHS = {
|
|
14
|
+
cursor: [join(homedir(), ".cursor", "mcp.json")],
|
|
15
|
+
"claude-code": [
|
|
16
|
+
join(homedir(), ".claude", "mcp.json"),
|
|
17
|
+
join(homedir(), ".claude.json"),
|
|
18
|
+
join(homedir(), ".claude", "claude_desktop_config.json"),
|
|
19
|
+
],
|
|
20
|
+
"claude-desktop": [join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json")],
|
|
21
|
+
codex: [join(homedir(), ".codex", "config.json")],
|
|
22
|
+
windsurf: [join(homedir(), ".windsurf", "mcp.json")],
|
|
23
|
+
vscode: [".vscode/mcp.json"],
|
|
24
|
+
};
|
|
25
|
+
export function getPiGlobalConfigPath(overridePath) {
|
|
26
|
+
return overridePath ? resolve(overridePath) : getAgentPath("mcp.json");
|
|
27
|
+
}
|
|
28
|
+
export function getGenericGlobalConfigPath() {
|
|
29
|
+
return GENERIC_GLOBAL_CONFIG_PATH;
|
|
30
|
+
}
|
|
31
|
+
export function getProjectConfigPath(cwd = process.cwd()) {
|
|
32
|
+
return resolve(cwd, PROJECT_CONFIG_NAME);
|
|
33
|
+
}
|
|
34
|
+
export function getProjectPiConfigPath(cwd = process.cwd()) {
|
|
35
|
+
return resolve(cwd, PROJECT_PI_CONFIG_NAME);
|
|
36
|
+
}
|
|
37
|
+
export function getConfigDiscoveryPaths(overridePath) {
|
|
38
|
+
return getConfigSources(overridePath).map((source) => ({
|
|
39
|
+
label: source.label,
|
|
40
|
+
path: source.readPath,
|
|
41
|
+
exists: existsSync(source.readPath),
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
export function findAvailableImportConfigs(cwd = process.cwd()) {
|
|
45
|
+
const discovered = [];
|
|
46
|
+
for (const importKind of Object.keys(IMPORT_PATHS)) {
|
|
47
|
+
const importPath = resolveImportPath(importKind, cwd);
|
|
48
|
+
if (importPath) {
|
|
49
|
+
discovered.push({ kind: importKind, path: importPath });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return discovered;
|
|
53
|
+
}
|
|
54
|
+
export function getMcpDiscoverySummary(overridePath, cwd = process.cwd()) {
|
|
55
|
+
const sources = getConfigSources(overridePath, cwd).map((source) => {
|
|
56
|
+
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
57
|
+
return {
|
|
58
|
+
id: source.id,
|
|
59
|
+
label: source.label,
|
|
60
|
+
path: source.readPath,
|
|
61
|
+
exists: existsSync(source.readPath),
|
|
62
|
+
scope: source.scope,
|
|
63
|
+
kind: source.shared ? "shared" : "pi",
|
|
64
|
+
serverCount: loaded ? Object.keys(loaded.mcpServers).length : 0,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
const imports = Object.keys(IMPORT_PATHS)
|
|
68
|
+
.map((kind) => {
|
|
69
|
+
const path = resolveImportPath(kind, cwd);
|
|
70
|
+
if (!path)
|
|
71
|
+
return null;
|
|
72
|
+
return {
|
|
73
|
+
kind,
|
|
74
|
+
path,
|
|
75
|
+
serverCount: getImportServerCount(kind, path),
|
|
76
|
+
};
|
|
77
|
+
})
|
|
78
|
+
.filter((value) => value !== null);
|
|
79
|
+
const totalServerCount = sources.reduce((sum, source) => sum + source.serverCount, 0);
|
|
80
|
+
const hasSharedServers = sources.some((source) => source.kind === "shared" && source.serverCount > 0);
|
|
81
|
+
const hasPiOwnedServers = sources.some((source) => source.kind === "pi" && source.serverCount > 0);
|
|
82
|
+
const hasAnyDetectedPaths = sources.some((source) => source.exists) || imports.length > 0;
|
|
83
|
+
const hasAnyConfig = totalServerCount > 0 || imports.some((entry) => entry.serverCount > 0) || hasAnyDetectedPaths;
|
|
84
|
+
const summaryWithoutRepoPrompt = {
|
|
85
|
+
sources,
|
|
86
|
+
imports,
|
|
87
|
+
hasAnyConfig,
|
|
88
|
+
hasAnyDetectedPaths,
|
|
89
|
+
hasSharedServers,
|
|
90
|
+
hasPiOwnedServers,
|
|
91
|
+
totalServerCount,
|
|
92
|
+
};
|
|
93
|
+
const fingerprint = JSON.stringify({
|
|
94
|
+
sources: sources.map((source) => [source.id, source.exists, source.serverCount]),
|
|
95
|
+
imports: imports.map((entry) => [entry.kind, entry.path, entry.serverCount]),
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
...summaryWithoutRepoPrompt,
|
|
99
|
+
fingerprint,
|
|
100
|
+
repoPrompt: detectRepoPrompt(summaryWithoutRepoPrompt, cwd),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function loadMcpConfig(overridePath) {
|
|
104
|
+
let config = { mcpServers: {} };
|
|
105
|
+
for (const source of getConfigSources(overridePath)) {
|
|
106
|
+
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
107
|
+
if (!loaded)
|
|
108
|
+
continue;
|
|
109
|
+
config = mergeConfigs(config, expandImports(loaded));
|
|
110
|
+
}
|
|
111
|
+
return config;
|
|
112
|
+
}
|
|
113
|
+
function getConfigSources(overridePath, cwd = process.cwd()) {
|
|
114
|
+
const userPath = getPiGlobalConfigPath(overridePath);
|
|
115
|
+
const projectPath = getProjectConfigPath(cwd);
|
|
116
|
+
const projectPiPath = getProjectPiConfigPath(cwd);
|
|
117
|
+
const sources = [];
|
|
118
|
+
if (GENERIC_GLOBAL_CONFIG_PATH !== userPath) {
|
|
119
|
+
sources.push({
|
|
120
|
+
id: "shared-global",
|
|
121
|
+
label: "user-global standard MCP",
|
|
122
|
+
readPath: GENERIC_GLOBAL_CONFIG_PATH,
|
|
123
|
+
writePath: userPath,
|
|
124
|
+
kind: "import",
|
|
125
|
+
importKind: "global MCP config",
|
|
126
|
+
shared: true,
|
|
127
|
+
scope: "global",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
sources.push({
|
|
131
|
+
id: "pi-global",
|
|
132
|
+
label: "Pi global override",
|
|
133
|
+
readPath: userPath,
|
|
134
|
+
writePath: userPath,
|
|
135
|
+
kind: "user",
|
|
136
|
+
shared: false,
|
|
137
|
+
scope: "global",
|
|
138
|
+
});
|
|
139
|
+
if (projectPath !== userPath) {
|
|
140
|
+
sources.push({
|
|
141
|
+
id: "shared-project",
|
|
142
|
+
label: "project standard MCP",
|
|
143
|
+
readPath: projectPath,
|
|
144
|
+
writePath: projectPath,
|
|
145
|
+
kind: "project",
|
|
146
|
+
shared: true,
|
|
147
|
+
scope: "project",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (projectPiPath !== userPath && projectPiPath !== projectPath) {
|
|
151
|
+
sources.push({
|
|
152
|
+
id: "pi-project",
|
|
153
|
+
label: "project Pi override",
|
|
154
|
+
readPath: projectPiPath,
|
|
155
|
+
writePath: projectPiPath,
|
|
156
|
+
kind: "project",
|
|
157
|
+
shared: false,
|
|
158
|
+
scope: "project",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return sources;
|
|
162
|
+
}
|
|
163
|
+
function mergeConfigs(base, next) {
|
|
164
|
+
return {
|
|
165
|
+
mcpServers: { ...base.mcpServers, ...next.mcpServers },
|
|
166
|
+
imports: mergeImports(base.imports, next.imports),
|
|
167
|
+
settings: next.settings ? { ...base.settings, ...next.settings } : base.settings,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function mergeImports(left, right) {
|
|
171
|
+
const merged = [...(left ?? []), ...(right ?? [])];
|
|
172
|
+
if (merged.length === 0)
|
|
173
|
+
return undefined;
|
|
174
|
+
return [...new Set(merged)];
|
|
175
|
+
}
|
|
176
|
+
function expandImports(config) {
|
|
177
|
+
if (!config.imports?.length)
|
|
178
|
+
return config;
|
|
179
|
+
const importedServers = {};
|
|
180
|
+
for (const importKind of config.imports) {
|
|
181
|
+
const importPath = resolveImportPath(importKind);
|
|
182
|
+
if (!importPath)
|
|
183
|
+
continue;
|
|
184
|
+
try {
|
|
185
|
+
const imported = JSON.parse(readFileSync(importPath, "utf-8"));
|
|
186
|
+
const servers = extractServers(imported, importKind);
|
|
187
|
+
for (const [name, definition] of Object.entries(servers)) {
|
|
188
|
+
if (!importedServers[name]) {
|
|
189
|
+
importedServers[name] = definition;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.warn(`Failed to import MCP config from ${importKind}:`, error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
imports: config.imports,
|
|
199
|
+
settings: config.settings,
|
|
200
|
+
mcpServers: { ...importedServers, ...config.mcpServers },
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function resolveImportPath(importKind, cwd = process.cwd()) {
|
|
204
|
+
const candidates = IMPORT_PATHS[importKind] ?? [];
|
|
205
|
+
for (const candidate of candidates) {
|
|
206
|
+
const fullPath = candidate.startsWith(".") ? resolve(cwd, candidate) : candidate;
|
|
207
|
+
if (existsSync(fullPath)) {
|
|
208
|
+
return fullPath;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function getImportServerCount(importKind, path) {
|
|
214
|
+
try {
|
|
215
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
216
|
+
return Object.keys(extractServers(raw, importKind)).length;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function readValidatedConfig(path, label) {
|
|
223
|
+
if (!existsSync(path))
|
|
224
|
+
return null;
|
|
225
|
+
try {
|
|
226
|
+
return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
console.warn(`Failed to load ${label}:`, error);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function validateConfig(raw) {
|
|
234
|
+
if (!raw || typeof raw !== "object") {
|
|
235
|
+
return { mcpServers: {} };
|
|
236
|
+
}
|
|
237
|
+
const obj = raw;
|
|
238
|
+
const servers = obj.mcpServers ?? obj["mcp-servers"] ?? {};
|
|
239
|
+
if (typeof servers !== "object" || servers === null || Array.isArray(servers)) {
|
|
240
|
+
return { mcpServers: {} };
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
mcpServers: servers,
|
|
244
|
+
imports: Array.isArray(obj.imports) ? obj.imports : undefined,
|
|
245
|
+
settings: obj.settings,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function extractServers(config, kind) {
|
|
249
|
+
if (!config || typeof config !== "object")
|
|
250
|
+
return {};
|
|
251
|
+
const obj = config;
|
|
252
|
+
let servers;
|
|
253
|
+
switch (kind) {
|
|
254
|
+
case "claude-desktop":
|
|
255
|
+
case "claude-code":
|
|
256
|
+
case "codex":
|
|
257
|
+
servers = obj.mcpServers;
|
|
258
|
+
break;
|
|
259
|
+
case "cursor":
|
|
260
|
+
case "windsurf":
|
|
261
|
+
case "vscode":
|
|
262
|
+
servers = obj.mcpServers ?? obj["mcp-servers"];
|
|
263
|
+
break;
|
|
264
|
+
default:
|
|
265
|
+
return {};
|
|
266
|
+
}
|
|
267
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
268
|
+
return {};
|
|
269
|
+
}
|
|
270
|
+
return servers;
|
|
271
|
+
}
|
|
272
|
+
function serializeRawConfig(raw) {
|
|
273
|
+
return `${JSON.stringify(raw, null, 2)}\n`;
|
|
274
|
+
}
|
|
275
|
+
function buildUnifiedDiff(beforeText, afterText) {
|
|
276
|
+
if (beforeText === afterText)
|
|
277
|
+
return "(no changes)";
|
|
278
|
+
const before = beforeText.split("\n");
|
|
279
|
+
const after = afterText.split("\n");
|
|
280
|
+
const rows = before.length;
|
|
281
|
+
const cols = after.length;
|
|
282
|
+
const lcs = Array.from({ length: rows + 1 }, () => Array(cols + 1).fill(0));
|
|
283
|
+
for (let i = rows - 1; i >= 0; i--) {
|
|
284
|
+
for (let j = cols - 1; j >= 0; j--) {
|
|
285
|
+
lcs[i][j] = before[i] === after[j]
|
|
286
|
+
? lcs[i + 1][j + 1] + 1
|
|
287
|
+
: Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const lines = ["--- before", "+++ after"];
|
|
291
|
+
let i = 0;
|
|
292
|
+
let j = 0;
|
|
293
|
+
while (i < rows || j < cols) {
|
|
294
|
+
if (i < rows && j < cols && before[i] === after[j]) {
|
|
295
|
+
lines.push(` ${before[i]}`);
|
|
296
|
+
i++;
|
|
297
|
+
j++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (j < cols && (i === rows || lcs[i][j + 1] >= lcs[i + 1][j])) {
|
|
301
|
+
lines.push(`+ ${after[j]}`);
|
|
302
|
+
j++;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (i < rows) {
|
|
306
|
+
lines.push(`- ${before[i]}`);
|
|
307
|
+
i++;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return lines.join("\n");
|
|
311
|
+
}
|
|
312
|
+
function buildConfigWritePreview(filePath, nextRaw) {
|
|
313
|
+
const existed = existsSync(filePath);
|
|
314
|
+
const beforeRaw = readRawConfigObject(filePath);
|
|
315
|
+
const beforeText = existed ? serializeRawConfig(beforeRaw) : "";
|
|
316
|
+
const afterText = serializeRawConfig(nextRaw);
|
|
317
|
+
return {
|
|
318
|
+
path: filePath,
|
|
319
|
+
existed,
|
|
320
|
+
changed: beforeText !== afterText,
|
|
321
|
+
beforeText,
|
|
322
|
+
afterText,
|
|
323
|
+
diffText: buildUnifiedDiff(beforeText, afterText),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function readRawConfigObject(filePath) {
|
|
327
|
+
if (!existsSync(filePath))
|
|
328
|
+
return {};
|
|
329
|
+
try {
|
|
330
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
331
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return {};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function writeRawConfigObject(filePath, raw) {
|
|
338
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
339
|
+
const tmpPath = `${filePath}.${process.pid}.tmp`;
|
|
340
|
+
writeFileSync(tmpPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
|
|
341
|
+
renameSync(tmpPath, filePath);
|
|
342
|
+
}
|
|
343
|
+
function getServersObject(raw) {
|
|
344
|
+
const existing = raw.mcpServers ?? raw["mcp-servers"] ?? {};
|
|
345
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
|
|
346
|
+
return {};
|
|
347
|
+
}
|
|
348
|
+
return existing;
|
|
349
|
+
}
|
|
350
|
+
function setServersObject(raw, servers) {
|
|
351
|
+
delete raw["mcp-servers"];
|
|
352
|
+
raw.mcpServers = servers;
|
|
353
|
+
}
|
|
354
|
+
function isRepoPromptServer(name, entry) {
|
|
355
|
+
const normalizedName = name.toLowerCase();
|
|
356
|
+
if (normalizedName.includes("repoprompt") || normalizedName === "rp") {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
const command = entry.command?.toLowerCase() ?? "";
|
|
360
|
+
if (command.includes("repoprompt") || command.includes("rp-mcp") || command.endsWith("repoprompt_cli")) {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
return (entry.args ?? []).some((arg) => typeof arg === "string" && arg.toLowerCase().includes("repoprompt"));
|
|
364
|
+
}
|
|
365
|
+
function findProjectRoot(cwd = process.cwd()) {
|
|
366
|
+
let current = resolve(cwd);
|
|
367
|
+
while (true) {
|
|
368
|
+
if (existsSync(join(current, ".git"))
|
|
369
|
+
|| existsSync(join(current, "package.json"))
|
|
370
|
+
|| existsSync(join(current, PROJECT_CONFIG_NAME))
|
|
371
|
+
|| existsSync(join(current, ".pi"))) {
|
|
372
|
+
return current;
|
|
373
|
+
}
|
|
374
|
+
const parent = dirname(current);
|
|
375
|
+
if (parent === current)
|
|
376
|
+
return null;
|
|
377
|
+
current = parent;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function buildRepoPromptEntry(executablePath) {
|
|
381
|
+
return {
|
|
382
|
+
command: executablePath,
|
|
383
|
+
args: [],
|
|
384
|
+
lifecycle: "lazy",
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function detectRepoPrompt(summary, cwd = process.cwd()) {
|
|
388
|
+
for (const source of summary.sources) {
|
|
389
|
+
if (source.kind !== "shared" || source.serverCount === 0)
|
|
390
|
+
continue;
|
|
391
|
+
const config = readValidatedConfig(source.path, `MCP config from ${source.path}`);
|
|
392
|
+
if (!config)
|
|
393
|
+
continue;
|
|
394
|
+
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
395
|
+
if (isRepoPromptServer(name, entry)) {
|
|
396
|
+
return { configured: true, configuredPath: source.path };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const executablePath = REPOPROMPT_BINARY_CANDIDATES.find((candidate) => existsSync(candidate));
|
|
401
|
+
if (!executablePath) {
|
|
402
|
+
return { configured: false };
|
|
403
|
+
}
|
|
404
|
+
const projectRoot = findProjectRoot(cwd);
|
|
405
|
+
const targetPath = projectRoot ? join(projectRoot, PROJECT_CONFIG_NAME) : GENERIC_GLOBAL_CONFIG_PATH;
|
|
406
|
+
return {
|
|
407
|
+
configured: false,
|
|
408
|
+
executablePath,
|
|
409
|
+
targetPath,
|
|
410
|
+
serverName: "repoprompt",
|
|
411
|
+
entry: buildRepoPromptEntry(executablePath),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
export function previewCompatibilityImports(importKinds, overridePath) {
|
|
415
|
+
const targetPath = getPiGlobalConfigPath(overridePath);
|
|
416
|
+
const raw = readRawConfigObject(targetPath);
|
|
417
|
+
const currentImports = Array.isArray(raw.imports) ? raw.imports.filter((value) => typeof value === "string") : [];
|
|
418
|
+
const merged = [...new Set([...currentImports, ...importKinds])];
|
|
419
|
+
const nextRaw = { ...raw, imports: merged };
|
|
420
|
+
setServersObject(nextRaw, getServersObject(nextRaw));
|
|
421
|
+
return buildConfigWritePreview(targetPath, nextRaw);
|
|
422
|
+
}
|
|
423
|
+
export function ensureCompatibilityImports(importKinds, overridePath) {
|
|
424
|
+
const targetPath = getPiGlobalConfigPath(overridePath);
|
|
425
|
+
const raw = readRawConfigObject(targetPath);
|
|
426
|
+
const currentImports = Array.isArray(raw.imports) ? raw.imports.filter((value) => typeof value === "string") : [];
|
|
427
|
+
const merged = [...new Set([...currentImports, ...importKinds])];
|
|
428
|
+
const added = merged.filter((kind) => !currentImports.includes(kind));
|
|
429
|
+
if (added.length === 0) {
|
|
430
|
+
return { path: targetPath, added: [] };
|
|
431
|
+
}
|
|
432
|
+
raw.imports = merged;
|
|
433
|
+
const servers = getServersObject(raw);
|
|
434
|
+
setServersObject(raw, servers);
|
|
435
|
+
writeRawConfigObject(targetPath, raw);
|
|
436
|
+
return { path: targetPath, added };
|
|
437
|
+
}
|
|
438
|
+
export function buildStarterProjectConfig() {
|
|
439
|
+
return {
|
|
440
|
+
mcpServers: {},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
export function previewStarterProjectConfig(cwd = process.cwd()) {
|
|
444
|
+
const targetPath = getProjectConfigPath(cwd);
|
|
445
|
+
const nextRaw = { mcpServers: buildStarterProjectConfig().mcpServers };
|
|
446
|
+
return buildConfigWritePreview(targetPath, nextRaw);
|
|
447
|
+
}
|
|
448
|
+
export function writeStarterProjectConfig(cwd = process.cwd()) {
|
|
449
|
+
const targetPath = getProjectConfigPath(cwd);
|
|
450
|
+
const raw = { mcpServers: buildStarterProjectConfig().mcpServers };
|
|
451
|
+
writeRawConfigObject(targetPath, raw);
|
|
452
|
+
return targetPath;
|
|
453
|
+
}
|
|
454
|
+
export function previewSharedServerEntry(filePath, serverName, entry) {
|
|
455
|
+
const raw = readRawConfigObject(filePath);
|
|
456
|
+
const nextRaw = { ...raw };
|
|
457
|
+
const servers = getServersObject(nextRaw);
|
|
458
|
+
servers[serverName] = entry;
|
|
459
|
+
setServersObject(nextRaw, servers);
|
|
460
|
+
return buildConfigWritePreview(filePath, nextRaw);
|
|
461
|
+
}
|
|
462
|
+
export function writeSharedServerEntry(filePath, serverName, entry) {
|
|
463
|
+
const raw = readRawConfigObject(filePath);
|
|
464
|
+
const servers = getServersObject(raw);
|
|
465
|
+
servers[serverName] = entry;
|
|
466
|
+
setServersObject(raw, servers);
|
|
467
|
+
writeRawConfigObject(filePath, raw);
|
|
468
|
+
return filePath;
|
|
469
|
+
}
|
|
470
|
+
export function getServerProvenance(overridePath) {
|
|
471
|
+
const provenance = new Map();
|
|
472
|
+
const userPath = getPiGlobalConfigPath(overridePath);
|
|
473
|
+
for (const source of getConfigSources(overridePath)) {
|
|
474
|
+
const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
|
|
475
|
+
if (!loaded)
|
|
476
|
+
continue;
|
|
477
|
+
if (loaded.imports?.length) {
|
|
478
|
+
for (const importKind of loaded.imports) {
|
|
479
|
+
const importPath = resolveImportPath(importKind);
|
|
480
|
+
if (!importPath)
|
|
481
|
+
continue;
|
|
482
|
+
try {
|
|
483
|
+
const imported = JSON.parse(readFileSync(importPath, "utf-8"));
|
|
484
|
+
const servers = extractServers(imported, importKind);
|
|
485
|
+
for (const name of Object.keys(servers)) {
|
|
486
|
+
if (!provenance.has(name)) {
|
|
487
|
+
provenance.set(name, { path: userPath, kind: "import", importKind });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch { }
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
for (const name of Object.keys(loaded.mcpServers)) {
|
|
495
|
+
provenance.set(name, {
|
|
496
|
+
path: source.writePath,
|
|
497
|
+
kind: source.kind,
|
|
498
|
+
importKind: source.importKind,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return provenance;
|
|
503
|
+
}
|
|
504
|
+
export function writeDirectToolsConfig(changes, provenance, fullConfig) {
|
|
505
|
+
const byPath = new Map();
|
|
506
|
+
for (const [serverName, value] of changes) {
|
|
507
|
+
const prov = provenance.get(serverName);
|
|
508
|
+
if (!prov)
|
|
509
|
+
continue;
|
|
510
|
+
const targetPath = prov.path;
|
|
511
|
+
if (!byPath.has(targetPath))
|
|
512
|
+
byPath.set(targetPath, []);
|
|
513
|
+
byPath.get(targetPath).push({ name: serverName, value, prov });
|
|
514
|
+
}
|
|
515
|
+
for (const [filePath, entries] of byPath) {
|
|
516
|
+
const raw = readRawConfigObject(filePath);
|
|
517
|
+
const servers = getServersObject(raw);
|
|
518
|
+
for (const { name, value, prov } of entries) {
|
|
519
|
+
if (prov.kind === "import") {
|
|
520
|
+
const fullDef = fullConfig.mcpServers[name];
|
|
521
|
+
if (fullDef) {
|
|
522
|
+
servers[name] = { ...fullDef, directTools: value };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else if (servers[name]) {
|
|
526
|
+
servers[name] = { ...servers[name], directTools: value };
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
setServersObject(raw, servers);
|
|
530
|
+
writeRawConfigObject(filePath, raw);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ConsentError } from "./errors.js";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
export class ConsentManager {
|
|
4
|
+
mode;
|
|
5
|
+
approvedServers = new Set();
|
|
6
|
+
deniedServers = new Set();
|
|
7
|
+
log = logger.child({ component: "ConsentManager" });
|
|
8
|
+
constructor(mode = "once-per-server") {
|
|
9
|
+
this.mode = mode;
|
|
10
|
+
this.log.debug("Initialized", { mode });
|
|
11
|
+
}
|
|
12
|
+
requiresPrompt(serverName) {
|
|
13
|
+
if (this.mode === "never")
|
|
14
|
+
return false;
|
|
15
|
+
if (this.deniedServers.has(serverName))
|
|
16
|
+
return true;
|
|
17
|
+
if (this.mode === "always")
|
|
18
|
+
return true;
|
|
19
|
+
return !this.approvedServers.has(serverName);
|
|
20
|
+
}
|
|
21
|
+
shouldCacheConsent() {
|
|
22
|
+
return this.mode !== "always";
|
|
23
|
+
}
|
|
24
|
+
registerDecision(serverName, approved) {
|
|
25
|
+
this.deniedServers.delete(serverName);
|
|
26
|
+
this.approvedServers.delete(serverName);
|
|
27
|
+
if (approved) {
|
|
28
|
+
this.approvedServers.add(serverName);
|
|
29
|
+
this.log.debug("Consent granted", { server: serverName });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.deniedServers.add(serverName);
|
|
33
|
+
this.log.debug("Consent denied", { server: serverName });
|
|
34
|
+
}
|
|
35
|
+
ensureApproved(serverName) {
|
|
36
|
+
if (this.mode === "never")
|
|
37
|
+
return;
|
|
38
|
+
if (this.deniedServers.has(serverName)) {
|
|
39
|
+
throw new ConsentError(serverName, { denied: true });
|
|
40
|
+
}
|
|
41
|
+
if (!this.approvedServers.has(serverName)) {
|
|
42
|
+
throw new ConsentError(serverName, { requiresApproval: true });
|
|
43
|
+
}
|
|
44
|
+
if (this.mode === "always") {
|
|
45
|
+
this.approvedServers.delete(serverName);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
clear(serverName) {
|
|
49
|
+
if (serverName) {
|
|
50
|
+
this.approvedServers.delete(serverName);
|
|
51
|
+
this.deniedServers.delete(serverName);
|
|
52
|
+
this.log.debug("Cleared consent for server", { server: serverName });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.approvedServers.clear();
|
|
56
|
+
this.deniedServers.clear();
|
|
57
|
+
this.log.debug("Cleared all consent records");
|
|
58
|
+
}
|
|
59
|
+
}
|