> ## Documentation Index
> Fetch the complete documentation index at: https://docs.braintrust.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP SDK

> Trace the tool calls, prompt requests, and resource reads your MCP server handles to debug failures and monitor production usage

The [Model Context Protocol](https://modelcontextprotocol.io/) SDK is the official toolkit for building MCP servers and clients. Braintrust traces requests and notifications on your MCP servers, and outgoing client requests in Python. Python also supports standalone [FastMCP](https://gofastmcp.com/).

> [!NOTE]
>
> This page covers tracing MCP clients and servers that you build. To connect an MCP client, such as Claude Code or Cursor, to Braintrust, see [Braintrust MCP](https://docs.braintrust.dev/integrations/developer-tools/mcp.md).

**TypeScript**

## Setup

Tracing requires Braintrust v3.42.0 or later, and `@modelcontextprotocol/sdk` v1.2.0 or later below v2.0.0, or `@modelcontextprotocol/server` v2.0.0 or later below v3.0.0. The examples on this page use the v2 server and client packages and require Node.js 20 or later.

**1. Install packages**

```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
pnpm add braintrust @modelcontextprotocol/server zod
```

```bash npm theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
npm install braintrust @modelcontextprotocol/server zod
```

**2. Set environment variables**

```bash title=".env" theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
BRAINTRUST_API_KEY=<your-braintrust-api-key>
```

## Auto-instrumentation

To trace your MCP server without modifying your application code, initialize a Braintrust logger, then run your server with Braintrust's import hook. Braintrust traces every `McpServer` and low-level `Server` that connects to a transport. MCP clients aren't traced.

**1. Initialize Braintrust and start your server**

```javascript title="mcp-server.js" theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { initLogger } from "braintrust";
import { z } from "zod";

initLogger({ projectName: "my-mcp-server" }); // Replace with your project name

const server = new McpServer({ name: "weather", version: "1.0.0" });

server.registerTool(
  "get_weather",
  {
    description: "Get the current weather for a city",
    inputSchema: z.object({ city: z.string() }),
  },
  async ({ city }) => ({
    content: [{ type: "text", text: `Sunny in ${city}` }],
  }),
);

await server.connect(new StdioServerTransport());
```

**2. Run with the import hook**

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
node --env-file=.env --import braintrust/hook.mjs mcp-server.js
```

The auto-instrumentation example uses plain JavaScript so `node --import` can run the file directly. The Braintrust APIs work the same in TypeScript projects. Compile your TypeScript to JavaScript and run the compiled file with the import hook, or [run TypeScript directly](https://docs.braintrust.dev/instrument/trace-llm-calls.md#auto-instrumentation) with a TypeScript loader.

> [!NOTE]
>
> If you're using a bundler, see [Trace LLM calls](https://docs.braintrust.dev/instrument/trace-llm-calls.md#auto-instrumentation) for plugin and loader setup.

> [accordion] Opt out of MCP tracing
>
> If you use auto-instrumentation for other libraries but want to exclude MCP servers, add `mcp` to the `BRAINTRUST_DISABLE_INSTRUMENTATION` environment variable.
>
> ```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
> BRAINTRUST_DISABLE_INSTRUMENTATION=mcp node --env-file=.env --import braintrust/hook.mjs mcp-server.js
> ```

## Manual instrumentation

To trace an MCP server manually, pass it to `wrapMCPServer()`. `wrapMCPServer()` accepts an `McpServer` or a low-level `Server` and returns the same instance. It traces handlers registered before and after the call, and works before or after you connect the server to a transport.

```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { initLogger, wrapMCPServer } from "braintrust";

initLogger({ projectName: "my-mcp-server" }); // Replace with your project name

const server = wrapMCPServer(new McpServer({ name: "weather", version: "1.0.0" }));

// Register tools, prompts, and resources on the server

await server.connect(new StdioServerTransport());
```

## Omit arguments and results

MCP spans record tool and prompt arguments, resource URIs, tool, prompt, and resource results, and notification parameters by default.

To keep tracing requests and notifications without recording this content, set `captureContent` to `false` in the `mcp` integration options of [`configureInstrumentation()`](https://docs.braintrust.dev/sdks/typescript/api-reference#configureinstrumentation). Import it from `braintrust/instrumentation` and call it in a separate file that loads before `braintrust`. A call from a file that imports `braintrust` has no effect.

```javascript title="configure.mjs" theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { configureInstrumentation } from "braintrust/instrumentation";

configureInstrumentation({
  integrations: {
    mcp: { captureContent: false },
  },
});
```

With auto-instrumentation, load the file before the hook:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
node --env-file=.env --import ./configure.mjs --import braintrust/hook.mjs mcp-server.js
```

With `wrapMCPServer()`, import `./configure.mjs` before any other import in your server's entry file.

When content capture is on, images and resource blobs in results are recorded as `<omitted>` unless you also set `BRAINTRUST_CAPTURE_ATTACHMENTS` to `1` or `true`, which uploads them as [attachments](https://docs.braintrust.dev/instrument/attachments.md). Audio and video content is always recorded as `<omitted>`.

## Link requests to the client's trace

Each request your server receives starts its own trace. To nest a request under the span that sent it, the client must include W3C trace context (`traceparent`, and optionally `tracestate` and `baggage`) in the request's `params._meta`. A Braintrust-instrumented client can produce these values with [`injectTraceContext()`](https://docs.braintrust.dev/sdks/typescript/api-reference#injecttracecontext). Call it inside the client span that sends the request. Install the client package alongside the server dependencies:

```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
pnpm add @modelcontextprotocol/client
```

```bash npm theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
npm install @modelcontextprotocol/client
```

```javascript title="client.mjs" theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { initLogger, injectTraceContext, traced } from "braintrust";

const logger = initLogger({ projectName: "my-mcp-server" }); // Replace with your project name

const client = new Client({ name: "weather-client", version: "1.0.0" });
await client.connect(
  new StdioClientTransport({
    command: "node",
    args: ["--env-file=.env", "--import", "braintrust/hook.mjs", "mcp-server.js"],
  }),
);

await traced(
  async () => {
    await client.callTool({
      name: "get_weather",
      arguments: { city: "Paris" },
      _meta: injectTraceContext(),
    });
  },
  { name: "ask-weather" },
);

await logger.flush();
await client.close();
```

The `tools/call get_weather` span nests under `ask-weather` in the client's trace. See [Trace distributed systems](https://docs.braintrust.dev/instrument/advanced-tracing.md#trace-distributed-systems) for how trace context propagates across services.

## What Braintrust traces

Auto-instrumentation and `wrapMCPServer()` produce the same spans, and spans that your handlers create nest under the request span. Braintrust records:

* Tool call spans (`tools/call <tool>`), recorded as tool spans, with `gen_ai.tool.name` in metadata. With content capture on, they also record the tool arguments as input and the tool result as output.
* Prompt request spans (`prompts/get <prompt>`), with `gen_ai.prompt.name` in metadata. With content capture on, they also record the prompt arguments as input and the prompt result as output.
* Spans for other requests, named after the MCP method, such as `initialize`, `tools/list`, and `resources/read`. With content capture on, `resources/read` spans also record the resource URI as input and the resource contents as output.
* Notification spans, named after the MCP method, such as `notifications/initialized`, `notifications/progress`, and `notifications/message`. `mcp.notification.direction` is `incoming` for notifications the server receives and `outgoing` for notifications it sends, and an outgoing notification sent for a request nests under that request's span.
* Progress and logging notification metadata, with progress values, level, and logger. With content capture on, notification spans also record the notification parameters.
* Request metadata, including `mcp.method.name`, `jsonrpc.request.id`, `mcp.session.id`, `mcp.protocol.version`, `mcp.resource.uri`, and the transport protocol.
* Client and server identity from the `initialize` exchange (`mcp.client.name`, `mcp.client.version`, `mcp.server.name`, and `mcp.server.version`, plus `title` when reported) on spans for the rest of the session. On HTTP transports without a session ID, later spans record only the identity and protocol version that each request and response report.
* Errors, including tool results marked as errors, JSON-RPC error responses, handler exceptions, and connections that close before the response is sent, with the error category in `error.type`. Spans for JSON-RPC error responses record the error code in `rpc.response.status_code`. Responses that reject the client's request, such as method not found or invalid params, record the code without marking the span as an error.
* Cancelled requests, marked with `mcp.cancelled`.

> [!NOTE]
>
> Task requests (`tasks/*` methods and task-augmented requests) aren't traced.

## Resources

* [Model Context Protocol TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
* [MCP server SDK on npm](https://www.npmjs.com/package/@modelcontextprotocol/server)
* [MCP client SDK on npm](https://www.npmjs.com/package/@modelcontextprotocol/client)
* [Braintrust package on npm](https://www.npmjs.com/package/braintrust)

**Python**

## Setup

Braintrust instruments the official MCP Python SDK (`mcp`) and standalone FastMCP (`fastmcp`), tracing both clients and servers.

Tracing requires the Braintrust Python SDK v0.49.1 or later, and `mcp` 1.28.1 or later (below 3.0) or standalone `fastmcp` 2.14.7 or later (below 5.0). The examples use the `FastMCP` class bundled with `mcp` 1.x.

**1. Install packages**

```bash uv theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
uv add "braintrust>=0.49.1" "mcp>=1.28.1,<2"
```

```bash pip theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
pip install "braintrust>=0.49.1" "mcp>=1.28.1,<2"
```

For standalone FastMCP, install `fastmcp>=2.14.7,<5` alongside Braintrust instead.

**2. Set environment variables**

```bash title=".env" theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
export BRAINTRUST_API_KEY=<your-braintrust-api-key>
```

## Auto-instrumentation

`auto_instrument()` traces MCP clients and servers without wrapping each one. Call `init_logger()` and `auto_instrument()` once at startup. On a server, call them before the server starts serving.

**1. Start the server**

This server uses the `FastMCP` class bundled with `mcp` 1.x:

```python server.py theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import braintrust

braintrust.init_logger(project="mcp-example")  # Replace with your project name
braintrust.auto_instrument()

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("calculator")


@mcp.tool()
def add(a: int, b: int) -> int:
    return a + b


if __name__ == "__main__":
    mcp.run(transport="streamable-http")
```

Run `python server.py` to start the server.

**2. Run the client**

This client calls the server's `add` tool. The tool call span is a child of the `agent` span, and the trace context travels to the server in the request's `_meta` field:

```python client.py theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import asyncio

import braintrust

braintrust.init_logger(project="mcp-example")  # Replace with your project name
braintrust.auto_instrument()

from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client


async def main():
    async with streamablehttp_client("http://localhost:8000/mcp") as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            with braintrust.start_span(name="agent"):
                result = await session.call_tool("add", {"a": 2, "b": 3})
                print(result.content)


asyncio.run(main())
```

Run `python client.py` in another terminal to call the tool.

`auto_instrument()` accepts two MCP keywords:

| Parameter | Default | Effect |
| - | - | - |
| `mcp_client` | `True` | Traces requests that MCP clients send. |
| `mcp_server` | `True` | Traces requests and notifications that MCP servers handle or send. |

For example, to trace only your server:

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
braintrust.auto_instrument(mcp_client=False)
```

You can call `auto_instrument()` again to change these settings without creating duplicate spans. If both `mcp_client` and `mcp_server` are `False`, the call skips MCP setup and leaves any previously installed MCP settings unchanged.

## Manual instrumentation

To trace individual clients or servers, wrap them with `wrap_mcp_server()` and `wrap_mcp_client()` from `braintrust.integrations.mcp`. Settings on a wrapped client or server take precedence over the `auto_instrument()` settings.

`wrap_mcp_server()` accepts a low-level `mcp.server.lowlevel.Server`, the bundled `FastMCP` (`mcp` 1.x) or `MCPServer` (`mcp` 2.x), or a standalone `fastmcp.FastMCP` server. It returns the same server.

**1. Start the server**

```python server.py theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
from braintrust import init_logger
from braintrust.integrations.mcp import wrap_mcp_server
from mcp.server.fastmcp import FastMCP

init_logger(project="mcp-example")  # Replace with your project name

mcp = wrap_mcp_server(FastMCP("calculator"))


@mcp.tool()
def add(a: int, b: int) -> int:
    return a + b


if __name__ == "__main__":
    mcp.run(transport="streamable-http")
```

Run `python server.py` to start the server.

**2. Run the client**

`wrap_mcp_client()` accepts an `mcp.ClientSession`, an `mcp.Client` (`mcp` 2.x), or a standalone `fastmcp.Client`, and returns the same object. Wrap it before connecting to include initialization, or wrap a session that is already connected.

```python client.py theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import asyncio

from braintrust import init_logger, start_span
from braintrust.integrations.mcp import wrap_mcp_client
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

init_logger(project="mcp-example")  # Replace with your project name


async def main():
    async with streamablehttp_client("http://localhost:8000/mcp") as (read, write, _):
        async with wrap_mcp_client(ClientSession(read, write)) as session:
            await session.initialize()
            with start_span(name="agent"):
                result = await session.call_tool("add", {"a": 2, "b": 3})
                print(result.content)


asyncio.run(main())
```

Run `python client.py` in another terminal to call the tool.

Both functions accept `capture_content` (default `True`). Pass `capture_content=False` to omit arguments and results for that client or server.

## Omit arguments and results

MCP tracing records arguments, results, resource contents, and server notification parameters by default. To omit this content for automatically instrumented MCP clients and servers, call `MCPIntegration.setup()` after `auto_instrument()` and before starting your clients or servers:

```python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import braintrust
from braintrust.integrations.mcp import MCPIntegration

braintrust.init_logger(project="mcp-example")
braintrust.auto_instrument()
MCPIntegration.setup(client=True, server=True, capture_content=False)
```

`MCPIntegration.setup()` enables only servers by default, so pass `client=True` to configure both sides. Calling `auto_instrument()` again restores content capture for automatically instrumented objects. Per-object `wrap_mcp_client()` and `wrap_mcp_server()` settings take precedence over process-wide settings.

## What Braintrust traces

Braintrust captures:

* **Client request spans**, one per request round trip, under the caller's current span. Each span records `mcp.span.kind: "client"` in metadata.
* **Server request spans**, from when the server receives a request until it sends the response. When the request carries trace context, the span is a child of the client's span. Otherwise it starts a new trace. Spans you create inside a handler, such as with `start_span()`, nest under it.
* **Server notification spans** for notifications the server receives or sends, such as progress and resource updates. A notification sent while handling a request nests under that request's span.
* **Span names and types**: `tools/call <tool name>` spans have the `tool` type. `prompts/get <prompt name>`, `resources/read`, and other method spans have the `function` type.
* **Content**: tool and prompt arguments as input, the `uri` of `resources/read` requests as input, and tool, prompt, and resource results as output. Inline base64 images, audio, and resource blobs are logged as [attachments](https://docs.braintrust.dev/instrument/attachments.md). Notification spans log their parameters. Listing and other discovery results aren't logged.
* **Metadata**, including `mcp.method.name`, `jsonrpc.request.id`, `mcp.session.id`, `mcp.protocol.version`, `mcp.resource.uri`, `gen_ai.tool.name`, `gen_ai.prompt.name`, client and server names and versions, and network details such as `client.address` for HTTP transports.
* **Errors**: failed requests record `error.type`, and JSON-RPC error responses record `rpc.response.status_code`. Tool results with `isError` mark the span as an error with `error.type: "tool_error"` without changing what your application receives. Server responses with caller-error codes, including invalid requests and unknown methods, record the status code without marking the span as an error. Cancelled requests record `mcp.cancelled: true` instead of an error.
* **Trace propagation**: client spans add W3C `traceparent`, `tracestate`, and Braintrust `baggage` entries to the request's `params._meta`, preserving its other entries.

When `capture_content` is `False`, spans omit arguments, results, and notification parameters, and request errors record only the error code, the exception type, or a generic tool error message.

If `opentelemetry-instrumentation-mcp` is enabled, it creates the client spans and propagates context instead, and Braintrust doesn't add duplicate client spans.

## Resources

* [Model Context Protocol documentation](https://modelcontextprotocol.io/introduction)
* [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)
* [FastMCP documentation](https://gofastmcp.com/)
* [Trace LLM calls](https://docs.braintrust.dev/instrument/trace-llm-calls.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.