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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
with:
run_install: false

- name: Setup Node ${{ matrix.node-version }}q
- name: Setup Node ${{ matrix.node-version }}
# Latest major (v4). See: https://github.com/actions/setup-node
uses: actions/setup-node@v4
with:
Expand All @@ -39,8 +39,8 @@ jobs:
- name: Typecheck and lint
run: pnpm check

- name: Unit tests
run: pnpm test:unit

- name: Build
run: pnpm build

- name: Unit tests
run: pnpm test:unit
58 changes: 29 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

With the new Iterable MCP server, you can now connect Iterable to your favorite AI tools like Cursor, Claude Desktop, and Claude Code!



## What is MCP?

MCP stands for Model Context Protocol. It's a new, open standard that lets AI tools (like Cursor or Claude Desktop) connect to external tools and APIs in a secure, structured way. MCP acts as a "bridge" between your AI app and platforms like Iterable, so you can ask questions or perform actions in plain English, and the AI translates those into safe API calls behind the scenes.
Expand Down Expand Up @@ -35,13 +33,13 @@ npx @iterable/mcp setup --advanced
```

What you’ll choose (optional):
- View user PII (`ITERABLE_USER_PII`)
- Enable Writes (Create/Update/Delete actions) (`ITERABLE_ENABLE_WRITES`)
- Enable actual sends (`ITERABLE_ENABLE_SENDS`) — requires Writes
- Enable access to user PII (`ITERABLE_USER_PII`)
- Enable writes (create/update/delete actions) (`ITERABLE_ENABLE_WRITES`)
- Enable sends (campaigns/journeys/events) (`ITERABLE_ENABLE_SENDS`) — requires writes

Safety notes:
- Sends require Writes to be enabled.
- When you use an existing Keychain key, your choices are saved per key.
- Enabling sends requires writes to be enabled.
- Permission settings are saved per key (see key management section below).
- Prompts are generated from read‑only tools for extra safety.

## Prefer a global install?
Expand All @@ -53,23 +51,24 @@ iterable-mcp setup

**Note:** The setup command automatically configures the correct command path.

## Install from source
Throughout this guide, commands are shown as `iterable-mcp` for brevity. If not globally installed, use `npx @iterable/mcp` instead (e.g., `npx @iterable/mcp keys list`).

### Install from source

```bash
git clone https://github.com/iterable/mcp-server.git
cd mcp-server
pnpm install-dev:cursor # or install-dev:claude-desktop or install-dev:claude-code
```

## Claude Code
### Claude Code

The `setup --claude-code` command automatically configures Claude Code by running `claude mcp add` for you and stores your API key securely in macOS Keychain.
The `setup --claude-code` command automatically configures Claude Code by running `claude mcp add` for you.

Alternatively, you can run it manually:
Alternatively, you can configure it manually:

```bash
# Manual installation (alternative to setup --claude-code)
# First, add your API key to the keychain (interactive prompts)
# Add your API key first (see API Key Management section below)
iterable-mcp keys add

# Then configure Claude Code
Expand All @@ -95,15 +94,15 @@ claude mcp add-from-claude-desktop

