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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.8.0] - 2026-04-12

### Added

- Windsurf IDE as a supported platform in `npx multicorn-proxy init`. Generates a proxy config and prints an `~/.codeium/windsurf/mcp_config.json` snippet using the Windsurf `mcpServers` / `serverUrl` schema.
- Auto-detection of existing Windsurf proxy entries (shows "● detected locally" in the platform selection list).

### Changed

- Next Steps block for Cursor and Windsurf rewritten as clear three-step numbered actions: download the IDE if needed, paste the config snippet, restart. Previous copy ("Config file: ...", "Restart Cursor to pick up MCP config changes") gave no guidance to first-time users.

## [0.7.0] - 2026-04-11

### Added
Expand Down
88 changes: 70 additions & 18 deletions src/proxy/__tests__/proxy.edge-cases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,58 @@ describe("config file parsing", () => {
expect(stderrBuffer).toContain("Bearer mcs_valid_key");
});

it("runInit completes Windsurf platform with proxy URL and Windsurf next steps", async () => {
captureStderr();
writeFileMock.mockResolvedValue(undefined);
mkdirMock.mockResolvedValue(undefined);
readFileMock.mockImplementation((path: string) =>
path.includes(".openclaw")
? Promise.resolve(MINIMAL_OPENCLAW_JSON)
: Promise.reject(new Error("ENOENT")),
);
global.fetch = vi.fn().mockImplementation((input: unknown) => {
const url = typeof input === "string" ? input : String(input);
if (url.includes("/api/v1/proxy/config")) {
return Promise.resolve({
ok: true,
status: 200,
json: () =>
Promise.resolve({
data: { proxy_url: "https://hosted.proxy.example/mcp" },
}),
});
}
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve({}),
});
});

mockPrompts({
"API key": "mcs_valid_key",
Select: "4",
"call this agent": "windsurf-agent",
"URL:": "https://upstream.example/mcp",
"Short name": "myproxy",
"Connect another": "n",
});

const config = await runInit("https://api.multicorn.ai");

expect(config).not.toBeNull();
if (!config) throw new Error("expected config");
expect(config.agents?.[0]?.platform).toBe("windsurf");
expect(config.defaultAgent).toBe("windsurf-agent");
expect(stderrBuffer).toContain("serverUrl");
expect(stderrBuffer).toContain("mcpServers");
expect(stderrBuffer).toContain("hosted.proxy.example");
expect(stderrBuffer).toContain("Bearer mcs_valid_key");
expect(stderrBuffer).toContain("To complete your Windsurf setup");
expect(stderrBuffer).toContain("~/.codeium/windsurf/mcp_config.json");
expect(stderrBuffer).toContain("Restart Windsurf");
});

// --- Platform detection labels ---

