Model Context Protocol
Trace requests across official TypeScript MCP SDK v2 client and server transports.
Wrap an MCP transport with instrumentMcpTransport() to trace every MCP request it sends or handles. When you instrument both the client and the server, a tool call shows up as one trace that spans both processes.
@telemetry-dev/mcp works with transports from the official TypeScript MCP SDK v2. To let an agent query telemetry.dev instead, read Connect an AI agent.
Install
Install the core SDK, this integration, and the official MCP package for your side of the connection:
npm i @telemetry-dev/sdk @telemetry-dev/mcp @modelcontextprotocol/clientnpm i @telemetry-dev/sdk @telemetry-dev/mcp @modelcontextprotocol/serverThe integration requires @modelcontextprotocol/client or @modelcontextprotocol/server version 2.x. MCP SDK v1 (@modelcontextprotocol/sdk) is not supported.
Set your API key in each process you instrument:
export TELEMETRY_DEV_API_KEY=td_live_...
Instrument a client
Start the telemetry.dev SDK, then wrap the transport before you connect it:
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { instrumentMcpTransport } from "@telemetry-dev/mcp";
import { init } from "@telemetry-dev/sdk";
init({ serviceName: "mcp-client" });
const client = new Client({ name: "example-client", version: "1.0.0" });
const transport = instrumentMcpTransport(
new StdioClientTransport({ command: "example-mcp-server" }),
);
await client.connect(transport);
Instrument a server
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { instrumentMcpTransport } from "@telemetry-dev/mcp";
import { init } from "@telemetry-dev/sdk";
init({ serviceName: "mcp-server" });
const server = new McpServer({ name: "example-server", version: "1.0.0" });
const transport = instrumentMcpTransport(new StdioServerTransport());
await server.connect(transport);
instrumentMcpTransport() changes the transport in place and returns it. Calling it again on the same transport has no effect.
The wrapper works the same way with Streamable HTTP transports: wrap each transport before you pass it to client.connect() or server.connect(). If createMcpHandler() creates server transports for you, wrap them in your server factory:
import { createMcpHandler, McpServer } from "@modelcontextprotocol/server";
import { instrumentMcpTransport } from "@telemetry-dev/mcp";
const handler = createMcpHandler(() => {
const server = new McpServer({ name: "example-server", version: "1.0.0" });
const connect = server.connect.bind(server);
server.connect = (transport) => connect(instrumentMcpTransport(transport));
return server;
});
Check the trace
Make one request, such as a tool call, then open Traces in your project. With both sides instrumented, the trace has a client span from the client process and a server span from the server process under it. With one side instrumented, you see only that side’s span.
What is traced
Each JSON-RPC request makes a client span on the side that sends it and a server span on the side that handles it. Each span records:
- The MCP method (
mcp.method.name) and JSON-RPC request ID - The protocol version, and the session ID when the transport has one
- The tool name for
tools/call, the prompt name forprompts/get, and the resource URI forresources/read,resources/subscribe, andresources/unsubscribe - Errors: JSON-RPC error responses and tool results marked
isError
Both spans record the JSON-RPC error code. The client span is always marked as an error. The server span is not marked as an error when the code means the caller sent a bad request, such as a parse error, an unknown method, or invalid params.
tools/call requests use the execute_tool operation. Other requests use the mcp operation.
Trace context travels in MCP request metadata by default, so the server span continues the client’s trace. OpenTelemetry baggage stays local unless you set propagateBaggage: true.
What is not traced
- Notifications. They do not produce spans.
- Metrics. The integration does not emit any.
- Tool arguments and results, unless you turn on payload capture.
Control payload capture
Tool arguments and successful tool results are not captured by default. Turn them on for a transport only when that content is safe to send:
const transport = instrumentMcpTransport(mcpTransport, {
capturePayloads: true,
});
The core SDK’s captureInput, captureOutput, mask, and maxAttributeLength settings still apply. capturePayloads lets tool content reach those controls; it does not override them. A failed tool result is marked as an error, but its content is not captured as output.
With payload capture off, method names, tool and prompt names, resource URIs, request and session IDs, and error details are still recorded. Keep secrets out of those fields.
Options
instrumentMcpTransport(transport, options) accepts:
| Option | Type | Default | Description |
|---|---|---|---|
capturePayloads |
boolean |
false |
Capture tool arguments and successful tool results |
propagateBaggage |
boolean |
false |
Send and accept OpenTelemetry baggage in MCP request metadata |
Configure the API key, service name, environment, and content controls with init() options.
The OpenTelemetry MCP semantic conventions are in Development status, so attribute names may change in future releases.