@iterable/mcp 1.2.1 → 1.3.0

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.
Files changed (40) hide show
  1. package/README.md +28 -3
  2. package/dist/config.d.ts +4 -4
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +2 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/install.d.ts +0 -6
  7. package/dist/install.d.ts.map +1 -1
  8. package/dist/install.js +121 -346
  9. package/dist/install.js.map +1 -1
  10. package/dist/key-manager.d.ts +37 -3
  11. package/dist/key-manager.d.ts.map +1 -1
  12. package/dist/key-manager.js +109 -53
  13. package/dist/key-manager.js.map +1 -1
  14. package/dist/keys-cli.d.ts +38 -0
  15. package/dist/keys-cli.d.ts.map +1 -1
  16. package/dist/keys-cli.js +454 -373
  17. package/dist/keys-cli.js.map +1 -1
  18. package/dist/server.js +1 -1
  19. package/dist/server.js.map +1 -1
  20. package/dist/tools/snippets.d.ts.map +1 -1
  21. package/dist/tools/snippets.js +23 -12
  22. package/dist/tools/snippets.js.map +1 -1
  23. package/dist/tools/templates.js +2 -2
  24. package/dist/tools/templates.js.map +1 -1
  25. package/dist/utils/cli-env.d.ts.map +1 -1
  26. package/dist/utils/cli-env.js +2 -1
  27. package/dist/utils/cli-env.js.map +1 -1
  28. package/dist/utils/command-info.d.ts +11 -0
  29. package/dist/utils/command-info.d.ts.map +1 -0
  30. package/dist/utils/command-info.js +25 -0
  31. package/dist/utils/command-info.js.map +1 -0
  32. package/dist/utils/endpoint-prompt.d.ts +1 -0
  33. package/dist/utils/endpoint-prompt.d.ts.map +1 -1
  34. package/dist/utils/endpoint-prompt.js +20 -4
  35. package/dist/utils/endpoint-prompt.js.map +1 -1
  36. package/dist/utils/tool-config.d.ts +1 -0
  37. package/dist/utils/tool-config.d.ts.map +1 -1
  38. package/dist/utils/tool-config.js +3 -0
  39. package/dist/utils/tool-config.js.map +1 -1
  40. package/package.json +4 -4
package/dist/install.js CHANGED
@@ -7,12 +7,11 @@ import path from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import { promisify } from "util";
9
9
  import { getKeyManager } from "./key-manager.js";
10
+ import { displayKeyDetails, saveKeyInteractive } from "./keys-cli.js";
11
+ import { COMMAND_NAME, KEYS_COMMAND_TABLE, NPX_PACKAGE_NAME, } from "./utils/command-info.js";
10
12
  import { getKeyStorageMessage } from "./utils/formatting.js";
11
13
  const { dirname, join } = path;
12
14
  // IMPORTANT: UI imports are loaded lazily inside functions to avoid ESM issues in Jest/CommonJS.
13
- // Executable/package names
14
- const LOCAL_BINARY_NAME = "iterable-mcp";
15
- const NPX_PACKAGE_NAME = "@iterable/mcp";
16
15
  // Get package version
17
16
  const packageJson = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
18
17
  const TOOL_NAMES = {
@@ -20,6 +19,7 @@ const TOOL_NAMES = {
20
19
  "claude-desktop": "Claude Desktop",
21
20
  "claude-code": "Claude Code",
22
21
  "gemini-cli": "Gemini CLI",
22
+ windsurf: "Windsurf",
23
23
  manual: "Manual Setup",
24
24
  };
25
25
  const TOOL_CONFIGS = {
@@ -34,6 +34,7 @@ const TOOL_CONFIGS = {
34
34
  }
35
35
  })(),
36
36
  cursor: path.join(os.homedir(), ".cursor", "mcp.json"),
37
+ windsurf: path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json"),
37
38
  "gemini-cli": path.join(os.homedir(), ".gemini", "settings.json"),
38
39
  };
39
40
  const execFileAsync = promisify(execFile);
@@ -83,24 +84,6 @@ export const buildMcpConfig = (options) => {
83
84
  env: options.env,
84
85
  };
85
86
  };
