diff --git a/src/extension/ui/src/assets/windsurf.svg b/src/extension/ui/src/assets/windsurf.svg new file mode 100644 index 00000000..15789639 --- /dev/null +++ b/src/extension/ui/src/assets/windsurf.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/extension/ui/src/components/tabs/YourClients.tsx b/src/extension/ui/src/components/tabs/YourClients.tsx index d95a0904..20be99f8 100644 --- a/src/extension/ui/src/components/tabs/YourClients.tsx +++ b/src/extension/ui/src/components/tabs/YourClients.tsx @@ -23,6 +23,7 @@ import ChatGPTIcon from '../../assets/chatgpt.svg'; import ClaudeIcon from '../../assets/claude-ai-icon.svg'; import CursorIcon from '../../assets/cursor.svg'; import GordonIcon from '../../assets/gordon-icon.png'; +import WindsurfIcon from '../../assets/windsurf.svg'; import { CATALOG_LAYOUT_SX, DOCKER_MCP_COMMAND } from '../../Constants'; // Initialize the Docker Desktop client @@ -36,6 +37,7 @@ const iconMap = { 'Claude Desktop': ClaudeIcon, Gordon: GordonIcon, Cursor: CursorIcon, + Windsurf: WindsurfIcon, }; const MCPClientSettings = ({ appProps }: MCPClientSettingsProps) => { diff --git a/src/extension/ui/src/mcp-clients/Windsurf.ts b/src/extension/ui/src/mcp-clients/Windsurf.ts new file mode 100644 index 00000000..b273f73e --- /dev/null +++ b/src/extension/ui/src/mcp-clients/Windsurf.ts @@ -0,0 +1,119 @@ +import { v1 } from "@docker/extension-api-client-types"; +import { escapeJSONForPlatformShell, getUser } from "../FileUtils"; +import { MCPClient, SAMPLE_MCP_CONFIG } from "./MCPTypes"; +import { DOCKER_MCP_COMMAND } from "../Constants"; +import { mergeDeep } from "../MergeDeep"; + +class WindsurfDesktopClient implements MCPClient { + name = 'Windsurf'; + url = 'https://windsurf.com/downloads'; + manualConfigSteps = [ + 'Open Windsurf Settings', + 'Click on the MCP tab', + 'Click on the Add new MCP server button', + 'Set name: MCP_DOCKER', + 'Set command:
' +
+        DOCKER_MCP_COMMAND +
+        '
' + ]; + expectedConfigPath = { + darwin: '$HOME/.codeium/mcp_config.json', + linux: '$HOME/.codeium/mcp_config.json', + win32: '$USERPROFILE\\.codeium\\mcp_config.json' + }; + readConfig = async (client: v1.DockerDesktopClient) => { + const platform = client.host.platform as keyof typeof this.expectedConfigPath; + const configPath = this.expectedConfigPath[platform].replace('$USER', await getUser(client)); + try { + const result = await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source=${configPath},target=/codeium_config/mcp_config.json`, 'alpine:latest', 'cat', '/codeium_config/mcp_config.json']); + return { + content: result.stdout, + path: configPath + }; + } catch (e) { + return { + content: null, + path: configPath + }; + } + }; + connect = async (client: v1.DockerDesktopClient) => { + const config = await this.readConfig(client); + let windsurfConfig = null; + try { + windsurfConfig = JSON.parse(config.content || '{}') as typeof SAMPLE_MCP_CONFIG; + if (windsurfConfig.mcpServers?.MCP_DOCKER) { + client.desktopUI.toast.success('Windsurf MCP server already connected.'); + return; + } + } catch (e) { + windsurfConfig = mergeDeep({}, SAMPLE_MCP_CONFIG); + } + const payload = mergeDeep(windsurfConfig, SAMPLE_MCP_CONFIG); + try { + await client.docker.cli.exec('run', + [ + '--rm', + '--mount', + `type=bind,source="${config.path}",target=/codeium_config/mcp_config.json`, + '--workdir', + '/codeium_config', 'vonwig/function_write_files:latest', + escapeJSONForPlatformShell({ files: [{ path: 'mcp_config.json', content: JSON.stringify(payload) }] }, client.host.platform) + ] + ); + client.desktopUI.toast.success('Connected Docker MCP Server to Windsurf.'); + } catch (e) { + if ((e as any).stderr) { + client.desktopUI.toast.error((e as any).stderr); + } else { + client.desktopUI.toast.error((e as Error).message); + } + } + }; + disconnect = async (client: v1.DockerDesktopClient) => { + const config = await this.readConfig(client); + if (!config.content) { + client.desktopUI.toast.error('No config found'); + return; + } + let windsurfConfig = null; + try { + windsurfConfig = JSON.parse(config.content) as typeof SAMPLE_MCP_CONFIG; + if (!windsurfConfig.mcpServers?.MCP_DOCKER) { + client.desktopUI.toast.error('Docker MCP Server not connected to Windsurf'); + return; + } + } catch (e) { + client.desktopUI.toast.error('Failed to disconnect. Invalid Windsurf config found at ' + config.path); + return; + } + const payload = { + ...windsurfConfig, + mcpServers: Object.fromEntries(Object.entries(windsurfConfig.mcpServers).filter(([key]) => key !== 'MCP_DOCKER')) + }; + try { + await client.docker.cli.exec('run', + [ + '--rm', + '--mount', + `type=bind,source="${config.path}",target=/codeium_config/mcp_config.json`, + '--workdir', + '/codeium_config', 'vonwig/function_write_files:latest', + escapeJSONForPlatformShell({ files: [{ path: 'mcp_config.json', content: JSON.stringify(payload) }] }, client.host.platform) + ] + ); + } catch (e) { + if ((e as any).stderr) { + client.desktopUI.toast.error((e as any).stderr); + } else { + client.desktopUI.toast.error((e as Error).message); + } + } + }; + validateConfig = (content: string) => { + const config = JSON.parse(content || '{}') as typeof SAMPLE_MCP_CONFIG; + return !!config.mcpServers?.MCP_DOCKER; + }; +} + +export default new WindsurfDesktopClient(); diff --git a/src/extension/ui/src/mcp-clients/index.ts b/src/extension/ui/src/mcp-clients/index.ts index 101c19d5..a67f5ec7 100644 --- a/src/extension/ui/src/mcp-clients/index.ts +++ b/src/extension/ui/src/mcp-clients/index.ts @@ -8,10 +8,12 @@ import Cursor from "./Cursor"; import ClaudeDesktop from "./ClaudeDesktop"; import Gordon from "./Gordon"; +import Windsurf from "./Windsurf"; import { MCPClient } from "./MCPTypes"; export const SUPPORTED_MCP_CLIENTS: MCPClient[] = [ Gordon, ClaudeDesktop, Cursor, + Windsurf, ] \ No newline at end of file