it("runInit shows 'detected locally' with dot icon when a platform is detected", async () => {
Expand Down Expand Up @@ -938,7 +990,7 @@ describe("config file parsing", () => {

mockPrompts({
"API key": "mcs_valid_key",
Select: "4",
Select: "5",
"Connect another": "n",
});

Expand All @@ -952,9 +1004,9 @@ describe("config file parsing", () => {
expect(openclawLine).not.toContain("detected locally");
});

// --- Option 4: Local MCP / Other ---
// --- Option 5: Local MCP / Other ---

it("runInit option 4 writes config with apiKey and baseUrl only (no agents, no defaultAgent)", async () => {
it("runInit option 5 writes config with apiKey and baseUrl only (no agents, no defaultAgent)", async () => {
captureStderr();
writeFileMock.mockResolvedValue(undefined);
mkdirMock.mockResolvedValue(undefined);
Expand All @@ -963,7 +1015,7 @@ describe("config file parsing", () => {

mockPrompts({
"API key": "mcs_valid_key",
Select: "4",
Select: "5",
"Connect another": "n",
});

Expand All @@ -989,7 +1041,7 @@ describe("config file parsing", () => {
expect(written["platform"]).toBeUndefined();
});

it("runInit option 4 does not prompt for target URL", async () => {
it("runInit option 5 does not prompt for target URL", async () => {
captureStderr();
writeFileMock.mockResolvedValue(undefined);
mkdirMock.mockResolvedValue(undefined);
Expand All @@ -998,7 +1050,7 @@ describe("config file parsing", () => {

mockPrompts({
"API key": "mcs_valid_key",
Select: "4",
Select: "5",
"Connect another": "n",
});

Expand All @@ -1011,7 +1063,7 @@ describe("config file parsing", () => {
expect(hasUrlPrompt).toBe(false);
});

it("runInit option 4 does not prompt for agent name", async () => {
it("runInit option 5 does not prompt for agent name", async () => {
captureStderr();
writeFileMock.mockResolvedValue(undefined);
mkdirMock.mockResolvedValue(undefined);
Expand All @@ -1020,7 +1072,7 @@ describe("config file parsing", () => {

mockPrompts({
"API key": "mcs_valid_key",
Select: "4",
Select: "5",
"Connect another": "n",
});

Expand All @@ -1031,7 +1083,7 @@ describe("config file parsing", () => {
expect(hasAgentNamePrompt).toBe(false);
});

it("runInit option 4 does not call createProxyConfig (no /api/v1/proxy/config POST)", async () => {
it("runInit option 5 does not call createProxyConfig (no /api/v1/proxy/config POST)", async () => {
captureStderr();
writeFileMock.mockResolvedValue(undefined);
mkdirMock.mockResolvedValue(undefined);
Expand All @@ -1041,7 +1093,7 @@ describe("config file parsing", () => {

mockPrompts({
"API key": "mcs_valid_key",
Select: "4",
Select: "5",
"Connect another": "n",
});

Expand All @@ -1053,7 +1105,7 @@ describe("config file parsing", () => {
expect(proxyConfigCalls).toHaveLength(0);
});

it("runInit option 4 prints the --wrap example command in success message", async () => {
it("runInit option 5 prints the --wrap example command in success message", async () => {
captureStderr();
writeFileMock.mockResolvedValue(undefined);
mkdirMock.mockResolvedValue(undefined);
Expand All @@ -1062,7 +1114,7 @@ describe("config file parsing", () => {

mockPrompts({
"API key": "mcs_valid_key",
Select: "4",
Select: "5",
"Connect another": "n",
});

Expand All @@ -1072,7 +1124,7 @@ describe("config file parsing", () => {
expect(stderrBuffer).toContain("@modelcontextprotocol/server-filesystem");
});

it("runInit option 4 config is loadable by loadConfig", async () => {
it("runInit option 5 config is loadable by loadConfig", async () => {
captureStderr();
writeFileMock.mockResolvedValue(undefined);
mkdirMock.mockResolvedValue(undefined);
Expand All @@ -1081,7 +1133,7 @@ describe("config file parsing", () => {

mockPrompts({
"API key": "mcs_valid_key",
Select: "4",
Select: "5",
"Connect another": "n",
});

Expand All @@ -1103,7 +1155,7 @@ describe("config file parsing", () => {
expect(loaded.baseUrl).toBe("https://api.multicorn.ai");
});

it("runInit option 4 summary does not render a trailing dash", async () => {
it("runInit option 5 summary does not render a trailing dash", async () => {
captureStderr();
writeFileMock.mockResolvedValue(undefined);
mkdirMock.mockResolvedValue(undefined);
Expand All @@ -1112,7 +1164,7 @@ describe("config file parsing", () => {

mockPrompts({
"API key": "mcs_valid_key",
Select: "4",
Select: "5",
"Connect another": "n",
});

Expand Down Expand Up @@ -1148,7 +1200,7 @@ describe("config file parsing", () => {
expect(plain).toContain("OpenClaw - my-oc-agent");
});

it("runInit option 4 does not print a Next steps block", async () => {
it("runInit option 5 does not print a Next steps block", async () => {
captureStderr();
writeFileMock.mockResolvedValue(undefined);
mkdirMock.mockResolvedValue(undefined);
Expand All @@ -1157,7 +1209,7 @@ describe("config file parsing", () => {

mockPrompts({
"API key": "mcs_valid_key",
Select: "4",
Select: "5",
"Connect another": "n",
});

Expand Down
Loading
Loading