For more information, see the [Claude Code MCP documentation](https://docs.claude.com/en/docs/claude-code/mcp).

## Manual configuration (Cursor & Claude Desktop)
### Manual configuration (Cursor & Claude Desktop)

The above commands will automatically configure your AI tool to use the MCP server by editing the appropriate configuration file, but you can also manually edit the appropriate configuration file:
- **Claude Desktop:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Cursor:** `~/.cursor/mcp.json`

**macOS (with Keychain):**
**Recommended: Using key manager:**
```bash
# First, add your API key to the keychain (interactive prompts)
# First, add your API key (interactive prompts)
iterable-mcp keys add
```

Expand All @@ -119,9 +118,9 @@ Then edit your config file:
}
```

Note: No `env` needed on macOS - API key and base URL are loaded from Keychain.
No `env` section needed - API key and base URL are loaded automatically.

**Windows/Linux (using environment variables):**
**Alternative: Environment variables:**
```json
{
"mcpServers": {
Expand Down Expand Up @@ -150,7 +149,7 @@ export ITERABLE_MCP_NPX_PATH="/path/to/custom/npx"
npx @iterable/mcp setup --cursor
```

Alternatively, you can manually edit your configuration file (after adding your key to the keychain):
Alternatively, you can manually edit your configuration file (after adding your key):

```json
{
Expand All @@ -163,7 +162,7 @@ Alternatively, you can manually edit your configuration file (after adding your
}
```

Note: No `env` needed on macOS — API key and base URL are loaded from Keychain.
No `env` section is needed if using the key manager.

## What you can do

Expand All @@ -185,7 +184,11 @@ Try these prompts:

### API Key Management

**macOS Keychain:** API keys are stored securely in Keychain. Each key is tied to its API endpoint (US, EU, or custom).
**Key Storage:**

API keys are stored in the `~/.iterable-mcp/keys.json` file and managed via the `iterable-mcp keys` commands. On macOS the actual API key values are stored in the system Keychain. On Linux, the API key values are stored directly in the file with restricted permissions (0o600). On Windows, the file is protected by default NTFS home directory permissions.

Each key is tied to its API endpoint (US, EU, or custom) and to its permissions (view PII, write operations, send messages).

**How Key Selection Works:**
- You can store multiple API keys with different names (e.g., "production", "staging", "dev")
Expand Down Expand Up @@ -213,14 +216,12 @@ iterable-mcp keys delete <key-id>
# To update a key: delete the old one and add a new one with the same name
```

**Windows/Linux:** Use `ITERABLE_API_KEY` and (optionally) `ITERABLE_BASE_URL` env vars.

### Environment variables

| Variable | Required | Description |
|----------|----------|-------------|
| `ITERABLE_API_KEY` | No* | Your Iterable API key (*Optional on macOS if using Keychain manager, Required on Windows and Linux) |
| `ITERABLE_BASE_URL` | No** | Base URL for the Iterable API (**Not needed on macOS when using key manager - URL is stored with each key) |
| `ITERABLE_API_KEY` | No* | Your Iterable API key (*Optional if using key manager, otherwise required) |
| `ITERABLE_BASE_URL` | No** | Base URL for the Iterable API (**Not needed when using key manager - URL is stored with each key) |
| `ITERABLE_DEBUG` | No | Set to `true` for API request logging |
| `LOG_LEVEL` | No | Set to `debug` for troubleshooting |
| `ITERABLE_USER_PII` | No | Set to `true` to enable tools that access user PII data (default: `false`) |
Expand Down Expand Up @@ -263,7 +264,7 @@ Integration tests make real API calls to Iterable and require a valid API key.
export ITERABLE_API_KEY=your_actual_api_key
```

2. Or on macOS, add a key to the keychain (interactive):
2. Or add a key to key manager (interactive):
```bash
iterable-mcp keys add
```
Expand All @@ -275,7 +276,7 @@ Integration tests make real API calls to Iterable and require a valid API key.
pnpm test:integration
```

**Note:** Integration tests require a valid API key (env or active macOS Keychain key). The suite fails fast if none is found.
**Note:** Integration tests require a valid API key (env var or active key manager key). The suite fails fast if none is found.

### Development workflow

Expand All @@ -299,8 +300,7 @@ pnpm run install-dev
## Troubleshooting

- Claude CLI missing: install `claude` CLI, then re-run `iterable-mcp setup --claude-code`.
- macOS Keychain issues: Ensure Keychain is available; re-run setup. Stale locks are auto‑recovered.

- macOS Keychain issues: Ensure Keychain is accessible and re-run setup if needed.

## Beta Feature Reminder
Iterable's MCP server is currently in beta. MCP functionality may change, be
Expand Down
12 changes: 7 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,8 @@ export async function loadMcpServerConfig(): Promise<McpServerConfig> {
process.env.ITERABLE_BASE_URL || "https://api.iterable.com";
let keyEnv: Record<string, string> | undefined;

// Try to load API key from KeyManager first (macOS only)
// SAFETY: Skip keyManager in test environments to prevent production data access
if (process.platform === "darwin" && process.env.NODE_ENV !== "test") {
if (process.env.NODE_ENV !== "test") {
try {
const keyManager = getKeyManager();
await keyManager.initialize();
Expand Down Expand Up @@ -90,12 +89,15 @@ export async function loadMcpServerConfig(): Promise<McpServerConfig> {
error: sanitizedMessage,
});
console.error(
"⚠️ Warning: Failed to load API key from macOS Keychain:",
"⚠️ Warning: Failed to load API key from key storage:",
sanitizedMessage
);

// Provide helpful guidance for sync issues
if (sanitizedMessage.includes("could not be found")) {
// Provide helpful guidance for sync issues (macOS Keychain specific)
if (
process.platform === "darwin" &&
sanitizedMessage.includes("could not be found")
) {
console.error(
"\n💡 This may be a sync issue. Restarting the server will automatically clean up orphaned keys."
);
Expand Down
67 changes: 44 additions & 23 deletions src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ const { dirname, join } = path;
const LOCAL_BINARY_NAME = "iterable-mcp";
const NPX_PACKAGE_NAME = "@iterable/mcp";

// Tool display names
const TOOL_NAMES = {
cursor: "Cursor",
"claude-desktop": "Claude Desktop",
"claude-code": "Claude Code",
manual: "Manual Setup",
} as const;

// Get package version
const packageJson = JSON.parse(
readFileSync(
Expand Down Expand Up @@ -187,6 +195,7 @@ export const setupMcpServer = async (): Promise<void> => {
...(args.includes("--claude-desktop") ? ["claude-desktop" as const] : []),
...(args.includes("--cursor") ? ["cursor" as const] : []),
...(args.includes("--claude-code") ? ["claude-code" as const] : []),
...(args.includes("--manual") ? ["manual" as const] : []),
];

// Detect how the command was invoked
Expand Down Expand Up @@ -215,6 +224,7 @@ export const setupMcpServer = async (): Promise<void> => {
[`${commandName} setup --claude-desktop`, "Configure for Claude Desktop"],
[`${commandName} setup --cursor`, "Configure for Cursor"],
[`${commandName} setup --claude-code`, "Configure for Claude Code"],
[`${commandName} setup --manual`, "Show manual config instructions"],
[
`${commandName} setup --cursor --claude-desktop`,
"Configure multiple tools",
Expand All @@ -230,7 +240,7 @@ export const setupMcpServer = async (): Promise<void> => {
console.log();
console.log();

showSection("Key Management" + chalk.gray(" (macOS only)"), icons.key);
showSection("Key Management", icons.key);
console.log();

const keysTable = createTable({
Expand Down Expand Up @@ -303,8 +313,8 @@ export const setupMcpServer = async (): Promise<void> => {
[
"• API keys prompted interactively (never in shell history)",
"• macOS: Keys stored securely in Keychain",
"• Windows/Linux: Keys stored in ~/.iterable-mcp with file permissions",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

~/.iterable-mcp/keys.json

"• Each key coupled to its endpoint (US/EU/custom)",
"• Windows/Linux: Use ITERABLE_API_KEY environment variable",
],
{ icon: icons.lock, theme: "info", padding: 1 }
);
Expand All @@ -320,7 +330,9 @@ export const setupMcpServer = async (): Promise<void> => {
showIterableLogo(packageJson.version);

const { selectedTools } = await inquirer.prompt<{
selectedTools: Array<"cursor" | "claude-desktop" | "claude-code">;
selectedTools: Array<
"cursor" | "claude-desktop" | "claude-code" | "manual"
>;
}>([
{
type: "checkbox",
Expand All @@ -330,6 +342,7 @@ export const setupMcpServer = async (): Promise<void> => {
{ name: "Cursor", value: "cursor" },
{ name: "Claude Desktop", value: "claude-desktop" },
{ name: "Claude Code (CLI)", value: "claude-code" },
{ name: "Other / Manual Setup", value: "manual" },
],
validate: (arr: any) =>
Array.isArray(arr) && arr.length > 0
Expand Down Expand Up @@ -358,15 +371,9 @@ export const setupMcpServer = async (): Promise<void> => {
const chalk = (await import("chalk")).default;
showIterableLogo(packageJson.version);
// Succinct overview of what will be configured
const prettyName = (t: "cursor" | "claude-desktop" | "claude-code") =>
t === "cursor"
? "Cursor"
: t === "claude-desktop"
? "Claude Desktop"
: "Claude Code";
console.log(
chalk.gray(
`Running setup to configure: ${tools.map(prettyName).join(", ")}`
`Running setup to configure: ${tools.map((t) => TOOL_NAMES[t]).join(", ")}`
)
);
console.log();
Expand Down Expand Up @@ -719,11 +726,6 @@ export const setupMcpServer = async (): Promise<void> => {
ITERABLE_ENABLE_WRITES: selectedEnv.ITERABLE_ENABLE_WRITES,
ITERABLE_ENABLE_SENDS: selectedEnv.ITERABLE_ENABLE_SENDS,
};
// On non-macOS, include API key and base URL in the written config
if (process.platform !== "darwin") {
if (apiKey) mcpEnv.ITERABLE_API_KEY = apiKey;
if (baseUrl) mcpEnv.ITERABLE_BASE_URL = baseUrl;
}
if (args.includes("--debug")) {
mcpEnv.ITERABLE_DEBUG = "true";
mcpEnv.LOG_LEVEL = "debug";
Expand Down Expand Up @@ -836,15 +838,13 @@ export const setupMcpServer = async (): Promise<void> => {
});

// Preflight confirmation summary before applying changes
const prettyName = (t: "cursor" | "claude-desktop" | "claude-code") =>
t === "cursor"
? "Cursor"
: t === "claude-desktop"
? "Claude Desktop"
: "Claude Code";
console.log(chalk.gray("Summary:"));
console.log(
formatKeyValue("Tools", tools.map(prettyName).join(", "), valueColor())
formatKeyValue(
"Tools",
tools.map((t) => TOOL_NAMES[t]).join(", "),
valueColor()
)
);
if (usedKeyName) {
console.log(formatKeyValue("API Key", usedKeyName, valueColor()));
Expand Down Expand Up @@ -889,9 +889,10 @@ export const setupMcpServer = async (): Promise<void> => {
}

const fileBasedTools = tools.filter(
(tool) => tool !== "claude-code"
(tool) => tool === "claude-desktop" || tool === "cursor"
) as Array<"claude-desktop" | "cursor">;
const needsClaudeCode = tools.includes("claude-code");
const needsManual = tools.includes("manual");

if (fileBasedTools.length > 0) {
const { updateToolConfig } = await import("./utils/tool-config.js");
Expand Down Expand Up @@ -964,12 +965,31 @@ export const setupMcpServer = async (): Promise<void> => {
spinner.succeed("Claude Code configured successfully");
}

if (needsManual) {
console.log();
showSection("Manual Configuration", icons.rocket);
console.log();

showInfo("Your API key has been stored.");
showInfo("Add the MCP server to your AI tool with these settings:");
console.log();
console.log(chalk.white.bold(" Type:") + " stdio");
console.log(chalk.white.bold(" Command:") + " npx");
console.log(chalk.white.bold(" Args:") + " -y @iterable/mcp");
console.log();

showInfo(
"Refer to your AI tool's documentation for configuration instructions."
);
}

// Build configured tools list
const configuredTools: string[] = [];
if (fileBasedTools.includes("cursor")) configuredTools.push("Cursor");
if (fileBasedTools.includes("claude-desktop"))
configuredTools.push("Claude Desktop");
if (needsClaudeCode) configuredTools.push("Claude Code");
if (needsManual) configuredTools.push("your AI tool");

const toolsList =
configuredTools.length === 1
Expand All @@ -983,6 +1003,7 @@ export const setupMcpServer = async (): Promise<void> => {
// Success!
console.log();
const nextSteps = [
...(needsManual ? ["Configure your AI tool as described above"] : []),
`Restart ${toolsList} to load the new configuration`,
"Start using Iterable MCP tools in your conversations",
...(needsClaudeCode
Expand Down
Loading
Loading