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

# MCP JS

> Trace Model Context Protocol clients and servers in TypeScript as one 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 [`@arizeai/openinference-instrumentation-mcp`](https://github.com/Arize-ai/openinference/tree/main/js/packages/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 install `MCPInstrumentation` **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. This example pairs it with [`@arizeai/openinference-instrumentation-openai`](https://github.com/Arize-ai/openinference/tree/main/js/packages/openinference-instrumentation-openai) so the server's tool emits an LLM span.

<Note>
  This is the TypeScript / JavaScript guide. For the Python instrumentor, see [MCP](/ax/integrations/python-agent-frameworks/model-context-protocol/mcp-tracing).
</Note>

## Prerequisites

* Node.js 18+
* 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 server's tool calls OpenAI, which supplies the span the instrumentor threads across the MCP boundary)

## 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 project — the scripts below import each as needed:

```bash theme={null}
npm install @modelcontextprotocol/sdk openai \
  @arizeai/openinference-instrumentation-mcp \
  @arizeai/openinference-instrumentation-openai \
  @arizeai/openinference-semantic-conventions \
  @opentelemetry/api \
  @opentelemetry/exporter-trace-otlp-proto \
  @opentelemetry/resources \
  @opentelemetry/sdk-trace-base \
  @opentelemetry/sdk-trace-node \
  @opentelemetry/semantic-conventions
```

## 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

MCP instrumentation is manual on both sides — `MCPInstrumentation` can't be auto-loaded because the MCP SDK has a non-traditional module structure, so you pass the stdio module explicitly. The **client** patches its stdio transport module and registers a tracer provider that exports to Arize AX:

```typescript theme={null}
// instrumentation.ts
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import {
  SEMRESATTRS_PROJECT_NAME,
} from "@arizeai/openinference-semantic-conventions";
import {
  MCPInstrumentation,
} from "@arizeai/openinference-instrumentation-mcp";
import * as ClientStdioModule from "@modelcontextprotocol/sdk/client/stdio.js";

const projectName = process.env.ARIZE_PROJECT_NAME ?? "mcp-tracing-example";

export const provider = new NodeTracerProvider({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: projectName,
    [SEMRESATTRS_PROJECT_NAME]: projectName,
  }),
  spanProcessors: [
    new SimpleSpanProcessor(
      new OTLPTraceExporter({
        url: "https://otlp.arize.com/v1/traces",
        headers: {
          "arize-space-id": process.env.ARIZE_SPACE_ID ?? "",
          "arize-api-key": process.env.ARIZE_API_KEY ?? "",
        },
      }),
    ),
  ],
});

provider.register();

const mcpInstrumentation = new MCPInstrumentation();
mcpInstrumentation.setTracerProvider(provider);
mcpInstrumentation.manuallyInstrument({
  clientStdioModule: ClientStdioModule,
});

console.log("Arize AX tracing initialized for MCP (client).");
```

## The MCP server

The server has a parallel setup. Its instrumentation patches the **server** stdio module and adds `OpenAIInstrumentation` so the tool's model call is captured as an LLM span. Because context is propagated across the MCP boundary, that span becomes a child of the client's trace.

```typescript theme={null}
// instrumentation_server.ts
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import {
  SEMRESATTRS_PROJECT_NAME,
} from "@arizeai/openinference-semantic-conventions";
import {
  MCPInstrumentation,
} from "@arizeai/openinference-instrumentation-mcp";
import {
  OpenAIInstrumentation,
} from "@arizeai/openinference-instrumentation-openai";
import * as ServerStdioModule from "@modelcontextprotocol/sdk/server/stdio.js";
import OpenAI from "openai";

const projectName = process.env.ARIZE_PROJECT_NAME ?? "mcp-tracing-example";

export const provider = new NodeTracerProvider({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: projectName,
    [SEMRESATTRS_PROJECT_NAME]: projectName,
  }),
  spanProcessors: [
    new SimpleSpanProcessor(
      new OTLPTraceExporter({
        url: "https://otlp.arize.com/v1/traces",
        headers: {
          "arize-space-id": process.env.ARIZE_SPACE_ID ?? "",
          "arize-api-key": process.env.ARIZE_API_KEY ?? "",
        },
      }),
    ),
  ],
});

provider.register();

const mcpInstrumentation = new MCPInstrumentation();
mcpInstrumentation.setTracerProvider(provider);
mcpInstrumentation.manuallyInstrument({
  serverStdioModule: ServerStdioModule,
});

const openaiInstrumentation = new OpenAIInstrumentation();
openaiInstrumentation.setTracerProvider(provider);
openaiInstrumentation.manuallyInstrument(OpenAI);
```

The server exposes one tool that answers a question with OpenAI. Two details matter for stdio transport: the server must not write to stdout (that's the MCP wire channel), and it flushes its spans before returning so the exports land before the client tears the subprocess down.

```typescript theme={null}
// server.ts

// Set up tracing before importing the MCP server APIs so the
// instrumentor patches the stdio transport.
import { provider } from "./instrumentation_server.js";

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
  StdioServerTransport,
} from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import OpenAI from "openai";

const openai = new OpenAI();

const server = new Server(
  { name: "Ocean Knowledge Server", version: "0.1.0" },
  { capabilities: { tools: {} } },
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "explain_ocean",
      description: "Explain an ocean topic in two sentences.",
      inputSchema: {
        type: "object",
        properties: { topic: { type: "string" } },
        required: ["topic"],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const topic = String(request.params.arguments?.topic ?? "the ocean");
  const completion = await openai.chat.completions.create({
    model: "gpt-5.4-mini",
    messages: [
      {
        role: "user",
        content: `Explain ${topic} in two sentences for a general audience.`,
      },
    ],
  });
  const answer = completion.choices[0]?.message?.content ?? "";
  // Export the server-side spans before returning — the client tears
  // this subprocess down as soon as it has the result.
  await provider.forceFlush();
  return { content: [{ type: "text", text: answer }] };
});

const transport = new StdioServerTransport();
await server.connect(transport);
```

## Run MCP

The client uses `StdioClientTransport` to launch `server.ts` as a subprocess and pipe MCP traffic over stdin/stdout. You only run the client — it spawns the server automatically. The `ocean_query` span wraps the tool call; the server's LLM span joins the same trace.

```typescript theme={null}
// example.ts

// Set up tracing before importing the MCP client APIs so the
// instrumentor patches the stdio transport.
import { provider } from "./instrumentation.js";

import { trace } from "@opentelemetry/api";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import {
  StdioClientTransport,
} from "@modelcontextprotocol/sdk/client/stdio.js";

// StdioClientTransport launches the server as a child process and pipes
// MCP traffic over stdin/stdout. It forwards the parent environment, so
// the server sees the same ARIZE_* / OPENAI_API_KEY values.
const transport = new StdioClientTransport({
  command: "npx",
  args: ["tsx", "server.ts"],
  env: process.env as Record<string, string>,
});

const client = new Client({ name: "Ocean Assistant", version: "0.1.0" });
await client.connect(transport);

const tracer = trace.getTracer("mcp-client");
const text = await tracer.startActiveSpan("ocean_query", async (span) => {
  const result = await client.callTool({
    name: "explain_ocean",
    arguments: { topic: "why the ocean is salty" },
  });
  span.end();
  const content = result.content as Array<{ type: string; text?: string }>;
  return content[0]?.text ?? "";
});

console.log(text);

await client.close();

// Flush any pending client spans before the process exits.
await provider.forceFlush();
```

Run the example with `npx tsx example.ts`.

### 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 `ocean_query` span from the client wrapping an `OpenAI Chat Completions` LLM span emitted by the server's tool. The server's span joining the client's trace is the value `MCPInstrumentation` provides.
3. If no traces appear, see [Troubleshooting](#troubleshooting).

### Check from the skill, CLI, or SDK

Confirm spans are actually reaching your Arize AX project. Use whichever fits your workflow — the skill and CLI work for any framework; the SDK check is shown for each language.

<Tabs>
  <Tab title="Arize skill (agent)">
    Install the [Arize Skills](https://github.com/Arize-ai/arize-skills) plugin and let your coding agent check for you:

    ```bash theme={null}
    npx skills add Arize-ai/arize-skills
    ```

    Then prompt your agent:

    > Use the `arize-trace` skill to export and analyze recent traces from my project. Confirm spans are arriving, and summarize any errors or latency issues.
  </Tab>

  <Tab title="AX CLI">
    Export recent spans for your project — any rows mean traces are landing:

    ```bash theme={null}
    ax spans export "$ARIZE_PROJECT_NAME" --space "$ARIZE_SPACE_ID" \
      --limit 5 --stdout | jq 'length'
    ```

    A non-zero count confirms spans reached Arize AX. Run `ax auth login` first if you have not authenticated. See the [`ax spans` reference](/api-clients/cli/spans).
  </Tab>

  <Tab title="SDK">
    Query the project's spans and check that at least one came back.

    <CodeGroup>
      ```python Python theme={null}
      import os
      from arize import ArizeClient

      client = ArizeClient(api_key=os.environ["ARIZE_API_KEY"])
      resp = client.spans.list(
          project=os.environ["ARIZE_PROJECT_NAME"],
          space=os.environ["ARIZE_SPACE_ID"],
          limit=5,
      )
      count = len(resp.spans)
      print(
          f"{count} span(s) found" if count else "No spans yet — recheck setup"
      )
      ```

      ```typescript TypeScript theme={null}
      // Reads ARIZE_API_KEY from the environment.
      import { listSpans } from "@arizeai/ax-client";

      const { data: spans } = await listSpans({
        project: process.env.ARIZE_PROJECT_NAME!,
        space: process.env.ARIZE_SPACE_ID!,
        limit: 5,
      });
      const count = spans.length;
      console.log(
        count ? `${count} span(s) found` : "No spans yet — recheck setup",
      );
      ```

      ```go Go theme={null}
      client, err := arize.NewClient(
          arize.Config{APIKey: os.Getenv("ARIZE_API_KEY")},
      )
      if err != nil {
          log.Fatal(err)
      }
      resp, err := client.Spans.List(ctx, spans.ListRequest{
          Project: os.Getenv("ARIZE_PROJECT_NAME"),
          Space:   os.Getenv("ARIZE_SPACE_ID"),
          Limit:   5,
      })
      if err != nil {
          log.Fatal(err)
      }
      fmt.Printf("%d span(s) found\n", len(resp.Spans))
      ```
    </CodeGroup>

    SDK span references: [Python](/api-clients/python/version-8/client-resources/spans) · [TypeScript](/api-clients/typescript/version-1/client-resources/spans) · [Go](/api-clients/go/version-2/client-resources/spans).
  </Tab>
</Tabs>

## Troubleshooting

* **No traces in Arize AX.** Confirm `ARIZE_SPACE_ID` and `ARIZE_API_KEY` are set in the same shell that runs `example.ts`. Enable OpenTelemetry debug logs with `export OTEL_LOG_LEVEL=debug` and re-run.
* **Client and server show up as separate traces.** `MCPInstrumentation.manuallyInstrument(...)` must run in **both** processes, before the MCP client/server modules are used — keep the `instrumentation.ts` / `instrumentation_server.js` import first in each entry point. If only one side is instrumented, context isn't propagated and each side gets its own trace.
* **Server span missing from the trace.** The server child is torn down as soon as the client has the tool result. Keep the `await provider.forceFlush()` inside the tool handler (before it returns) so the server's spans export before the subprocess exits.
* **`Connection closed` from the client.** Almost always something the server wrote to stdout before the MCP handshake completed. Keep server logging on `console.error` (stderr) — anything on stdout corrupts the MCP wire protocol over stdio.
* **`401` from OpenAI.** Verify `OPENAI_API_KEY` is set and has access to the model in the example. The client passes `env: process.env` to the subprocess, so the single `export OPENAI_API_KEY=...` covers the server too.
* **Different LLM provider on the server.** Swap the server's `openai.OpenAI()` and model name for the provider you want, and add the corresponding OpenInference instrumentor to `instrumentation_server.ts`. The client's instrumentation does not 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/js/packages/openinference-instrumentation-mcp" title="OpenInference MCP Instrumentor (JavaScript / TypeScript)" horizontal />

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

  <Card icon="book-open" href="/ax/integrations/python-agent-frameworks/model-context-protocol/mcp-tracing" title="MCP (Python) tracing" horizontal />
</CardGroup>
