Skip to content

Commit 1c5b085

Browse files
committed
feat(web): add hosted and self-host MCP client configs
1 parent 9aef0c2 commit 1c5b085

4 files changed

Lines changed: 347 additions & 151 deletions

File tree

surfsense_web/app/(home)/mcp-server/page.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,13 @@ export const metadata: Metadata = {
5050
},
5151
};
5252

53-
/* Mirrors surfsense_mcp/README.md: the real Cursor config. */
53+
/* The hosted Cursor config; mirrors lib/mcp/clients.ts. */
5454
const CURSOR_CONFIG = `{
5555
"mcpServers": {
5656
"surfsense": {
57-
"command": "uv",
58-
"args": ["run", "--directory", ".../surfsense_mcp",
59-
"python", "-m", "surfsense_mcp"],
60-
"env": {
61-
"SURFSENSE_BASE_URL": "https://api.surfsense.com",
62-
"SURFSENSE_API_KEY": "ss_pat_..."
57+
"url": "https://mcp.surfsense.com/mcp",
58+
"headers": {
59+
"Authorization": "Bearer ss_pat_..."
6360
}
6461
}
6562
}
@@ -76,7 +73,7 @@ const STEPS = [
7673
icon: TerminalSquare,
7774
title: "Add the server to your client",
7875
description:
79-
"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.",
76+
"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.",
8077
},
8178
{
8279
icon: Server,
@@ -135,7 +132,7 @@ const FAQ: FaqItem[] = [
135132
{
136133
question: "Which MCP clients does it work with?",
137134
answer:
138-
"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.",
135+
"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.",
139136
},
140137
{
141138
question: "How is usage billed?",
@@ -278,9 +275,9 @@ export default function McpServerPage() {
278275
Step-by-step setup for every agent
279276
</h2>
280277
<p className="mt-3 max-w-2xl text-muted-foreground leading-relaxed">
281-
Pick your client, follow its two steps, and paste the config. Replace the placeholder
282-
path with your surfsense_mcp checkout and the key with one from API Playground → API
283-
Keys — or grab a pre-filled config from the playground itself.
278+
Pick your client, choose <strong>Hosted</strong> or <strong>Self-host</strong>, and
279+
paste the config. Replace the key with one from API Playground → API Keys — or grab a
280+
pre-filled config from the playground itself.
284281
</p>
285282
</Reveal>
286283
<Reveal>

surfsense_web/components/connectors-marketing/api-mcp-tabs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ function buildMcp({ mcpTool }: ApiSample): string {
206206
const config = {
207207
mcpServers: {
208208
surfsense: {
209-
url: "https://mcp.surfsense.com",
209+
url: "https://mcp.surfsense.com/mcp",
210210
headers: { Authorization: "Bearer ${SURFSENSE_API_KEY}" },
211211
},
212212
},

surfsense_web/components/mcp/agent-setup-tabs.tsx

Lines changed: 69 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
DEFAULT_SERVER_DIR,
1010
MCP_CLIENTS,
1111
type McpSnippetOptions,
12+
type McpTransport,
13+
REMOTE_URL,
1214
} from "@/lib/mcp/clients";
1315

1416
function CopyButton({ text }: { text: string }) {
@@ -38,48 +40,82 @@ function CopyButton({ text }: { text: string }) {
3840
);
3941
}
4042

43+
const TRANSPORTS: { id: McpTransport; label: string; hint: string }[] = [
44+
{ id: "remote", label: "Hosted", hint: "mcp.surfsense.com — nothing to install" },
45+
{ id: "stdio", label: "Self-host", hint: "run the server against your own backend" },
46+
];
47+
4148
/**
42-
* Per-agent MCP setup instructions as tabs: pick a client, follow its steps,
43-
* copy its exact config. Used on the /mcp-server marketing page and in the
44-
* API playground; `options` fills in real values where the caller has them.
49+
* Per-agent MCP setup instructions as tabs: pick a client, then Hosted or
50+
* Self-host, and copy its exact config. Used on the /mcp-server marketing page
51+
* and in the API playground; `options` fills in real values where the caller
52+
* has them.
4553
*/
4654
export function AgentSetupTabs({ options }: { options?: Partial<McpSnippetOptions> }) {
55+
const [transport, setTransport] = useState<McpTransport>("remote");
56+
4757
const resolved: McpSnippetOptions = {
48-
baseUrl: options?.baseUrl || "https://api.surfsense.com",
58+
remoteUrl: options?.remoteUrl || REMOTE_URL,
4959
apiKey: options?.apiKey || API_KEY_PLACEHOLDER,
60+
baseUrl: options?.baseUrl || "https://api.surfsense.com",
5061
serverDir: options?.serverDir || DEFAULT_SERVER_DIR,
5162
};
5263

64+
const active = TRANSPORTS.find((t) => t.id === transport) ?? TRANSPORTS[0];
65+
5366
return (
54-
<Tabs defaultValue={MCP_CLIENTS[0].id} className="w-full">
55-
<TabsList className="flex h-auto flex-wrap justify-start gap-1">
56-
{MCP_CLIENTS.map((client) => (
57-
<TabsTrigger key={client.id} value={client.id}>
58-
{client.label}
59-
</TabsTrigger>
60-
))}
61-
</TabsList>
62-
{MCP_CLIENTS.map((client) => {
63-
const config = client.buildConfig(resolved);
64-
return (
65-
<TabsContent key={client.id} value={client.id} className="space-y-3">
66-
<ol className="list-decimal space-y-1 pl-5 text-sm leading-relaxed text-muted-foreground">
67-
{client.steps.map((step) => (
68-
<li key={step}>{step}</li>
69-
))}
70-
</ol>
71-
<div>
72-
<p className="mb-1.5 font-mono text-xs text-muted-foreground">{client.configFile}</p>
73-
<div className="relative">
74-
<CopyButton text={config} />
75-
<pre className="overflow-x-auto rounded-lg border bg-muted/50 p-4 font-mono text-xs leading-relaxed">
76-
<code>{config}</code>
77-
</pre>
67+
<div className="space-y-4">
68+
<div className="flex flex-wrap items-center gap-3">
69+
<div className="inline-flex rounded-lg border bg-muted/40 p-0.5">
70+
{TRANSPORTS.map((t) => (
71+
<Button
72+
key={t.id}
73+
variant={t.id === transport ? "secondary" : "ghost"}
74+
size="sm"
75+
className="h-7 px-3 text-xs"
76+
onClick={() => setTransport(t.id)}
77+
aria-pressed={t.id === transport}
78+
>
79+
{t.label}
80+
</Button>
81+
))}
82+
</div>
83+
<span className="text-xs text-muted-foreground">{active.hint}</span>
84+
</div>
85+
86+
<Tabs defaultValue={MCP_CLIENTS[0].id} className="w-full">
87+
<TabsList className="flex h-auto flex-wrap justify-start gap-1">
88+
{MCP_CLIENTS.map((client) => (
89+
<TabsTrigger key={client.id} value={client.id}>
90+
{client.label}
91+
</TabsTrigger>
92+
))}
93+
</TabsList>
94+
{MCP_CLIENTS.map((client) => {
95+
const snippet = client[transport];
96+
const config = snippet.build(resolved);
97+
return (
98+
<TabsContent key={client.id} value={client.id} className="space-y-3">
99+
<ol className="list-decimal space-y-1 pl-5 text-sm leading-relaxed text-muted-foreground">
100+
{snippet.steps.map((step) => (
101+
<li key={step}>{step}</li>
102+
))}
103+
</ol>
104+
<div>
105+
<p className="mb-1.5 font-mono text-xs text-muted-foreground">
106+
{snippet.configFile}
107+
</p>
108+
<div className="relative">
109+
<CopyButton text={config} />
110+
<pre className="overflow-x-auto rounded-lg border bg-muted/50 p-4 font-mono text-xs leading-relaxed">
111+
<code>{config}</code>
112+
</pre>
113+
</div>
78114
</div>
79-
</div>
80-
</TabsContent>
81-
);
82-
})}
83-
</Tabs>
115+
</TabsContent>
116+
);
117+
})}
118+
</Tabs>
119+
</div>
84120
);
85121
}

0 commit comments

Comments
 (0)