Skip to content

Commit b332b73

Browse files
authored
Add support for Windsurf (#19)
1 parent 6b6f808 commit b332b73

File tree

5 files changed

+146
-8
lines changed

5 files changed

+146
-8
lines changed

README.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
![Iterable MCP Server Setup](images/iterable-mcp-setup.png)
44

55

6-
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!
6+
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!
77

88
## What is MCP?
99

@@ -94,14 +94,15 @@ claude mcp add-from-claude-desktop
9494

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

97-
### Manual configuration (Cursor, Claude Desktop & Gemini CLI)
97+
### Manual configuration (Cursor, Claude Desktop, Windsurf & Gemini CLI)
9898

9999
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:
100100
- **Claude Desktop:** `~/Library/Application Support/Claude/claude_desktop_config.json`
101101
- **Cursor:** `~/.cursor/mcp.json`
102+
- **Windsurf:** `~/.codeium/windsurf/mcp_config.json`
102103
- **Gemini CLI:** `~/.gemini/settings.json`
103104

104-
All three use the same configuration format:
105+
All four use the same configuration format:
105106

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

309+
### Client-specific limitations
310+
311+
#### Windsurf (Codeium)
312+
313+
**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.
314+
315+
**Workaround:** Use restricted permissions to stay under the 100-tool limit:
316+
- With default permissions (all disabled): 26 tools ✅
317+
- With PII only: 37 tools ✅
318+
- With PII + writes: 86 tools ✅
319+
- With all permissions: 104 tools ❌ (exceeds Windsurf limit)
320+
321+
You can configure permissions when adding a key:
322+
```bash
323+
iterable-mcp keys add --advanced
324+
```
325+
326+
Or update an existing key's permissions:
327+
```bash
328+
iterable-mcp keys update <key-name> --advanced
329+
```
330+
331+
**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.
332+
308333
## Beta Feature Reminder
309334
Iterable's MCP server is currently in beta. MCP functionality may change, be
310335
suspended, or be discontinued at any time without notice. This software is

src/install.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@ const packageJson = JSON.parse(
2929
)
3030
) as { version: string };
3131

32-
// Tool display names
32+
// Tool display names (ordered by popularity/recommendation)
3333
type ToolName = keyof typeof TOOL_NAMES;
3434
const TOOL_NAMES = {
3535
cursor: "Cursor",
3636
"claude-desktop": "Claude Desktop",
3737
"claude-code": "Claude Code",
3838
"gemini-cli": "Gemini CLI",
39+
windsurf: "Windsurf",
3940
manual: "Manual Setup",
4041
} as const;
4142

@@ -66,6 +67,7 @@ const TOOL_CONFIGS = {
6667
}
6768
})(),
6869
cursor: path.join(os.homedir(), ".cursor", "mcp.json"),
70+
windsurf: path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json"),
6971
"gemini-cli": path.join(os.homedir(), ".gemini", "settings.json"),
7072
} as const satisfies Record<string, string>;
7173

