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
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
![Iterable MCP Server Setup](images/iterable-mcp-setup.png)


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

## What is MCP?

Expand Down Expand Up @@ -94,14 +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 & Gemini CLI)
### Manual configuration (Cursor, Claude Desktop, Windsurf & Gemini CLI)

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`
- **Windsurf:** `~/.codeium/windsurf/mcp_config.json`
- **Gemini CLI:** `~/.gemini/settings.json`

All three use the same configuration format:
All four use the same configuration format:

**Recommended: Using key manager:**
```bash
Expand Down Expand Up @@ -305,6 +306,30 @@ pnpm run install-dev
- Claude CLI missing: install `claude` CLI, then re-run `iterable-mcp setup --claude-code`.
- macOS Keychain issues: Ensure Keychain is accessible and re-run setup if needed.

### 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 **104 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: 86 tools ✅
- With all permissions: 104 tools ❌ (exceeds Windsurf limit)

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

Or update an existing key's permissions:
```bash
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.

## Beta Feature Reminder
Iterable's MCP server is currently in beta. MCP functionality may change, be
suspended, or be discontinued at any time without notice. This software is
Expand Down
14 changes: 10 additions & 4 deletions src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ const packageJson = JSON.parse(
)
) as { version: string };

// Tool display names
// Tool display names (ordered by popularity/recommendation)
type ToolName = keyof typeof TOOL_NAMES;
const TOOL_NAMES = {
cursor: "Cursor",
"claude-desktop": "Claude Desktop",
"claude-code": "Claude Code",
"gemini-cli": "Gemini CLI",
windsurf: "Windsurf",
manual: "Manual Setup",
} as const;

Expand Down Expand Up @@ -66,6 +67,7 @@ const TOOL_CONFIGS = {
}
})(),
cursor: path.join(os.homedir(), ".cursor", "mcp.json"),
windsurf: path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json"),
"gemini-cli": path.join(os.homedir(), ".gemini", "settings.json"),
} as const satisfies Record<string, string>;

Expand Down Expand Up @@ -200,10 +202,11 @@ export const setupMcpServer = async (): Promise<void> => {
const advanced = args.includes("--advanced");
const autoUpdate = args.includes("--auto-update");
let tools: ToolName[] = [
...(args.includes("--claude-desktop") ? ["claude-desktop" as const] : []),
...(args.includes("--cursor") ? ["cursor" as const] : []),
...(args.includes("--claude-desktop") ? ["claude-desktop" as const] : []),
...(args.includes("--claude-code") ? ["claude-code" as const] : []),
...(args.includes("--gemini-cli") ? ["gemini-cli" as const] : []),
...(args.includes("--windsurf") ? ["windsurf" as const] : []),
...(args.includes("--manual") ? ["manual" as const] : []),
];

Expand All @@ -224,13 +227,14 @@ export const setupMcpServer = async (): Promise<void> => {
});

setupTable.push(
[`${COMMAND_NAME} setup --cursor`, "Configure for Cursor"],
[
`${COMMAND_NAME} setup --claude-desktop`,
"Configure for Claude Desktop",
],
[`${COMMAND_NAME} setup --cursor`, "Configure for Cursor"],
[`${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"],
[
`${COMMAND_NAME} setup --cursor --claude-desktop`,
Expand Down Expand Up @@ -563,6 +567,7 @@ export const setupMcpServer = async (): Promise<void> => {
{ name: "Claude Desktop", value: "claude-desktop" },
{ name: "Claude Code (CLI)", value: "claude-code" },
{ name: "Gemini CLI", value: "gemini-cli" },
{ name: "Windsurf", value: "windsurf" },
{ name: "Other / Manual Setup", value: "manual" },
],
validate: (arr) =>
Expand Down Expand Up @@ -700,9 +705,10 @@ export const setupMcpServer = async (): Promise<void> => {
if (fileBasedTools.includes("cursor")) configuredTools.push("Cursor");
if (fileBasedTools.includes("claude-desktop"))
configuredTools.push("Claude Desktop");
if (needsClaudeCode) configuredTools.push("Claude Code");
if (fileBasedTools.includes("gemini-cli"))
configuredTools.push("Gemini CLI");
if (needsClaudeCode) configuredTools.push("Claude Code");
if (fileBasedTools.includes("windsurf")) configuredTools.push("Windsurf");
if (needsManual) configuredTools.push("your AI tool");

const toolsList =
Expand Down
4 changes: 4 additions & 0 deletions src/utils/tool-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export function getCursorConfigPath(): string {
return path.join(os.homedir(), ".cursor", "mcp.json");
}

export function getWindsurfConfigPath(): string {
return path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json");
}

export function getClaudeDesktopConfigPath(): string {
switch (process.platform) {
case "darwin":
Expand Down
18 changes: 17 additions & 1 deletion tests/unit/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ describe("MCP Server Setup Configuration", () => {
expect(cursorPath).toContain("mcp.json");
});

it("should generate correct Windsurf config path", () => {
const windsurfPath = path.join(
os.homedir(),
".codeium",
"windsurf",
"mcp_config.json"
);
expect(windsurfPath).toContain(".codeium");
expect(windsurfPath).toContain("windsurf");
expect(windsurfPath).toContain("mcp_config.json");
});

it("should generate correct Claude Desktop path on Darwin", () => {
if (process.platform === "darwin") {
const claudePath = path.join(
Expand Down Expand Up @@ -218,10 +230,14 @@ describe("MCP Server Setup Configuration", () => {

// Simulate what each tool would get
const cursorConfig = { mcpServers: { iterable: baseConfig } };
const windsurfConfig = { mcpServers: { iterable: baseConfig } };
const claudeDesktopConfig = { mcpServers: { iterable: baseConfig } };
const claudeCodeConfig = baseConfig; // Claude Code uses JSON directly

// All should have the same iterable config
// All file-based tools should have the same iterable config
expect(cursorConfig.mcpServers.iterable).toEqual(
windsurfConfig.mcpServers.iterable
);
expect(cursorConfig.mcpServers.iterable).toEqual(
claudeDesktopConfig.mcpServers.iterable
);
Expand Down
87 changes: 87 additions & 0 deletions tests/unit/tool-config-paths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, expect, it } from "@jest/globals";
import os from "os";
import path from "path";

import {
getClaudeDesktopConfigPath,
getCursorConfigPath,
getWindsurfConfigPath,
} from "../../src/utils/tool-config.js";

describe("tool-config paths", () => {
describe("getCursorConfigPath", () => {
it("returns path in home directory .cursor folder", () => {
const configPath = getCursorConfigPath();
expect(configPath).toBe(path.join(os.homedir(), ".cursor", "mcp.json"));
});

it("returns a path ending with mcp.json", () => {
const configPath = getCursorConfigPath();
expect(configPath).toMatch(/mcp\.json$/);
});
});

describe("getWindsurfConfigPath", () => {
it("returns path in home directory .codeium/windsurf folder", () => {
const configPath = getWindsurfConfigPath();
expect(configPath).toBe(
path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json")
);
});

it("returns a path ending with mcp_config.json", () => {
const configPath = getWindsurfConfigPath();
expect(configPath).toMatch(/mcp_config\.json$/);
});

it("returns a path containing .codeium", () => {
const configPath = getWindsurfConfigPath();
expect(configPath).toContain(".codeium");
});

it("returns a path containing windsurf", () => {
const configPath = getWindsurfConfigPath();
expect(configPath).toContain("windsurf");
});
});

describe("getClaudeDesktopConfigPath", () => {
it("returns a path ending with claude_desktop_config.json", () => {
const configPath = getClaudeDesktopConfigPath();
expect(configPath).toMatch(/claude_desktop_config\.json$/);
});

it("returns platform-specific path", () => {
const configPath = getClaudeDesktopConfigPath();

switch (process.platform) {
case "darwin":
expect(configPath).toContain("Library");
expect(configPath).toContain("Application Support");
expect(configPath).toContain("Claude");
break;
case "win32":
expect(configPath).toContain("Claude");
break;
default:
// Linux and others use XDG_CONFIG_HOME or ~/.config
expect(configPath).toContain("Claude");
break;
}
});
});

describe("path consistency", () => {
it("all config paths are absolute", () => {
expect(path.isAbsolute(getCursorConfigPath())).toBe(true);
expect(path.isAbsolute(getWindsurfConfigPath())).toBe(true);
expect(path.isAbsolute(getClaudeDesktopConfigPath())).toBe(true);
});

it("all config paths end with .json", () => {
expect(getCursorConfigPath()).toMatch(/\.json$/);
expect(getWindsurfConfigPath()).toMatch(/\.json$/);
expect(getClaudeDesktopConfigPath()).toMatch(/\.json$/);
});
});
});