# MCP (auto-instrument your server)

Add Brizz observability to an MCP server you operate, in Python (FastMCP) or TypeScript.

This page is for **adding Brizz to an MCP server you run**. If you want to connect Brizz *to* an AI agent (Claude, Cursor, Codex) so the agent can query your Brizz data, see the [MCP server integration](/docs/integrations/mcp-server/overview.md) instead.

Brizz auto-instruments every MCP tool call on your server — arguments, results, errors, and session context are captured as spans without touching tool bodies. Works for **Python** (via [FastMCP](https://gofastmcp.com/)) and **TypeScript** (via [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk)).

For the full SDK reference, see the [Python SDK guide](/docs/sdks/python.md) or the [Node.js / TypeScript SDK guide](/docs/sdks/typescript.md).

:::warning Initialization Order
Call `Brizz.initialize()` **before** constructing the MCP server. The auto-instrumentation hooks the MCP protocol module at init time — if the server is built first, tool calls won't be traced.
:::

## Python (FastMCP)

### Install

:::tabs
:::tab[pip]
```bash
pip install brizz fastmcp
```
:::tab[uv]
```bash
uv add brizz fastmcp
```
:::tab[poetry]
```bash
poetry add brizz fastmcp
```
:::

Install `fastmcp` alongside `brizz` — Brizz instruments your FastMCP server automatically once it's present in the environment.

### Server

```python
import os
from brizz import Brizz
from fastmcp import FastMCP

Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-mcp-server",
)

mcp = FastMCP("my-mcp-server")

@mcp.tool()
def echo(text: str) -> str:
    """Echo a message back."""
    return text

if __name__ == "__main__":
    mcp.run(transport="http", host="127.0.0.1", port=8765)
```

Supported transports: `http` (streamable HTTP, default), `sse`, and `stdio`.

### FastAPI

Mount the MCP server alongside an existing FastAPI API. See [FastMCP's FastAPI guide](https://gofastmcp.com/integrations/fastapi).

#### Basic mounting

Serve the MCP server as a sub-app instead of calling `mcp.run(...)`:

```python
import os
import uvicorn
from brizz import Brizz
from fastapi import FastAPI
from fastmcp import FastMCP

Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-mcp-server",
)

mcp = FastMCP("my-mcp-server")

@mcp.tool()
def echo(text: str) -> str:
    """Echo a message back."""
    return text

# Build the MCP app and mount it. The endpoint lands at <mount-prefix>/mcp.
mcp_app = mcp.http_app(path="/mcp")
app = FastAPI(lifespan=mcp_app.lifespan)   # FastAPI must use the MCP app's lifespan
app.mount("/mcp-server", mcp_app)          # served at http://127.0.0.1:8000/mcp-server/mcp

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)
```

:::warning Lifespan
Pass the MCP app's lifespan to FastAPI (`FastAPI(lifespan=mcp_app.lifespan)`). Without it the MCP server never starts up and every request fails.
:::

Telemetry is identical to the standalone server — `Brizz.initialize()` instruments the `FastMCP(...)` instance no matter how it's served.

## TypeScript

### Install

:::tabs
:::tab[npm]
```bash
npm install @brizz/sdk zod
```
:::tab[yarn]
```bash
yarn add @brizz/sdk zod
```
:::tab[pnpm]
```bash
pnpm add @brizz/sdk zod
```
:::

### Server

Pass `instrumentModules.mcp.protocolModule` so Brizz can patch the MCP protocol module even when bundled by Next.js, Webpack, or run under `tsx`.

```typescript
import { randomUUID } from 'node:crypto';
import { createServer } from 'node:http';

import { Brizz } from '@brizz/sdk';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import * as protocolModule from '@modelcontextprotocol/sdk/shared/protocol.js';
import { z } from 'zod';

Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY,
  appName: 'my-mcp-server',
  instrumentModules: { mcp: { protocolModule } },
});

const server = new McpServer({ name: 'my-mcp-server', version: '1.0.0' });

server.registerTool(
  'echo',
  { description: 'Echoes the input text back', inputSchema: { text: z.string() } },
  async ({ text }) => {
    return { content: [{ type: 'text', text: `echo:${text}` }] };
  },
);

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),
});
await server.connect(transport);

createServer((req, res) => void transport.handleRequest(req, res))
  .listen(8765, '127.0.0.1', () => {
    console.log('MCP server listening on http://127.0.0.1:8765/mcp');
  });
```

For `stdio` transport, replace the HTTP setup with `new StdioServerTransport()` from `@modelcontextprotocol/sdk/server/stdio.js`.

## Identity & custom attributes

Two kinds of attributes show up on a tool-call trace — keep them separate:

- **Identity** (who is calling — user, company/tenant) belongs to your auth layer and should land on the whole tool call: the `tools/call` span **and** every child span it creates. Attach it once per call at your auth layer; the exact calls differ per SDK (see below), because Python authenticates before the span opens while TypeScript authenticates after.
- **Tool-specific attributes** (anything about a single tool's work) belong to that one tool's span. Set them inside the tool with `set_current_span_custom_properties` / `setCurrentSpanCustomProperties`.

When identity comes from an HTTP `Authorization` header (as below), it requires an HTTP-based transport (`http`/`sse`); `stdio` carries no headers.

### Python

Authenticate in a middleware and propagate identity with `custom_properties`. The middleware runs before FastMCP opens the `tools/call` span, so that span and its children inherit the attributes:

```python
from brizz import custom_properties, set_current_span_custom_properties
from fastmcp.server.dependencies import get_http_headers
from fastmcp.server.middleware import Middleware

class AuthMiddleware(Middleware):
    async def on_call_tool(self, context, call_next):
        # `authorization` is excluded from get_http_headers() by default — opt it back in.
        identity = resolve_identity(get_http_headers(include={"authorization"}))
        # -> {"user.id": "...", "company.id": "..."}; propagates to tools/call + children.
        with custom_properties(identity):
            return await call_next(context)

mcp.add_middleware(AuthMiddleware())

@mcp.tool()
def echo(text: str) -> str:
    set_current_span_custom_properties({"echo.text_length": str(len(text))})  # this span only
    return text
```

### TypeScript

Brizz opens the `tools/call` span before your handler runs, so it's already the active span inside the handler. Wrap each handler to authenticate, stamp that active span, and propagate to its children:

```typescript
import { callWithProperties, setCurrentSpanCustomProperties } from '@brizz/sdk';

function withAuth(handler) {
  return (args, extra) => {
    const identity = resolveIdentity(extra.requestInfo?.headers); // { 'user.id': '...', 'company.id': '...' }
    setCurrentSpanCustomProperties(identity);                      // stamp the active tools/call span
    return callWithProperties(identity, () => handler(args, extra)); // propagate to child spans
  };
}

server.registerTool(
  'echo',
  { description: 'Echoes the input text back', inputSchema: { text: z.string() } },
  withAuth(({ text }) => {
    setCurrentSpanCustomProperties({ 'echo.text_length': text.length }); // this span only
    return { content: [{ type: 'text', text: `echo:${text}` }] };
  }),
);
```

## Complete example

End-to-end servers you can copy and run. Identity flows from an `Authorization` header, so use the HTTP transport (`stdio` carries no headers).

### Python (FastMCP)

Run with `python server.py` (`BRIZZ_API_KEY` required).

```python
# server.py
import os
from brizz import Brizz, custom_properties, set_current_span_custom_properties
from fastmcp import FastMCP
from fastmcp.server.dependencies import get_http_headers
from fastmcp.server.middleware import Middleware

# Initialize Brizz BEFORE constructing FastMCP — auto-instrumentation hooks
# the MCP protocol module at init time.
Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-mcp-server",
    environment=os.getenv("APP_ENV", "development"),
)

mcp = FastMCP("my-mcp-server")


def resolve_identity(headers: dict[str, str]) -> dict[str, str]:
    # Replace with your real auth — JWT decode, API key lookup, etc.
    token = headers.get("authorization", "")
    return {"user.id": "user-42", "company.id": "acme"} if token else {}


class AuthMiddleware(Middleware):
    async def on_call_tool(self, context, call_next):
        # `authorization` is excluded from get_http_headers() by default — opt in.
        identity = resolve_identity(get_http_headers(include={"authorization"}))
        # Propagates to the tools/call span AND every child span the tool creates.
        with custom_properties(identity):
            return await call_next(context)


mcp.add_middleware(AuthMiddleware())


@mcp.tool()
def echo(text: str) -> str:
    """Echo a message back."""
    # Tool-specific attributes go on THIS span only.
    set_current_span_custom_properties({"echo.text_length": str(len(text))})
    return text


if __name__ == "__main__":
    mcp.run(transport="http", host="127.0.0.1", port=8765)
```

### TypeScript

Run with `node server.mjs` (`BRIZZ_API_KEY` required).

```typescript
// server.ts
import { randomUUID } from 'node:crypto';
import { createServer } from 'node:http';

import {
  Brizz,
  callWithProperties,
  setCurrentSpanCustomProperties,
} from '@brizz/sdk';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import * as protocolModule from '@modelcontextprotocol/sdk/shared/protocol.js';
import { z } from 'zod';

// Init BEFORE constructing the server. instrumentModules.mcp.protocolModule
// is required so Brizz patches the right module under bundlers / tsx.
Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY!,
  appName: 'my-mcp-server',
  environment: process.env.NODE_ENV ?? 'development',
  instrumentModules: { mcp: { protocolModule } },
});

function resolveIdentity(headers?: Record<string, string | string[] | undefined>) {
  // Replace with your real auth — JWT decode, API key lookup, etc.
  const auth = headers?.['authorization'];
  return auth ? { 'user.id': 'user-42', 'company.id': 'acme' } : {};
}

function withAuth<A>(handler: (args: A, extra: any) => any) {
  return (args: A, extra: any) => {
    const identity = resolveIdentity(extra.requestInfo?.headers);
    // Stamp the already-open tools/call span...
    setCurrentSpanCustomProperties(identity);
    // ...and propagate identity to any child spans the handler creates.
    return callWithProperties(identity, () => handler(args, extra));
  };
}

const server = new McpServer({ name: 'my-mcp-server', version: '1.0.0' });

server.registerTool(
  'echo',
  { description: 'Echoes the input text back', inputSchema: { text: z.string() } },
  withAuth(({ text }: { text: string }) => {
    setCurrentSpanCustomProperties({ 'echo.text_length': text.length });
    return { content: [{ type: 'text', text: `echo:${text}` }] };
  }),
);

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),
});
await server.connect(transport);

createServer((req, res) => void transport.handleRequest(req, res))
  .listen(8765, '127.0.0.1', () => {
    console.log('MCP server listening on http://127.0.0.1:8765/mcp');
  });
```

**Gotchas**

- Call `Brizz.initialize()` **before** constructing the MCP server. Hooks install at init time.
- TypeScript: pass `instrumentModules.mcp.protocolModule` so the patch survives bundlers (Next.js, Webpack, `tsx`).
- Identity goes at the auth boundary (middleware / `withAuth`) so it lands on the `tools/call` span and every child. Tool-specific attributes go inside the tool body via `set_current_span_custom_properties` / `setCurrentSpanCustomProperties`.
- HTTP-header–based identity requires `http` or `sse` transport — `stdio` carries no headers.

## Sessions on a stateless server

A stateless MCP server handles every request on a fresh connection. That keeps it
cheap and easy to scale, but it means the server cannot tell that ten tool calls
came from one person doing one thing — so in Brizz each call arrives as its own
one-call session, and there is no conversation to read.

Turn on session tracking and Brizz asks the calling client to carry that context
for you:

- Your server publishes one extra tool, `brizz_start_session`
  ([rename it](#configuration)). The client calls it once and gets back a session id.
- Every one of your own tools gains a `brizz_mcp_session_id` parameter
  ([rename it](#configuration)), and its description tells the client to send that
  id back on every call.
- Optionally, a `brizz_intent` parameter asks what the user is trying to
  accomplish, in the client's own words.

That is the whole handshake, and it rides on the tool schemas the client already
reads. Nothing is required of your customers — they don't install anything, change
anything, or even know it happened.

### The handshake

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Your MCP server

    C->>S: tools/list
    S-->>C: your tools (+ brizz_mcp_session_id)<br/>+ brizz_start_session

    C->>S: tools/call brizz_start_session
    S-->>C: "a1b2c3…"

    Note over C: keeps the id for<br/>the whole conversation

    C->>S: tools/call search_orders (a1b2c3…)
    C->>S: tools/call get_order (a1b2c3…)
    C->>S: tools/call request_refund (a1b2c3…)

    Note over S: all three land in<br/>one Brizz session
```

Every call above arrives on its own connection. The id is what ties them together.

### Turn it on

:::tabs
:::tab[Python]
```python
Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-mcp-server",
    mcp_session_tracking=True,
)
```
:::tab[TypeScript]
```typescript
Brizz.initialize({
  appName: 'my-mcp-server',
  apiKey: process.env.BRIZZ_API_KEY,
  mcpSessionTracking: true,
});
```
:::

You can also set `BRIZZ_MCP_SESSION_TRACKING=true` instead of passing the flag.

### Configuration

`True` turns everything on with the default names. Pass the config object instead to
choose what Brizz asks for and what it calls things.

:::tabs
:::tab[Python]
```python
from brizz import MCPSessionTrackingConfig

Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-mcp-server",
    mcp_session_tracking=MCPSessionTrackingConfig(
        enabled=True,
        intent=True,
        session_param_name="brizz_mcp_session_id",
        session_tool_name="brizz_start_session",
    ),
)
```

| Option | Default | What it does |
| --- | --- | --- |
| `enabled` | `False` | Master switch. Everything else applies only when this is on. |
| `intent` | `True` | Also ask what the user is trying to accomplish. |
| `session_param_name` | `"brizz_mcp_session_id"` | Name of the parameter added to your tools. |
| `session_tool_name` | `"brizz_start_session"` | Name of the tool that issues the session id. |

:::tab[TypeScript]
```typescript
Brizz.initialize({
  appName: 'my-mcp-server',
  apiKey: process.env.BRIZZ_API_KEY,
  mcpSessionTracking: {
    enabled: true,
    intent: true,
    sessionParamName: 'brizz_mcp_session_id',
    sessionToolName: 'brizz_start_session',
  },
});
```

| Option | Default | What it does |
| --- | --- | --- |
| `enabled` | `false` | Master switch. Everything else applies only when this is on. |
| `intent` | `true` | Also ask what the user is trying to accomplish. |
| `sessionParamName` | `'brizz_mcp_session_id'` | Name of the parameter added to your tools. |
| `sessionToolName` | `'brizz_start_session'` | Name of the tool that issues the session id. |

:::

Rename the parameter or the tool when the defaults would collide with something your
server already publishes. The tool descriptions Brizz writes use whatever names you
pick, so the calling client is told the right ones.

### What changes for you

Nothing in your code. Your tools keep their own signatures and never receive the
extra parameters — Brizz removes them before your tool runs, and keeps them out of
the arguments it records. Tools you register while the server is running get the
same treatment, because Brizz works on whatever your server publishes at the moment
it publishes it.

A client that ignores the handshake keeps working exactly as before; those calls
simply fall back to one session each.

Your `additionalProperties` setting is left as you wrote it. If a tool's schema is
closed (`additionalProperties: false`), Brizz also marks `brizz_intent` required on
that tool, because a closed schema is how OpenAI strict function calling is
signalled and strict mode requires every property to be listed in `required` — so
the tool stays usable for clients that forward your schemas into strict mode.

:::info
This is off by default, because it changes the tools your server advertises.
:::

## Where the data lands

Once your MCP server is reporting, Brizz adds an **MCP Servers** tab to the dashboard for that service — an operations console built around tool calls rather than conversations:

- **KPI strip** — total tool invocations, sessions, success rate, and the count of distinct issues affecting the service.
- **Tool calls** — call volume over time, with markers for tool-definition changes so a shift lines up with a change to the tool itself.
- **Tool inventory** — a paged, sortable list with call volume, usage share, issue count, p95 latency, last-call time, and a health stripe per tool. Filter it to one or more tools, or open a row for its detail drawer; registered tools with no calls in the selected range appear as inactive.
- **Tool latency over time** — avg, p50, p75, or p95 for one selected tool. It defaults to the slowest tool for the selected statistic within the active date range and filters.
- **Top issues** — the most impactful issues for the service.

:::info
The MCP Servers tab appears only for services registered as MCP servers, and it's a plan-gated feature. If you're reporting MCP telemetry but don't see the tab, [contact us](mailto:support@brizz.ai).
:::

## See also

- [MCP server integration](/docs/integrations/mcp-server/overview.md) — the *other* MCP page: connecting Brizz to AI agents (not the other way around).
- [Python SDK](/docs/sdks/python.md) and [Node.js / TypeScript SDK](/docs/sdks/typescript.md) — full SDK references.
- [Identify users](/docs/instrument/identify-users.md) — propagating identity from auth into MCP tool spans.
