diff --git a/README.md b/README.md index 2f61918..5acf086 100644 --- a/README.md +++ b/README.md @@ -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? @@ -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 @@ -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 --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 diff --git a/src/install.ts b/src/install.ts index be78e2f..4219028 100644 --- a/src/install.ts +++ b/src/install.ts @@ -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; @@ -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; @@ -200,10 +202,11 @@ export const setupMcpServer = async (): Promise => { 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] : []), ]; @@ -224,13 +227,14 @@ export const setupMcpServer = async (): Promise => { }); 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`, @@ -563,6 +567,7 @@ export const setupMcpServer = async (): Promise => { { 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) => @@ -700,9 +705,10 @@ export const setupMcpServer = async (): Promise => { 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 = diff --git a/src/utils/tool-config.ts b/src/utils/tool-config.ts index 707dd6b..dc2af0b 100644 --- a/src/utils/tool-config.ts +++ b/src/utils/tool-config.ts @@ -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": diff --git a/tests/unit/install.test.ts b/tests/unit/install.test.ts index 4f43711..208c8b0 100644 --- a/tests/unit/install.test.ts +++ b/tests/unit/install.test.ts @@ -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( @@ -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 ); diff --git a/tests/unit/tool-config-paths.test.ts b/tests/unit/tool-config-paths.test.ts new file mode 100644 index 0000000..09f405e --- /dev/null +++ b/tests/unit/tool-config-paths.test.ts @@ -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$/); + }); + }); +});