Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/mcp-python-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Python MCP native tracing documentation

on:
pull_request:
branches: [main]
paths:
- src/pages/docs/integrations/traceai/mcp-python.mdx
- src/pages/docs/integrations/traceai/mcp.mdx
- scripts/mcp-python-native.test.py
- .github/workflows/mcp-python-docs.yml

permissions:
contents: read

jobs:
native-tracing:
name: Native MCP (Python ${{ matrix.python }}, MCP ${{ matrix.mcp }})
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python: ['3.11', '3.13']
mcp: ['==2.3.0', '>=2.3.0,<3']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install published native dependencies
run: python -m pip install 'mcp${{ matrix.mcp }}' opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
- name: Run exact snippets and lifecycle checks with loopback networking only
run: |
TEST_PYTHON="$(command -v python)"
TEST_HOME="$(mktemp -d)"
sudo env TEST_PYTHON="$TEST_PYTHON" TEST_HOME="$TEST_HOME" \
unshare --net sh -c 'ip link set lo up; env -i PATH=/usr/bin:/bin HOME="$TEST_HOME" "$TEST_PYTHON" -I scripts/mcp-python-native.test.py -v'
144 changes: 144 additions & 0 deletions scripts/mcp-python-native.test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""Execute the published MCP snippets against a loopback OTLP receiver.

