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

# MCP (Model Context Protocol)

> Trace the calls your agents make through MCP clients, and the tools of your own MCP servers, in TypeScript and Python. On by default, metadata only.

> [!NOTE]
>
> Available via: **auto-instrumentation** of the official MCP client packages (TypeScript and Python), plus `mcpServerTool()` / `@mcp_server_tool` for tools in your own MCP server. Nothing to configure; one switch turns it off.

When your agent calls a tool through an MCP client, the call can fail in four different places: the model picked a tool the server doesn't list, the model sent arguments that don't fit the tool, the server answered with an error, or the connection broke. A plain tool span can't tell these apart. The SDK records enough on each call, in your process, for Zespan to say which one it was.

## What is traced

Each call through a patched MCP client becomes an `mcp_client` span (see [Span kinds](https://docs.zespan.com/reference/span-kinds.md)):

| Span | Recorded |
| - | - |
| `initialize` | server name and version, transport, protocol version, or the failure. Not recorded when a client reconnects with a transport that already has a session id: the MCP client skips the handshake then, so there is nothing to record (a reconnect that fails is still recorded) |
| `tools/list` | number of tools and a hash of the whole list (names and input schemas) |
| `tools/call` | tool name, server name and version, transport, protocol version when the client exposes it, **whether the tool was in the server's list**, **whether your arguments fit the tool's input schema**, a hash of that schema and of the list the call was made against, the JSON-RPC error code, and whether the server marked the result as a tool error |

* **"Was the tool listed"** needs a `tools/list` call earlier in the same client. A call made before any list is recorded without this answer (never as "not listed"), and so is a call made when only the first page of a paged list was read.
* **"Do the arguments fit"** is checked in your process against the schema the server sent: required keys and JSON types only. The answer is `true`, `false` or `unknown` (when the schema uses something the SDK doesn't check). `false` is recorded only for a definite miss.
* A result the server flagged as an error is recorded as a **tool error**, not as a connection failure. A closed connection or a timeout is recorded as such. Servers differ in how they report an unknown tool (an error result on some, a JSON-RPC error on others); the recorded "was the tool listed" answer is the same either way.
* Hashes are of the schema with the serializer's noise removed (`$schema` and a top-level `additionalProperties: false`), so upgrading the library a server is built with doesn't look like the tool list changing.

Only ids, names, hashes, counts, codes and true/false answers leave your process. **Tool arguments, results and schemas are never sent by this feature**, and the error message of a failed call is attached only when tool input and output storage is on for the project, with URL credentials, query strings and bearer tokens removed first.

A call you cancel yourself (an aborted `signal` in TypeScript, a cancelled task in Python) is not recorded as a failure.

## Supported clients

> [tab] TypeScript
>
> `@modelcontextprotocol/sdk` (1.x) and `@modelcontextprotocol/client` (2.x), either or both. Tested with `@modelcontextprotocol/sdk` 1.30.1, 1.31.0 and 1.32.1 and `@modelcontextprotocol/client` 2.3.1. Both are optional peer dependencies: nothing is installed for you.
>
> ```ts theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
> import { initZespan } from "@zespan/sdk";
>
> initZespan({ apiKey: "zsp_..." }); // that's all: clients created after this are traced
> ```

> [tab] Python
>
> The `mcp` package (1.x and 2.x), on Python 3.10 or newer (`mcp` itself needs 3.10). Tested with `mcp` 1.30.0, 2.1.1, 2.2.0 and 2.3.0. On Python 3.9 nothing is patched.
>
> ```python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
> import zespan
>
> zespan.init(api_key="zsp_...")  # that's all: ClientSession and Client are traced
> ```
>
> `pip install zespan[mcp]` installs a compatible `mcp`.

The SDK finds the client by its methods, not by its version number: a client that doesn't have the methods it needs is left alone rather than guessed at. Patching never changes what your code gets back: the result and the error are exactly what the MCP client produced (an error nobody handles stays unhandled), and a problem inside the SDK is swallowed. In TypeScript your code resumes one microtask later than without the SDK.

### Python: one record per call

Version 2 of the `mcp` package emits its own OpenTelemetry client span for every request. When Zespan's OpenTelemetry exporter is installed and pointed at Zespan (`enable_otel=True` with `otel_endpoint` set), that span reaches Zespan, so the SDK adds its attributes to it (the span is closed a moment later, after the result is known) and sends nothing else. Without the exporter, inside a Zespan trace (where the library's span would sit in another trace), or for a call you cancelled, the SDK leaves the library's span alone and records the call itself. Either way a call is recorded at most once.

## Turn it off

Set `mcpAutopatch: false` (TypeScript) or `mcp_autopatch=False` (Python) in the SDK options, or set the environment variable `ZESPAN_MCP_AUTOPATCH=off`. It is read at call time, so you can switch it without restarting a long-lived process that already created clients.

## Your own MCP server

Wrap the handler of a tool in your own MCP server so each call is an `mcp_server_tool` span that **joins the caller's trace** from the request's `_meta.traceparent`. Model calls and other work inside the handler nest under it.

> [tab] TypeScript
>
> ```ts theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
> import { mcpServerTool } from "@zespan/sdk";
>
> server.registerTool(
>   "search_orders",
>   { description: "Look up an order", inputSchema: { id: z.string() } },
>   mcpServerTool("search_orders", async ({ id }, extra) => lookup(id), { server: "orders" }),
> );
> ```

> [tab] Python
>
> ```python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
> from zespan import mcp_server_tool
>
> @mcp.tool()
> @mcp_server_tool("search_orders", server="orders")
> async def search_orders(id: str) -> str:
>     return await lookup(id)
> ```

The handler's result and errors pass through unchanged (sync and async). A missing or malformed `traceparent` is simply not joined; the call is still recorded in the current trace.

## Tools from MCP adapters

Tool spans from an MCP adapter in [Google ADK](https://docs.zespan.com/sdk/integrations/google-adk.md), [LangChain](https://docs.zespan.com/sdk/integrations/langchain.md) and the Vercel AI SDK carry the tool name as `mcp_tool_name` (and `zespan.mcp.server` when the adapter names the server). No span is added. An adapter is recognised by its class name (ADK's `McpTool`), its module in Python, or by an `mcp_server` key in the tool's metadata. The LangChain MCP adapters build ordinary structured tools, so for LangChain add `mcp_server` to the tool's metadata; for the Vercel AI SDK, put it in `experimental_telemetry.metadata`.

ADK and the LangChain adapters call the official MCP client underneath, so the same call is usually also an `mcp_client` span.

## Where it shows up

* The spans appear in the trace view with the span kind `mcp_client` (or `mcp_server_tool`).
* Each `tools/call` is also stored as an MCP tool call, with the server name and version, the JSON-RPC error code and the schema hashes.
* Zespan uses the recorded answers (tool listed, arguments valid, error code, tool error, connection failure, tool list changed together with a server version change) to say whether a failed MCP call started with the model or with the MCP server.

## Limits

* A tool the model provider runs on its own servers (a hosted MCP tool inside a model call) never passes through your client and can't be seen.
* Whether the protocol version is recorded depends on the client library: version 1 of the TypeScript client has no accessor for it, so it is recorded only when the transport reports it.
* The OpenTelemetry conventions for MCP are still changing; Zespan maps the current attribute names and keeps the older error-code name readable.

## Next steps

> [card] [Google ADK](https://docs.zespan.com/sdk/integrations/google-adk.md)
>
> Agents built with ADK, including MCP toolsets.

> [card] [Span kinds](https://docs.zespan.com/reference/span-kinds.md)
>
> Where `mcp_client` and `mcp_server_tool` sit among the other kinds.

> [card] [Environment variables](https://docs.zespan.com/platform/environment-variables.md)
>
> The `ZESPAN_MCP_AUTOPATCH` switch.

> [card] [OpenTelemetry](https://docs.zespan.com/sdk/otel-integration.md)
>
> Send MCP spans from any OpenTelemetry setup.


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