@@ -200,10 +202,11 @@ export const setupMcpServer = async (): Promise<void> => {
200202
const advanced = args.includes("--advanced");
201203
const autoUpdate = args.includes("--auto-update");
202204
let tools: ToolName[] = [
203-
...(args.includes("--claude-desktop") ? ["claude-desktop" as const] : []),
204205
...(args.includes("--cursor") ? ["cursor" as const] : []),
206+
...(args.includes("--claude-desktop") ? ["claude-desktop" as const] : []),
205207
...(args.includes("--claude-code") ? ["claude-code" as const] : []),
206208
...(args.includes("--gemini-cli") ? ["gemini-cli" as const] : []),
209+
...(args.includes("--windsurf") ? ["windsurf" as const] : []),
207210
...(args.includes("--manual") ? ["manual" as const] : []),
208211
];
209212

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

226229
setupTable.push(
230+
[`${COMMAND_NAME} setup --cursor`, "Configure for Cursor"],
227231
[
228232
`${COMMAND_NAME} setup --claude-desktop`,
229233
"Configure for Claude Desktop",
230234
],
231-
[`${COMMAND_NAME} setup --cursor`, "Configure for Cursor"],
232235
[`${COMMAND_NAME} setup --claude-code`, "Configure for Claude Code"],
233236
[`${COMMAND_NAME} setup --gemini-cli`, "Configure for Gemini CLI"],
237+
[`${COMMAND_NAME} setup --windsurf`, "Configure for Windsurf"],
234238
[`${COMMAND_NAME} setup --manual`, "Show manual config instructions"],
235239
[
236240
`${COMMAND_NAME} setup --cursor --claude-desktop`,
@@ -563,6 +567,7 @@ export const setupMcpServer = async (): Promise<void> => {
563567
{ name: "Claude Desktop", value: "claude-desktop" },
564568
{ name: "Claude Code (CLI)", value: "claude-code" },
565569
{ name: "Gemini CLI", value: "gemini-cli" },
570+
{ name: "Windsurf", value: "windsurf" },
566571
{ name: "Other / Manual Setup", value: "manual" },
567572
],
568573
validate: (arr) =>
@@ -700,9 +705,10 @@ export const setupMcpServer = async (): Promise<void> => {
700705
if (fileBasedTools.includes("cursor")) configuredTools.push("Cursor");
701706
if (fileBasedTools.includes("claude-desktop"))
702707
configuredTools.push("Claude Desktop");
708+
if (needsClaudeCode) configuredTools.push("Claude Code");
703709
if (fileBasedTools.includes("gemini-cli"))
704710
configuredTools.push("Gemini CLI");
705-
if (needsClaudeCode) configuredTools.push("Claude Code");
711+
if (fileBasedTools.includes("windsurf")) configuredTools.push("Windsurf");
706712
if (needsManual) configuredTools.push("your AI tool");
707713

708714
const toolsList =

src/utils/tool-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export function getCursorConfigPath(): string {
66
return path.join(os.homedir(), ".cursor", "mcp.json");
77
}
88

9+
export function getWindsurfConfigPath(): string {
10+
return path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json");
11+
}
12+
913
export function getClaudeDesktopConfigPath(): string {
1014
switch (process.platform) {
1115
case "darwin":

tests/unit/install.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ describe("MCP Server Setup Configuration", () => {
1212
expect(cursorPath).toContain("mcp.json");
1313
});
1414

15+
it("should generate correct Windsurf config path", () => {
16+
const windsurfPath = path.join(
17+
os.homedir(),
18+
".codeium",
19+
"windsurf",
20+
"mcp_config.json"
21+
);
22+
expect(windsurfPath).toContain(".codeium");
23+
expect(windsurfPath).toContain("windsurf");
24+
expect(windsurfPath).toContain("mcp_config.json");
25+
});
26+
1527
it("should generate correct Claude Desktop path on Darwin", () => {
1628
if (process.platform === "darwin") {
1729
const claudePath = path.join(
@@ -218,10 +230,14 @@ describe("MCP Server Setup Configuration", () => {
218230

219231
// Simulate what each tool would get
220232
const cursorConfig = { mcpServers: { iterable: baseConfig } };
233+
const windsurfConfig = { mcpServers: { iterable: baseConfig } };
221234
const claudeDesktopConfig = { mcpServers: { iterable: baseConfig } };
222235
const claudeCodeConfig = baseConfig; // Claude Code uses JSON directly
223236

224-
// All should have the same iterable config
237+
// All file-based tools should have the same iterable config
238+
expect(cursorConfig.mcpServers.iterable).toEqual(
239+
windsurfConfig.mcpServers.iterable
240+
);
225241
expect(cursorConfig.mcpServers.iterable).toEqual(
226242
claudeDesktopConfig.mcpServers.iterable
227243
);
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, expect, it } from "@jest/globals";
2+
import os from "os";
3+
import path from "path";
4+
5+
import {
6+
getClaudeDesktopConfigPath,
7+
getCursorConfigPath,
8+
getWindsurfConfigPath,
9+
} from "../../src/utils/tool-config.js";
10+
11+
describe("tool-config paths", () => {
12+
describe("getCursorConfigPath", () => {
13+
it("returns path in home directory .cursor folder", () => {
14+
const configPath = getCursorConfigPath();
15+
expect(configPath).toBe(path.join(os.homedir(), ".cursor", "mcp.json"));
16+
});
17+
18+
it("returns a path ending with mcp.json", () => {
19+
const configPath = getCursorConfigPath();
20+
expect(configPath).toMatch(/mcp\.json$/);
21+
});
22+
});
23+
24+
describe("getWindsurfConfigPath", () => {
25+
it("returns path in home directory .codeium/windsurf folder", () => {
26+
const configPath = getWindsurfConfigPath();
27+
expect(configPath).toBe(
28+
path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json")
29+
);
30+
});
31+
32+
it("returns a path ending with mcp_config.json", () => {
33+
const configPath = getWindsurfConfigPath();
34+
expect(configPath).toMatch(/mcp_config\.json$/);
35+
});
36+
37+
it("returns a path containing .codeium", () => {
38+
const configPath = getWindsurfConfigPath();
39+
expect(configPath).toContain(".codeium");
40+
});
41+
42+
it("returns a path containing windsurf", () => {
43+
const configPath = getWindsurfConfigPath();
44+
expect(configPath).toContain("windsurf");
45+
});
46+
});
47+
48+
describe("getClaudeDesktopConfigPath", () => {
49+
it("returns a path ending with claude_desktop_config.json", () => {
50+
const configPath = getClaudeDesktopConfigPath();
51+
expect(configPath).toMatch(/claude_desktop_config\.json$/);
52+
});
53+
54+
it("returns platform-specific path", () => {
55+
const configPath = getClaudeDesktopConfigPath();
56+
57+
switch (process.platform) {
58+
case "darwin":
59+
expect(configPath).toContain("Library");
60+
expect(configPath).toContain("Application Support");
61+
expect(configPath).toContain("Claude");
62+
break;
63+
case "win32":
64+
expect(configPath).toContain("Claude");
65+
break;
66+
default:
67+
// Linux and others use XDG_CONFIG_HOME or ~/.config
68+
expect(configPath).toContain("Claude");
69+
break;
70+
}
71+
});
72+
});
73+
74+
describe("path consistency", () => {
75+
it("all config paths are absolute", () => {
76+
expect(path.isAbsolute(getCursorConfigPath())).toBe(true);
77+
expect(path.isAbsolute(getWindsurfConfigPath())).toBe(true);
78+
expect(path.isAbsolute(getClaudeDesktopConfigPath())).toBe(true);
79+
});
80+
81+
it("all config paths end with .json", () => {
82+
expect(getCursorConfigPath()).toMatch(/\.json$/);
83+
expect(getWindsurfConfigPath()).toMatch(/\.json$/);
84+
expect(getClaudeDesktopConfigPath()).toMatch(/\.json$/);
85+
});
86+
});
87+
});

0 commit comments

Comments
 (0)