@aipermission/mcp 0.2.38 → 0.2.40
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/README.md +58 -16
- package/dist/cli-flags.js +81 -0
- package/dist/cli.js +37 -6
- package/dist/client-registry.js +281 -0
- package/dist/doctor.js +134 -0
- package/dist/init.js +235 -161
- package/dist/install-skill.js +66 -151
- package/dist/instructions.js +1 -0
- package/dist/private-file.js +36 -0
- package/dist/server.js +8 -4
- package/package.json +4 -2
- package/server.json +2 -2
package/dist/init.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import readline from "node:readline/promises";
|
|
7
6
|
import { promisify } from "node:util";
|
|
8
7
|
import { pathToFileURL } from "node:url";
|
|
9
8
|
import { stdin as input, stdout as output } from "node:process";
|
|
9
|
+
import { parse as parseTOML } from "smol-toml";
|
|
10
|
+
import { parseCommandFlags } from "./cli-flags.js";
|
|
10
11
|
import { DEFAULT_API_URL, normalizeLocalAPIURL } from "./local-url.js";
|
|
12
|
+
import { adaptMCPServerConfig, getClient, MCP_PROVIDERS, resolveMCPConfigTarget, resolveMCPPrintTarget } from "./client-registry.js";
|
|
13
|
+
import { commitSkillInstallation, prepareSkillInstallation } from "./install-skill.js";
|
|
11
14
|
import {
|
|
12
15
|
atomicWritePrivateFile,
|
|
13
16
|
privateLockPath,
|
|
@@ -35,78 +38,61 @@ const color = {
|
|
|
35
38
|
yellow: useColor ? "\x1b[33m" : "",
|
|
36
39
|
};
|
|
37
40
|
|
|
38
|
-
const providers = [
|
|
39
|
-
{
|
|
40
|
-
id: "codex",
|
|
41
|
-
label: "OpenAI Codex",
|
|
42
|
-
description: "Writes ~/.codex/config.toml",
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: "claude-code",
|
|
46
|
-
label: "Claude Code",
|
|
47
|
-
description: "Writes .mcp.json in the current project",
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
id: "cursor",
|
|
51
|
-
label: "Cursor",
|
|
52
|
-
description: "Writes .cursor/mcp.json in the current project",
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
id: "vscode",
|
|
56
|
-
label: "VS Code",
|
|
57
|
-
description: "Writes .vscode/mcp.json in the current project",
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
id: "windsurf",
|
|
61
|
-
label: "Windsurf",
|
|
62
|
-
description: "Writes ~/.codeium/windsurf/mcp_config.json",
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
id: "antigravity",
|
|
66
|
-
label: "Google Antigravity",
|
|
67
|
-
description: "Writes ~/.gemini/antigravity/mcp_config.json",
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
id: "gemini",
|
|
71
|
-
label: "Gemini CLI",
|
|
72
|
-
description: "Writes ~/.gemini/settings.json",
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
id: "custom",
|
|
76
|
-
label: "Custom / copy-paste",
|
|
77
|
-
description: "Prints config snippets only",
|
|
78
|
-
},
|
|
79
|
-
];
|
|
80
|
-
|
|
81
41
|
export async function runInit(argv = []) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
42
|
+
return runConfiguration("init", argv);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function runSetup(argv = []) {
|
|
46
|
+
return runConfiguration("setup", argv);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function runConfiguration(command, argv) {
|
|
50
|
+
const flags = parseCommandFlags(command, argv);
|
|
51
|
+
const interactive = Boolean(input.isTTY && output.isTTY);
|
|
52
|
+
assertProviderSelectionAvailable(flags.provider, interactive);
|
|
85
53
|
const rl = readline.createInterface({ input, output });
|
|
86
54
|
try {
|
|
87
55
|
const provider = flags.provider
|
|
88
56
|
? findProvider(flags.provider)
|
|
89
|
-
: await selectProvider("Which AI client should use this token?",
|
|
90
|
-
const name = sanitizeName(flags.name || (await ask(rl, "MCP server name", "aipermission")));
|
|
57
|
+
: await selectProvider("Which AI client should use this token?", MCP_PROVIDERS);
|
|
58
|
+
const name = sanitizeName(flags.name || (interactive ? await ask(rl, "MCP server name", "aipermission") : "aipermission"));
|
|
91
59
|
const apiUrl = normalizeURL(flags.apiUrl || DEFAULT_API_URL);
|
|
92
|
-
const
|
|
60
|
+
const outputTarget =
|
|
61
|
+
provider.id === "custom"
|
|
62
|
+
? undefined
|
|
63
|
+
: flags.print
|
|
64
|
+
? resolveMCPPrintTarget(provider.id, flags.scope)
|
|
65
|
+
: resolveMCPConfigTarget(provider.id, flags.scope);
|
|
66
|
+
const shouldPrepareSkill = command === "setup" && (!flags.print || provider.id === "custom");
|
|
67
|
+
const preparedSkill = shouldPrepareSkill ? await prepareSetupSkill(provider.id, flags) : undefined;
|
|
68
|
+
if (provider.id === "custom" || flags.print) {
|
|
69
|
+
const skillResult = preparedSkill ? await reportInstalledSkill(preparedSkill) : undefined;
|
|
70
|
+
printPlaceholderConfigNotice();
|
|
71
|
+
printProviderConfig(name, provider.id, apiUrl, outputTarget);
|
|
72
|
+
if (command === "setup" && flags.print && provider.id !== "custom") {
|
|
73
|
+
console.error("No files were changed. Run install-skill separately to install the native operator skill.");
|
|
74
|
+
}
|
|
75
|
+
return { provider: provider.id, name, printed: true, skill: skillResult };
|
|
76
|
+
}
|
|
93
77
|
|
|
78
|
+
const stdinToken = flags.tokenStdin ? (await readStdin()).trim() : "";
|
|
79
|
+
const token = await resolveToken({ ...flags, stdinToken }, rl);
|
|
94
80
|
if (!token) {
|
|
95
81
|
throw new Error("API token is required.");
|
|
96
82
|
}
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const result = await writeProviderConfig(provider.id, name, config, { force: Boolean(flags.force) });
|
|
83
|
+
const config = adaptMCPServerConfig(provider.id, buildMCPServerConfig({ apiUrl, token }));
|
|
84
|
+
const skillResult = preparedSkill ? await reportInstalledSkill(preparedSkill) : undefined;
|
|
85
|
+
const result = await writeProviderConfig(provider.id, name, config, {
|
|
86
|
+
force: Boolean(flags.force),
|
|
87
|
+
scope: flags.scope,
|
|
88
|
+
homeDir: flags.home,
|
|
89
|
+
projectDir: flags.projectDir,
|
|
90
|
+
});
|
|
106
91
|
console.log("");
|
|
107
92
|
console.log(`${color.green}Configured ${provider.label}${color.reset}`);
|
|
108
93
|
console.log(`${color.dim}Name:${color.reset} ${name}`);
|
|
109
94
|
console.log(`${color.dim}Path:${color.reset} ${result.path}`);
|
|
95
|
+
console.log(`${color.dim}Scope:${color.reset} ${result.scope}`);
|
|
110
96
|
if (result.gitExcluded) {
|
|
111
97
|
console.log(`${color.dim}Git:${color.reset} added ${result.gitExcludeEntry} to .git/info/exclude`);
|
|
112
98
|
}
|
|
@@ -115,6 +101,7 @@ export async function runInit(argv = []) {
|
|
|
115
101
|
`${color.yellow}Keep this config private:${color.reset} it contains an AIPermission bearer token. If it is committed, revoke the token.`,
|
|
116
102
|
);
|
|
117
103
|
console.log(`${color.yellow}Restart the AI client so it reloads MCP servers.${color.reset}`);
|
|
104
|
+
return { provider: provider.id, name, config: result, skill: skillResult };
|
|
118
105
|
} finally {
|
|
119
106
|
rl.close();
|
|
120
107
|
}
|
|
@@ -127,44 +114,39 @@ export function assertProviderSelectionAvailable(provider, interactive) {
|
|
|
127
114
|
}
|
|
128
115
|
|
|
129
116
|
export function parseFlags(argv) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
if (key === "token") {
|
|
151
|
-
throw new Error("--token is not supported; use the hidden prompt or --token-stdin");
|
|
152
|
-
}
|
|
153
|
-
result[key] = inlineValue ?? argv[i + 1] ?? "";
|
|
154
|
-
if (inlineValue === undefined) {
|
|
155
|
-
i += 1;
|
|
156
|
-
}
|
|
117
|
+
return parseCommandFlags("init", argv);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function prepareSetupSkill(client, flags) {
|
|
121
|
+
return prepareSkillInstallation({
|
|
122
|
+
client,
|
|
123
|
+
scope: flags.skillScope || flags.scope,
|
|
124
|
+
source: flags.skillSource,
|
|
125
|
+
homeDir: flags.home,
|
|
126
|
+
projectDir: flags.projectDir,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function reportInstalledSkill(prepared) {
|
|
131
|
+
const result = await commitSkillInstallation(prepared);
|
|
132
|
+
if (!result.path) {
|
|
133
|
+
console.log("");
|
|
134
|
+
console.log(result.content);
|
|
135
|
+
return result;
|
|
157
136
|
}
|
|
137
|
+
console.log("");
|
|
138
|
+
console.log(`${color.green}Installed native operator skill${color.reset}`);
|
|
139
|
+
console.log(`${color.dim}Path:${color.reset} ${result.path}`);
|
|
140
|
+
console.log(`${color.dim}Scope:${color.reset} ${result.scope}`);
|
|
158
141
|
return result;
|
|
159
142
|
}
|
|
160
143
|
|
|
161
144
|
function findProvider(idOrLabel) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
145
|
+
try {
|
|
146
|
+
return getClient(idOrLabel);
|
|
147
|
+
} catch {
|
|
165
148
|
throw new Error(`Unknown provider: ${idOrLabel}`);
|
|
166
149
|
}
|
|
167
|
-
return provider;
|
|
168
150
|
}
|
|
169
151
|
|
|
170
152
|
async function selectProvider(title, items) {
|
|
@@ -314,62 +296,51 @@ export function buildMCPServerConfig({ apiUrl, token }) {
|
|
|
314
296
|
}
|
|
315
297
|
|
|
316
298
|
export async function writeProviderConfig(providerID, name, config, options = {}) {
|
|
317
|
-
const projectRoot = process.cwd();
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
await assertProjectConfigWritable(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const filePath = path.join(homeRoot, ".codeium", "windsurf", "mcp_config.json");
|
|
347
|
-
await writeJSONMCPConfig(filePath, name, config, "mcpServers", { trustedRoot: homeRoot });
|
|
348
|
-
return { path: filePath };
|
|
349
|
-
}
|
|
350
|
-
if (providerID === "antigravity") {
|
|
351
|
-
const filePath = path.join(homeRoot, ".gemini", "antigravity", "mcp_config.json");
|
|
352
|
-
await writeJSONMCPConfig(filePath, name, config, "mcpServers", { trustedRoot: homeRoot });
|
|
353
|
-
return { path: filePath };
|
|
354
|
-
}
|
|
355
|
-
if (providerID === "gemini") {
|
|
356
|
-
const filePath = path.join(homeRoot, ".gemini", "settings.json");
|
|
357
|
-
await writeJSONMCPConfig(filePath, name, config, "mcpServers", { trustedRoot: homeRoot });
|
|
358
|
-
return { path: filePath };
|
|
359
|
-
}
|
|
360
|
-
throw new Error(`Unsupported provider: ${providerID}`);
|
|
299
|
+
const projectRoot = options.projectDir || process.cwd();
|
|
300
|
+
const target = resolveMCPConfigTarget(providerID, options.scope, {
|
|
301
|
+
homeDir: options.homeDir,
|
|
302
|
+
projectDir: projectRoot,
|
|
303
|
+
env: options.env,
|
|
304
|
+
});
|
|
305
|
+
const providerConfig = adaptMCPServerConfig(providerID, config);
|
|
306
|
+
let protection = {};
|
|
307
|
+
if (target.projectConfig) {
|
|
308
|
+
await assertProjectConfigWritable(target.path, options);
|
|
309
|
+
protection = await protectGitIgnoredConfig(target.path, projectRoot, { allowTracked: Boolean(options.force) });
|
|
310
|
+
}
|
|
311
|
+
const trustedRoot = target.trustedRoot;
|
|
312
|
+
const writeOptions = {
|
|
313
|
+
trustedRoot,
|
|
314
|
+
beforeWrite: target.projectConfig
|
|
315
|
+
? async () => {
|
|
316
|
+
await assertProjectConfigWritable(target.path, options);
|
|
317
|
+
await options.beforeWrite?.();
|
|
318
|
+
await assertProjectConfigWritable(target.path, options);
|
|
319
|
+
}
|
|
320
|
+
: options.beforeWrite,
|
|
321
|
+
};
|
|
322
|
+
if (target.format === "toml") {
|
|
323
|
+
await writeTOMLMCPConfig(target.path, name, providerConfig, writeOptions);
|
|
324
|
+
} else {
|
|
325
|
+
await writeJSONMCPConfig(target.path, name, providerConfig, target.rootKey, writeOptions);
|
|
326
|
+
}
|
|
327
|
+
return { path: target.path, scope: target.scope, ...protection };
|
|
361
328
|
}
|
|
362
329
|
|
|
363
330
|
export async function writeJSONMCPConfig(filePath, name, config, rootKey, options = {}) {
|
|
364
331
|
await withPrivateFileLock(
|
|
365
332
|
filePath,
|
|
366
333
|
async () => {
|
|
334
|
+
await options.beforeWrite?.();
|
|
367
335
|
let root = {};
|
|
368
336
|
try {
|
|
369
337
|
root = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
370
338
|
} catch (error) {
|
|
371
339
|
if (error.code !== "ENOENT") {
|
|
372
|
-
|
|
340
|
+
redactParseError(error);
|
|
341
|
+
throw new Error(`Could not parse JSON config at ${filePath}; the existing file was left unchanged`, {
|
|
342
|
+
cause: error,
|
|
343
|
+
});
|
|
373
344
|
}
|
|
374
345
|
}
|
|
375
346
|
if (!root || typeof root !== "object" || Array.isArray(root)) root = {};
|
|
@@ -380,6 +351,7 @@ export async function writeJSONMCPConfig(filePath, name, config, rootKey, option
|
|
|
380
351
|
: Object.create(null);
|
|
381
352
|
Object.defineProperty(servers, name, { value: config, enumerable: true, configurable: true, writable: true });
|
|
382
353
|
root[rootKey] = servers;
|
|
354
|
+
await options.beforeWrite?.();
|
|
383
355
|
await writePrivateFile(filePath, `${JSON.stringify(root, null, 2)}\n`, options);
|
|
384
356
|
},
|
|
385
357
|
options,
|
|
@@ -400,19 +372,23 @@ function splitInputKeys(value) {
|
|
|
400
372
|
return keys;
|
|
401
373
|
}
|
|
402
374
|
|
|
403
|
-
async function
|
|
375
|
+
export async function writeTOMLMCPConfig(filePath, name, config, options = {}) {
|
|
404
376
|
await withPrivateFileLock(
|
|
405
377
|
filePath,
|
|
406
378
|
async () => {
|
|
379
|
+
await options.beforeWrite?.();
|
|
407
380
|
let current = "";
|
|
408
381
|
try {
|
|
409
382
|
current = await fs.readFile(filePath, "utf8");
|
|
410
383
|
} catch (error) {
|
|
411
384
|
if (error.code !== "ENOENT") throw error;
|
|
412
385
|
}
|
|
413
|
-
const next =
|
|
414
|
-
const block =
|
|
415
|
-
|
|
386
|
+
const next = removeTOMLServer(current, name).trimEnd();
|
|
387
|
+
const block = tomlServerBlock(name, config);
|
|
388
|
+
const outputContent = `${next ? `${next}\n\n` : ""}${block}\n`;
|
|
389
|
+
parseTOMLDocument(outputContent, filePath);
|
|
390
|
+
await options.beforeWrite?.();
|
|
391
|
+
await writePrivateFile(filePath, outputContent, options);
|
|
416
392
|
},
|
|
417
393
|
options,
|
|
418
394
|
);
|
|
@@ -426,7 +402,7 @@ async function assertProjectConfigWritable(filePath, options = {}) {
|
|
|
426
402
|
if (options.force) {
|
|
427
403
|
return;
|
|
428
404
|
}
|
|
429
|
-
const tracked = await gitTrackedPath(filePath);
|
|
405
|
+
const tracked = await gitTrackedPath(filePath, options.projectDir || process.cwd());
|
|
430
406
|
if (!tracked) {
|
|
431
407
|
return;
|
|
432
408
|
}
|
|
@@ -438,8 +414,8 @@ async function assertProjectConfigWritable(filePath, options = {}) {
|
|
|
438
414
|
);
|
|
439
415
|
}
|
|
440
416
|
|
|
441
|
-
async function protectGitIgnoredConfig(filePath) {
|
|
442
|
-
const repository = await discoverGitRepository(
|
|
417
|
+
async function protectGitIgnoredConfig(filePath, startDir = process.cwd(), options = {}) {
|
|
418
|
+
const repository = await discoverGitRepository(startDir);
|
|
443
419
|
if (!repository) {
|
|
444
420
|
return {};
|
|
445
421
|
}
|
|
@@ -478,7 +454,7 @@ async function protectGitIgnoredConfig(filePath) {
|
|
|
478
454
|
{ trustedRoot: path.dirname(excludePath) },
|
|
479
455
|
);
|
|
480
456
|
await assertGitIgnored(repository, [
|
|
481
|
-
relativePath,
|
|
457
|
+
...(options.allowTracked ? [] : [relativePath]),
|
|
482
458
|
privateTemporaryCheckPath(filePath, repository.workTree),
|
|
483
459
|
privateStagingCheckPath(filePath, repository.workTree),
|
|
484
460
|
lockRelativePath,
|
|
@@ -495,8 +471,24 @@ async function protectGitIgnoredConfig(filePath) {
|
|
|
495
471
|
};
|
|
496
472
|
}
|
|
497
473
|
|
|
498
|
-
async function
|
|
499
|
-
const repository = await discoverGitRepository(
|
|
474
|
+
export async function inspectProjectConfigProtection(filePath, startDir = process.cwd()) {
|
|
475
|
+
const repository = await discoverGitRepository(startDir);
|
|
476
|
+
if (!repository) return { repository: false };
|
|
477
|
+
const relativePath = path.relative(repository.workTree, filePath).split(path.sep).join("/");
|
|
478
|
+
if (relativePath.startsWith("../") || path.isAbsolute(relativePath)) return { repository: false };
|
|
479
|
+
const tracked = await gitTrackedPath(filePath, startDir);
|
|
480
|
+
if (tracked) throw new Error(`MCP config is tracked by Git: ${tracked}`);
|
|
481
|
+
await assertGitIgnored(repository, [
|
|
482
|
+
relativePath,
|
|
483
|
+
privateTemporaryCheckPath(filePath, repository.workTree),
|
|
484
|
+
privateStagingCheckPath(filePath, repository.workTree),
|
|
485
|
+
path.relative(repository.workTree, privateLockPath(filePath)).split(path.sep).join("/"),
|
|
486
|
+
]);
|
|
487
|
+
return { repository: true, relativePath };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function gitTrackedPath(filePath, startDir = process.cwd()) {
|
|
491
|
+
const repository = await discoverGitRepository(startDir);
|
|
500
492
|
if (!repository) {
|
|
501
493
|
return "";
|
|
502
494
|
}
|
|
@@ -542,7 +534,7 @@ async function discoverGitRepository(startDir) {
|
|
|
542
534
|
async function assertGitIgnored(repository, relativePaths) {
|
|
543
535
|
for (const relativePath of relativePaths) {
|
|
544
536
|
try {
|
|
545
|
-
await execFileAsync("git", ["-C", repository.workTree, "check-ignore", "
|
|
537
|
+
await execFileAsync("git", ["-C", repository.workTree, "check-ignore", "-q", "--", relativePath], {
|
|
546
538
|
windowsHide: true,
|
|
547
539
|
});
|
|
548
540
|
} catch (error) {
|
|
@@ -586,20 +578,27 @@ function gitErrorMessage(error) {
|
|
|
586
578
|
return String(error.stderr || error.message || error).trim();
|
|
587
579
|
}
|
|
588
580
|
|
|
589
|
-
function
|
|
590
|
-
|
|
591
|
-
|
|
581
|
+
function redactParseError(error, format = "JSON") {
|
|
582
|
+
error.message = `${format} parsing failed`;
|
|
583
|
+
error.stack = `${error.name || "Error"}: ${error.message}`;
|
|
584
|
+
return error;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function removeTOMLServer(source, name) {
|
|
588
|
+
if (!source.trim()) return source;
|
|
589
|
+
parseTOMLDocument(source, "existing TOML config");
|
|
592
590
|
const lines = source.split(/\r?\n/);
|
|
593
591
|
const kept = [];
|
|
594
592
|
let skipping = false;
|
|
593
|
+
const scanState = { multiline: "" };
|
|
595
594
|
for (const line of lines) {
|
|
596
|
-
const
|
|
597
|
-
const
|
|
598
|
-
if (
|
|
595
|
+
const header = scanTOMLHeader(line, scanState);
|
|
596
|
+
const selected = header?.[0] === "mcp_servers" && header?.[1] === name;
|
|
597
|
+
if (selected) {
|
|
599
598
|
skipping = true;
|
|
600
599
|
continue;
|
|
601
600
|
}
|
|
602
|
-
if (
|
|
601
|
+
if (header && skipping) {
|
|
603
602
|
skipping = false;
|
|
604
603
|
}
|
|
605
604
|
if (!skipping) {
|
|
@@ -609,10 +608,57 @@ function removeCodexServer(source, name) {
|
|
|
609
608
|
return kept.join("\n");
|
|
610
609
|
}
|
|
611
610
|
|
|
612
|
-
function
|
|
611
|
+
function scanTOMLHeader(line, state) {
|
|
612
|
+
if (state.multiline) {
|
|
613
|
+
if (hasMultilineDelimiter(line, state.multiline)) state.multiline = "";
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
const trimmed = line.trimStart();
|
|
617
|
+
if (trimmed.startsWith("[") && !trimmed.startsWith("[[")) {
|
|
618
|
+
try {
|
|
619
|
+
return findMarkerPath(parseTOML(`${line}\n__aipermission_header_marker = true\n`));
|
|
620
|
+
} catch {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
for (const delimiter of ['"""', "'''"]) {
|
|
625
|
+
if (!hasMultilineDelimiter(line, delimiter)) continue;
|
|
626
|
+
if ((line.split(delimiter).length - 1) % 2 === 1) state.multiline = delimiter;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function hasMultilineDelimiter(line, delimiter) {
|
|
633
|
+
const comment = line.indexOf("#");
|
|
634
|
+
return (comment < 0 ? line : line.slice(0, comment)).includes(delimiter);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function findMarkerPath(value, pathParts = []) {
|
|
638
|
+
if (!value || typeof value !== "object") return null;
|
|
639
|
+
if (value.__aipermission_header_marker === true) return pathParts;
|
|
640
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
641
|
+
const match = findMarkerPath(nested, [...pathParts, key]);
|
|
642
|
+
if (match) return match;
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function parseTOMLDocument(contents, location) {
|
|
648
|
+
try {
|
|
649
|
+
return parseTOML(contents);
|
|
650
|
+
} catch (error) {
|
|
651
|
+
redactParseError(error, "TOML");
|
|
652
|
+
throw new Error(`Could not parse TOML config at ${location}; the existing file was left unchanged`, { cause: error });
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function tomlServerBlock(name, config) {
|
|
657
|
+
const fields = Object.entries(config)
|
|
658
|
+
.filter(([key]) => key !== "env")
|
|
659
|
+
.map(([key, value]) => `${key} = ${tomlValue(value)}`);
|
|
613
660
|
return `[mcp_servers.${tomlKey(name)}]
|
|
614
|
-
|
|
615
|
-
args = [${config.args.map(tomlString).join(", ")}]
|
|
661
|
+
${fields.join("\n")}
|
|
616
662
|
enabled = true
|
|
617
663
|
|
|
618
664
|
[mcp_servers.${tomlKey(name)}.env]
|
|
@@ -621,17 +667,38 @@ AIPERMISSION_API_URL = ${tomlString(config.env.AIPERMISSION_API_URL)}
|
|
|
621
667
|
AIPERMISSION_API_TOKEN = ${tomlString(config.env.AIPERMISSION_API_TOKEN)}`;
|
|
622
668
|
}
|
|
623
669
|
|
|
624
|
-
function
|
|
670
|
+
function printProviderConfig(name, provider, apiUrl, target) {
|
|
671
|
+
const baseConfig = buildMCPServerConfig({ apiUrl, token: "YOUR_TOKEN_HERE" });
|
|
672
|
+
const previewConfig = provider === "custom" ? baseConfig : adaptMCPServerConfig(provider, baseConfig);
|
|
625
673
|
console.log("");
|
|
626
674
|
console.log(`${color.bold}${color.cyan}Copy-paste config:${color.reset}`);
|
|
627
675
|
console.log("");
|
|
628
|
-
|
|
676
|
+
if (target?.format === "toml") {
|
|
677
|
+
console.log(tomlPreviewServerBlock(name, previewConfig));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
console.log(JSON.stringify({ [target?.rootKey || "mcpServers"]: { [name]: previewConfig } }, null, 2));
|
|
629
681
|
}
|
|
630
682
|
|
|
631
|
-
|
|
683
|
+
// Keep stdout rendering separate from the secret-bearing TOML writer.
|
|
684
|
+
function tomlPreviewServerBlock(name, config) {
|
|
685
|
+
const fields = Object.entries(config)
|
|
686
|
+
.filter(([key]) => key !== "env")
|
|
687
|
+
.map(([key, value]) => `${key} = ${tomlValue(value)}`);
|
|
688
|
+
return `[mcp_servers.${tomlKey(name)}]
|
|
689
|
+
${fields.join("\n")}
|
|
690
|
+
enabled = true
|
|
691
|
+
|
|
692
|
+
[mcp_servers.${tomlKey(name)}.env]
|
|
693
|
+
NODE_ENV = "production"
|
|
694
|
+
AIPERMISSION_API_URL = ${tomlString(config.env.AIPERMISSION_API_URL)}
|
|
695
|
+
AIPERMISSION_API_TOKEN = "YOUR_TOKEN_HERE"`;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function printPlaceholderConfigNotice() {
|
|
632
699
|
console.log("");
|
|
633
|
-
console.log(`${color.yellow}
|
|
634
|
-
console.log(`${color.yellow}
|
|
700
|
+
console.log(`${color.yellow}Preview:${color.reset} the printed config uses YOUR_TOKEN_HERE and contains no bearer token.`);
|
|
701
|
+
console.log(`${color.yellow}Replace the placeholder through the client's private environment or config mechanism.${color.reset}`);
|
|
635
702
|
}
|
|
636
703
|
|
|
637
704
|
export function sanitizeName(value) {
|
|
@@ -663,6 +730,13 @@ export function tomlString(value) {
|
|
|
663
730
|
return JSON.stringify(String(value));
|
|
664
731
|
}
|
|
665
732
|
|
|
733
|
+
function tomlValue(value) {
|
|
734
|
+
if (typeof value === "string") return tomlString(value);
|
|
735
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
736
|
+
if (Array.isArray(value)) return `[${value.map(tomlValue).join(", ")}]`;
|
|
737
|
+
throw new Error(`Unsupported TOML MCP config value: ${typeof value}`);
|
|
738
|
+
}
|
|
739
|
+
|
|
666
740
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
667
741
|
await runInit(process.argv.slice(2));
|
|
668
742
|
}
|