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

# MCP

> Trace Model Context Protocol clients and servers as a unified trace with OpenInference and send spans to Arize AX.

[Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open protocol that lets agents call tools, fetch resources, and receive prompts from independent server processes. The [`openinference-instrumentation-mcp`](https://github.com/Arize-ai/openinference/tree/main/python/instrumentation/openinference-instrumentation-mcp) package is unusual: it doesn't emit any spans of its own. Instead, it propagates OpenTelemetry context across the MCP wire protocol so that spans created independently in the **client** and the **server** join into a single unified trace.

That means you need to install MCPInstrumentor **alongside** another instrumentor that does emit spans (one of the OpenInference framework or LLM instrumentors) in **both** processes, and have both write to the same Arize AX project.

## Prerequisites

* Python 3.10+
* An Arize AX account ([sign up](https://arize.com/sign-up/))
* An `OPENAI_API_KEY` from the [OpenAI Platform](https://platform.openai.com/api-keys) (the example uses the OpenAI Agents SDK as the span-producing instrumentor)

## Launch Arize AX

1. Sign in to your [Arize AX account](https://app.arize.com/).
2. From **Space Settings**, copy your **Space ID** and **API Key**. You will set them as `ARIZE_SPACE_ID` and `ARIZE_API_KEY` below.

## Install

Install the union of client and server dependencies into the same environment — the scripts below import each as needed:

```bash theme={null}
pip install arize-otel \
  openinference-instrumentation-mcp \
  openinference-instrumentation-openai-agents \
  openai-agents mcp fastmcp openai
```

## Configure credentials

```bash theme={null}
export ARIZE_SPACE_ID="<your-space-id>"
export ARIZE_API_KEY="<your-api-key>"
export ARIZE_PROJECT_NAME="mcp-tracing-example"
export OPENAI_API_KEY="<your-openai-api-key>"
```

The client launches the server as a child process and forwards its environment, so both ends see the same `ARIZE_PROJECT_NAME` and write into the same Arize AX project.

## Setup tracing

The setup is identical in both processes — `register()` then both instrumentors. Two important details for the **server**:

1. Pass `verbose=False` to `register()`. The default Arize AX banner prints to stdout, which is the MCP wire-protocol channel; any stray output corrupts the protocol and the client disconnects.
2. Run `register()` and the instrumentors **before** importing `mcp.server.fastmcp` so the patches catch the FastMCP module load.

<CodeGroup>
  ```python Client (instrumentation_client.py) theme={null}
  import os

  from arize.otel import register
  from openinference.instrumentation.mcp import MCPInstrumentor
  from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor

  tracer_provider = register(
      space_id=os.environ["ARIZE_SPACE_ID"],
      api_key=os.environ["ARIZE_API_KEY"],
      project_name=os.environ["ARIZE_PROJECT_NAME"],
  )

  MCPInstrumentor().instrument(tracer_provider=tracer_provider)
  OpenAIAgentsInstrumentor().instrument(tracer_provider=tracer_provider)
  print("Arize AX tracing initialized for MCP (client).")
  ```

  ```python Server (instrumentation_server.py) theme={null}
  import os

  from arize.otel import register
  from openinference.instrumentation.mcp import MCPInstrumentor
  from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor

  # verbose=False suppresses the Arize banner — anything written to stdout
  # would corrupt the MCP wire protocol when the server is launched via stdio.
  tracer_provider = register(
      space_id=os.environ["ARIZE_SPACE_ID"],
      api_key=os.environ["ARIZE_API_KEY"],
      project_name=os.environ["ARIZE_PROJECT_NAME"],
      verbose=False,
  )

  MCPInstrumentor().instrument(tracer_provider=tracer_provider)
  OpenAIAgentsInstrumentor().instrument(tracer_provider=tracer_provider)
  ```
</CodeGroup>

## Run MCP

The client and server are two separate Python files. The client uses `MCPServerStdio` to launch the server as a subprocess and pipe MCP traffic over stdin/stdout. You only run `python client.py` — it spawns `python server.py` automatically.

<CodeGroup>
  ```python client.py theme={null}
  import asyncio
  import os

  # Set up tracing before any agents/mcp imports.
  from instrumentation_client import tracer_provider

  from agents import Agent, Runner
  from agents.mcp import MCPServerStdio


  async def main() -> None:
      # MCPServerStdio launches the server as a child process. Pass `env`
      # explicitly — MCP's stdio transport does not inherit the parent
      # environment by default, so without this the server can't read
      # ARIZE_*  / OPENAI_API_KEY.
      async with MCPServerStdio(
          name="Ocean Knowledge Server",
          params={
              "command": "python",
              "args": ["server.py"],
              "env": dict(os.environ),
          },
          client_session_timeout_seconds=30,
      ) as server:
          agent = Agent(
              name="Ocean Assistant",
              instructions=(
                  "Use the explain_ocean tool to answer the user's question."
              ),
              mcp_servers=[server],
          )
          result = await Runner.run(
              starting_agent=agent,
              input="Why is the ocean salty? Answer in two sentences.",
          )
          print(result.final_output)


  if __name__ == "__main__":
      asyncio.run(main())
  ```

  ```python server.py theme={null}
  # Set up tracing before importing FastMCP so the instrumentor patches it.
  from instrumentation_server import tracer_provider

  import openai
  from mcp.server.fastmcp import FastMCP
  from pydantic import BaseModel

  mcp = FastMCP("Ocean Knowledge Server")
  client = openai.OpenAI()


  class OceanQuery(BaseModel):
      topic: str


  @mcp.tool()
  def explain_ocean(request: OceanQuery) -> dict:
      """Return a short two-sentence explanation about a given ocean topic."""
      response = client.chat.completions.create(
          model="gpt-5",
          messages=[
              {
                  "role": "user",
                  "content": (
                      f"Explain {request.topic} in two sentences for a "
                      f"general audience."
                  ),
              }
          ],
      )
      return {
          "topic": request.topic,
          "answer": response.choices[0].message.content,
      }


  if __name__ == "__main__":
      mcp.run()
  ```
</CodeGroup>

### Expected output

```text wrap theme={null}
Arize AX tracing initialized for MCP (client).
The ocean is salty because rivers continuously dissolve mineral salts from rocks and soil and carry them to the sea, where they accumulate over millions of years. Water leaves the ocean through evaporation but the salts remain, steadily concentrating until reaching today's roughly 3.5% salinity.
```

## Verify in Arize AX

1. Open your Arize AX space and select project **`mcp-tracing-example`**.
2. You should see a single new trace within \~30 seconds containing **both** client- and server-side spans: an `Agent workflow` root span (AGENT) wrapping `Ocean Assistant`, `turn` (CHAIN), `mcp_tools` (CHAIN), `response` (LLM) child spans from the client, and an `explain_ocean` (TOOL) span emitted by the server. The server's tool span is parented under the client's agent — that unified parenting is the value `MCPInstrumentor` provides.
3. If no traces appear, see [Troubleshooting](#troubleshooting).

## Troubleshooting

* **No traces in Arize AX.** Confirm `ARIZE_SPACE_ID` and `ARIZE_API_KEY` are set in the same shell that runs `client.py`. Enable OpenTelemetry debug logs with `export OTEL_LOG_LEVEL=debug` and re-run.
* **`Connection closed` from `MCPServerStdio`.** Almost always something the server wrote to stdout before the MCP handshake completed. Confirm the server's `register()` call uses `verbose=False`, and remove any `print(...)` statements from server module-level code.
* **`KeyError: 'ARIZE_PROJECT_NAME'` (or any other env var) in the server.** The client is not passing its environment through to the subprocess. Make sure the `params` dict in `MCPServerStdio(...)` includes `"env": dict(os.environ)`.
* **Client and server show up as separate traces.** `MCPInstrumentor().instrument(...)` must run in **both** processes, before `mcp` / `agents.mcp` is imported. If only one side is instrumented, context isn't propagated and each side gets its own trace.
* **`401` from OpenAI.** Verify `OPENAI_API_KEY` is set and has access to `gpt-5`. The client and the server's tool both call OpenAI; both need the key. The single `export OPENAI_API_KEY=...` from [Configure credentials](#configure-credentials) covers both because the client passes `env=dict(os.environ)` to the subprocess.
* **Different LLM provider on the server.** Swap the server's `openai.OpenAI()` + the model name for the provider you want, and add the corresponding OpenInference instrumentor to **`instrumentation_server.py`** (e.g. `openinference-instrumentation-anthropic` for Anthropic). The client's instrumentation does not need to change.

## Resources

<CardGroup>
  <Card icon="book-open" href="https://modelcontextprotocol.io/" title="Model Context Protocol Specification" horizontal />

  <Card icon="terminal" href="https://github.com/Arize-ai/openinference/tree/main/python/instrumentation/openinference-instrumentation-mcp" title="OpenInference MCP Instrumentor" horizontal />

  <Card icon="github" href="https://github.com/modelcontextprotocol/python-sdk" title="MCP Python SDK" horizontal />

  <Card icon="github" href="https://github.com/Arize-ai/phoenix/tree/main/tutorials/mcp/tracing_between_mcp_client_and_server" title="End-to-End Example (Client + Server)" horizontal />
</CardGroup>
