diff --git a/.github/workflows/mcp-python-docs.yml b/.github/workflows/mcp-python-docs.yml
new file mode 100644
index 000000000..d69be809f
--- /dev/null
+++ b/.github/workflows/mcp-python-docs.yml
@@ -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'
diff --git a/scripts/mcp-python-native.test.py b/scripts/mcp-python-native.test.py
new file mode 100644
index 000000000..018b4028f
--- /dev/null
+++ b/scripts/mcp-python-native.test.py
@@ -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()
diff --git a/src/lib/integrations.mjs b/src/lib/integrations.mjs
index 13713bdba..a29553b74 100644
--- a/src/lib/integrations.mjs
+++ b/src/lib/integrations.mjs
@@ -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'] },
diff --git a/src/pages/docs/integrations/traceai/mcp-python.mdx b/src/pages/docs/integrations/traceai/mcp-python.mdx
new file mode 100644
index 000000000..e05bbf6b7
--- /dev/null
+++ b/src/pages/docs/integrations/traceai/mcp-python.mdx
@@ -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.
+
+
+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).
+
+
+## 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.
+
+
+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.
+
+
+## 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.
diff --git a/src/pages/docs/integrations/traceai/mcp.mdx b/src/pages/docs/integrations/traceai/mcp.mdx
index b511d1504..b92010568 100644
--- a/src/pages/docs/integrations/traceai/mcp.mdx
+++ b/src/pages/docs/integrations/traceai/mcp.mdx
@@ -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.
+
+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.
+
+
## 1. Installation
First install the traceAI package to access the observability framework
```bash Python
-pip install traceAI-mcp
+pip install "traceAI-mcp>=0.1.3" "mcp>=1.8.1,<2"
```
```bash JS/TS