Metadata-Version: 2.4
Name: mcp-telemetry
Version: 0.3.0b4
Summary: Zero-config observability for MCP servers. OTel GenAI conventions. Redaction on by default.
Author: threadwire
License: MIT
Project-URL: Homepage, https://github.com/threadwire/mcp-telemetry
Project-URL: Repository, https://github.com/threadwire/mcp-telemetry
Keywords: mcp,model-context-protocol,observability,opentelemetry,ai,agents,tracing
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: otlp
Requires-Dist: httpx>=0.27; extra == "otlp"
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == "mcp"
Provides-Extra: admin
Requires-Dist: fastmcp>=2.0; extra == "admin"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: httpx>=0.27; extra == "dev"
Requires-Dist: mcp>=1.0; extra == "dev"
Requires-Dist: fastmcp>=2.0; extra == "dev"
Dynamic: license-file

# mcp-telemetry

[![License: MIT](https://img.shields.io/badge/license-MIT-brightgreen)](LICENSE)
[![python](https://img.shields.io/badge/python-%3E%3D3.10-3670A0)](#)
[![runtime deps](https://img.shields.io/badge/core_deps-0-brightgreen)](#)

Zero-config observability for MCP servers. OTel GenAI conventions. **Redaction on by default.**

MCP hit **97 million monthly SDK downloads** — and the production playbook is still being
written. OWASP's MCP Top 10 puts **"Lack of Audit and Telemetry"** at the top of the risk
list. This repo is that gap, filled in three lines.

## Install

```bash
pip install mcp-telemetry            # core — zero dependencies
pip install 'mcp-telemetry[otlp]'    # + OTLP/HTTP export (httpx)
```

## Use — no changes to your server logic

```python
import mcp_telemetry as mt

mt.auto()                                # patches the official `mcp` SDK, writes JSONL

@mt.wrap_tool_call("issues.fetch", server="gh")
def fetch_issue(issue_id, token=""):
    ...
```

Every call emits an OTel GenAI `gen_ai.client.tool_call` span with:

- **input fingerprint** — SHA-256 hash, never the raw payload
- **secret scrubbing** — `token`, `secret`, `api_key`-style keys → `[REDACTED]`
- latency, status, error type, server name

Manual spans and traces work too:

```python
with mt.session():                    # one trace for the whole agent turn
    with mt.span("chat.step"):
        ...
```

## Distributed traces cross servers

Propagation is built in. A `traceparent` header on an inbound MCP request starts a
**continuation**, not a new trace — the span stamps `parent_span_id` and the trace id
carries through:

```python
from mcp_telemetry.propagator import parse_traceparent
store.start(parse_traceparent(my_header).trace_id)
```

Pair with [`mcp-hub`](https://github.com/threadwire/mcp-hub): set the gateway's
`telemetryUrl` to this server and every hub-hosted call streams in as a span
whose trace continues whatever `traceparent` the client sent — one trace down
to the upstream and back. See [`examples/responder.py`](examples/responder.py)
for a stdlib-only server that records the far side.

## Watch the firehose

```bash
mcp-trace                 # last 25 spans, ANSI table
mcp-trace --tail          # follow the JSONL feed
mcp-trace --json | jq .   # pipe raw records anywhere
mcp-trace --replay store.jsonl --console   # offline replay → OTLP-shaped output
mcp-trace --serve 8901    # live dashboard + JSON + SSE + /ingest
mcp-trace --serve 8901 --token "$MCP_TRACE_TOKEN"  # protect /ingest
```

`--serve` opens a dark single-file dashboard on `http://127.0.0.1:8901`
(autopolling stats, top-tools latency table, per-upstream health table,
realtime spans over SSE) while keeping the raw `/traces` and `/stats` JSON
endpoints. `POST /ingest` appends straight into the feed — this is where
[`mcp-hub`](https://github.com/threadwire/mcp-hub) points its `telemetryUrl`,
so **client → hub → upstream** spans land in the same dashboard. Spans carrying
a `server` field are aggregated into the upstreams table, so a sick gateway
pops immediately. Add `--max-bytes N` to keep the feed bounded (3 generations).

Containerized (pair it with the hub via the repo-root `docker-compose.yml`):

```bash
docker build -t mcp-trace .          # wheel-based, unprivileged, feed on /data
docker run -p 8901:8901 -e MCP_TRACE_TOKEN=secret \
  -v trace-data:/data mcp-trace
```

`/ingest` is localhost-only by default; add `--token` to require a Bearer
header, and `--rate N` to cap request spam. Binding a non-loopback address
without `--token` is refused outright.

## Exporters

| Exporter          | Where                          | Deps    |
|-------------------|--------------------------------|---------|
| `JsonlExporter`   | `mcp-telemetry.jsonl`          | none    |
| `TextExporter`    | live stderr panel              | none    |
| `OtlpExporter`    | Jaeger/Grafana/Datadog via OTLP/HTTP | `[otlp]` |

Spans follow OTel GenAI semantic conventions (`gen_ai.client.tool_call`,
`gen_ai.agent.invoke`) so traces land in your existing stack without a transform layer.

## Extended surface

- **Sampling** — `parent_based`, `ratio`, `rate_limited` (`mcp_telemetry.sampler`)
- **Metrics** — `Registry` + histogram buckets, `metrics_from_store` (`mcp_telemetry.metrics`)
- **OTel provider** — builds OTLP-shaped telemetry, `OtelProvider.export_built` (`mcp_telemetry.otel_provider`)
- **fastmcp** — opt-in shim: `mt.make_server()`, `patch_fastmcp` (`mcp_telemetry.fastmcp`)
- **Offline replay** — re-deliver any recorded JSONL through the exporter stack

## Overhead

`examples/bench.py`: **~78µs median, ~85µs p95 per instrumented call** (Python 3.14).
There's no free lunch, but at that cost you can trace every tool call in a hot agent loop.

## Design

- `monkey.py` — monkeypatches the official `mcp` SDK's `call_tool`; idempotent, no-ops cleanly when the SDK is absent
- `redact.py` — fingerprinting + secret scrubbing, deterministic hashes
- `store.py` / `api.py` — trace lifecycle + the three-line public surface
- `propagator.py` — W3C `traceparent`/`tracestate` continuation between services
- `replay.py` / `cli.py` — offline re-export + `mcp-trace` renderer/tail/replay
- Core import graph is **stdlib-only**; `httpx` lives behind `[otlp]`

MIT. Ship it.
