Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 15 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

[![npm version](https://img.shields.io/npm/v/@iterable/mcp.svg)](https://www.npmjs.com/package/@iterable/mcp)

![Iterable MCP Server](images/iterable-mcp-server.png)

Talk to your Iterable data using natural language. Ask questions or give instructions like *"How many campaigns did we send last week?"*, *"Show me my most recent templates"*, or *"Build me a beautiful email template that does the following..."* and get instant answers without writing code or navigating dashboards.

**Supported AI clients:**
Expand All @@ -28,9 +26,6 @@ Talk to your Iterable data using natural language. Ask questions or give instruc
npx @iterable/mcp setup
```

![Iterable MCP Server Setup](images/iterable-mcp-server-setup.gif)


To always use the latest version (auto-update on each restart), add `--auto-update`:

```bash
Expand Down Expand Up @@ -81,27 +76,38 @@ Each key is tied to its API endpoint (US, EU, or custom) and to its permissions
- The MCP server automatically uses whichever key is currently active
- Your first key is automatically set as active
- Switch between keys using the `activate` command
- After `keys activate`, restart your AI client (or reload its MCP servers) so the Iterable MCP process picks up the new active key. Some clients may require fully quitting and reopening the application.

```bash
# List stored keys (shows which one is active with ● ACTIVE badge)
npx @iterable/mcp keys list

# Add a new key (interactive: prompts for name, region/endpoint, and API key)
# Your first key becomes active automatically
# Uses secure defaults: PII, writes, and sends disabled
npx @iterable/mcp keys add

# Add a key and choose permissions (PII / writes / sends) interactively
npx @iterable/mcp keys add --advanced

# Switch to a different key by name or ID (also switches endpoint)
npx @iterable/mcp keys activate production
npx @iterable/mcp keys activate staging

# Update an existing key (interactive: prompts for new values)
# Update an existing key (interactive: prompts for new name/endpoint/API key)
# Without --advanced, existing permissions are left unchanged
npx @iterable/mcp keys update <name-or-id>

# Update a key and change its permissions (required to enable PII / writes / sends)
npx @iterable/mcp keys update <name-or-id> --advanced

# Delete a key by ID (requires ID for safety)
# Note: Cannot delete the currently active key - activate another first
npx @iterable/mcp keys delete <key-id>
```

**Permissions:** By default, `keys add` and `keys update` do not prompt for elevated permissions. New keys get secure defaults (PII, writes, and sends disabled); updates keep the key's current permissions. Pass `--advanced` to choose User PII, Writes, and/or Sends for that key (same options and risks as [`setup --advanced`](#installation)).

## Advanced setup

### Prefer a global install?
Expand Down Expand Up @@ -230,6 +236,7 @@ Variables marked as **managed** are automatically configured by the key manager.
| `ITERABLE_ENABLE_SENDS` | ✅ | Set to `true` to enable tools that can send messages (default: `false`). Requires writes to be enabled |
| `ITERABLE_DEBUG` | | Set to `true` for API request logging |
| `LOG_LEVEL` | | Set to `debug` for troubleshooting |
| `ITERABLE_UI_THEME` | | Force terminal color theme: `dark` or `light` (auto-detected by default) |

### Custom endpoints

Expand All @@ -246,47 +253,9 @@ Variables marked as **managed** are automatically configured by the key manager.

### Client-specific limitations

#### Windsurf (Codeium)

**Tool limit:** Windsurf has a [maximum limit of 100 tools](https://docs.windsurf.com/windsurf/cascade/mcp) that Cascade can access at any given time. When all permissions are enabled (`ITERABLE_USER_PII=true`, `ITERABLE_ENABLE_WRITES=true`, `ITERABLE_ENABLE_SENDS=true`), the Iterable MCP server exposes **105 tools**, which exceeds this limit.

**Workaround:** Use restricted permissions to stay under the 100-tool limit:
- With default permissions (all disabled): 26 tools ✅
- With PII only: 37 tools ✅
- With PII + writes: 87 tools ✅
- With all permissions: 105 tools ❌ (exceeds Windsurf limit)

You can configure permissions when adding a key:
```bash
npx @iterable/mcp keys add --advanced
```

Or update an existing key's permissions:
```bash
npx @iterable/mcp keys update <key-name> --advanced
```

**Process persistence:** After switching API keys with `keys activate`, you must **fully restart Windsurf** (quit and reopen the application). Windsurf keeps MCP server processes running in the background, and they don't automatically reload when you switch keys.

#### Antigravity

**Tool limit:** Antigravity has a maximum limit of 100 tools per MCP server. When all permissions are enabled (`ITERABLE_USER_PII=true`, `ITERABLE_ENABLE_WRITES=true`, `ITERABLE_ENABLE_SENDS=true`), the Iterable MCP server exposes **105 tools**, which exceeds this limit.

**Workaround:** Use restricted permissions to stay under the 100-tool limit:
- With default permissions (all disabled): 26 tools ✅
- With PII only: 37 tools ✅
- With PII + writes: 87 tools ✅
- With all permissions: 105 tools ❌ (exceeds Antigravity limit)
#### Tool limits (Windsurf & Antigravity)

You can configure permissions when adding a key:
```bash
npx @iterable/mcp keys add --advanced
```

Or update an existing key's permissions:
```bash
npx @iterable/mcp keys update <key-name> --advanced
```
Windsurf ([100-tool limit](https://docs.windsurf.com/windsurf/cascade/mcp)) and Antigravity each allow at most **100 tools** per MCP server. Enabling all permissions (PII, writes, and sends) can push this server over that limit. If tools fail to load, restrict permissions with [`keys add --advanced` / `keys update --advanced`](#api-key-management) (for example, leave sends disabled). See [Installation](#installation) for what each permission enables and the associated risks, and [TOOLS.md](TOOLS.md) for the current tool list.

## Contributing

Expand Down
34 changes: 27 additions & 7 deletions src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import { type ApiKeyMetadata, getKeyManager } from "./key-manager.js";
import { displayKeyDetails, saveKeyInteractive } from "./keys-cli.js";
import {
COMMAND_NAME,
DOCUMENTATION_URL,
KEYS_COMMAND_TABLE,
NPX_PACKAGE_NAME,
UI_THEME_HINT,
} from "./utils/command-info.js";
import { getKeyStorageMessage } from "./utils/formatting.js";

Expand Down Expand Up @@ -243,6 +245,10 @@ export const setupMcpServer = async (): Promise<void> => {
[`${COMMAND_NAME} setup --gemini-cli`, "Configure for Gemini CLI"],
[`${COMMAND_NAME} setup --windsurf`, "Configure for Windsurf"],
[`${COMMAND_NAME} setup --antigravity`, "Configure for Antigravity"],
[
`${COMMAND_NAME} setup --advanced`,
"Choose PII, writes, and sends during setup",
],
[`${COMMAND_NAME} setup --manual`, "Show manual config instructions"],
[
`${COMMAND_NAME} setup --cursor --claude-desktop`,
Expand All @@ -264,14 +270,16 @@ export const setupMcpServer = async (): Promise<void> => {

const keysTable = createTable({
head: ["Command", "Description"],
colWidths: [45, 40],
colWidths: [42, 48],
style: "normal",
});

keysTable.push(...KEYS_COMMAND_TABLE);

console.log(keysTable.toString());
console.log();
console.log(chalk.gray(` ${UI_THEME_HINT}`));
console.log();
console.log();

showSection("Environment Variables", icons.globe);
Expand Down Expand Up @@ -302,7 +310,8 @@ export const setupMcpServer = async (): Promise<void> => {
chalk.gray("Enable message sending (default: false)"),
],
["ITERABLE_MCP_NODE_PATH", chalk.gray("Custom node executable path")],
["ITERABLE_MCP_NPX_PATH", chalk.gray("Custom npx executable path")]
["ITERABLE_MCP_NPX_PATH", chalk.gray("Custom npx executable path")],
["ITERABLE_UI_THEME", chalk.gray("Force terminal colors: dark or light")]
);

console.log(envTable.toString());
Expand Down Expand Up @@ -351,13 +360,11 @@ export const setupMcpServer = async (): Promise<void> => {
} = await import("./utils/ui.js");
const chalk = (await import("chalk")).default;
showIterableLogo(packageJson.version);
console.log();
const ora = (await import("ora")).default;
const spinner = ora();

try {
// Step 1: API Key Configuration
console.log();
showSection("API Key Configuration", icons.key);
console.log();

Expand Down Expand Up @@ -458,6 +465,9 @@ export const setupMcpServer = async (): Promise<void> => {
formatKeyValue,
linkColor,
showBox: (await import("./utils/ui.js")).showBox,
showWarning,
showRestartNotice: (await import("./utils/ui.js"))
.showRestartNotice,
},
keySpinner,
{
Expand Down Expand Up @@ -500,9 +510,15 @@ export const setupMcpServer = async (): Promise<void> => {
linkColor,
});
console.log();
showInfo(
`To modify permissions, run: ${chalk.cyan(`${COMMAND_NAME} keys update "${selectedExistingMeta?.name}" --advanced`)}`
);
if (advanced) {
showWarning(
`--advanced only applies when adding a new key. To change permissions for "${selectedExistingMeta?.name}", run: ${chalk.cyan(`${COMMAND_NAME} keys update "${selectedExistingMeta?.name}" --advanced`)}`
);
} else {
showInfo(
`To modify permissions, run: ${chalk.cyan(`${COMMAND_NAME} keys update "${selectedExistingMeta?.name}" --advanced`)}`
);
}
} else {
const keyManager = getKeyManager();
await keyManager.initialize();
Expand All @@ -523,6 +539,9 @@ export const setupMcpServer = async (): Promise<void> => {
formatKeyValue,
linkColor,
showBox: (await import("./utils/ui.js")).showBox,
showWarning,
showRestartNotice: (await import("./utils/ui.js"))
.showRestartNotice,
},
keySpinner,
{
Expand Down Expand Up @@ -744,6 +763,7 @@ export const setupMcpServer = async (): Promise<void> => {
`Try: 'list my Iterable campaigns' in ${toolsList}`,
`Manage keys with '${COMMAND_NAME} keys list'`,
`Switch keys with '${COMMAND_NAME} keys activate <name>'`,
`Documentation: ${DOCUMENTATION_URL}`,
];

showCompletion("Setup Complete!", nextSteps, tips);
Expand Down
63 changes: 34 additions & 29 deletions src/keys-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import inquirer from "inquirer";
import path from "path";
import { fileURLToPath } from "url";

import { COMMAND_NAME, KEYS_COMMAND_TABLE } from "./utils/command-info.js";
import {
ADVANCED_PERMISSIONS_WARNING,
COMMAND_NAME,
KEYS_ADVANCED_HINT,
KEYS_COMMAND_TABLE,
UI_THEME_HINT,
} from "./utils/command-info.js";

const { dirname, join } = path;

Expand Down Expand Up @@ -161,7 +167,8 @@ export async function saveKeyInteractive(
showInfo,
formatKeyValue,
linkColor,
showBox,
showWarning,
showRestartNotice,
} = ui;

// Use local variables so we can reassign if duplicate detected
Expand Down Expand Up @@ -202,18 +209,7 @@ export async function saveKeyInteractive(
const updatedKey = (await keyManager.getKeyMetadata(key.id))!;

if (!options?.skipRestartNotice) {
console.log();
showBox(
"Action Required",
[
chalk.yellow("Restart your AI tools to use this key"),
"",
chalk.gray(
"The MCP server will automatically load the active key when it starts"
),
],
{ icon: icons.zap, theme: "warning" }
);
showRestartNotice();
}

return updatedKey;
Expand Down Expand Up @@ -392,6 +388,10 @@ export async function saveKeyInteractive(
};

if (options?.advanced) {
console.log();
showWarning(ADVANCED_PERMISSIONS_WARNING);
console.log();

const currentPerms: string[] = [];
if (selectedEnv.ITERABLE_USER_PII === "true") currentPerms.push("pii");
if (selectedEnv.ITERABLE_ENABLE_WRITES === "true")
Expand Down Expand Up @@ -437,7 +437,7 @@ export async function saveKeyInteractive(
"Using secure default permissions (PII, Writes, and Sends disabled)"
);
}
showInfo(`Run with --advanced to configure advanced permissions`);
showInfo(KEYS_ADVANCED_HINT);
console.log();
}

Expand Down Expand Up @@ -541,6 +541,8 @@ export async function handleKeysCommand(): Promise<void> {
showSection,
showSuccess,
linkColor,
showWarning,
showRestartNotice,
} = await loadUi();
const spinner = await getSpinner();

Expand Down Expand Up @@ -678,6 +680,8 @@ export async function handleKeysCommand(): Promise<void> {
formatKeyValue,
linkColor,
showBox,
showWarning,
showRestartNotice,
},
spinner,
hasAdvancedFlag ? { advanced: true } : undefined
Expand Down Expand Up @@ -716,6 +720,8 @@ export async function handleKeysCommand(): Promise<void> {
formatKeyValue,
linkColor,
showBox,
showWarning,
showRestartNotice,
},
spinner,
{ advanced: hasAdvancedFlag }
Expand Down Expand Up @@ -767,17 +773,7 @@ export async function handleKeysCommand(): Promise<void> {
showSuccess(`"${keyToActivate.name}" is now your active API key`);
}

showBox(
"Action Required",
[
chalk.yellow("Restart your AI tools to use this key"),
"",
chalk.gray(
"The MCP server will automatically load the active key when it starts"
),
],
{ icon: icons.zap, theme: "warning" }
);
showRestartNotice();
break;
}

Expand Down Expand Up @@ -841,7 +837,7 @@ export async function handleKeysCommand(): Promise<void> {

const commandsTable = createTable({
head: ["Command", "Description"],
colWidths: [45, 40],
colWidths: [42, 48],
style: "normal",
});

Expand All @@ -852,6 +848,8 @@ export async function handleKeysCommand(): Promise<void> {

console.log(commandsTable.toString());
console.log();
console.log(chalk.gray(` ${UI_THEME_HINT}`));
console.log();
console.log();

showSection("Examples", icons.fire);
Expand All @@ -860,18 +858,25 @@ export async function handleKeysCommand(): Promise<void> {
console.log(chalk.white.bold(" Add API keys"));
console.log(
chalk.gray(
" (Interactive prompts: name, region, API key, PII, Writes, Sends)"
" (Interactive prompts: name, region, and API key; + permissions with --advanced)"
)
);
console.log();
console.log(chalk.cyan(` ${COMMAND_NAME} keys add`));
console.log(
chalk.cyan(` ${COMMAND_NAME} keys add --advanced`) +
chalk.gray(" # also choose permissions")
);
console.log();
console.log();
console.log(chalk.white.bold(" Manage your keys"));
console.log();
console.log(chalk.cyan(` ${COMMAND_NAME} keys list`));
console.log(chalk.cyan(` ${COMMAND_NAME} keys add`));
console.log(chalk.cyan(` ${COMMAND_NAME} keys update production`));
console.log(
chalk.cyan(` ${COMMAND_NAME} keys update production --advanced`) +
chalk.gray(" # change permissions")
);
console.log(chalk.cyan(` ${COMMAND_NAME} keys activate production`));
console.log(
chalk.cyan(
Expand Down
2 changes: 2 additions & 0 deletions src/utils/cli-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export async function loadUi(): Promise<any> {
showIterableLogo: () => {},
showSection: () => {},
showSuccess: () => {},
showWarning: () => {},
showRestartNotice: () => {},
linkColor: () => (s: string) => s,
};
}
Expand Down
Loading
Loading