Install the page's Python dependencies, then run this file with Python.
No model, cloud service, application credentials, or traceAI package is used.
"""
from __future__ import annotations

import base64
import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile
from threading import Thread
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import unittest

from google.protobuf.json_format import MessageToDict
from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import ExportTraceServiceRequest

PAGE = Path(__file__).resolve().parents[1] / "src/pages/docs/integrations/traceai/mcp-python.mdx"


def attributes(entries):
return {entry["key"]: next(iter(entry["value"].values())) for entry in entries}


class NativeMCPGuideTest(unittest.TestCase):
def run_guide(self, *, concurrent=False, failure=False):
page = PAGE.read_text()
snippets = dict(re.findall(r'```python title="([^"]+)"\n(.*?)\n```', page, re.S))
self.assertEqual(set(snippets), {"telemetry.py", "server.py", "client.py"})
self.assertNotIn("traceai_mcp", "".join(snippets.values()))
self.assertIn("https://api.futureagi.com/tracer/v1/traces", page)
self.assertIn("FI_HIDE_ERROR_DETAILS", page)
self.assertIn("do not control native MCP telemetry", page)
if concurrent:
snippets["client.py"] = snippets["client.py"].replace(
'result = await session.call_tool("add", {"a": 2, "b": 3})',
'results = await asyncio.gather(*(session.call_tool("add", {"a": n, "b": 3}) for n in range(4)))\n'
' assert [r.content[0].text for r in results] == ["3", "4", "5", "6"]\n'
' result = results[2]',
)
if failure:
snippets["server.py"] = snippets["server.py"].replace(
"return a + b", 'raise ValueError("public-tool-failure")'
)
requests = []

class Handler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path != "/tracer/v1/traces":
self.send_error(404)
return
message = ExportTraceServiceRequest()
message.ParseFromString(self.rfile.read(int(self.headers["Content-Length"])))
requests.append((dict(self.headers), MessageToDict(message)))
self.send_response(200)
self.send_header("Content-Length", "0")
self.end_headers()

def log_message(self, *args):
pass

with ThreadingHTTPServer(("127.0.0.1", 0), Handler) as receiver:
thread = Thread(target=receiver.serve_forever, daemon=True)
thread.start()
try:
with tempfile.TemporaryDirectory(prefix="mcp-native-guide-") as tmp:
for name, source in snippets.items():
Path(tmp, name).write_text(source)
env = {
"PATH": os.defpath,
"HOME": tmp,
"FI_API_KEY": "synthetic-api-key",
"FI_SECRET_KEY": "synthetic-secret-key",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT":
f"http://127.0.0.1:{receiver.server_port}/tracer/v1/traces",
}
run = subprocess.run(
[sys.executable, str(Path(tmp, "client.py"))],
cwd=tmp, env=env, capture_output=True, text=True, timeout=45,
)
self.assertEqual(run.returncode, 0, run.stderr)
if failure:
self.assertEqual(run.stdout.strip(), "Error executing tool add")
else:
self.assertEqual(run.stdout.strip(), "5")
finally:
receiver.shutdown()
thread.join()

self.assertTrue(requests, "shutdown must deliver spans from both processes")
spans = []
for headers, payload in requests:
headers = {key.lower(): value for key, value in headers.items()}
self.assertEqual(headers["x-api-key"], "synthetic-api-key")
self.assertEqual(headers["x-secret-key"], "synthetic-secret-key")
for group in payload["resourceSpans"]:
resource = attributes(group["resource"]["attributes"])
self.assertEqual(resource["project_name"], "mcp_project")
self.assertEqual(resource["project_type"], "observe")
self.assertEqual(resource["service.name"], "mcp-example")
for scope in group["scopeSpans"]:
self.assertEqual(scope["scope"]["name"], "mcp-python-sdk")
spans.extend(scope["spans"])

tools = [s for s in spans if attributes(s.get("attributes", []))
.get("mcp.method.name") == "tools/call"]
clients = [s for s in tools if s["kind"] == "SPAN_KIND_CLIENT"]
servers = [s for s in tools if s["kind"] == "SPAN_KIND_SERVER"]
expected = 4 if concurrent else 1
self.assertEqual(len(clients), expected, tools)
self.assertEqual(len(servers), expected, tools)
self.assertEqual(len({s["spanId"] for s in tools}), expected * 2)
for server in servers:
parent = next(s for s in clients if s["spanId"] == server["parentSpanId"])
self.assertEqual(server["traceId"], parent["traceId"])
self.assertNotEqual(int.from_bytes(base64.b64decode(server["traceId"]), "big"), 0)
attrs = attributes(server["attributes"])
self.assertEqual(attrs["gen_ai.operation.name"], "execute_tool")
self.assertEqual(attrs["gen_ai.tool.name"], "add")
self.assertEqual(server["name"], "tools/call add")
for key in ("input.value", "output.value", "gen_ai.tool.call.arguments", "gen_ai.tool.call.result"):
self.assertNotIn(key, attrs)
if failure:
self.assertEqual(server["status"]["code"], "STATUS_CODE_ERROR")
else:
self.assertNotEqual(server.get("status", {}).get("code"), "STATUS_CODE_ERROR")
return len(spans)

def test_exact_customer_snippets(self):
self.run_guide()

def test_concurrent_requests_have_one_client_server_pair_each(self):
self.run_guide(concurrent=True)

def test_tool_failure_sets_native_server_error(self):
self.run_guide(failure=True)


if __name__ == "__main__":
unittest.main()
3 changes: 2 additions & 1 deletion src/lib/integrations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ export const integrations = [
{ id: 'instructor', name: 'Instructor', href: '/docs/integrations/traceai/instructor', category: 'frameworks', summary: 'Trace structured-output requests and validation.', method: 'traceAI SDK', logo: 'instructor' },
{ id: 'promptflow', name: 'Prompt Flow', href: '/docs/integrations/traceai/promptflow', category: 'frameworks', summary: 'Trace OpenAI calls inside Prompt Flow pipelines.', method: 'OpenAI client', logo: 'promptflow', aliases: ['PromptFlow'] },
{ id: 'guardrails', name: 'Guardrails AI', href: '/docs/integrations/traceai/guardrails', category: 'frameworks', summary: 'Capture validation and guard execution spans.', method: 'traceAI SDK', logo: 'guardrails' },
{ id: 'mcp', name: 'Model Context Protocol', href: '/docs/integrations/traceai/mcp', category: 'frameworks', summary: 'Trace MCP client and server interactions.', method: 'traceAI SDK', logo: 'mcp', aliases: ['MCP'] },
{ id: 'mcp-python', name: 'MCP for Python', href: '/docs/integrations/traceai/mcp-python', category: 'frameworks', summary: 'Export native MCP 2.x client requests and server tool spans.', method: 'Native OTLP', logo: 'mcp', aliases: ['Model Context Protocol Python'] },
{ id: 'mcp', name: 'Model Context Protocol', href: '/docs/integrations/traceai/mcp', category: 'frameworks', summary: 'Propagate context with TypeScript and existing Python MCP integrations.', method: 'traceAI SDK', logo: 'mcp', aliases: ['MCP'] },
{ id: 'mastra', name: 'Mastra', href: '/docs/integrations/traceai/mastra', category: 'frameworks', summary: 'Trace TypeScript agents and workflows in Mastra.', method: 'traceAI SDK', logo: 'mastra' },
{ id: 'vercel-ai-sdk', name: 'Vercel AI SDK', href: '/docs/integrations/traceai/vercel', category: 'frameworks', summary: 'Capture model and tool calls from the Vercel AI SDK.', method: 'traceAI SDK', logo: 'vercel' },
{ id: 'tanstack-ai', name: 'TanStack AI', href: '/docs/integrations/frameworks/tanstack-ai', category: 'frameworks', summary: 'Send otelMiddleware chat, model-call and tool spans from Node.js.', method: 'OpenTelemetry', logo: 'generic-framework', aliases: ['TanStack'] },
Expand Down
161 changes: 161 additions & 0 deletions src/pages/docs/integrations/traceai/mcp-python.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
title: "Python MCP tracing with Future AGI"
description: "Send native Python MCP client and server spans to Future AGI with OpenTelemetry."
---

Trace Python MCP requests and tool execution in Future AGI using the MCP SDK's native OpenTelemetry support. This example runs a local calculator over stdio; it needs no model API key or Node.js installation.

Use Python 3.11–3.13 and MCP 2.3 or later in the 2.x series. This guide uses the `mcp` package, not the separate `fastmcp` package.

<Note>
Do not enable `MCPInstrumentor` for this native route. MCP already creates request spans and propagates W3C trace context. If you use MCP 1.x, see [existing traceAI installations](#existing-traceai-installations).
</Note>

## Install

```bash
pip install "mcp>=2.3.0,<3" opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
```

## Configure tracing

Set your Future AGI credentials and the OTLP/HTTP trace endpoint:

```bash
export FI_API_KEY="your-futureagi-api-key"
export FI_SECRET_KEY="your-futureagi-secret-key"
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://api.futureagi.com/tracer/v1/traces"
```

Save this standard OpenTelemetry configuration as `telemetry.py`. Both the client and server load it. The resource attributes route spans to the `mcp_project` Observe project; `service.name` alone does not select a Future AGI project.

```python title="telemetry.py"
import os

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

provider = TracerProvider(
resource=Resource.create({
"service.name": "mcp-example",
"project_name": "mcp_project",
"project_type": "observe",
})
)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
endpoint=os.environ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"],
headers={
"X-Api-Key": os.environ["FI_API_KEY"],
"X-Secret-Key": os.environ["FI_SECRET_KEY"],
},
)))
trace.set_tracer_provider(provider)
```

If your application already configures a global OpenTelemetry provider, add the resource attributes and exporter to that configuration instead of installing a second provider.

<Warning>
Native MCP spans include method and tool names. Successful tool calls do not include arguments or results by default, but failures can include exception messages and stack traces. Do not put secrets in tool names or exception messages. Apply your OpenTelemetry Collector's redaction policy before export if error details may contain sensitive data. `FI_HIDE_INPUTS`, `FI_HIDE_OUTPUTS`, and `FI_HIDE_ERROR_DETAILS` do not control native MCP telemetry.
</Warning>

## Create the server

Save this as `server.py` beside `telemetry.py`:

```python title="server.py"
from telemetry import provider
from mcp.server.mcpserver import MCPServer

server = MCPServer("calculator")


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


if __name__ == "__main__":
try:
server.run(transport="stdio")
finally:
provider.shutdown()
```

Keep stdout reserved for the MCP protocol. Send server logs to stderr.

## Call the tool

Save this as `client.py` in the same directory. The client starts the server and passes only the tracing environment variables it needs.

```python title="client.py"
import asyncio
import os
import sys
from pathlib import Path

from telemetry import provider
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


async def main():
parameters = StdioServerParameters(
command=sys.executable,
args=[str(Path(__file__).with_name("server.py"))],
env={key: os.environ[key] for key in (
"FI_API_KEY", "FI_SECRET_KEY", "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"
)},
)
async with stdio_client(parameters) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("add", {"a": 2, "b": 3})
print(result.content[0].text)


if __name__ == "__main__":
try:
asyncio.run(main())
finally:
provider.shutdown()
```

Run it from that directory:

```bash
python client.py
```

The result is `5`. Open **Observe** in Future AGI and select **mcp_project**. Find the `tools/call` client span and its `tools/call add` server child. The server span carries `gen_ai.operation.name=execute_tool` and `gen_ai.tool.name=add`, so Future AGI can classify the tool operation. Initialization has its own spans; a client span and its server child represent the two sides of one request, not duplicate instrumentation.

For a remote MCP server, configure tracing in the server process too. A client cannot export a server's spans on its behalf. Instrument any model SDK used inside your tools separately if you also need model usage and token counts.

## Existing traceAI installations

On MCP 1.x, `traceAI-mcp` propagates context between transports and handlers; it does not create spans. Keep your existing application, agent, or model instrumentation to produce spans on both sides of the connection. Install the published package with Python 3.11–3.13:

```bash
pip install "traceAI-mcp>=0.1.3" "mcp>=1.8.1,<2"
```

Enable it in each process before importing the transport functions:

```python
from traceai_mcp import MCPInstrumentor

MCPInstrumentor().instrument()
```

Configure your existing tracer provider and exporter in both processes. If you must remain on MCP 1.8.1, also pin `pydantic==2.11.10`; that MCP release uses a private Pydantic API removed in newer versions. Prefer upgrading MCP instead of applying this Pydantic pin to newer releases.

When migrating to the native MCP 2.x route, remove `MCPInstrumentor` setup and retain one provider/export pipeline per process. See the [package lifecycle reference](https://github.com/future-agi/traceAI/tree/dev/python/frameworks/mcp#instrumentation-lifecycle) for disabling existing wrappers.

## Troubleshooting

- **No spans:** install the SDK and exporter, configure the global provider in each process before requests run, and call `provider.shutdown()` on exit. Check exporter errors on stderr.
- **Only client spans:** the remote server also needs a provider and exporter. Installing the client package does not instrument a separately deployed server.
- **Authentication or routing errors:** check both Future AGI keys, the `/tracer/v1/traces` endpoint, and the `project_name` / `project_type` resource attributes.
- **No tool arguments or model tokens:** native MCP operation spans are not model instrumentation or a transcript of tool payloads. Instrument the model SDK separately and review its content-capture controls.
6 changes: 5 additions & 1 deletion src/pages/docs/integrations/traceai/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ description: "Propagate trace context across MCP transports and connect it to ap

Connect trace context across MCP client/server transports. MCP instrumentation propagates context; it does not create a complete agent or tool span tree by itself. The Python example uses OpenAI Agents instrumentation to create those spans. The TypeScript example creates an application span around an MCP request.

<Note>
For new Python integrations, use [native MCP 2.x tracing](/docs/integrations/traceai/mcp-python). It creates client and server tool spans without an agent framework or model API key. Do not add `MCPInstrumentor` to that native setup. The Python example below is the existing MCP 1.x propagation route; the TypeScript instructions are separate.
</Note>

## 1. Installation
First install the traceAI package to access the observability framework

<CodeGroup titles={["Python", "JS/TS"]}>

```bash Python
pip install traceAI-mcp
pip install "traceAI-mcp>=0.1.3" "mcp>=1.8.1,<2"
```

```bash JS/TS
Expand Down
Loading