@aipermission/mcp 0.2.38 → 0.2.39

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/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
- const flags = parseFlags(argv);
83
- assertProviderSelectionAvailable(flags.provider, Boolean(input.isTTY && output.isTTY));
84
- const stdinToken = flags.tokenStdin ? (await readStdin()).trim() : "";
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?", providers);
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 token = await resolveToken({ ...flags, stdinToken }, rl);
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 config = buildMCPServerConfig({ apiUrl, token });
99
- if (provider.id === "custom" || flags.print) {
100
- printTokenConfigWarning();
101
- printCustomConfig(name, config);
102
- return;
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
- const result = {};
131
- for (let i = 0; i < argv.length; i += 1) {
132
- const arg = argv[i];
133
- if (!arg.startsWith("--")) {
134
- continue;
135
- }
136
- const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
137
- const key = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
138
- if (key === "print") {
139
- result.print = true;
140
- continue;
141
- }
142
- if (key === "force") {
143
- result.force = true;
144
- continue;
145
- }
146
- if (key === "tokenStdin") {
147
- result.tokenStdin = true;
148
- continue;
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
- const normalized = String(idOrLabel).trim().toLowerCase();
163
- const provider = providers.find((item) => item.id === normalized || item.label.toLowerCase() === normalized);
164
- if (!provider) {
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 homeRoot = os.homedir();
319
- if (providerID === "codex") {
320
- const filePath = path.join(homeRoot, ".codex", "config.toml");
321
- await writeCodexConfig(filePath, name, config, { trustedRoot: homeRoot });
322
- return { path: filePath };
323
- }
324
- if (providerID === "claude-code") {
325
- const filePath = path.join(projectRoot, ".mcp.json");
326
- await assertProjectConfigWritable(filePath, options);
327
- const protection = await protectGitIgnoredConfig(filePath);
328
- await writeJSONMCPConfig(filePath, name, config, "mcpServers", { trustedRoot: projectRoot });
329
- return { path: filePath, ...protection };
330
- }
331
- if (providerID === "cursor") {
332
- const filePath = path.join(projectRoot, ".cursor", "mcp.json");
333
- await assertProjectConfigWritable(filePath, options);
334
- const protection = await protectGitIgnoredConfig(filePath);
335
- await writeJSONMCPConfig(filePath, name, config, "mcpServers", { trustedRoot: projectRoot });
336
- return { path: filePath, ...protection };
337
- }
338
- if (providerID === "vscode") {
339
- const filePath = path.join(projectRoot, ".vscode", "mcp.json");
340
- await assertProjectConfigWritable(filePath, options);
341
- const protection = await protectGitIgnoredConfig(filePath);
342
- await writeJSONMCPConfig(filePath, name, config, "servers", { trustedRoot: projectRoot });
343
- return { path: filePath, ...protection };
344
- }
345
- if (providerID === "windsurf") {
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
- throw new Error(`Could not read JSON config at ${filePath}: ${error.message}`, { cause: error });
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 writeCodexConfig(filePath, name, config, options = {}) {
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 = removeCodexServer(current, name).trimEnd();
414
- const block = codexServerBlock(name, config);
415
- await writePrivateFile(filePath, `${next ? `${next}\n\n` : ""}${block}\n`, options);
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(process.cwd());
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 gitTrackedPath(filePath) {
499
- const repository = await discoverGitRepository(process.cwd());
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", "--no-index", "-q", "--", relativePath], {
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 removeCodexServer(source, name) {
590
- const main = `[mcp_servers.${tomlKey(name)}]`;
591
- const env = `[mcp_servers.${tomlKey(name)}.env]`;
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 trimmed = line.trim();
597
- const isHeader = trimmed.startsWith("[") && trimmed.endsWith("]");
598
- if (trimmed === main || trimmed === env) {
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 (isHeader && skipping) {
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 codexServerBlock(name, config) {
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
- command = ${tomlString(config.command)}
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 printCustomConfig(name, config) {
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
- console.log(JSON.stringify({ mcpServers: { [name]: config } }, null, 2));
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
- function printTokenConfigWarning() {
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}Warning:${color.reset} the printed config contains an AIPermission bearer token.`);
634
- console.log(`${color.yellow}Keep it private and revoke the token if it is shared or committed.${color.reset}`);
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
  }