86
- /**
87
- * Merge selected env flags with key metadata env (if provided), preferring the key's
88
- * persisted values. This ensures the written MCP configs reflect the authoritative
89
- * values saved to the key after any updates in the setup flow.
90
- */
91
- export function resolveFinalMcpEnv(selectedEnv, keyEnv) {
92
- const normalize = (v) => (v === "true" ? "true" : "false");
93
- const result = { ...selectedEnv };
94
- if (keyEnv) {
95
- result.ITERABLE_USER_PII = normalize(keyEnv.ITERABLE_USER_PII ?? result.ITERABLE_USER_PII);
96
- result.ITERABLE_ENABLE_WRITES = normalize(keyEnv.ITERABLE_ENABLE_WRITES ?? result.ITERABLE_ENABLE_WRITES);
97
- result.ITERABLE_ENABLE_SENDS = normalize(keyEnv.ITERABLE_ENABLE_SENDS ?? result.ITERABLE_ENABLE_SENDS);
98
- }
99
- result.ITERABLE_USER_PII = normalize(result.ITERABLE_USER_PII);
100
- result.ITERABLE_ENABLE_WRITES = normalize(result.ITERABLE_ENABLE_WRITES);
101
- result.ITERABLE_ENABLE_SENDS = normalize(result.ITERABLE_ENABLE_SENDS);
102
- return result;
103
- }
104
87
  /**
105
88
  * Pick only permission-related env flags for persistence into key metadata.
106
89
  */
