Skip to content

Commit 6527e6f

Browse files
committed
fix(dashboard): safer MCP setup prompt, admin-aware key step, self-hosted snippets, and shared setup pieces
The agent prompt keeps the key out of committed files and replies, asks for an existing key instead of a new one, names the self-hosted ingestion URL, covers AWS Lambda with flushMcp and uses the Inspector CLI. Non-admins see who can create a key instead of a button that always fails, self-hosted snippets pass apiUrl, the privacy line names successful results, progress and the last step follow the copy, and collapsed setup rows are inert. The agent buttons and highlighted snippet block are shared with onboarding's ConnectApp.
1 parent 9f1b773 commit 6527e6f

4 files changed

Lines changed: 179 additions & 189 deletions

File tree

‎apps/dashboard/app/(main)/mcp/page.tsx‎

Lines changed: 57 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,19 @@ import {
1616
CaretRightIcon,
1717
CaretUpDownIcon,
1818
ChartBarIcon,
19-
CheckIcon,
2019
OpenExternalIcon,
2120
PlugIcon,
2221
XMarkIcon,
2322
} from "@databuddy/ui/icons";
23+
import { isSelfHosted, publicConfig } from "@databuddy/env/public";
2424
import {
2525
keepPreviousData,
2626
useMutation,
27+
useQuery,
2728
useQueryClient,
2829
} from "@tanstack/react-query";
2930
import { parseAsString, useQueryStates } from "nuqs";
30-
import { Suspense, useMemo, useState } from "react";
31-
import { createHighlighterCoreSync } from "shiki/core";
32-
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
33-
import bash from "shiki/langs/bash.mjs";
34-
import json from "shiki/langs/json.mjs";
35-
import tsx from "shiki/langs/tsx.mjs";
36-
import vesper from "shiki/themes/vesper.mjs";
31+
import { Suspense, useState } from "react";
3732
import { toast } from "sonner";
3833
import {
3934
CODING_AGENTS,
@@ -47,6 +42,10 @@ import { AiProductIcon } from "@/components/icon";
4742
import { TopBar } from "@/components/layout/top-bar";
4843
import { useOrganizationsContext } from "@/components/providers/organizations-provider";
4944
import { List } from "@/components/ui/composables/list";
45+
import {
46+
CodingAgentButtons,
47+
SnippetBlock,
48+
} from "@/components/websites/connect-app";
5049
import { SetupRow, type SetupRowStatus } from "@/components/websites/setup-row";
5150
import { useChartPreferences } from "@/hooks/use-chart-preferences";
5251
import { useDateFilters } from "@/hooks/use-date-filters";
@@ -121,14 +120,10 @@ const FILTERS = {
121120
website_id: parseAsString,
122121
};
123122

124-
const highlighter = createHighlighterCoreSync({
125-
themes: [vesper],
126-
langs: [bash, json, tsx],
127-
engine: createJavaScriptRegexEngine(),
128-
});
129-
130123
const INSTALL_COMMAND = "bun add @databuddy/sdk@latest";
131124
const KEY_PLACEHOLDER = "dbdy_your_key";
125+
const API_URL = JSON.stringify(publicConfig.urls.basket);
126+
const API_URL_OPTION = isSelfHosted ? `\n apiUrl: ${API_URL},` : "";
132127

133128
const SETUP_SNIPPETS = [
134129
{
@@ -140,7 +135,7 @@ const SETUP_SNIPPETS = [
140135
import { trackMcp } from "@databuddy/sdk/mcp";
141136
142137
const server = trackMcp(
143-
new McpServer({ name: "my-server", version: "1.0.0" })
138+
new McpServer({ name: "my-server", version: "1.0.0" })${isSelfHosted ? `,\n { apiUrl: ${API_URL} }` : ""}
144139
);`,
145140
},
146141
{
@@ -153,7 +148,7 @@ import { waitUntil } from "@vercel/functions";
153148
import { trackMcp } from "@databuddy/sdk/mcp";
154149
155150
const handler = createMcpHandler(() =>
156-
trackMcp(new McpServer({ name: "my-server", version: "1.0.0" }), {
151+
trackMcp(new McpServer({ name: "my-server", version: "1.0.0" }), {${API_URL_OPTION}
157152
waitUntil,
158153
})
159154
);
@@ -171,7 +166,7 @@ import { trackMcp } from "@databuddy/sdk/mcp";
171166
172167
const handler = createMcpHandler(() =>
173168
trackMcp(new McpServer({ name: "my-server", version: "1.0.0" }), {
174-
apiKey: env.DATABUDDY_API_KEY,
169+
apiKey: env.DATABUDDY_API_KEY,${API_URL_OPTION}
175170
waitUntil,
176171
})
177172
);
@@ -200,43 +195,6 @@ export default {
200195
},
201196
] as const;
202197

203-
function SnippetBlock({
204-
code,
205-
isCopied,
206-
lang,
207-
onCopy,
208-
}: {
209-
code: string;
210-
isCopied: boolean;
211-
lang: "bash" | "json" | "tsx";
212-
onCopy: () => void;
213-
}) {
214-
const html = useMemo(
215-
() => highlighter.codeToHtml(code, { lang, theme: "vesper" }),
216-
[code, lang]
217-
);
218-
return (
219-
<div className="group relative overflow-hidden rounded border border-border">
220-
<div
221-
className={cn(
222-
"overflow-x-auto font-mono text-[13px] leading-relaxed",
223-
"[&>pre]:m-0 [&>pre]:overflow-visible [&>pre]:p-4 [&>pre]:leading-relaxed",
224-
"[&>pre>code]:block [&>pre>code]:w-full"
225-
)}
226-
dangerouslySetInnerHTML={{ __html: html }}
227-
/>
228-
<Button
229-
className="absolute top-2 right-2"
230-
onClick={onCopy}
231-
size="sm"
232-
variant="secondary"
233-
>
234-
{isCopied ? "Copied" : "Copy"}
235-
</Button>
236-
</div>
237-
);
238-
}
239-
240198
function formatMs(ms: number | null) {
241199
if (ms === null) {
242200
return "–";
@@ -451,10 +409,17 @@ function Setup({ organizationId }: { organizationId?: string }) {
451409
});
452410
},
453411
});
412+
const role = useQuery({
413+
...orpc.apikeys.getMyRole.queryOptions({
414+
input: { organizationId: organizationId ?? "" },
415+
}),
416+
enabled: Boolean(organizationId),
417+
});
454418
const secret = createKey.data?.secret;
455419
const envLine = `DATABUDDY_API_KEY=${secret ?? KEY_PLACEHOLDER}`;
456420
const [hasOwnKey, setHasOwnKey] = useState(false);
457421
const [copied, setCopied] = useState<string | null>(null);
422+
const [hasCopied, setHasCopied] = useState(false);
458423
const [promptAgent, setPromptAgent] = useState<string | null>(null);
459424
const [isManualOpen, setIsManualOpen] = useState(false);
460425

@@ -471,6 +436,7 @@ function Setup({ organizationId }: { organizationId?: string }) {
471436
return;
472437
}
473438
setCopied(id);
439+
setHasCopied(true);
474440
setTimeout(() => setCopied(null), COPY_SUCCESS_TIMEOUT);
475441
trackAppEvent(APP_EVENTS.mcpSetupCopied, { block: id, method });
476442
if (method === "ai") {
@@ -485,7 +451,8 @@ function Setup({ organizationId }: { organizationId?: string }) {
485451
: hasOwnKey
486452
? "skipped"
487453
: "active";
488-
const wrapStatus: SetupRowStatus = promptAgent ? "waiting" : "active";
454+
const wrapStatus: SetupRowStatus = hasCopied ? "waiting" : "active";
455+
const doneSteps = [keyStatus !== "active", hasCopied].filter(Boolean).length;
489456
const toggle = (id: SetupRowId) => () =>
490457
setOpen((current) => (current === id ? null : id));
491458
const agentName = CODING_AGENTS.find(
@@ -495,7 +462,7 @@ function Setup({ organizationId }: { organizationId?: string }) {
495462
return (
496463
<div className="mx-auto w-full max-w-3xl px-4 py-6 sm:px-6 lg:py-10">
497464
<div className="mb-4 flex items-center justify-between gap-3">
498-
<h1 className="font-semibold text-xl">Set up MCP Analytics</h1>
465+
<h2 className="font-semibold text-xl">Set up MCP Analytics</h2>
499466
<Button asChild size="sm" variant="ghost">
500467
<a
501468
href="https://www.databuddy.cc/docs/sdk/mcp"
@@ -510,7 +477,7 @@ function Setup({ organizationId }: { organizationId?: string }) {
510477
<Card className="gap-0 py-0">
511478
<Card.Header className="gap-3 border-border border-b bg-card px-5 py-4">
512479
<Card.Title>Your first tool call, in three steps</Card.Title>
513-
<Progress size="sm" value={keyStatus === "active" ? 0 : 100 / 3} />
480+
<Progress size="sm" value={(doneSteps / 3) * 100} />
514481
</Card.Header>
515482

516483
<SetupRow
@@ -538,23 +505,29 @@ function Setup({ organizationId }: { organizationId?: string }) {
538505
A key with the Event Tracking scope, read from DATABUDDY_API_KEY
539506
in your server's environment.
540507
</p>
508+
{role.data && !role.data.canEditScopes ? (
509+
<p className="text-pretty text-muted-foreground text-sm">
510+
Only organization owners and admins can create API keys. Ask
511+
one of them for a key with the Event Tracking scope.
512+
</p>
513+
) : null}
541514
<div className="flex flex-wrap items-center gap-2">
542-
<Button
543-
disabled={!organizationId}
544-
loading={createKey.isPending}
545-
onClick={() =>
546-
organizationId &&
547-
createKey.mutate({
548-
name: "MCP analytics",
549-
organizationId,
550-
type: "automation",
551-
resources: { global: ["track:events"] },
552-
})
553-
}
554-
size="sm"
555-
>
556-
Create a key
557-
</Button>
515+
{role.data?.canEditScopes && organizationId ? (
516+
<Button
517+
loading={createKey.isPending}
518+
onClick={() =>
519+
createKey.mutate({
520+
name: `MCP analytics ${new Date().toISOString().slice(0, 10)}`,
521+
organizationId,
522+
type: "automation",
523+
resources: { global: ["track:events"] },
524+
})
525+
}
526+
size="sm"
527+
>
528+
Create a key
529+
</Button>
530+
) : null}
558531
<Button
559532
onClick={() => {
560533
setHasOwnKey(true);
@@ -571,9 +544,7 @@ function Setup({ organizationId }: { organizationId?: string }) {
571544
{getUserFacingErrorMessage(
572545
createKey.error,
573546
"Couldn't create an API key."
574-
)}{" "}
575-
Ask an organization admin for a key with the Event Tracking
576-
scope.
547+
)}
577548
</p>
578549
) : null}
579550
</div>
@@ -594,34 +565,15 @@ function Setup({ organizationId }: { organizationId?: string }) {
594565
</p>
595566
<p className="text-pretty text-muted-foreground text-xs">
596567
Your agent wraps your MCP server and makes a test call. Tool
597-
arguments and results never leave your server.
568+
arguments and successful results never leave your server; failed
569+
calls send their error message.
598570
</p>
599-
<div className="flex flex-wrap gap-2">
600-
{CODING_AGENTS.map((agent) => (
601-
<Button
602-
className="border border-border bg-background hover:bg-accent"
603-
key={agent.id}
604-
onClick={() =>
605-
copy(agent.id, generateMcpAgentPrompt(secret), "ai")
606-
}
607-
size="sm"
608-
variant="ghost"
609-
>
610-
{copied === agent.id ? (
611-
<CheckIcon className="size-4 text-success" />
612-
) : (
613-
<img
614-
alt=""
615-
className={cn("size-4", agent.invert && "dark:invert")}
616-
height={16}
617-
src={`/ai/${agent.icon}.svg`}
618-
width={16}
619-
/>
620-
)}
621-
{agent.name}
622-
</Button>
623-
))}
624-
</div>
571+
<CodingAgentButtons
572+
copied={copied}
573+
onCopy={(agentId) =>
574+
copy(agentId, generateMcpAgentPrompt(secret), "ai")
575+
}
576+
/>
625577
</div>
626578

627579
<div>
@@ -642,7 +594,7 @@ function Setup({ organizationId }: { organizationId?: string }) {
642594
</Button>
643595
<div
644596
className={cn(
645-
"grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none",
597+
"grid transition-[grid-template-rows] duration-200 ease-in-out motion-reduce:transition-none",
646598
isManualOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
647599
)}
648600
inert={!isManualOpen}
@@ -699,7 +651,7 @@ function Setup({ organizationId }: { organizationId?: string }) {
699651
<SetupRow
700652
detail="This page switches to your analytics when it arrives"
701653
expanded={false}
702-
status="waiting"
654+
status={hasCopied ? "waiting" : "pending"}
703655
title="Waiting for the first tool call"
704656
/>
705657
</Card>

‎apps/dashboard/app/(main)/websites/[id]/_components/utils/code-generators.ts‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -279,14 +279,13 @@ ${agentFeedbackSection(apiUrl, websiteId, setupSession)}`;
279279
}
280280

281281
export function generateMcpAgentPrompt(apiKey?: string): string {
282-
const basketOption = isSelfHosted
283-
? `, { apiUrl: ${JSON.stringify(publicConfig.urls.basket)} }`
284-
: "";
282+
const apiUrl = JSON.stringify(publicConfig.urls.basket);
283+
const basketOption = isSelfHosted ? `, { apiUrl: ${apiUrl} }` : "";
285284
const dashboardUrl = isSelfHosted
286285
? publicConfig.urls.dashboard
287286
: "https://app.databuddy.cc";
288287
return `Add Databuddy MCP analytics to the MCP server in this repository, so every tool call shows up on the Databuddy MCP Analytics page: which tools AI clients call, how fast they answer, and why they fail. Tool arguments and successful results never leave the server; only the length of the returned text and the error message of failed calls are sent.
289-
288+
${isSelfHosted ? `\nThis is a self-hosted Databuddy instance: pass \`apiUrl: ${apiUrl}\` in the options of every \`trackMcp\` call.\n` : ""}
290289
## References
291290
- MCP analytics docs: https://www.databuddy.cc/docs/sdk/mcp
292291
- LLMs.txt: https://www.databuddy.cc/llms.txt
@@ -306,15 +305,15 @@ const server = trackMcp(
306305
\`\`\`
307306
- \`@modelcontextprotocol/server\` 2.x builds a server per request, so call \`trackMcp\` inside the \`createMcpHandler\` or \`serveStdio\` factory.
308307
- Vercel \`mcp-handler\`: call \`trackMcp(server)\` inside the callback it passes the server to, and set \`serverInfo: { name, version }\`.
309-
4. Read the API key from \`DATABUDDY_API_KEY\`. Never hardcode or commit it. ${apiKey ? `The key is \`${apiKey}\`: put it in the server's local env file (make sure that file is gitignored) and tell me to add it to the hosting provider's secrets.` : `Ask me to create a key with the Event Tracking scope at ${dashboardUrl}/organizations/settings#api-keys.`} Add \`DATABUDDY_API_KEY=\` without a value to \`.env.example\` if the repository has one.
310-
5. Serverless (Vercel, Cloudflare Workers, Netlify, AWS Lambda): a function can stop before the batch is sent, so pass the platform's \`waitUntil\`: \`trackMcp(server, { waitUntil })\`. On Vercel import it from \`@vercel/functions\`; on Cloudflare Workers import \`env\` and \`waitUntil\` from \`cloudflare:workers\` and pass \`apiKey: env.DATABUDDY_API_KEY\`.
308+
4. Read the API key from \`DATABUDDY_API_KEY\`. ${apiKey ? `The key is \`${apiKey}\`: put it in the server's local env file (make sure that file is gitignored) and tell me to add it to the hosting provider's secrets.` : `Ask me for my key; if I don't have one, I can create one with the Event Tracking scope at ${dashboardUrl}/organizations/settings#api-keys.`} Never commit the key: use the placeholder \`dbdy_your_key\` in every committed file, including MCP client configs, README and docs, and never repeat the key in replies, commit messages or PR text. Add \`DATABUDDY_API_KEY=\` without a value to \`.env.example\` if the repository has one.
309+
5. Serverless: a function can stop before the batch is sent, so pass the platform's \`waitUntil\`: \`trackMcp(server, { waitUntil })\`. On Vercel import it from \`@vercel/functions\`; on Cloudflare Workers import \`env\` and \`waitUntil\` from \`cloudflare:workers\` and pass \`apiKey: env.DATABUDDY_API_KEY\`; on Netlify use \`context.waitUntil\`. AWS Lambda has no \`waitUntil\`, so \`await flushMcp()\` (from \`@databuddy/sdk/mcp\`) before the handler returns.
311310
6. stdio servers started by a desktop client (Claude Desktop, Cursor, VS Code) only receive the variables in the client config, so \`DATABUDDY_API_KEY\` belongs in the server's \`env\` block there. Update any client config examples in the README accordingly.
312311
7. If the server ends itself with \`process.exit\`, for example in a SIGINT or SIGTERM handler, call \`await flushMcp()\` (from \`@databuddy/sdk/mcp\`) first.
313312
8. Only if the server has no error convention yet: return failed tool calls as \`isError\` results whose text is JSON with a stable, low-cardinality snake_case code, for example \`{"error":{"code":"not_found","message":"Website not found"}}\`. The MCP Analytics page groups failures by that code. Do not rewrite existing error handling.
314313
315314
## Verification
316315
317-
1. Run the server with \`DATABUDDY_API_KEY\` set and call any tool once, for example with \`npx @modelcontextprotocol/inspector\`.
316+
1. Run the server with \`DATABUDDY_API_KEY\` set and call any tool once with the MCP Inspector CLI: \`npx @modelcontextprotocol/inspector --cli <command that starts the server> --method tools/call --tool-name <tool>\`, or \`npx @modelcontextprotocol/inspector --cli <server URL> --transport http --method tools/call --tool-name <tool>\` for an HTTP server.
318317
2. Temporarily pass \`debug: true\` to \`trackMcp\` to log a missing key or a rejected batch to stderr, then remove it.
319318
3. The Databuddy MCP Analytics page checks for the first call every few seconds and switches to the analytics view on its own.
320319

0 commit comments

Comments
 (0)