# MCP Servers

> See which AI clients call your MCP servers, which tools they use, how fast they answer, and where they fail


import { Callout, CodeBlock, Tab, Tabs } from "@/components/docs";

The MCP page shows every tool call that Claude, Claude Code, Cursor, ChatGPT, Codex and other AI clients make to your MCP servers: which tools they reach for, how long each call takes, how much context it hands back to the model, and which calls fail and why.

Arguments and successful results never leave your server; only the length of the text a tool returns is recorded. Failed calls send their error message, up to 512 characters, unless you [keep error messages private](#keep-error-messages-private).

<Callout type="info">
  **Package**: `@databuddy/sdk` 3.2.0+ | **Import**: `@databuddy/sdk/mcp`
</Callout>

## Setup

Install the SDK and set an API key with the **Event Tracking** scope, created under **Organization Settings → API Keys**:

<CodeBlock language="bash">
  {`bun add @databuddy/sdk@latest
DATABUDDY_API_KEY=dbdy_your_key`}
</CodeBlock>

Wrap your server once, before or after you register tools:

<CodeBlock language="ts">
  {`import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { trackMcp } from "@databuddy/sdk/mcp";

const server = trackMcp(
  new McpServer({ name: "my-server", version: "1.0.0" })
);`}
</CodeBlock>

`trackMcp` returns the server you pass it, and also accepts the low-level `Server`.

With `@modelcontextprotocol/server` 2.x, `createMcpHandler` builds a server per request, so wrap it inside the factory:

<CodeBlock language="ts">
  {`export const handler = createMcpHandler(() =>
  trackMcp(new McpServer({ name: "my-server", version: "1.0.0" }))
);`}
</CodeBlock>

Long-running servers send calls in batches every second, and a stdio server that exits on its own sends its last batch before it exits. If your server ends itself with `process.exit`, for example in a SIGINT or SIGTERM handler, send the last batch first:

<CodeBlock language="ts">
  {`import { flushMcp } from "@databuddy/sdk/mcp";

process.on("SIGTERM", async () => {
  await flushMcp();
  process.exit(0);
});`}
</CodeBlock>

## Serverless

A serverless function can stop before the batch goes out. Pass your platform's `waitUntil` and each call is sent before the function stops:

<Tabs items={['Vercel', 'Cloudflare Workers']}>
<Tab value="Vercel">
<CodeBlock language="ts">
  {`import { createMcpHandler, McpServer } from "@modelcontextprotocol/server";
import { waitUntil } from "@vercel/functions";

export const handler = createMcpHandler(() =>
  trackMcp(new McpServer({ name: "my-server", version: "1.0.0" }), {
    waitUntil,
  })
);`}
</CodeBlock>

With Vercel's `mcp-handler`, wrap the server it passes you, before or after you register your tools:

<CodeBlock language="ts">
  {`import { waitUntil } from "@vercel/functions";
import { createMcpHandler } from "mcp-handler";

const handler = createMcpHandler((server) => {
  trackMcp(server, { waitUntil });
});

export { handler as GET, handler as POST };`}
</CodeBlock>
</Tab>
<Tab value="Cloudflare Workers">
<CodeBlock language="ts">
  {`import { createMcpHandler, McpServer } from "@modelcontextprotocol/server";

export default {
  fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const handler = createMcpHandler(() =>
      trackMcp(new McpServer({ name: "my-server", version: "1.0.0" }), {
        waitUntil: ctx.waitUntil.bind(ctx),
      })
    );
    return handler.fetch(request);
  },
};`}
</CodeBlock>
</Tab>
</Tabs>

## Servers, websites and environments

Calls belong to the organization that owns the API key, and the MCP page shows all of them. Three things separate them, with no setup:

- **Server**: the `name` your MCP server registers with. Give each server its own name.
- **Website**: when the server runs in an app that has a Databuddy website ID (`NEXT_PUBLIC_DATABUDDY_CLIENT_ID` or `DATABUDDY_WEBSITE_ID`), its calls are linked to that website.
- **Environment**: `VERCEL_ENV`, or `NODE_ENV`, so local and preview calls stay apart from production.

The page shows a picker for each one as soon as there's more than one value. Click a client to see only its calls.

## Options

| Option | Default | Description |
| --- | --- | --- |
| `apiKey` | `DATABUDDY_API_KEY` | API key with the Event Tracking scope |
| `websiteId` | Detected from your Databuddy website ID | Website to link calls to |
| `environment` | `VERCEL_ENV` or `NODE_ENV` | Environment label |
| `apiUrl` | `https://basket.databuddy.cc` | Ingestion endpoint, for self-hosted Databuddy |
| `beforeSend` | | Edit a call before it's sent, or return `null` to drop it |
| `waitUntil` | | Your platform's `waitUntil`, so serverless functions send each call before they stop |
| `debug` | `false` | Log a missing API key, an unsupported server, and every rejected or failed send to stderr |

Without an API key, `trackMcp` leaves the server untouched. An API key limited to some websites can only send calls linked to one of them.

## What gets recorded

Each tool call records the tool name, whether it failed and its error message (the first text of an `isError` result or the thrown message, unwrapped to its `message` when it is a JSON error body, up to 512 characters), how long your handler took, how many characters of text the tool returned (divide by about 4 for tokens), the client's name and version, your server's name and version, the MCP session ID when your server keeps sessions, and the user agent.

A call that asks the client for more input is recorded once, when it returns its final result. Calls that a client runs as a background task aren't recorded.

Clients are named from the `clientInfo` they send. Stateless servers that build a new server per request only see it on the first request, so Databuddy falls back to the user agent: ChatGPT sends `openai-mcp`, claude.ai sends `Anthropic/ClaudeAI`. Clients it can't name show up as **Unknown client** with their user agent. The user agent needs `@modelcontextprotocol/sdk` 1.13.2 or later, or `@modelcontextprotocol/server` 2.x; on older versions, stateless servers show every call as **Unknown client**.

Tracking never changes what a client receives. It doesn't throw, doesn't delay responses, and sends in the background with a 5 second timeout. If Databuddy rejects the calls, for example because of a wrong API key, `trackMcp` logs the reason once to stderr. Each recorded call counts as one event on your plan.

### Keep error messages private

Error messages can include input your handler put in them. To keep them on your server and still count the call as failed, blank the message:

<CodeBlock language="ts">
  {`trackMcp(server, {
  beforeSend: (call) => (call.error === undefined ? call : { ...call, error: "" }),
});`}
</CodeBlock>