@@ -146,16 +129,13 @@ export const setupMcpServer = async () => {
146
129
  const advanced = args.includes("--advanced");
147
130
  const autoUpdate = args.includes("--auto-update");
148
131
  let tools = [
149
- ...(args.includes("--claude-desktop") ? ["claude-desktop"] : []),
150
132
  ...(args.includes("--cursor") ? ["cursor"] : []),
133
+ ...(args.includes("--claude-desktop") ? ["claude-desktop"] : []),
151
134
  ...(args.includes("--claude-code") ? ["claude-code"] : []),
152
135
  ...(args.includes("--gemini-cli") ? ["gemini-cli"] : []),
136
+ ...(args.includes("--windsurf") ? ["windsurf"] : []),
153
137
  ...(args.includes("--manual") ? ["manual"] : []),
154
138
  ];
155
- // Detect how the command was invoked
156
- const isNpx = process.argv[1]?.includes("npx") ||
157
- process.env.npm_execpath?.includes("npx");
158
- const commandName = isNpx ? `npx ${NPX_PACKAGE_NAME}` : LOCAL_BINARY_NAME;
159
139
  if (showHelp) {
160
140
  const { createTable, icons, showBox, showIterableLogo, showSection } = await import("./utils/ui.js");
161
141
  const chalk = (await import("chalk")).default;
@@ -168,11 +148,14 @@ export const setupMcpServer = async () => {
168
148
  colWidths: [45, 40],
169
149
  style: "normal",
170
150
  });
171
- setupTable.push([`${commandName} setup --claude-desktop`, "Configure for Claude Desktop"], [`${commandName} setup --cursor`, "Configure for Cursor"], [`${commandName} setup --claude-code`, "Configure for Claude Code"], [`${commandName} setup --gemini-cli`, "Configure for Gemini CLI"], [`${commandName} setup --manual`, "Show manual config instructions"], [
172
- `${commandName} setup --cursor --claude-desktop`,
151
+ setupTable.push([`${COMMAND_NAME} setup --cursor`, "Configure for Cursor"], [
152
+ `${COMMAND_NAME} setup --claude-desktop`,
153
+ "Configure for Claude Desktop",
154
+ ], [`${COMMAND_NAME} setup --claude-code`, "Configure for Claude Code"], [`${COMMAND_NAME} setup --gemini-cli`, "Configure for Gemini CLI"], [`${COMMAND_NAME} setup --windsurf`, "Configure for Windsurf"], [`${COMMAND_NAME} setup --manual`, "Show manual config instructions"], [
155
+ `${COMMAND_NAME} setup --cursor --claude-desktop`,
173
156
  "Configure multiple tools",
174
- ], [`${commandName} setup --cursor --debug`, "Enable debug logging"], [
175
- `${commandName} setup --cursor --auto-update`,
157
+ ], [`${COMMAND_NAME} setup --cursor --debug`, "Enable debug logging"], [
158
+ `${COMMAND_NAME} setup --cursor --auto-update`,
176
159
  "Always use latest version",
177
160
  ]);
178
161
  console.log(setupTable.toString());
@@ -185,7 +168,7 @@ export const setupMcpServer = async () => {
185
168
  colWidths: [45, 40],
186
169
  style: "normal",
187
170
  });
188
- keysTable.push([`${commandName} keys list`, "View all stored API keys"], [`${commandName} keys add`, "Add a new API key"], [`${commandName} keys activate <name>`, "Switch to a different key"], [`${commandName} keys delete <id>`, "Remove a key by ID"]);
171
+ keysTable.push(...KEYS_COMMAND_TABLE);
189
172
  console.log(keysTable.toString());
190
173
  console.log();
191
174
  console.log();
@@ -213,7 +196,7 @@ export const setupMcpServer = async () => {
213
196
  console.log();
214
197
  showBox("Quick Start", [
215
198
  chalk.white.bold("1. Run setup for your AI tool:"),
216
- chalk.cyan(` ${commandName} setup --cursor`),
199
+ chalk.cyan(` ${COMMAND_NAME} setup --cursor`),
217
200
  "",
218
201
  chalk.white.bold("2. Restart your AI tool"),
219
202
  "",
@@ -227,38 +210,11 @@ export const setupMcpServer = async () => {
227
210
  ], { icon: icons.lock, theme: "info", padding: 1 });
228
211
  return;
229
212
  }
230
- // New behavior: if no tools were provided as flags, prompt user to select one or more
231
- if (tools.length === 0) {
232
- // Show logo first when no flags provided
233
- const { showIterableLogo } = await import("./utils/ui.js");
234
- console.clear();
235
- showIterableLogo(packageJson.version);
236
- const { selectedTools } = await inquirer.prompt([
237
- {
238
- type: "checkbox",
239
- name: "selectedTools",
240
- message: "Select your AI tools to configure:",
241
- choices: [
242
- { name: "Cursor", value: "cursor" },
243
- { name: "Claude Desktop", value: "claude-desktop" },
244
- { name: "Claude Code (CLI)", value: "claude-code" },
245
- { name: "Gemini CLI", value: "gemini-cli" },
246
- { name: "Other / Manual Setup", value: "manual" },
247
- ],
248
- validate: (arr) => Array.isArray(arr) && arr.length > 0
249
- ? true
250
- : "Please select at least one tool",
251
- },
252
- ]);
253
- tools = selectedTools;
254
- }
255
- // Start the setup flow
213
+ // Start the setup flow - show logo and load UI
256
214
  console.clear();
257
- const { formatKeyValue, icons, showCompletion, showError, showInfo, showIterableLogo, showSection, showSuccess, showWarning, linkColor, valueColor, } = await import("./utils/ui.js");
215
+ const { formatKeyValue, icons, showCompletion, showError, showInfo, showIterableLogo, showSection, showSuccess, showWarning, linkColor, } = await import("./utils/ui.js");
258
216
  const chalk = (await import("chalk")).default;
259
217
  showIterableLogo(packageJson.version);
260
- // Succinct overview of what will be configured
261
- console.log(chalk.gray(`Running setup to configure: ${tools.map((t) => TOOL_NAMES[t]).join(", ")}`));
262
218
  console.log();
263
219
  const ora = (await import("ora")).default;
264
220
  const spinner = ora();
@@ -268,9 +224,6 @@ export const setupMcpServer = async () => {
268
224
  showSection("API Key Configuration", icons.key);
269
225
  console.log();
270
226
  let apiKey;
271
- let keyName;
272
- let baseUrl;
273
- let usedKeyName;
274
227
  let usedExistingKey = false;
275
228
  let selectedExistingMeta;
276
229
  // 1) Offer using API key from environment
@@ -303,6 +256,8 @@ export const setupMcpServer = async () => {
303
256
  ]);
304
257
  if (useExisting) {
305
258
  const { formatKeychainChoiceLabel } = await import("./utils/ui.js");
259
+ // Find the active key to use as default
260
+ const activeKey = keys.find((k) => k.isActive);
306
261
  const { chosenId } = await inquirer.prompt([
307
262
  {
308
263
  type: "list",
@@ -313,6 +268,7 @@ export const setupMcpServer = async () => {
313
268
  value: k.id,
314
269
  short: k.name,
315
270
  })),
271
+ default: activeKey?.id,
316
272
  pageSize: Math.min(10, keys.length),
317
273
  },
318
274
  ]);
@@ -320,11 +276,8 @@ export const setupMcpServer = async () => {
320
276
  const meta = keys.find((k) => k.id === chosenId);
321
277
  if (!meta)
322
278
  throw new Error("Selected key metadata not found");
323
- baseUrl = meta.baseUrl;
324
- usedKeyName = meta.name;
325
279
  usedExistingKey = true;
326
280
  selectedExistingMeta = meta;
327
- showSuccess(`Using existing key "${meta.name}"`);
328
281
  }
329
282
  catch (e) {
330
283
  showError(e instanceof Error
@@ -333,275 +286,128 @@ export const setupMcpServer = async () => {
333
286
  process.exit(1);
334
287
  }
335
288
  }
289
+ else {
290
+ console.log();
291
+ }
336
292
  }
337
293
  }
338
- // 3) If neither env nor existing key chosen, prompt for a new key
339
294
  if (!apiKey && !usedExistingKey) {
340
- const { newApiKey } = await inquirer.prompt([
341
- {
342
- type: "password",
343
- name: "newApiKey",
344
- message: "Enter your Iterable API key:",
345
- validate: (input) => {
346
- if (!input)
347
- return "API key is required";
348
- if (!/^[a-f0-9]{32}$/.test(input))
349
- return "API key must be a 32-character lowercase hexadecimal string";
350
- return true;
351
- },
352
- mask: "*",
353
- },
354
- ]);
355
- apiKey = newApiKey;
356
- }
357
- // Step 2: API Endpoint Selection
358
- console.log();
359
- showSection("API Endpoint Configuration", icons.globe);
360
- console.log();
361
- if (!baseUrl) {
362
- if (process.env.ITERABLE_BASE_URL) {
363
- baseUrl = process.env.ITERABLE_BASE_URL;
364
- showSuccess(`Using API endpoint from environment: ${baseUrl}`);
295
+ const keyManager = getKeyManager();
296
+ await keyManager.initialize();
297
+ const { getSpinner } = await import("./utils/cli-env.js");
298
+ const keySpinner = await getSpinner();
299
+ try {
300
+ const newKeyId = await saveKeyInteractive(keyManager, null, {
301
+ chalk,
302
+ icons,
303
+ showError,
304
+ showSuccess,
305
+ showInfo,
306
+ formatKeyValue,
307
+ linkColor,
308
+ showBox: (await import("./utils/ui.js")).showBox,
309
+ }, keySpinner, {
310
+ skipRestartNotice: true,
311
+ advanced,
312
+ autoActivate: true,
313
+ });
314
+ selectedExistingMeta =
315
+ (await keyManager.getKeyMetadata(newKeyId)) ?? undefined;
365
316
  }
366
- else {
367
- const { promptForIterableBaseUrl } = await import("./utils/endpoint-prompt.js");
368
- try {
369
- baseUrl = await promptForIterableBaseUrl({
370
- inquirer,
371
- icons,
372
- chalk,
373
- showError,
374
- });
375
- }
376
- catch {
377
- process.exit(1);
378
- }
317
+ catch (error) {
318
+ showError(error instanceof Error ? error.message : "Failed to add key");
319
+ process.exit(1);
379
320
  }
380
321
  }
381
- else {
382
- showSuccess(`Using API endpoint from selected key: ${baseUrl}`);
383
- }
384
- console.log();
385
- console.log(formatKeyValue("Selected endpoint", baseUrl, linkColor()));
386
- console.log();
387
- // Step 3: Store the key securely
388
- let existingKeyWithValue = null;
389
- const keyManager = getKeyManager();
390
- spinner.start("Initializing secure key storage...");
391
- await keyManager.initialize();
392
- spinner.succeed("Key storage ready");
393
- // Check if key already exists
394
- if (!usedExistingKey && apiKey) {
395
- spinner.start("Checking for existing keys...");
396
- existingKeyWithValue = await keyManager.findKeyByValue(apiKey);
397
- spinner.stop();
398
- }
399
- if (usedExistingKey) {
400
- // Offer to activate the selected existing key if not already active
322
+ else if (usedExistingKey) {
323
+ const keyManager = getKeyManager();
324
+ await keyManager.initialize();
325
+ console.log();
401
326
  if (!selectedExistingMeta?.isActive) {
402
- const { activateExisting } = await inquirer.prompt([
403
- {
404
- type: "confirm",
405
- name: "activateExisting",
406
- message: `Set "${selectedExistingMeta?.name}" as your active API key?`,
407
- default: true,
408
- },
409
- ]);
410
- if (activateExisting) {
411
- await keyManager.setActiveKey(selectedExistingMeta.id);
412
- showSuccess(`"${selectedExistingMeta?.name}" is now your active key`);
413
- }
414
- else {
415
- showInfo("Keeping your current active key");
416
- }
327
+ const { getSpinner } = await import("./utils/cli-env.js");
328
+ const activateSpinner = await getSpinner();
329
+ activateSpinner.start("Activating key...");
330
+ await keyManager.setActiveKey(selectedExistingMeta.id);
331
+ activateSpinner.succeed("Key activated successfully");
332
+ showSuccess(`"${selectedExistingMeta?.name}" is now your active API key`);
417
333
  }
418
334
  else {
419
335
  showSuccess(`"${selectedExistingMeta?.name}" is already your active key`);
420
336
  }
421
- }
422
- else if (existingKeyWithValue) {
423
- showInfo(`This API key is already stored as "${existingKeyWithValue.name}"`);
424
- usedKeyName = existingKeyWithValue.name;
425
- if (!existingKeyWithValue.isActive) {
426
- const { activateExisting } = await inquirer.prompt([
427
- {
428
- type: "confirm",
429
- name: "activateExisting",
430
- message: `Set "${existingKeyWithValue.name}" as your active API key?`,
431
- default: true,
432
- },
433
- ]);
434
- if (activateExisting) {
435
- await keyManager.setActiveKey(existingKeyWithValue.id);
436
- showSuccess(`"${existingKeyWithValue.name}" is now your active key`);
437
- }
438
- else {
439
- showInfo("Keeping your current active key");
440
- }
441
- }
442
- else {
443
- showSuccess(`"${existingKeyWithValue.name}" is already your active key`);
444
- }
337
+ console.log();
338
+ displayKeyDetails(selectedExistingMeta, {
339
+ chalk,
340
+ formatKeyValue,
341
+ linkColor,
342
+ });
343
+ console.log();
344
+ showInfo(`To modify permissions, run: ${chalk.cyan(`${COMMAND_NAME} keys update "${selectedExistingMeta?.name}"`)}`);
445
345
  }
446
346
  else {
447
- // New key - prompt for name
448
- const { newKeyName } = await inquirer.prompt([
449
- {
450
- type: "input",
451
- name: "newKeyName",
452
- message: "Enter a name for this API key:",
453
- default: "default",
454
- validate: (input) => {
455
- if (input && input.length > 50)
456
- return "Key name must be 50 characters or less";
457
- return true;
458
- },
459
- },
460
- ]);
461
- keyName = newKeyName || "default";
462
- spinner.start("Storing API key securely...");
347
+ const keyManager = getKeyManager();
348
+ await keyManager.initialize();
349
+ const { getSpinner } = await import("./utils/cli-env.js");
350
+ const keySpinner = await getSpinner();
463
351
  try {
464
- const newKeyId = await keyManager.addKey(keyName, apiKey, baseUrl);
465
- spinner.succeed("API key stored successfully!");
466
- console.log();
467
- console.log(formatKeyValue("Name", keyName, chalk.white.bold));
468
- console.log(formatKeyValue("ID", newKeyId, chalk.gray));
469
- console.log(formatKeyValue("Endpoint", baseUrl, chalk.cyan));
470
- console.log();
471
- usedKeyName = keyName;
472
- // Check if we should activate this key
473
- const allKeys = await keyManager.listKeys();
474
- if (allKeys.length > 1) {
475
- const { activateNew } = await inquirer.prompt([
476
- {
477
- type: "confirm",
478
- name: "activateNew",
479
- message: `Set "${keyName}" as your active API key?`,
480
- default: true,
481
- },
482
- ]);
483
- if (activateNew) {
484
- await keyManager.setActiveKey(newKeyId);
485
- showSuccess(`"${keyName}" is now your active key`);
486
- }
487
- else {
488
- showInfo(`Keeping your current active key. Run 'iterable-mcp keys activate "${keyName}"' to switch later.`);
489
- }
490
- }
352
+ const newKeyId = await saveKeyInteractive(keyManager, null, {
353
+ chalk,
354
+ icons,
355
+ showError,
356
+ showSuccess,
357
+ showInfo,
358
+ formatKeyValue,
359
+ linkColor,
360
+ showBox: (await import("./utils/ui.js")).showBox,
361
+ }, keySpinner, {
362
+ skipRestartNotice: true,
363
+ advanced,
364
+ prefilledApiKey: apiKey,
365
+ autoActivate: true,
366
+ });
367
+ selectedExistingMeta =
368
+ (await keyManager.getKeyMetadata(newKeyId)) ?? undefined;
491
369
  }
492
370
  catch (error) {
493
- const { sanitizeString } = await import("./utils/sanitize.js");
494
- spinner.fail("Failed to store API key");
495
- console.log();
496
- const msg = error instanceof Error
497
- ? sanitizeString(error.message)
498
- : "Unknown error";
499
- showError(msg);
500
- console.log();
501
- showInfo("Your API key was not stored. You can re-run 'iterable-mcp setup' to try again.");
502
- showInfo("If the problem persists, verify storage access and disk permissions.");
371
+ showError(error instanceof Error ? error.message : "Failed to add key");
503
372
  process.exit(1);
504
373
  }
505
374
  }
506
- // Step 4: Privacy & Security Settings (Basic by default; Advanced via --advanced)
375
+ if (!selectedExistingMeta) {
376
+ showError("Failed to get key metadata after setup");
377
+ process.exit(1);
378
+ }
379
+ let mcpEnv = pickPersistablePermissionEnv(selectedExistingMeta.env || {});
380
+ if (args.includes("--debug")) {
381
+ mcpEnv.ITERABLE_DEBUG = "true";
382
+ mcpEnv.LOG_LEVEL = "debug";
383
+ }
384
+ mcpEnv = enforceSendsRequiresWrites(mcpEnv, (msg) => showWarning(msg));
385
+ // Step 5: Configure AI Tools
507
386
  console.log();
508
- showSection("Privacy & Security Settings", icons.lock);
387
+ showSection("Configure AI Tools", icons.zap);
509
388
  console.log();
510
- // Default conservative env flags
511
- let selectedEnv = {
512
- ITERABLE_USER_PII: "false",
513
- ITERABLE_ENABLE_WRITES: "false",
514
- ITERABLE_ENABLE_SENDS: "false",
515
- };
516
- if (advanced) {
517
- const { permFlags } = await inquirer.prompt([
389
+ if (tools.length === 0) {
390
+ const { selectedTools } = await inquirer.prompt([
518
391
  {
519
392
  type: "checkbox",
520
- name: "permFlags",
521
- message: "Select permissions to enable (default: none):",
393
+ name: "selectedTools",
394
+ message: "Select your AI tools to configure:",
522
395
  choices: [
523
- { name: "View PII (access user data)", value: "pii" },
524
- {
525
- name: "Write operations (create/update/delete)",
526
- value: "writes",
527
- },
528
- { name: "Sends (campaigns/journeys/events)", value: "sends" },
396
+ { name: "Cursor", value: "cursor" },
397
+ { name: "Claude Desktop", value: "claude-desktop" },
398
+ { name: "Claude Code (CLI)", value: "claude-code" },
399
+ { name: "Gemini CLI", value: "gemini-cli" },
400
+ { name: "Windsurf", value: "windsurf" },
401
+ { name: "Other / Manual Setup", value: "manual" },
529
402
  ],
530
- pageSize: 6,
531
- default: [],
403
+ validate: (arr) => Array.isArray(arr) && arr.length > 0
404
+ ? true
405
+ : "Please select at least one tool",
532
406
  },
533
407
  ]);
534
- selectedEnv = {
535
- ITERABLE_USER_PII: permFlags.includes("pii")
536
- ? "true"
537
- : "false",
538
- ITERABLE_ENABLE_WRITES: permFlags.includes("writes")
539
- ? "true"
540
- : "false",
541
- ITERABLE_ENABLE_SENDS: permFlags.includes("sends")
542
- ? "true"
543
- : "false",
544
- };
545
- }
546
- let mcpEnv = {
547
- ITERABLE_USER_PII: selectedEnv.ITERABLE_USER_PII,
548
- ITERABLE_ENABLE_WRITES: selectedEnv.ITERABLE_ENABLE_WRITES,
549
- ITERABLE_ENABLE_SENDS: selectedEnv.ITERABLE_ENABLE_SENDS,
550
- };
551
- if (args.includes("--debug")) {
552
- mcpEnv.ITERABLE_DEBUG = "true";
553
- mcpEnv.LOG_LEVEL = "debug";
554
- }
555
- // Enforce permission invariants before displaying
556
- mcpEnv = enforceSendsRequiresWrites(mcpEnv, (msg) => showWarning(msg));
557
- console.log();
558
- console.log(formatKeyValue("User PII Access", mcpEnv.ITERABLE_USER_PII === "true"
559
- ? chalk.green("Enabled")
560
- : chalk.gray("Disabled")));
561
- console.log(formatKeyValue("Write Operations", mcpEnv.ITERABLE_ENABLE_WRITES === "true"
562
- ? chalk.green("Enabled")
563
- : chalk.gray("Disabled")));
564
- console.log(formatKeyValue("Sends", mcpEnv.ITERABLE_ENABLE_SENDS === "true"
565
- ? chalk.green("Enabled")
566
- : chalk.gray("Disabled")));
567
- // Only show the default note when all flags are disabled
568
- if (mcpEnv.ITERABLE_USER_PII !== "true" &&
569
- mcpEnv.ITERABLE_ENABLE_WRITES !== "true" &&
570
- mcpEnv.ITERABLE_ENABLE_SENDS !== "true") {
571
- console.log();
572
- console.log(chalk.gray("Note: These are disabled by default. If you absolutely need to enable these, run 'iterable-mcp setup --advanced'"));
573
- }
574
- else {
408
+ tools = selectedTools;
575
409
  console.log();
576
- showWarning("Warning: Elevated capabilities enabled. Sends, writes, and/or PII access may now occur.");
577
- }
578
- console.log();
579
- // If we used an existing key, update its env overrides with chosen settings
580
- if (usedKeyName) {
581
- try {
582
- await keyManager.updateKeyEnv(usedKeyName, pickPersistablePermissionEnv(mcpEnv));
583
- // Re-read the key metadata and prefer its persisted env when writing
584
- // tool configurations. This addresses cases where the key was activated
585
- // during setup and ensures configs reflect the updated values.
586
- const keys = await keyManager.listKeys();
587
- const updatedMeta = keys.find((k) => k.name === usedKeyName || k.id === usedKeyName);
588
- if (updatedMeta?.env) {
589
- mcpEnv = resolveFinalMcpEnv(mcpEnv, updatedMeta.env);
590
- }
591
- }
592
- catch (err) {
593
- if (process.env.ITERABLE_DEBUG === "true") {
594
- console.warn("Non-fatal: failed to persist per-key env settings", err);
595
- }
596
- }
597
410
  }
598
- // Enforce again after merging persisted key env
599
- mcpEnv = enforceSendsRequiresWrites(mcpEnv, (msg) => showWarning(msg));
600
- // Step 5: Configure AI Tools
601
- console.log();
602
- showSection("Configuring AI Tools", icons.zap);
603
- console.log();
604
- // Ask about auto-update unless flag was provided
605
411
  let useAutoUpdate = autoUpdate;
606
412
  if (!autoUpdate) {
607
413
  const { enableAutoUpdate } = await inquirer.prompt([
@@ -612,6 +418,7 @@ export const setupMcpServer = async () => {
612
418
  default: false,
613
419
  },
614
420
  ]);
421
+ console.log();
615
422
  useAutoUpdate = enableAutoUpdate;
616
423
  }
617
424
  const iterableMcpConfig = buildMcpConfig({
@@ -619,35 +426,6 @@ export const setupMcpServer = async () => {
619
426
  env: omitPersistablePermissionEnv(mcpEnv),
620
427
  autoUpdate: useAutoUpdate,
621
428
  });
622
- // Preflight confirmation summary before applying changes
623
- console.log(chalk.gray("Summary:"));
624
- console.log(formatKeyValue("Tools", tools.map((t) => TOOL_NAMES[t]).join(", "), valueColor()));
625
- if (usedKeyName) {
626
- console.log(formatKeyValue("API Key", usedKeyName, valueColor()));
627
- }
628
- console.log(formatKeyValue("Endpoint", baseUrl, valueColor()));
629
- console.log(formatKeyValue("User PII", mcpEnv.ITERABLE_USER_PII === "true"
630
- ? chalk.green("Enabled")
631
- : chalk.gray("Disabled")));
632
- console.log(formatKeyValue("Writes", mcpEnv.ITERABLE_ENABLE_WRITES === "true"
633
- ? chalk.green("Enabled")
634
- : chalk.gray("Disabled")));
635
- console.log(formatKeyValue("Sends", mcpEnv.ITERABLE_ENABLE_SENDS === "true"
636
- ? chalk.green("Enabled")
637
- : chalk.gray("Disabled")));
638
- console.log();
639
- const { proceed } = await inquirer.prompt([
640
- {
641
- type: "confirm",
642
- name: "proceed",
643
- message: "Proceed with configuration?",
644
- default: true,
645
- },
646
- ]);
647
- if (!proceed) {
648
- showInfo("Setup cancelled by user. No changes were made.");
649
- return;
650
- }
651
429
  const fileBasedTools = tools.filter((tool) => tool in TOOL_CONFIGS);
652
430
  const needsClaudeCode = tools.includes("claude-code");
653
431
  const needsManual = tools.includes("manual");
@@ -677,11 +455,10 @@ export const setupMcpServer = async () => {
677
455
  spinner.fail("Claude CLI not found");
678
456
  showError("Please install Claude Code first: https://docs.claude.com/en/docs/claude-code/overview");
679
457
  console.log();
680
- showInfo("After installing, re-run: iterable-mcp setup --claude-code");
458
+ showInfo(`After installing, re-run: ${COMMAND_NAME} setup --claude-code`);
681
459
  process.exit(1);
682
460
  }
683
461
  const configJson = JSON.stringify(iterableMcpConfig);
684
- // Remove existing config (ignore errors)
685
462
  await execFileAsync("claude", ["mcp", "remove", "iterable"]).catch(() => { });
686
463
  spinner.start("Configuring Claude Code...");
687
464
  await new Promise((resolve, reject) => {
@@ -701,11 +478,9 @@ export const setupMcpServer = async () => {
701
478
  spinner.succeed("Claude Code configured successfully");
702
479
  }
703
480
  if (needsManual) {
704
- console.log();
705
481
  showSection("Manual Configuration", icons.rocket);
706
482
  console.log();
707
483
  const { type, command, args, env } = iterableMcpConfig;
708
- showInfo("Your API key has been stored.");
709
484
  showInfo("Add the MCP server to your AI tool with these settings:");
710
485
  console.log();
711
486
  console.log(chalk.white.bold(" Type:") + ` ${type}`);
@@ -721,16 +496,17 @@ export const setupMcpServer = async () => {
721
496
  console.log();
722
497
  showInfo("Refer to your AI tool's documentation for configuration instructions.");
723
498
  }
724
- // Build configured tools list
725
499
  const configuredTools = [];
726
500
  if (fileBasedTools.includes("cursor"))
727
501
  configuredTools.push("Cursor");
728
502
  if (fileBasedTools.includes("claude-desktop"))
729
503
  configuredTools.push("Claude Desktop");
730
- if (fileBasedTools.includes("gemini-cli"))
731
- configuredTools.push("Gemini CLI");
732
504
  if (needsClaudeCode)
733
505
  configuredTools.push("Claude Code");
506
+ if (fileBasedTools.includes("gemini-cli"))
507
+ configuredTools.push("Gemini CLI");
508
+ if (fileBasedTools.includes("windsurf"))
509
+ configuredTools.push("Windsurf");
734
510
  if (needsManual)
735
511
  configuredTools.push("your AI tool");
736
512
  const toolsList = configuredTools.length === 1
@@ -740,7 +516,6 @@ export const setupMcpServer = async () => {
740
516
  : configuredTools.slice(0, -1).join(", ") +
741
517
  ", and " +
742
518
  configuredTools[configuredTools.length - 1];
743
- // Success!
744
519
  console.log();
745
520
  const nextSteps = [
746
521
  ...(needsManual ? ["Configure your AI tool as described above"] : []),
@@ -752,8 +527,8 @@ export const setupMcpServer = async () => {
752
527
  ];
753
528
  const tips = [
754
529
  `Try: 'list my Iterable campaigns' in ${toolsList}`,
755
- "Manage keys with 'iterable-mcp keys list'",
756
- "Switch keys with 'iterable-mcp keys activate <name>'",
530
+ `Manage keys with '${COMMAND_NAME} keys list'`,
531
+ `Switch keys with '${COMMAND_NAME} keys activate <name>'`,
757
532
  ];
758
533
  showCompletion("Setup Complete!", nextSteps, tips);
759
534
  }