feat(web): add hosted and self-host MCP client configs

This commit is contained in:
CREDO23 2026-07-07 18:06:04 +02:00
parent 6da1dd8f86
commit 6b204a2f8c
4 changed files with 350 additions and 154 deletions

View file

@ -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
</h2>
<p className="mt-3 max-w-2xl text-muted-foreground leading-relaxed">
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 <strong>Hosted</strong> or <strong>Self-host</strong>, and
paste the config. Replace the key with one from API Playground API Keys or grab a
pre-filled config from the playground itself.
</p>
</Reveal>
<Reveal>

View file

@ -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}" },
},
},

View file

@ -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<McpSnippetOptions> }) {
const [transport, setTransport] = useState<McpTransport>("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 (
<Tabs defaultValue={MCP_CLIENTS[0].id} className="w-full">
<TabsList className="flex h-auto flex-wrap justify-start gap-1">
{MCP_CLIENTS.map((client) => (
<TabsTrigger key={client.id} value={client.id}>
{client.label}
</TabsTrigger>
))}
</TabsList>
{MCP_CLIENTS.map((client) => {
const config = client.buildConfig(resolved);
return (
<TabsContent key={client.id} value={client.id} className="space-y-3">
<ol className="list-decimal space-y-1 pl-5 text-sm leading-relaxed text-muted-foreground">
{client.steps.map((step) => (
<li key={step}>{step}</li>
))}
</ol>
<div>
<p className="mb-1.5 font-mono text-xs text-muted-foreground">{client.configFile}</p>
<div className="relative">
<CopyButton text={config} />
<pre className="overflow-x-auto rounded-lg border bg-muted/50 p-4 font-mono text-xs leading-relaxed">
<code>{config}</code>
</pre>
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-3">
<div className="inline-flex rounded-lg border bg-muted/40 p-0.5">
{TRANSPORTS.map((t) => (
<Button
key={t.id}
variant={t.id === transport ? "secondary" : "ghost"}
size="sm"
className="h-7 px-3 text-xs"
onClick={() => setTransport(t.id)}
aria-pressed={t.id === transport}
>
{t.label}
</Button>
))}
</div>
<span className="text-xs text-muted-foreground">{active.hint}</span>
</div>
<Tabs defaultValue={MCP_CLIENTS[0].id} className="w-full">
<TabsList className="flex h-auto flex-wrap justify-start gap-1">
{MCP_CLIENTS.map((client) => (
<TabsTrigger key={client.id} value={client.id}>
{client.label}
</TabsTrigger>
))}
</TabsList>
{MCP_CLIENTS.map((client) => {
const snippet = client[transport];
const config = snippet.build(resolved);
return (
<TabsContent key={client.id} value={client.id} className="space-y-3">
<ol className="list-decimal space-y-1 pl-5 text-sm leading-relaxed text-muted-foreground">
{snippet.steps.map((step) => (
<li key={step}>{step}</li>
))}
</ol>
<div>
<p className="mb-1.5 font-mono text-xs text-muted-foreground">
{snippet.configFile}
</p>
<div className="relative">
<CopyButton text={config} />
<pre className="overflow-x-auto rounded-lg border bg-muted/50 p-4 font-mono text-xs leading-relaxed">
<code>{config}</code>
</pre>
</div>
</div>
</div>
</TabsContent>
);
})}
</Tabs>
</TabsContent>
);
})}
</Tabs>
</div>
);
}

View file

@ -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 <dir> 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 <dir> 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,
},
},
];