diff --git a/surfsense_web/app/(home)/mcp-server/page.tsx b/surfsense_web/app/(home)/mcp-server/page.tsx index 6dfc69622..5e4961b1e 100644 --- a/surfsense_web/app/(home)/mcp-server/page.tsx +++ b/surfsense_web/app/(home)/mcp-server/page.tsx @@ -50,16 +50,13 @@ export const metadata: Metadata = { }, }; -/* Mirrors surfsense_mcp/README.md: the real Cursor config. */ +/* The hosted Cursor config; mirrors lib/mcp/clients.ts. */ const CURSOR_CONFIG = `{ "mcpServers": { "surfsense": { - "command": "uv", - "args": ["run", "--directory", ".../surfsense_mcp", - "python", "-m", "surfsense_mcp"], - "env": { - "SURFSENSE_BASE_URL": "https://api.surfsense.com", - "SURFSENSE_API_KEY": "ss_pat_..." + "url": "https://mcp.surfsense.com/mcp", + "headers": { + "Authorization": "Bearer ss_pat_..." } } } @@ -76,7 +73,7 @@ const STEPS = [ icon: TerminalSquare, title: "Add the server to your client", description: - "Drop the config into Cursor's mcp.json, run claude mcp add for Claude Code, or paste it into Claude Desktop. Point it at the cloud or your own self-hosted instance.", + "Point your client at https://mcp.surfsense.com/mcp with your key in an Authorization header — the hosted config for Cursor, Claude Code, and others is one paste. Prefer stdio? Switch to Self-host and run it against your own backend.", }, { icon: Server, @@ -135,7 +132,7 @@ const FAQ: FaqItem[] = [ { question: "Which MCP clients does it work with?", answer: - "Any MCP client that supports stdio servers. Claude Code, Codex, OpenCode, Cursor, Claude Desktop, VS Code, Windsurf, and Gemini CLI are documented with copy-paste configs on this page, and the same command works in custom agent harnesses built on the MCP SDK.", + "Any MCP client that speaks remote (streamable HTTP) or stdio. Claude Code, Codex, OpenCode, Cursor, Claude Desktop, VS Code, Windsurf, and Gemini CLI all have copy-paste configs on this page — Hosted for the one-paste https://mcp.surfsense.com/mcp endpoint, or Self-host for stdio against your own backend.", }, { question: "How is usage billed?", @@ -278,9 +275,9 @@ export default function McpServerPage() { Step-by-step setup for every agent

- Pick your client, follow its two steps, and paste the config. Replace the placeholder - path with your surfsense_mcp checkout and the key with one from API Playground → API - Keys — or grab a pre-filled config from the playground itself. + Pick your client, choose Hosted or Self-host, and + paste the config. Replace the key with one from API Playground → API Keys — or grab a + pre-filled config from the playground itself.

diff --git a/surfsense_web/components/connectors-marketing/api-mcp-tabs.tsx b/surfsense_web/components/connectors-marketing/api-mcp-tabs.tsx index 6ca19c47b..f25faf95b 100644 --- a/surfsense_web/components/connectors-marketing/api-mcp-tabs.tsx +++ b/surfsense_web/components/connectors-marketing/api-mcp-tabs.tsx @@ -206,7 +206,7 @@ function buildMcp({ mcpTool }: ApiSample): string { const config = { mcpServers: { surfsense: { - url: "https://mcp.surfsense.com", + url: "https://mcp.surfsense.com/mcp", headers: { Authorization: "Bearer ${SURFSENSE_API_KEY}" }, }, }, diff --git a/surfsense_web/components/mcp/agent-setup-tabs.tsx b/surfsense_web/components/mcp/agent-setup-tabs.tsx index 1701b8924..3790441f2 100644 --- a/surfsense_web/components/mcp/agent-setup-tabs.tsx +++ b/surfsense_web/components/mcp/agent-setup-tabs.tsx @@ -9,6 +9,8 @@ import { DEFAULT_SERVER_DIR, MCP_CLIENTS, type McpSnippetOptions, + type McpTransport, + REMOTE_URL, } from "@/lib/mcp/clients"; function CopyButton({ text }: { text: string }) { @@ -38,48 +40,82 @@ function CopyButton({ text }: { text: string }) { ); } +const TRANSPORTS: { id: McpTransport; label: string; hint: string }[] = [ + { id: "remote", label: "Hosted", hint: "mcp.surfsense.com — nothing to install" }, + { id: "stdio", label: "Self-host", hint: "run the server against your own backend" }, +]; + /** - * Per-agent MCP setup instructions as tabs: pick a client, follow its steps, - * copy its exact config. Used on the /mcp-server marketing page and in the - * API playground; `options` fills in real values where the caller has them. + * Per-agent MCP setup instructions as tabs: pick a client, then Hosted or + * Self-host, and copy its exact config. Used on the /mcp-server marketing page + * and in the API playground; `options` fills in real values where the caller + * has them. */ export function AgentSetupTabs({ options }: { options?: Partial }) { + const [transport, setTransport] = useState("remote"); + const resolved: McpSnippetOptions = { - baseUrl: options?.baseUrl || "https://api.surfsense.com", + remoteUrl: options?.remoteUrl || REMOTE_URL, apiKey: options?.apiKey || API_KEY_PLACEHOLDER, + baseUrl: options?.baseUrl || "https://api.surfsense.com", serverDir: options?.serverDir || DEFAULT_SERVER_DIR, }; + const active = TRANSPORTS.find((t) => t.id === transport) ?? TRANSPORTS[0]; + return ( - - - {MCP_CLIENTS.map((client) => ( - - {client.label} - - ))} - - {MCP_CLIENTS.map((client) => { - const config = client.buildConfig(resolved); - return ( - -
    - {client.steps.map((step) => ( -
  1. {step}
  2. - ))} -
-
-

{client.configFile}

-
- -
-									{config}
-								
+
+
+
+ {TRANSPORTS.map((t) => ( + + ))} +
+ {active.hint} +
+ + + + {MCP_CLIENTS.map((client) => ( + + {client.label} + + ))} + + {MCP_CLIENTS.map((client) => { + const snippet = client[transport]; + const config = snippet.build(resolved); + return ( + +
    + {snippet.steps.map((step) => ( +
  1. {step}
  2. + ))} +
+
+

+ {snippet.configFile} +

+
+ +
+										{config}
+									
+
-
- - ); - })} - + + ); + })} + +
); } diff --git a/surfsense_web/lib/mcp/clients.ts b/surfsense_web/lib/mcp/clients.ts index 923e99049..795832ad1 100644 --- a/surfsense_web/lib/mcp/clients.ts +++ b/surfsense_web/lib/mcp/clients.ts @@ -1,42 +1,75 @@ /** - * MCP client setup catalog: one entry per popular agent, with the exact - * config file, steps, and snippet needed to connect the SurfSense MCP server. - * Shared by the marketing /mcp-server page and the API playground so the - * instructions can never drift apart. + * MCP client setup catalog: one entry per popular agent, each with a hosted + * (remote) snippet and a self-host (stdio) snippet, plus the exact config file + * and steps. Shared by the marketing /mcp-server page and the API playground so + * the instructions can never drift apart. + * + * Remote snippets point at the hosted server and pass the key as a Bearer token; + * every client's exact remote field is verified against its own docs (Windsurf + * uses `serverUrl`, Gemini CLI `httpUrl`, VS Code needs `type: "http"`, OpenCode + * `type: "remote"` + `oauth: false`, Codex needs the rmcp flag, and Claude + * Desktop has no config-file remote support so it uses the `mcp-remote` bridge). */ +export type McpTransport = "remote" | "stdio"; + export interface McpSnippetOptions { - /** SurfSense backend URL the server should call. */ - baseUrl: string; + /** Hosted MCP endpoint (Bearer-authenticated). */ + remoteUrl: string; /** API key value or placeholder to show in the snippet. */ apiKey: string; - /** Absolute path to the surfsense_mcp directory. */ + /** SurfSense backend URL a self-hosted server should call. */ + baseUrl: string; + /** Absolute path to the surfsense_mcp directory (self-host). */ serverDir: string; } +export interface McpSnippet { + /** Where the snippet goes: a file path or "Terminal". */ + configFile: string; + language: "json" | "toml" | "bash"; + steps: string[]; + build: (options: McpSnippetOptions) => string; +} + export interface McpClient { id: string; label: string; - /** Where the snippet goes: a file path or "Terminal". */ - configFile: string; - language: "json" | "toml" | "bash"; - steps: string[]; - buildConfig: (options: McpSnippetOptions) => string; + remote: McpSnippet; + stdio: McpSnippet; } +export const REMOTE_URL = "https://mcp.surfsense.com/mcp"; export const DEFAULT_SERVER_DIR = "/path/to/SurfSense/surfsense_mcp"; export const API_KEY_PLACEHOLDER = "ss_pat_your_key_here"; -function serverArgs(serverDir: string): string[] { - return ["run", "--directory", serverDir, "python", "-m", "surfsense_mcp"]; -} - function json(value: unknown): string { return JSON.stringify(value, null, 2); } -/** The `mcpServers` JSON shape shared by Cursor, Claude Desktop, Windsurf, and Gemini CLI. */ -function standardJson({ baseUrl, apiKey, serverDir }: McpSnippetOptions): string { +function bearer(apiKey: string): string { + return `Bearer ${apiKey}`; +} + +function serverArgs(serverDir: string): string[] { + return ["run", "--directory", serverDir, "python", "-m", "surfsense_mcp"]; +} + +/** The `mcpServers` remote shape shared by Cursor, Windsurf, and Gemini CLI. */ +function remoteMcpServers(urlField: "url" | "serverUrl" | "httpUrl") { + return ({ remoteUrl, apiKey }: McpSnippetOptions): string => + json({ + mcpServers: { + surfsense: { + [urlField]: remoteUrl, + headers: { Authorization: bearer(apiKey) }, + }, + }, + }); +} + +/** The `mcpServers` stdio shape shared by Cursor, Claude Desktop, Windsurf, Gemini CLI. */ +function stdioMcpServers({ baseUrl, apiKey, serverDir }: McpSnippetOptions): string { return json({ mcpServers: { surfsense: { @@ -52,125 +85,255 @@ export const MCP_CLIENTS: McpClient[] = [ { id: "claude-code", label: "Claude Code", - configFile: "Terminal", - language: "bash", - steps: [ - "Run this command in a terminal (any directory).", - "Start Claude Code and run /mcp — surfsense should be listed as connected.", - ], - buildConfig: ({ baseUrl, apiKey, serverDir }) => - [ - "claude mcp add surfsense \\", - ` -e SURFSENSE_BASE_URL=${baseUrl} \\`, - ` -e SURFSENSE_API_KEY=${apiKey} \\`, - ` -- uv run --directory ${serverDir} python -m surfsense_mcp`, - ].join("\n"), + remote: { + configFile: "Terminal", + language: "bash", + steps: [ + "Run this command in a terminal (any directory).", + "Start Claude Code and run /mcp — surfsense should be listed as connected.", + ], + build: ({ remoteUrl, apiKey }) => + [ + `claude mcp add --transport http surfsense ${remoteUrl} \\`, + ` --header "Authorization: ${bearer(apiKey)}"`, + ].join("\n"), + }, + stdio: { + configFile: "Terminal", + language: "bash", + steps: [ + "Run this command in a terminal (any directory).", + "Start Claude Code and run /mcp — surfsense should be listed as connected.", + ], + build: ({ baseUrl, apiKey, serverDir }) => + [ + "claude mcp add surfsense \\", + ` -e SURFSENSE_BASE_URL=${baseUrl} \\`, + ` -e SURFSENSE_API_KEY=${apiKey} \\`, + ` -- uv run --directory ${serverDir} python -m surfsense_mcp`, + ].join("\n"), + }, }, { id: "codex", label: "Codex", - configFile: "~/.codex/config.toml", - language: "toml", - steps: [ - "Add this to ~/.codex/config.toml (or run `codex mcp add surfsense -- uv run --directory python -m surfsense_mcp`).", - "Restart Codex; `codex mcp list` should show surfsense.", - ], - buildConfig: ({ baseUrl, apiKey, serverDir }) => - [ - "[mcp_servers.surfsense]", - 'command = "uv"', - `args = ${JSON.stringify(serverArgs(serverDir))}`, - "", - "[mcp_servers.surfsense.env]", - `SURFSENSE_BASE_URL = "${baseUrl}"`, - `SURFSENSE_API_KEY = "${apiKey}"`, - ].join("\n"), + remote: { + configFile: "~/.codex/config.toml", + language: "toml", + steps: [ + "Add this to ~/.codex/config.toml. The rmcp flag must sit above every [mcp_servers.*] table.", + "Restart Codex; `codex mcp list` should show surfsense.", + ], + build: ({ remoteUrl, apiKey }) => + [ + "experimental_use_rmcp_client = true", + "", + "[mcp_servers.surfsense]", + `url = "${remoteUrl}"`, + "", + "[mcp_servers.surfsense.http_headers]", + `Authorization = "${bearer(apiKey)}"`, + ].join("\n"), + }, + stdio: { + configFile: "~/.codex/config.toml", + language: "toml", + steps: [ + "Add this to ~/.codex/config.toml (or run `codex mcp add surfsense -- uv run --directory python -m surfsense_mcp`).", + "Restart Codex; `codex mcp list` should show surfsense.", + ], + build: ({ baseUrl, apiKey, serverDir }) => + [ + "[mcp_servers.surfsense]", + 'command = "uv"', + `args = ${JSON.stringify(serverArgs(serverDir))}`, + "", + "[mcp_servers.surfsense.env]", + `SURFSENSE_BASE_URL = "${baseUrl}"`, + `SURFSENSE_API_KEY = "${apiKey}"`, + ].join("\n"), + }, }, { id: "opencode", label: "OpenCode", - configFile: "opencode.json", - language: "json", - steps: [ - "Add this to opencode.json in your project root (or ~/.config/opencode/opencode.json for all projects).", - "Note OpenCode's format: the key is `mcp`, the command is one array, and env vars go under `environment`.", - ], - buildConfig: ({ baseUrl, apiKey, serverDir }) => - json({ - $schema: "https://opencode.ai/config.json", - mcp: { - surfsense: { - type: "local", - command: ["uv", ...serverArgs(serverDir)], - enabled: true, - environment: { SURFSENSE_BASE_URL: baseUrl, SURFSENSE_API_KEY: apiKey }, + remote: { + configFile: "opencode.json", + language: "json", + steps: [ + "Add this to opencode.json in your project root (or ~/.config/opencode/opencode.json for all projects).", + "`oauth: false` tells OpenCode to use the Bearer key instead of starting an OAuth flow.", + ], + build: ({ remoteUrl, apiKey }) => + json({ + $schema: "https://opencode.ai/config.json", + mcp: { + surfsense: { + type: "remote", + url: remoteUrl, + enabled: true, + oauth: false, + headers: { Authorization: bearer(apiKey) }, + }, }, - }, - }), + }), + }, + stdio: { + configFile: "opencode.json", + language: "json", + steps: [ + "Add this to opencode.json in your project root (or ~/.config/opencode/opencode.json for all projects).", + "Note OpenCode's format: the key is `mcp`, the command is one array, and env vars go under `environment`.", + ], + build: ({ baseUrl, apiKey, serverDir }) => + json({ + $schema: "https://opencode.ai/config.json", + mcp: { + surfsense: { + type: "local", + command: ["uv", ...serverArgs(serverDir)], + enabled: true, + environment: { SURFSENSE_BASE_URL: baseUrl, SURFSENSE_API_KEY: apiKey }, + }, + }, + }), + }, }, { id: "cursor", label: "Cursor", - configFile: "~/.cursor/mcp.json", - language: "json", - steps: [ - "Add this to ~/.cursor/mcp.json (global, keeps the key out of your repo) or a project's .cursor/mcp.json.", - "Refresh the server in Cursor Settings → MCP; its 18 tools should appear.", - ], - buildConfig: standardJson, + remote: { + configFile: "~/.cursor/mcp.json", + language: "json", + steps: [ + "Add this to ~/.cursor/mcp.json (global, keeps the key out of your repo) or a project's .cursor/mcp.json.", + "Refresh the server in Cursor Settings → MCP; its 18 tools should appear.", + ], + build: remoteMcpServers("url"), + }, + stdio: { + configFile: "~/.cursor/mcp.json", + language: "json", + steps: [ + "Add this to ~/.cursor/mcp.json (global, keeps the key out of your repo) or a project's .cursor/mcp.json.", + "Refresh the server in Cursor Settings → MCP; its 18 tools should appear.", + ], + build: stdioMcpServers, + }, }, { id: "claude-desktop", label: "Claude Desktop", - configFile: "claude_desktop_config.json", - language: "json", - steps: [ - "Open Settings → Developer → Edit Config to reach claude_desktop_config.json and add this.", - "Restart Claude Desktop; surfsense appears under the tools icon.", - ], - buildConfig: standardJson, + remote: { + configFile: "claude_desktop_config.json", + language: "json", + steps: [ + "Claude Desktop can't take a remote URL directly, so this uses the mcp-remote bridge (needs Node 18+).", + "Open Settings → Developer → Edit Config, add this, and restart Claude Desktop.", + ], + build: ({ remoteUrl, apiKey }) => + json({ + mcpServers: { + surfsense: { + command: "npx", + args: ["-y", "mcp-remote", remoteUrl, "--header", `Authorization: ${bearer(apiKey)}`], + }, + }, + }), + }, + stdio: { + configFile: "claude_desktop_config.json", + language: "json", + steps: [ + "Open Settings → Developer → Edit Config to reach claude_desktop_config.json and add this.", + "Restart Claude Desktop; surfsense appears under the tools icon.", + ], + build: stdioMcpServers, + }, }, { id: "vscode", label: "VS Code", - configFile: ".vscode/mcp.json", - language: "json", - steps: [ - "Add this to .vscode/mcp.json in your workspace (or run the MCP: Add Server command).", - "Open Copilot Chat in agent mode and click the tools icon to confirm surfsense is loaded.", - ], - buildConfig: ({ baseUrl, apiKey, serverDir }) => - json({ - servers: { - surfsense: { - type: "stdio", - command: "uv", - args: serverArgs(serverDir), - env: { SURFSENSE_BASE_URL: baseUrl, SURFSENSE_API_KEY: apiKey }, + remote: { + configFile: ".vscode/mcp.json", + language: "json", + steps: [ + "Add this to .vscode/mcp.json in your workspace (or run the MCP: Add Server command).", + "VS Code requires an explicit `type` field — `http` for the hosted server.", + ], + build: ({ remoteUrl, apiKey }) => + json({ + servers: { + surfsense: { + type: "http", + url: remoteUrl, + headers: { Authorization: bearer(apiKey) }, + }, }, - }, - }), + }), + }, + stdio: { + configFile: ".vscode/mcp.json", + language: "json", + steps: [ + "Add this to .vscode/mcp.json in your workspace (or run the MCP: Add Server command).", + "Open Copilot Chat in agent mode and click the tools icon to confirm surfsense is loaded.", + ], + build: ({ baseUrl, apiKey, serverDir }) => + json({ + servers: { + surfsense: { + type: "stdio", + command: "uv", + args: serverArgs(serverDir), + env: { SURFSENSE_BASE_URL: baseUrl, SURFSENSE_API_KEY: apiKey }, + }, + }, + }), + }, }, { id: "windsurf", label: "Windsurf", - configFile: "~/.codeium/windsurf/mcp_config.json", - language: "json", - steps: [ - "Add this to ~/.codeium/windsurf/mcp_config.json (or Windsurf Settings → Cascade → MCP Servers).", - "Press the refresh button in the MCP panel to pick up the server.", - ], - buildConfig: standardJson, + remote: { + configFile: "~/.codeium/windsurf/mcp_config.json", + language: "json", + steps: [ + "Add this to ~/.codeium/windsurf/mcp_config.json (or Windsurf Settings → Cascade → MCP Servers).", + "Windsurf uses `serverUrl` (not `url`) for remote servers; press refresh in the MCP panel.", + ], + build: remoteMcpServers("serverUrl"), + }, + stdio: { + configFile: "~/.codeium/windsurf/mcp_config.json", + language: "json", + steps: [ + "Add this to ~/.codeium/windsurf/mcp_config.json (or Windsurf Settings → Cascade → MCP Servers).", + "Press the refresh button in the MCP panel to pick up the server.", + ], + build: stdioMcpServers, + }, }, { id: "gemini-cli", label: "Gemini CLI", - configFile: "~/.gemini/settings.json", - language: "json", - steps: [ - "Add this to ~/.gemini/settings.json (or .gemini/settings.json in a project).", - "Run /mcp inside Gemini CLI to confirm the surfsense server and its tools.", - ], - buildConfig: standardJson, + remote: { + configFile: "~/.gemini/settings.json", + language: "json", + steps: [ + "Add this to ~/.gemini/settings.json (or .gemini/settings.json in a project).", + "Gemini CLI uses `httpUrl` for streamable-HTTP servers; run /mcp to confirm surfsense.", + ], + build: remoteMcpServers("httpUrl"), + }, + stdio: { + configFile: "~/.gemini/settings.json", + language: "json", + steps: [ + "Add this to ~/.gemini/settings.json (or .gemini/settings.json in a project).", + "Run /mcp inside Gemini CLI to confirm the surfsense server and its tools.", + ], + build: stdioMcpServers, + }, }, ];