diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5251d28..57fc101 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,10 +18,10 @@ jobs:
with:
python-version: "3.11"
- - name: Install ruff
+ - name: Install ruff (same version as requirements-dev.txt)
run: |
python -m pip install --upgrade pip
- pip install ruff
+ pip install "$(grep '^ruff' requirements-dev.txt)"
- name: Run ruff
run: |
@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13"]
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
@@ -46,7 +46,6 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
pip install -e .
- pip install pytest-cov
- name: Run tests with coverage
run: |
@@ -76,7 +75,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
- node-version: "20"
+ node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
@@ -89,6 +88,11 @@ jobs:
run: |
cd frontend
npm run build
+
+ - name: Committed UI build is up to date
+ run: |
+ git diff --stat --exit-code -- mcphawk/web/static || \
+ (echo "Run 'make build-frontend' and commit mcphawk/web/static" && exit 1)
- name: Install Python build dependencies
run: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 62aea88..8587aa6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -24,7 +24,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
- node-version: "20"
+ node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
@@ -79,15 +79,11 @@ jobs:
run: |
twine check dist/*
- - name: Publish to PyPI
- env:
- TWINE_USERNAME: __token__
- TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
- run: |
- twine upload dist/*
+ - name: Publish to PyPI (Trusted Publishing, no token)
+ uses: pypa/gh-action-pypi-publish@release/v1
- name: Upload release assets
- uses: softprops/action-gh-release@v1
+ uses: softprops/action-gh-release@v2
with:
files: |
dist/*.whl
diff --git a/.gitignore b/.gitignore
index dd3d470..330087f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
*.db
tests/test_logs/
.claude
-CLAUDE.md
.DS_Store
# Node modules
@@ -215,3 +214,6 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/
+
+# Leftover from the removed 0.x terminal UI (kept locally, not part of the package)
+mcphawk/tui/
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..7aa269c
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,71 @@
+# AGENTS.md
+
+MCPHawk is DevTools for MCP: it records real traffic between MCP clients and servers
+(stdio wrapper, HTTP proxy, passive sniffer), stores it in SQLite, and serves a web UI plus
+its own MCP server over the same data. Targets MCP spec 2026-07-28 and still supports the
+older `initialize` handshake era.
+
+## Rules
+
+- Use the local venv (`.venv/bin/python`) and Makefile targets when they exist.
+- Add dependencies to `requirements.txt` / `requirements-dev.txt` and `pyproject.toml`;
+ never `pip install` ad hoc.
+- Before finishing: `make lint` and `make test`. Coverage must stay above 85%
+ (`make coverage` fails below it).
+- New tests: `tests/unit/` for isolated functions, `tests/integration//` for anything
+ touching the DB, processes, network or HTTP. Build traffic with `tests/traffic.py`.
+- Follow the MCP spec and the official SDK instead of inventing custom protocol behaviour.
+- Write PEP 8 from the start (ruff, line length 88, Python 3.10+).
+
+## Layout
+
+```
+capture/ -> store/recorder.py -> SQLite -> query.py + analysis/ -> web/app.py, mcp_server.py
+```
+
+- `protocol/`: JSON-RPC framing, MCP semantics for both eras, secret masking, token estimates
+- `store/recorder.py`: request/response pairing, multi round-trip chains, client/server identity
+- `query.py`: the one read layer; the web API and the MCP server must not query SQL themselves
+- `runs.py`: agent runs, computed at read time (client group, split at 5 min idle gaps).
+ `sessions.client_key` stores only which client process a session belongs to
+- `install/`: client config locations and install/uninstall
+- `otel/`: OpenTelemetry export (`semconv.py` maps to the MCP semantic conventions,
+ `exporter.py` streams OTLP from `mcphawk up`, `prometheus.py` serves `/metrics`)
+- `frontend/`: Vue 3 app, built into `mcphawk/web/static` (committed)
+
+## Commands
+
+```bash
+make install # deps + editable install + frontend deps
+make test # or test-unit / test-integration / test-e2e ...
+make dev # mcphawk up on :8484 + Vite on :5173
+make build-frontend # then commit mcphawk/web/static; CI fails if it is stale
+make demo # demo traffic from three SDK servers
+```
+
+## Gotchas
+
+- MCP SDK 2.x: `FastMCP` is now `mcp.server.mcpserver.MCPServer`; the client is
+ `mcp.client.client.Client`.
+- The SDK's stdio client gives child processes a filtered environment. Pass `env=` when
+ spawning `mcphawk wrap` in tests or examples, or data lands in `~/.mcphawk` instead of
+ `MCPHAWK_DB`.
+- Tests isolate `MCPHAWK_HOME` / `MCPHAWK_DB` in `tests/conftest.py`; never point anything
+ at the real `~/.mcphawk`.
+- Async fixtures must not hold an SDK `Client` open across yield (anyio cancel scopes);
+ open the client inside the test.
+- The stdio shim forwards bytes before recording them, and its two pump threads race: a
+ response can be recorded before its request. The recorder pairs these; keep it that way.
+- Mutating API routes need the `X-MCPHawk: 1` header and a localhost `Host` (CSRF and DNS
+ rebinding guard). The SDK's `/mcp` endpoint also rejects non-localhost hosts, so use
+ `base_url="http://127.0.0.1:8484"` with `TestClient`.
+- Secrets are masked before storage. Replay refuses requests or commands that still contain
+ the mask.
+- Remote HTTP servers without static headers are assumed to use OAuth and are not proxied by
+ default: their tokens are bound to the server URL.
+- Claude Code retitles its process with its version number; `capture/process.py` falls back
+ to `argv[0]` for client names.
+- OpenTelemetry is an optional extra: only `otel/exporter.py` may import the SDK, and only
+ after `mcphawk.otel.available()`; `semconv.py` and `prometheus.py` must work without it.
+ Use the convention names verbatim; anything of ours goes under `mcphawk.*`, and
+ `examples/grafana/mcphawk-dashboard.json` is checked against `/metrics` by a test.
diff --git a/Makefile b/Makefile
index bb8e260..dd72df6 100644
--- a/Makefile
+++ b/Makefile
@@ -1,83 +1,82 @@
-.PHONY: install install-frontend build build-frontend dev dev-backend dev-frontend test test-unit test-integration test-db test-network test-cli test-web test-mcp test-watch coverage coverage-report lint format format-unsafe clean
+.PHONY: install install-backend install-frontend build build-frontend dev dev-backend dev-frontend demo test test-unit test-integration test-db test-capture test-network test-install test-cli test-web test-mcp test-e2e coverage lint format clean
-# Install all dependencies
+# Setup
install: install-backend install-frontend
install-backend:
- pip3 install -e .
pip install -r requirements-dev.txt
+ pip install -e .
install-frontend:
cd frontend && npm install
-# Build for production
+# Build (the built UI is committed to mcphawk/web/static)
build: build-frontend
+ python -m build
build-frontend:
cd frontend && npm run build
-# Development commands
+# Development: API on 8484, Vite with hot reload on 5173
dev:
- @echo "Starting both backend and frontend..."
@make -j 2 dev-backend dev-frontend
dev-backend:
- mcphawk web --port 3000
+ mcphawk up
dev-frontend:
cd frontend && npm run dev
-# Testing
+# Generate realistic demo traffic into the default database
+demo:
+ python examples/demo/run_demo.py
+
+# Tests
test:
- python3 -m pytest -v
+ python -m pytest -v
test-unit:
- python3 -m pytest tests/unit -v
+ python -m pytest tests/unit -v
test-integration:
- python3 -m pytest tests/integration -v
+ python -m pytest tests/integration -v
test-db:
- python3 -m pytest tests/integration/db -v
+ python -m pytest tests/integration/db -v
+
+test-capture:
+ python -m pytest tests/integration/capture -v
test-network:
- python3 -m pytest tests/integration/network -v
+ python -m pytest tests/integration/network -v
+
+test-install:
+ python -m pytest tests/integration/install -v
test-cli:
- python3 -m pytest tests/integration/cli -v
+ python -m pytest tests/integration/cli -v
test-web:
- python3 -m pytest tests/integration/web -v
+ python -m pytest tests/integration/web -v
test-mcp:
- python3 -m pytest tests/integration/mcp -v
+ python -m pytest tests/integration/mcp -v
-test-watch:
- python3 -m pytest -v --watch
+test-e2e:
+ python -m pytest tests/integration/e2e -v
-# Coverage
+# Coverage (fails under 85%, see pyproject.toml)
coverage:
- python3 -m pytest -v --cov=mcphawk --cov-report=html --cov-report=term
+ python -m pytest --cov=mcphawk --cov-report=html --cov-report=term --cov-report=xml
-coverage-report:
- python3 -m pytest -v --cov=mcphawk --cov-report=html --cov-report=term --cov-report=xml
- @echo "Coverage report generated in htmlcov/index.html"
-
-# Linting
+# Code quality
lint:
ruff check .
format:
ruff check . --fix
-format-unsafe:
- ruff check . --fix --unsafe-fixes
-
-# Clean
clean:
- rm -rf frontend/node_modules
- rm -rf frontend/dist
- rm -rf mcphawk/web/static/*
- find . -type d -name __pycache__ -exec rm -rf {} +
- find . -type d -name .pytest_cache -exec rm -rf {} +
- find . -type d -name .coverage -exec rm -rf {} +
\ No newline at end of file
+ rm -rf frontend/node_modules build dist *.egg-info htmlcov .coverage coverage.xml
+ find . -type d -name __pycache__ -prune -exec rm -rf {} +
+ find . -type d -name .pytest_cache -prune -exec rm -rf {} +
diff --git a/README.md b/README.md
index 4b2d3f3..1e6c776 100644
--- a/README.md
+++ b/README.md
@@ -1,352 +1,267 @@

-
+
[](https://github.com/tech4242/mcphawk/actions/workflows/ci.yml)
[](https://codecov.io/gh/tech4242/mcphawk)
+ [](https://pypi.org/project/mcphawk/)
[](https://www.python.org/downloads/)
- [](https://typer.tiangolo.com/)
- [](https://fastapi.tiangolo.com/)
- [](https://vuejs.org/)
- [](https://github.com/astral-sh/ruff)
- [](https://www.python.org/dev/peps/pep-0008/)
+ [](https://modelcontextprotocol.io/specification/2026-07-28)
[](https://opensource.org/licenses/MIT)
-MCPHawk is a new Logging & Monitoring solution for **Model Context Protocol (MCP)** traffic, providing deep visibility into MCP client-server interactions. It started off as a mix between Wireshark and mcpinspector, purpose-built for the MCP ecosystem, and is now slowly turning into something more.
-
-**Key Capabilities:**
-- **Protocol-Aware Capture**: Understands MCP's JSON-RPC 2.0 transport layer, capturing and reassembling messages from stdio pipes and HTTP streams
-- **Transport Agnostic**: Monitors MCP traffic across all standard transports (stdio, HTTP Streaming, HTTP+SSE)
-- **Full Message Reconstruction**: Advanced stream reassembly handles fragmented packets, chunked HTTP transfers, SSE streams, and stdio pipes
-
-
-
-## Core Features
-
-### 🔍 MCP Protocol Analysis
-- **Complete JSON-RPC 2.0 Support**: Correctly identifies and categorizes all MCP message types
- - **Requests**: Method calls with unique IDs for correlation
- - **Responses**: Success results and error responses with matching IDs
- - **Notifications**: Fire-and-forget method calls without IDs
- - **Batch Operations**: Support for JSON-RPC batch requests/responses
-- **Transport-Specific Handling**: See MCP Transport Support table below for full details
- - **Chunked Transfer**: Handles HTTP chunked transfer encoding transparently
-- **Protocol Compliance**: Validates JSON-RPC 2.0 structure and MCP-specific extensions
-
-### 🚀 Advanced Capture Capabilities
-- **Auto-Discovery Mode**: Intelligently detects MCP traffic on any port using pattern matching
-- **TCP Stream Reassembly**: Reconstructs complete messages from fragmented packets
-- **Multi-Stream Tracking**: Simultaneously monitors multiple MCP client-server connections
-- **IPv4/IPv6 Dual Stack**: Native support for both IP protocols
-- **Zero-Copy Architecture**: Efficient packet processing without client/server overhead
-
-### 📊 Analysis & Visualization
-- **Real-Time Web Dashboard**: Live traffic visualization with WebSocket updates
-- **Message Flow Visualization**: Track request-response pairs using JSON-RPC IDs
-- **Traffic Statistics**: Method frequency, error rates, response times
-- **Search & Filter**: Query by method name, message type, content patterns
-- **Export Capabilities**: Save captured sessions for offline analysis
-
-### 🛠️ Developer Experience
-- **MCP Server Integration**: Query captured data using MCP protocol itself
- - FastMCP-based implementation for maximum compatibility
- - Available tools: `query_traffic`, `search_traffic`, `get_stats`, `list_methods`
- - Supports both stdio and HTTP transports
-- **Multiple Interfaces**:
- - Web UI for interactive exploration
- - CLI for scripting and automation
- - MCP server for programmatic access
-- **Flexible Deployment**:
- - Standalone sniffer mode
- - Integrated web + sniffer
- - Historical log analysis without active capture
-
-### MCP Transport Support
-
-| Official MCP Transport | Protocol Version | Capture Support | Details |
-|------------------------|------------------|:---------------:|---------|
-| **stdio** | All versions | ✅ Full | Process wrapper transparently captures stdin/stdout between client and server |
-| **HTTP Streaming** | 2025-03-26+ | ✅ Full | HTTP POST with optional SSE streaming responses |
-| **HTTP+SSE** (deprecated) | 2024-11-05 | ✅ Full | Legacy transport with separate SSE endpoint |
-
-Note: Raw TCP traffic with JSON-RPC is also captured and marked as "unknown" transport type
-
-## Comparison with Similar Tools
-
-| Feature | MCPHawk | mcpinspector | Wireshark |
-|-----------------------------------------------|:---------:|:------------:|:---------:|
-| Passive sniffing (no proxy needed) | ✅ | ❌ | ✅ |
-| MCP/JSON-RPC protocol awareness | ✅ | ✅ | ❌ |
-| SSE/Chunked HTTP support | ✅ | ❓ | ❌ |
-| TCP stream reassembly | ✅ | ❌ | ✅ |
-| Auto-detect MCP traffic | ✅ | ❌ | ❌ |
-| Web UI for live/historical traffic | ✅ | ✅ | ❌ |
-| JSON-RPC message type detection | ✅ | ❌ | ❌ |
-| MCP server for data access | ✅ | ❌ | ❌ |
-| No client/server config needed | ✅ | ❌ | ✅ |
-| Interactive testing/debugging | ❌ | ✅ | ❌ |
-| Proxy/MITM capabilities | ✅ (stdio) | ✅ | ❌ |
-
-**When to use each tool:**
-- **MCPHawk**: Passive monitoring, protocol analysis, debugging MCP implementations, understanding traffic patterns
-- **mcpinspector**: Active testing, crafting requests, interactive debugging with proxy
-- **Wireshark**: General network analysis, non-MCP protocols, packet-level inspection
-
-## TLS/HTTPS Limitations
-
-MCPHawk captures **unencrypted** MCP traffic only. It cannot decrypt:
-- HTTPS/WSS (WebSocket Secure) connections
-- TLS-encrypted TCP connections
-- Any SSL/TLS encrypted traffic
-
-**This tool is ideal for:**
-- 🛠️ **Local MCP development** - Debug your MCP server implementations
-- 🔍 **Understanding MCP protocol** - See actual JSON-RPC message flow
-- 🐛 **Troubleshooting local tools** - Monitor Claude Desktop, Cline, etc. with YOUR local MCP servers
-- 📊 **Development/staging environments** - Where TLS is often disabled
-
-## Installation
-
-### For Users
-
-```bash
-# Install from PyPI
-pip install mcphawk
-
-# Or install directly from GitHub
-pip install git+https://github.com/tech4242/mcphawk.git
-```
+**MCPHawk is DevTools for the Model Context Protocol.** It records the real traffic
+between your MCP clients (Claude Code, Claude Desktop, Cursor, VS Code, your own agent)
+and their servers, and shows it the way a browser's network tab would: every call your
+agent made, in order, across all its servers, with what it cost and what went wrong.
-### Requirements
+The same data is available to your agent: add MCPHawk as an MCP server and Claude Code can
+find the failing call, read it, fix your server and check again, linking you to exactly
+what it looked at.
-- **macOS/Linux**: Requires `sudo` for packet capture (standard for network sniffers)
-- **Python**: 3.9 or higher
-- **Permissions**: Must run with elevated privileges to access network interfaces
+
-### Quick Start
+## Get started in a minute
```bash
-# Get help
-mcphawk --help
-
-# Get help for specific command
-mcphawk sniff --help
-mcphawk web --help
+pip install mcphawk # or prefix the commands below with `uvx`
+mcphawk install # route your clients' MCP servers through MCPHawk
+ # ...restart your MCP clients and use them as usual...
+mcphawk up --open # open the UI at http://127.0.0.1:8484
+```
-# Start web UI with auto-detect mode (requires sudo on macOS)
-sudo mcphawk web --auto-detect
+`mcphawk install` finds the MCP servers of Claude Desktop, Claude Code, Cursor and VS Code,
+backs up each config file, and puts MCPHawk in front of every server it can record without
+getting in the way:
-# Monitor MCP traffic on a specific port (console output)
-sudo mcphawk sniff --port 3000
+```text
+claude-desktop (user) ~/Library/Application Support/Claude/claude_desktop_config.json
+ + filesystem recorded (stdio wrapper)
+ + github recorded (stdio wrapper)
-# Monitor multiple ports with a custom filter
-sudo mcphawk sniff --filter "tcp port 3000 or tcp port 8080"
+claude-code (user) ~/.claude.json
+ + postgres recorded (stdio wrapper)
+ + docs-search recorded (HTTP proxy)
+ linear skipped: remote server without static headers, probably OAuth (--force-http)
-# Auto-detect MCP traffic on any port
-sudo mcphawk sniff --auto-detect
+cursor (user) ~/.cursor/mcp.json
+ + playwright recorded (stdio wrapper)
+ sentry skipped: remote server without static headers, probably OAuth (--force-http)
-# Start web UI with sniffer on specific port
-sudo mcphawk web --port 3000
+5 server(s) will be recorded, 2 skipped. Continue? [Y/n]:
+```
-# Start web UI with custom filter for multiple ports
-sudo mcphawk web --filter "tcp port 3000 or tcp port 8080"
+Nothing else about your servers changes, and `mcphawk uninstall` puts every entry back.
+The **Setup** page shows the same picture at any time:
-# View historical logs only (no active sniffing)
-sudo mcphawk web --no-sniffer
+
-# Custom web server configuration
-sudo mcphawk web --port 3000 --host 0.0.0.0 --web-port 9000
+Let your agent read the traffic too:
-# Enable debug output for troubleshooting
-sudo mcphawk sniff --port 3000 --debug
-sudo mcphawk web --port 3000 --debug
+```bash
+claude mcp add mcphawk -- mcphawk mcp
+```
-# Wrap an MCP server to capture stdio traffic
-mcphawk wrap /path/to/mcp-server --arg1 --arg2
+No client handy? `make demo` drives three real MCP servers so the UI has something to show.
-# Example: Wrap Context7 MCP server to monitor Claude Desktop's documentation lookups
-mcphawk wrap npx -y @upstash/context7-mcp@latest
+## Agent runs
-# Claude Desktop config to use the wrapped version:
-# {
-# "mcpServers": {
-# "context7": {
-# "command": "mcphawk",
-# "args": ["wrap", "npx", "-y", "@upstash/context7-mcp@latest"]
-# }
-# }
-# }
+Everything in MCPHawk is organised around **agent runs**. A run is everything one client
+did in one stretch of work, across all of its MCP servers: ask Claude Code to fix a flaky
+test and the filesystem reads, the GitHub calls and the ticket it files all land on one
+timeline, in the order they happened.
-# Start MCP server with Streamable HTTP transport (default)
-mcphawk mcp --transport http --mcp-port 8765
+* **One client, all its servers.** stdio servers are tied to the client process that
+ started them; HTTP servers join when they report the same client name at the same time.
+* **Split by pauses.** Five minutes without a single call ends the run, so a Claude Code
+ window that stays open all day becomes one run per task, not one endless log.
+* **Links stay valid.** A run keeps its address as more traffic arrives, so you can paste
+ it into an issue or let the agent hand it to you.
-# Start MCP server with stdio transport (for Claude Desktop integration)
-mcphawk mcp --transport stdio
+Sessions (one client talking to one server) are still there underneath: click a server
+name above the timeline to see just that connection.
-# Start sniffer with integrated MCP server (HTTP transport)
-sudo mcphawk sniff --port 3000 --with-mcp --mcp-transport http
+## What you can do with it
-# Start web UI with integrated MCP server
-sudo mcphawk web --port 3000 --with-mcp --mcp-transport http --mcp-port 8765
-```
+### See what the agent actually did
-## MCP Server Integration
+The timeline shows every call of a run as a waterfall: which server, which tool, how long
+it took, how big the result was, and whether it failed. Filter by status, or search inside
+request and response payloads. Multi round-trip requests from the 2026-07-28 spec (the
+server asks for input, the client retries) are shown as one chain.
-MCPHawk includes a built-in MCP server, allowing you to query captured traffic through the Model Context Protocol itself. This creates powerful possibilities:
+### Inspect a single call
-- **AI-Powered Analysis**: Connect Claude or other LLMs to analyze traffic patterns
-- **Automated Monitoring**: Build agents that detect anomalies or specific behaviors
-- **Integration Testing**: Programmatically verify MCP interactions in CI/CD pipelines
+Click any call for its details: the tool result rendered the way the model received it
+(text, images, embedded resources), the full request and response, HTTP headers, and a
+link you can share. **Replay** sends the same request again, optionally with edited
+arguments, and records the replay next to the original so you can compare.
-
+
-### Available Tools
+### Find out what your servers cost in context
-The MCP server exposes these tools for traffic analysis:
+Every tool definition is sent to the model on every turn, whether the tool is used or not.
+**Context cost** estimates the tokens each server and each tool adds per turn, splits
+descriptions from schemas, marks tools that were never called, and lists the results that
+blew up the conversation.
-| Tool | Description | Parameters |
-|------|-------------|------------|
-| `query_traffic` | Fetch captured logs with pagination | `limit`, `offset` |
-| `get_log` | Retrieve specific log entry | `log_id` |
-| `search_traffic` | Search logs by content or type | `search_term`, `message_type`, `traffic_type`, `limit` |
-| `get_stats` | Get traffic statistics | None |
-| `list_methods` | List unique JSON-RPC methods | None |
+
-### Transport Options
+### Know what went wrong, first
-#### HTTP Transport (Development & Testing)
+**Problems** collects everything worth a look, most severe first: JSON-RPC errors, tool
+errors, calls that never got an answer, slow calls, agents repeating the same call, and spec
+violations for the protocol version each session negotiated (missing `resultType` or cache
+hints, missing `Mcp-Method` headers, stray output on stdout, deprecated features).
-The HTTP transport uses Server-Sent Events (SSE) for streaming responses:
+
-```bash
-# Start MCP server
-mcphawk mcp --transport http --mcp-port 8765
+### Catch regressions between versions
-# Initialize session (note: returns SSE stream)
-curl -N -X POST http://localhost:8765/mcp \
- -H 'Accept: text/event-stream' \
- -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'
+**Compare** puts two sessions of a server side by side: tools added, removed or changed
+(with the change in tokens per turn) and how each call's count, errors and latency moved.
-# Example response (SSE format):
-# event: message
-# data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05",...}}
-```
+### Send it to Grafana, Datadog or any OpenTelemetry backend
-#### stdio Transport (Production & Claude Desktop)
+MCPHawk streams what it records as OpenTelemetry **metrics, logs and traces** over OTLP,
+using the [OpenTelemetry conventions for MCP](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/mcp.md),
+so MCP traffic shows up next to everything else you monitor. It works for every server
+MCPHawk records, including ones that have no instrumentation of their own.
-For Claude Desktop integration:
+Try it locally with Grafana's all-in-one OpenTelemetry image:
-```json
-{
- "mcpServers": {
- "mcphawk": {
- "command": "mcphawk",
- "args": ["mcp", "--transport", "stdio"]
- }
- }
-}
+```bash
+docker run -p 3000:3000 -p 4318:4318 grafana/otel-lgtm
+pip install 'mcphawk[otel]'
+OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 mcphawk up --otlp
```
-The stdio transport follows the standard MCP communication pattern:
-1. Client sends `initialize` request
-2. Server responds with capabilities
-3. Client sends `initialized` notification
-4. Normal tool calls can proceed
-
-See [examples/mcp_sdk_client.py](examples/mcp_sdk_client.py) for HTTP client example or [examples/stdio_client.py](examples/stdio_client.py) for stdio communication.
-
-## Platform Support
-
-### Tested Platforms
-- ✅ **macOS** (Apple Silicon & Intel) - Fully tested
-- ✅ **Linux** (Ubuntu, Debian) - Fully tested
-- ⚠️ **Windows** - Experimental (Scapy should work but untested)
-
-### Known Limitations
+Then import [`examples/grafana/mcphawk-dashboard.json`](examples/grafana/mcphawk-dashboard.json)
+in Grafana (http://localhost:3000): requests, failures, error rate, p95 latency per tool,
+the failing tools, and what each server's tool definitions cost per turn.
+
+| Signal | What is sent |
+|---|---|
+| Metrics | `mcp.client.operation.duration` (request count, failures by `error.type`, latency), `mcphawk.tool.result.tokens`, `mcphawk.tool.definition.tokens` |
+| Logs | One record per MCP message: method, direction, tool, ids, errors. Payloads only with `--otlp-payloads` (masked, capped) |
+| Traces | One span per call. It joins the agent's own trace when the client sends a `traceparent`; otherwise each agent run is one trace. Every span links back into the MCPHawk UI |
+
+Configuration uses the standard OpenTelemetry variables (`OTEL_EXPORTER_OTLP_ENDPOINT`,
+`OTEL_EXPORTER_OTLP_HEADERS`, `OTEL_SERVICE_NAME`, ...), so any OTLP backend works, for
+example the Datadog Agent's OTLP receiver. Already run Prometheus? `mcphawk up` also
+serves the same metrics at `http://127.0.0.1:8484/metrics` for scraping, and the dashboard
+works with either. To send past traffic, run `mcphawk export --otlp --run `.
+
+### Let your agent debug with you
+
+MCPHawk is an MCP server too. Your agent can list runs, read a failing call, check context
+cost and compare versions, and every answer links back into the UI.
+
+## How traffic is captured
+
+| Mode | Command | Works for | Notes |
+|---|---|---|---|
+| **Wrap** (stdio) | `mcphawk wrap -- ` | Any stdio server | What `mcphawk install` sets up. Bytes are forwarded before they are recorded, so capture never slows the client down. |
+| **Proxy** (HTTP) | `mcphawk install --include-http`, or `mcphawk proxy --target URL` | Streamable HTTP and legacy HTTP+SSE, including HTTPS and remote servers | Runs inside `mcphawk up`. Remote servers that use OAuth are skipped by default: their tokens are bound to the server URL. |
+| **Sniff** (passive) | `sudo mcphawk sniff --port 3000` | Plaintext local HTTP or raw TCP | No config change at all, but needs capture privileges and cannot read TLS. |
+
+### Protocol support
+
+Both protocol generations are first-class:
+
+* **2026-07-28 (stateless):** identity from `_meta`, `server/discover`, multi round-trip
+ requests (`input_required` → retry), `subscriptions/listen`, `Mcp-Method`/`Mcp-Name`
+ headers, cache hints.
+* **2024-11-05 to 2025-11-25:** the `initialize` handshake, `Mcp-Session-Id`, legacy
+ HTTP+SSE endpoints, server-initiated sampling, elicitation and roots.
+
+## MCP tools for agents
+
+`mcphawk mcp` (stdio) or `http://127.0.0.1:8484/mcp` while `mcphawk up` runs:
+
+| Tool | Answers |
+|---|---|
+| `list_runs` | What did my agents do recently? |
+| `list_sessions` | Which client talked to which server? |
+| `get_session` | Who talked to whom, and how did each call go? |
+| `get_exchange` | What exactly was sent and returned? (capped, truncation is marked) |
+| `find_problems` | What went wrong, most severe first? |
+| `context_cost` | Which servers and tools are eating my context window? |
+| `compare_sessions` | What changed since the last version of my server? |
+
+Every result links into the web UI. Replay is deliberately not exposed to agents. If you
+run the UI on a port other than 8484, set `MCPHAWK_URL` (e.g. `http://127.0.0.1:9000`) for
+`mcphawk mcp` so its links point to the right place.
+
+## Privacy and safety
+
+* Everything stays on your machine in `~/.mcphawk/mcphawk.db` (`MCPHAWK_DB` to change).
+* Secrets are masked **before** they are stored: auth headers, keys named like
+ `token`/`api_key`/`password`, and common token formats (API keys, JWTs, bearer tokens,
+ private keys). Use `--no-mask` only if you need raw values, for example to replay a call
+ that needs its credentials.
+* The UI binds to `127.0.0.1`. State-changing API calls (replay, clearing data) require a
+ custom header and a localhost `Host`, so other websites cannot trigger them.
+* `mcphawk install` backs up every file it touches to `~/.mcphawk/backups/`, and
+ `uninstall` restores entries structurally, keeping edits you made in between.
+
+## How it compares
+
+| | MCPHawk | [mcpsnoop](https://github.com/kerlenton/mcpsnoop) | [MCP Inspector](https://github.com/modelcontextprotocol/inspector) | [MCP Shark](https://github.com/mcp-shark/mcp-shark) |
+|---|:-:|:-:|:-:|:-:|
+| Real client traffic (Claude, Cursor, …) | ✅ | ✅ | ❌ own client | ✅ |
+| One-command setup for all clients | ✅ | ❌ per server | n/a | ✅ |
+| Passive capture without config changes | ✅ | ❌ | ❌ | ❌ |
+| Web UI | ✅ | ❌ terminal | ✅ | ✅ |
+| Cross-server run timeline | ✅ | ❌ | ❌ | ❌ |
+| Context cost per tool | ✅ | ❌ | ❌ | ❌ |
+| Spec lint per protocol version | ✅ | ✅ | ❌ | ❌ |
+| MCP server so agents can query traffic | ✅ | ❌ | ❌ | ❌ |
+| Replay | ✅ | ✅ | ✅ | ✅ playground |
+| OpenTelemetry (OTLP) export | ✅ | ✅ | ❌ | ❌ |
+| HAR export, CI mode | ❌ not yet | ✅ | ❌ | partial |
+
+Use the Inspector to poke at a server interactively; use MCPHawk to see what really happens
+when your agent uses it.
+
+## CLI
-- Requires elevated privileges (`sudo`) on macOS/Linux for packet capture
-- Limited to localhost/loopback interface monitoring
-- Cannot decrypt TLS/HTTPS traffic (WSS, HTTPS)
-- IPv6 support requires explicit interface configuration on some systems
-- High traffic volumes (>1000 msgs/sec) may impact performance
-
-### Troubleshooting
-
-**Permission Denied Error:**
-```bash
-# On macOS/Linux, use sudo:
-sudo mcphawk web --auto-detect
+```
+mcphawk up Web UI, API, proxy, /mcp and /metrics on one port (default command)
+ --otlp streams metrics, logs and traces to OTEL_EXPORTER_OTLP_ENDPOINT
+mcphawk install Route client configs through MCPHawk (--include-http, --dry-run, --client)
+mcphawk uninstall Restore the original configs
+mcphawk status Where data lives, what was captured, which servers are routed
+mcphawk wrap Record one stdio server: mcphawk wrap --name fs -- npx -y @mcp/fs ~/
+mcphawk proxy Record one HTTP server: mcphawk proxy --target https://example.com/mcp
+mcphawk mcp MCPHawk's own MCP server (stdio or --transport http)
+mcphawk sniff Passive capture (needs sudo)
+mcphawk export Send captured traffic to an OpenTelemetry backend (--otlp)
+mcphawk clear Delete captured traffic
```
-**No Traffic Captured:**
-- Ensure the MCP server/client is using localhost (127.0.0.1 or ::1)
-- Check if traffic is on the expected port
-- Try auto-detect mode to find MCP traffic: `--auto-detect`
-- Verify traffic is unencrypted (not HTTPS/TLS)
-- On macOS, ensure Terminal has permission to capture packets in System Preferences
-
-**SSE/HTTP Responses Not Showing:**
-- Confirm the server uses standard SSE format (event: message\ndata: {...}\n\n)
-- Check if responses use chunked transfer encoding
-- Enable debug mode to see detailed packet analysis: `--debug`
-
-## Potential Upcoming Features
-
-Vote for features by opening a GitHub issue!
-
-- [x] **Auto-detect MCP traffic** - Automatically discover MCP traffic on any port without prior configuration
-- [x] **MCP Server Interface** - Expose captured traffic via MCP server for AI agents to query and analyze traffic patterns
-- [x] **Stdio capture** - Transparent process wrapper to capture stdin/stdout communication
-- [ ] **Protocol Version Detection** - Identify and display MCP protocol version from captured traffic
-- [ ] **Smart Search & Filtering** - Search by method name, params, or any JSON field with regex support
-- [ ] **Performance Analytics** - Request/response timing, method frequency charts, and latency distribution
-- [ ] **Export & Share** - Export sessions as JSON/CSV, generate shareable links, create HAR-like files
-- [ ] **Test Generation** - Auto-generate test cases from captured traffic
-- [ ] **Error Analysis** - Highlight errors, group similar issues, show error trends
-- [ ] **Session Management** - Save/load capture sessions, compare sessions side-by-side
-- [ ] **Interactive Replay** - Click any request to re-send it, edit and replay captured messages
-- [ ] **Real-time Alerts** - Alert on specific methods or error patterns with webhook support
-- [ ] **Visualization** - Sequence diagrams, resource heat maps, method dependency graphs
-
-... and a few more off the deep end:
-- [ ] **TLS/HTTPS Support (MITM Proxy Mode)** - Optional man-in-the-middle proxy with certificate installation for encrypted traffic
-- [ ] **External Decryption Integration** - Import decrypted streams from Wireshark, Chrome DevTools, or SSLKEYLOGFILE
-
-## For Developers
+## Development
```bash
-# Set up Python environment
-python3 -m venv .venv
-source .venv/bin/activate # On Windows: .venv\Scripts\activate
-
-# Install backend dependencies
-pip3 install -r requirements-dev.txt
-pip3 install -e .
-
-# Install frontend dependencies and build
-cd frontend
-npm install
-npm run build
-cd ..
-
-# Run tests
-python3 -m pytest -v
+python3 -m venv .venv && source .venv/bin/activate
+make install # Python deps from requirements-dev.txt + editable install + frontend deps
+make dev # API on :8484 and the Vite dev server with hot reload on :5173
+make test # unit + integration tests (coverage must stay above 85%)
+make lint
+make build-frontend # the built UI in mcphawk/web/static is committed
```
-### Some Vue options:
+The architecture in one line: every capture mode feeds a `Recorder` (pairing, chains,
+identity, masking) that writes to SQLite; the web API and the MCP server are thin views over
+one `Query` layer and the `analysis` modules.
-```bash
-# Option 1: Use make (recommended)
-make dev # Runs both frontend and backend
+## Upgrading from 0.x
-# Option 2: Run separately
-# Terminal 1 - Frontend with hot reload
-cd frontend && npm run dev
+1.0 is a rewrite. The database moved to `~/.mcphawk/` with a new schema (old captures are
+not migrated), the Textual/terminal UI is gone, and `mcphawk web` became `mcphawk up`.
+Existing `mcphawk wrap ` entries in client configs keep working and are recognised
+by `install`/`uninstall`.
-# Terminal 2 - Backend
-mcphawk web --port 3000
+## License
-# Option 3: Watch mode
-cd frontend && npm run build:watch # Auto-rebuild on changes
-mcphawk web --port 3000 # In another terminal
-```
+MIT
diff --git a/docs/images/cost.jpg b/docs/images/cost.jpg
new file mode 100644
index 0000000..470a734
Binary files /dev/null and b/docs/images/cost.jpg differ
diff --git a/docs/images/inspector.jpg b/docs/images/inspector.jpg
new file mode 100644
index 0000000..52a639b
Binary files /dev/null and b/docs/images/inspector.jpg differ
diff --git a/docs/images/problems.jpg b/docs/images/problems.jpg
new file mode 100644
index 0000000..3f53f1b
Binary files /dev/null and b/docs/images/problems.jpg differ
diff --git a/docs/images/setup.jpg b/docs/images/setup.jpg
new file mode 100644
index 0000000..1af1eae
Binary files /dev/null and b/docs/images/setup.jpg differ
diff --git a/docs/images/timeline.jpg b/docs/images/timeline.jpg
new file mode 100644
index 0000000..8b262f3
Binary files /dev/null and b/docs/images/timeline.jpg differ
diff --git a/examples/branding/mcphawk_screenshot.png b/examples/branding/mcphawk_screenshot.png
deleted file mode 100644
index 7844a63..0000000
Binary files a/examples/branding/mcphawk_screenshot.png and /dev/null differ
diff --git a/examples/demo/run_demo.py b/examples/demo/run_demo.py
new file mode 100644
index 0000000..c8df3a5
--- /dev/null
+++ b/examples/demo/run_demo.py
@@ -0,0 +1,66 @@
+"""Generate realistic MCP traffic through `mcphawk wrap`, then open the UI.
+
+ python examples/demo/run_demo.py # first task
+ python examples/demo/run_demo.py --second # run 5+ minutes later: a new agent run
+ mcphawk up --open
+
+Runs three servers (one modern 2026-07-28, two with the legacy handshake) from
+a single client process, so they show up as one run with a shared timeline.
+"""
+
+import asyncio
+import os
+import sys
+from pathlib import Path
+
+from mcp.client.client import Client
+from mcp.client.stdio import StdioServerParameters
+
+SERVERS = Path(__file__).with_name("servers.py")
+
+
+def wrapped(name: str) -> StdioServerParameters:
+ return StdioServerParameters(
+ command=sys.executable,
+ args=["-m", "mcphawk", "wrap", "--name", name, "--", sys.executable, str(SERVERS), name],
+ env=dict(os.environ), # the SDK passes a filtered env by default; keep MCPHAWK_DB
+ )
+
+
+async def first_task() -> None:
+ """Research a rollback, with a loop and a failing ticket along the way."""
+ async with (
+ Client(wrapped("weather")) as weather,
+ Client(wrapped("docs"), mode="legacy") as docs,
+ Client(wrapped("flaky"), mode="legacy") as tickets,
+ ):
+ for client in (weather, docs, tickets):
+ await client.list_tools()
+ await docs.call_tool("search_docs", {"query": "deploy rollback", "limit": 5})
+ await weather.call_tool("get_weather", {"city": "Berlin"})
+ await weather.call_tool("forecast", {"city": "Berlin", "days": 5})
+ await weather.call_tool("get_weather", {"city": "Atlantis"})
+ await docs.call_tool("read_page", {"page_id": "runbook-42"})
+ for _ in range(3): # an agent stuck in a loop
+ await docs.call_tool("search_docs", {"query": "rollback", "limit": 3})
+ await tickets.call_tool("create_ticket", {"title": "Rollback failed", "priority": "urgent"})
+ await tickets.call_tool("create_ticket", {"title": "Rollback failed"})
+ await tickets.call_tool("slow_report", {})
+
+
+async def second_task() -> None:
+ """A later, shorter piece of work: check the weather and file a ticket."""
+ async with (
+ Client(wrapped("weather")) as weather,
+ Client(wrapped("flaky"), mode="legacy") as tickets,
+ ):
+ for client in (weather, tickets):
+ await client.list_tools()
+ await weather.call_tool("forecast", {"city": "Lisbon", "days": 3})
+ await weather.call_tool("get_weather", {"city": "Lisbon"})
+ await tickets.call_tool("create_ticket", {"title": "Offsite weather check"})
+
+
+if __name__ == "__main__":
+ asyncio.run(second_task() if "--second" in sys.argv else first_task())
+ print("Done. Run `mcphawk up --open` to explore the traffic.")
diff --git a/examples/demo/servers.py b/examples/demo/servers.py
new file mode 100644
index 0000000..4f4f2a0
--- /dev/null
+++ b/examples/demo/servers.py
@@ -0,0 +1,82 @@
+"""Three small MCP servers for the MCPHawk demo: python servers.py ."""
+
+import asyncio
+import random
+import sys
+
+from mcp.server.mcpserver import MCPServer
+
+weather = MCPServer("weather", version="1.4.0", instructions="Current weather and forecasts.")
+
+
+@weather.tool()
+def get_weather(city: str) -> str:
+ """Current weather for a city."""
+ if city.lower() == "atlantis":
+ raise ValueError("unknown city")
+ return f"{city}: {random.choice(['sunny', 'light rain', 'overcast'])}, {random.randint(8, 27)}°C"
+
+
+@weather.tool()
+def forecast(city: str, days: int = 3) -> str:
+ """Daily forecast for the next few days."""
+ return "\n".join(f"Day {d + 1}: {random.randint(8, 27)}°C" for d in range(days))
+
+
+docs = MCPServer("docs-search", version="0.9.2", instructions=(
+ "Search the internal documentation. Always prefer search_docs over reading whole pages. "
+ "Results are ranked by relevance and include section anchors. " * 6))
+
+_VERBOSE = (
+ "Searches every document in the knowledge base, including archived pages, design notes, "
+ "meeting transcripts and API references. Supports boolean operators, phrase queries, "
+ "field filters (author:, team:, updated:), fuzzy matching and synonym expansion. " * 8)
+
+
+@docs.tool(description=_VERBOSE)
+def search_docs(query: str, limit: int = 10, include_archived: bool = False,
+ team: str | None = None, updated_after: str | None = None) -> str:
+ return "\n\n".join(
+ f"## Result {i + 1}: {query} in practice\n" + ("Lorem ipsum dolor sit amet. " * 60)
+ for i in range(limit))
+
+
+@docs.tool()
+def read_page(page_id: str) -> str:
+ """Read one documentation page in full."""
+ return f"# Page {page_id}\n" + ("A very long page body. " * 3000)
+
+
+@docs.tool()
+def list_spaces() -> list[str]:
+ """List documentation spaces."""
+ return ["engineering", "product", "support"]
+
+
+@docs.tool()
+def export_pdf(page_id: str) -> str:
+ """Export a page as PDF (rarely useful for agents)."""
+ return "ok"
+
+
+flaky = MCPServer("tickets", version="2.0.0")
+
+
+@flaky.tool()
+async def create_ticket(title: str, priority: str = "normal") -> str:
+ """Create a support ticket."""
+ await asyncio.sleep(random.uniform(0.05, 0.4))
+ if priority == "urgent":
+ raise RuntimeError("upstream ticketing API timed out")
+ return f"Created TCK-{random.randint(1000, 9999)}: {title}"
+
+
+@flaky.tool()
+async def slow_report() -> str:
+ """Generate a weekly report (slow)."""
+ await asyncio.sleep(1.5)
+ return "Report ready."
+
+
+if __name__ == "__main__":
+ {"weather": weather, "docs": docs, "flaky": flaky}[sys.argv[1]].run("stdio")
diff --git a/examples/grafana/mcphawk-dashboard.json b/examples/grafana/mcphawk-dashboard.json
new file mode 100644
index 0000000..87f7d94
--- /dev/null
+++ b/examples/grafana/mcphawk-dashboard.json
@@ -0,0 +1,616 @@
+{
+ "title": "MCPHawk: MCP traffic",
+ "uid": "mcphawk-mcp-traffic",
+ "description": "Requests, failures, latency and context cost of MCP servers, from MCPHawk (OTLP metrics or the /metrics endpoint).",
+ "tags": [
+ "mcp",
+ "mcphawk"
+ ],
+ "timezone": "browser",
+ "schemaVersion": 39,
+ "version": 1,
+ "editable": true,
+ "time": {
+ "from": "now-6h",
+ "to": "now"
+ },
+ "refresh": "30s",
+ "templating": {
+ "list": [
+ {
+ "name": "datasource",
+ "label": "Prometheus",
+ "type": "datasource",
+ "query": "prometheus",
+ "current": {},
+ "hide": 0
+ },
+ {
+ "name": "server",
+ "label": "Server",
+ "type": "query",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "query": {
+ "query": "label_values(mcp_client_operation_duration_seconds_count, mcphawk_server_name)",
+ "refId": "server"
+ },
+ "definition": "label_values(mcp_client_operation_duration_seconds_count, mcphawk_server_name)",
+ "includeAll": true,
+ "multi": true,
+ "allValue": ".*",
+ "current": {
+ "selected": true,
+ "text": [
+ "All"
+ ],
+ "value": [
+ "$__all"
+ ]
+ },
+ "refresh": 2,
+ "sort": 1
+ },
+ {
+ "name": "client",
+ "label": "Client",
+ "type": "query",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "query": {
+ "query": "label_values(mcp_client_operation_duration_seconds_count, mcphawk_client_name)",
+ "refId": "client"
+ },
+ "definition": "label_values(mcp_client_operation_duration_seconds_count, mcphawk_client_name)",
+ "includeAll": true,
+ "multi": true,
+ "allValue": ".*",
+ "current": {
+ "selected": true,
+ "text": [
+ "All"
+ ],
+ "value": [
+ "$__all"
+ ]
+ },
+ "refresh": 2,
+ "sort": 1
+ }
+ ]
+ },
+ "panels": [
+ {
+ "id": 1,
+ "type": "stat",
+ "title": "Requests",
+ "description": "MCP requests answered in the selected time range.",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "gridPos": {
+ "h": 4,
+ "w": 6,
+ "x": 0,
+ "y": 0
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "expr": "sum(increase(mcp_client_operation_duration_seconds_count{mcphawk_server_name=~\"$server\", mcphawk_client_name=~\"$client\"}[$__range]))",
+ "legendFormat": "",
+ "refId": "A",
+ "instant": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "color": {
+ "mode": "fixed",
+ "fixedColor": "text"
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "colorMode": "value",
+ "graphMode": "none",
+ "textMode": "value"
+ }
+ },
+ {
+ "id": 2,
+ "type": "stat",
+ "title": "Failed requests",
+ "description": "JSON-RPC errors, tool errors and cancellations.",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "gridPos": {
+ "h": 4,
+ "w": 6,
+ "x": 6,
+ "y": 0
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "expr": "sum(increase(mcp_client_operation_duration_seconds_count{mcphawk_server_name=~\"$server\", mcphawk_client_name=~\"$client\", error_type!=\"\"}[$__range])) or vector(0)",
+ "legendFormat": "",
+ "refId": "A",
+ "instant": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "color": {
+ "mode": "fixed",
+ "fixedColor": "red"
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "colorMode": "value",
+ "graphMode": "none",
+ "textMode": "value"
+ }
+ },
+ {
+ "id": 3,
+ "type": "stat",
+ "title": "Error rate",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "gridPos": {
+ "h": 4,
+ "w": 6,
+ "x": 12,
+ "y": 0
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "expr": "sum(rate(mcp_client_operation_duration_seconds_count{mcphawk_server_name=~\"$server\", mcphawk_client_name=~\"$client\", error_type!=\"\"}[$__range])) / sum(rate(mcp_client_operation_duration_seconds_count{mcphawk_server_name=~\"$server\", mcphawk_client_name=~\"$client\"}[$__range]))",
+ "legendFormat": "",
+ "refId": "A",
+ "instant": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "percentunit",
+ "color": {
+ "mode": "fixed",
+ "fixedColor": "orange"
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "colorMode": "value",
+ "graphMode": "none",
+ "textMode": "value"
+ }
+ },
+ {
+ "id": 4,
+ "type": "stat",
+ "title": "Tool definitions per turn",
+ "description": "Estimated tokens every model turn spends on tool definitions (\u22484 chars/token).",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "gridPos": {
+ "h": 4,
+ "w": 6,
+ "x": 18,
+ "y": 0
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "expr": "sum(mcphawk_tool_definition_tokens{mcphawk_server_name=~\"$server\"})",
+ "legendFormat": "",
+ "refId": "A",
+ "instant": true
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "color": {
+ "mode": "fixed",
+ "fixedColor": "purple"
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "colorMode": "value",
+ "graphMode": "none",
+ "textMode": "value"
+ }
+ },
+ {
+ "id": 5,
+ "type": "timeseries",
+ "title": "Requests per second by server",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 4
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "expr": "sum by (mcphawk_server_name) (rate(mcp_client_operation_duration_seconds_count{mcphawk_server_name=~\"$server\", mcphawk_client_name=~\"$client\"}[$__rate_interval]))",
+ "legendFormat": "{{mcphawk_server_name}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "reqps",
+ "custom": {
+ "drawStyle": "line",
+ "fillOpacity": 15,
+ "lineWidth": 1,
+ "stacking": {
+ "mode": "normal",
+ "group": "A"
+ }
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 6,
+ "type": "timeseries",
+ "title": "Failures by error type",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 4
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "expr": "sum by (error_type) (rate(mcp_client_operation_duration_seconds_count{mcphawk_server_name=~\"$server\", mcphawk_client_name=~\"$client\", error_type!=\"\"}[$__rate_interval]))",
+ "legendFormat": "{{error_type}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "reqps",
+ "custom": {
+ "drawStyle": "line",
+ "fillOpacity": 15,
+ "lineWidth": 1,
+ "stacking": {
+ "mode": "normal",
+ "group": "A"
+ }
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 7,
+ "type": "timeseries",
+ "title": "Tool call latency p95",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 12
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "expr": "histogram_quantile(0.95, sum by (le, gen_ai_tool_name) (rate(mcp_client_operation_duration_seconds_bucket{mcphawk_server_name=~\"$server\", mcphawk_client_name=~\"$client\", mcp_method_name=\"tools/call\"}[$__rate_interval])))",
+ "legendFormat": "{{gen_ai_tool_name}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "s",
+ "custom": {
+ "drawStyle": "line",
+ "fillOpacity": 15,
+ "lineWidth": 1,
+ "stacking": {
+ "mode": "none",
+ "group": "A"
+ }
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 8,
+ "type": "timeseries",
+ "title": "Tool result tokens per minute",
+ "description": "Estimated tokens tool results added to the conversation.",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 12
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "expr": "sum by (gen_ai_tool_name) (rate(mcphawk_tool_result_tokens_sum{mcphawk_server_name=~\"$server\", mcphawk_client_name=~\"$client\"}[$__rate_interval])) * 60",
+ "legendFormat": "{{gen_ai_tool_name}}",
+ "refId": "A"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short",
+ "custom": {
+ "drawStyle": "line",
+ "fillOpacity": 15,
+ "lineWidth": 1,
+ "stacking": {
+ "mode": "normal",
+ "group": "A"
+ }
+ }
+ },
+ "overrides": []
+ },
+ "options": {
+ "legend": {
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "desc"
+ }
+ }
+ },
+ {
+ "id": 9,
+ "type": "table",
+ "title": "Failing tools",
+ "description": "",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 20
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "expr": "sort_desc(sum by (mcphawk_server_name, gen_ai_tool_name, error_type) (increase(mcp_client_operation_duration_seconds_count{mcphawk_server_name=~\"$server\", mcphawk_client_name=~\"$client\", error_type!=\"\"}[$__range]))) > 0",
+ "legendFormat": "",
+ "refId": "A",
+ "instant": true,
+ "format": "table"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "transformations": [
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": {
+ "Time": true
+ },
+ "renameByName": {
+ "Value": "Value",
+ "gen_ai_tool_name": "Tool",
+ "error_type": "Error",
+ "mcphawk_server_name": "Server"
+ }
+ }
+ }
+ ],
+ "options": {
+ "showHeader": true,
+ "sortBy": [
+ {
+ "displayName": "Value",
+ "desc": true
+ }
+ ]
+ }
+ },
+ {
+ "id": 10,
+ "type": "table",
+ "title": "Tool definition cost",
+ "description": "Tokens each tool definition adds to every model turn, largest first.",
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 20
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "${datasource}"
+ },
+ "expr": "sort_desc(sum by (mcphawk_server_name, gen_ai_tool_name) (mcphawk_tool_definition_tokens{mcphawk_server_name=~\"$server\"}))",
+ "legendFormat": "",
+ "refId": "A",
+ "instant": true,
+ "format": "table"
+ }
+ ],
+ "fieldConfig": {
+ "defaults": {
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "transformations": [
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": {
+ "Time": true
+ },
+ "renameByName": {
+ "Value": "Value",
+ "gen_ai_tool_name": "Tool",
+ "error_type": "Error",
+ "mcphawk_server_name": "Server"
+ }
+ }
+ }
+ ],
+ "options": {
+ "showHeader": true,
+ "sortBy": [
+ {
+ "displayName": "Value",
+ "desc": true
+ }
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/http_sse_example.py b/examples/http_sse_example.py
deleted file mode 100644
index c484c62..0000000
--- a/examples/http_sse_example.py
+++ /dev/null
@@ -1,198 +0,0 @@
-#!/usr/bin/env python3
-"""
-Proper HTTP+SSE MCP client traffic generator.
-
-This simulates the legacy HTTP+SSE transport pattern as documented:
-1. GET request to /sse endpoint to establish SSE connection
-2. Server sends "endpoint" event with the message endpoint URL
-3. Client POSTs JSON-RPC messages to the message endpoint
-
-This example creates traffic that MCPHawk can properly detect as HTTP+SSE.
-"""
-
-import json
-import threading
-import time
-from http.server import BaseHTTPRequestHandler, HTTPServer
-
-import requests
-
-
-class MockHTTPSSEServer(BaseHTTPRequestHandler):
- """Mock server that implements HTTP+SSE pattern."""
-
- def log_message(self, format, *args):
- """Suppress default logging."""
- pass
-
- def do_GET(self):
- """Handle GET request for SSE connection."""
- if self.path == '/sse':
- print("[Mock Server] Received GET /sse - sending SSE response")
- self.send_response(200)
- self.send_header('Content-Type', 'text/event-stream')
- self.send_header('Cache-Control', 'no-cache')
- self.send_header('Connection', 'keep-alive')
- self.end_headers()
-
- # Send the endpoint event as per HTTP+SSE spec
- endpoint_event = 'event: endpoint\ndata: {"url": "/messages"}\n\n'
- self.wfile.write(endpoint_event.encode())
- self.wfile.flush()
-
- # For this example, close after sending endpoint
- # Real servers would keep the connection open for streaming
- return
- else:
- self.send_error(404)
-
- def do_POST(self):
- """Handle POST request to message endpoint."""
- if self.path == '/messages':
- print("[Mock Server] Received POST /messages")
- content_length = int(self.headers['Content-Length'])
- post_data = self.rfile.read(content_length)
-
- # Parse the JSON-RPC request
- try:
- request = json.loads(post_data)
- print(f"[Mock Server] Request: {request}")
-
- # Send a simple response
- response = {
- "jsonrpc": "2.0",
- "result": {"initialized": True},
- "id": request.get("id")
- }
-
- self.send_response(200)
- self.send_header('Content-Type', 'application/json')
- self.end_headers()
- self.wfile.write(json.dumps(response).encode())
- except Exception as e:
- print(f"[Mock Server] Error: {e}")
- self.send_error(400)
- else:
- self.send_error(404)
-
-
-def run_mock_server(port=8766):
- """Run the mock HTTP+SSE server in a thread."""
- server = HTTPServer(('localhost', port), MockHTTPSSEServer)
- server_thread = threading.Thread(target=server.serve_forever)
- server_thread.daemon = True
- server_thread.start()
- return server
-
-
-def simulate_http_sse_client(server_port=8766):
- """Simulate HTTP+SSE client traffic pattern."""
-
- print("\nSimulating HTTP+SSE MCP Client (Legacy Pattern)")
- print("=" * 50)
-
- server_url = f"http://localhost:{server_port}"
-
- # Step 1: Establish SSE connection with GET request
- print("\n1. Establishing SSE connection...")
- print(f" GET {server_url}/sse")
- print(" Accept: text/event-stream")
-
- # Use requests library for better control
- session = requests.Session()
-
- endpoint_url = None
-
- try:
- # Make GET request with SSE accept header
- headers = {'Accept': 'text/event-stream'}
- response = session.get(f"{server_url}/sse", headers=headers, stream=True, timeout=2)
-
- print(f" Response: {response.status_code} {response.reason}")
- print(f" Content-Type: {response.headers.get('Content-Type')}")
-
- # Read the endpoint event with timeout on iter_lines
- try:
- for line in response.iter_lines(decode_unicode=True, chunk_size=1):
- if line:
- print(f" SSE: {line}")
- if line.startswith('data:'):
- data = json.loads(line[5:].strip())
- endpoint_url = data.get('url')
- break
- except (requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError):
- print(" SSE connection closed/timed out (expected)")
-
- response.close() # Close the streaming connection
-
- except Exception as e:
- print(f" Error during GET: {e}")
-
- # Always try the POST request, even if GET had issues
- if not endpoint_url:
- endpoint_url = "/messages" # Default endpoint for HTTP+SSE
- print(f"\n2. Using default endpoint URL: {endpoint_url}")
- else:
- print(f"\n2. Server sent endpoint URL: {endpoint_url}")
-
- # Step 2: Send JSON-RPC request to the endpoint
- print("\n3. Sending JSON-RPC request to endpoint...")
- print(f" POST {server_url}{endpoint_url}")
- print(" Content-Type: application/json")
-
- try:
- # Send initialize request
- initialize_request = {
- "jsonrpc": "2.0",
- "method": "initialize",
- "params": {
- "protocolVersion": "2024-11-05",
- "capabilities": {},
- "clientInfo": {
- "name": "http-sse-test",
- "version": "1.0.0"
- }
- },
- "id": 1
- }
-
- # Important: No Accept header with dual types for HTTP+SSE POST
- post_response = session.post(
- f"{server_url}{endpoint_url}",
- json=initialize_request,
- headers={'Content-Type': 'application/json'},
- timeout=5
- )
-
- print(f" Response: {post_response.status_code}")
- if post_response.status_code == 200:
- print(f" Result: {post_response.json()}")
-
- except Exception as e:
- print(f" Error during POST: {e}")
-
- print("\n" + "=" * 50)
- print("HTTP+SSE pattern demonstration complete")
- print("Check MCPHawk to see the detected transport type:")
- print("- GET /sse with Accept: text/event-stream → HTTP+SSE")
- print("- Server sends 'endpoint' event → Confirms HTTP+SSE")
- print("- POST to endpoint without dual Accept → HTTP+SSE pattern")
-
-
-if __name__ == "__main__":
- print("HTTP+SSE MCP Client Example (Proper Implementation)")
- print("This demonstrates the legacy HTTP+SSE transport pattern")
- print("with a mock server that properly implements the protocol\n")
-
- # Start mock server
- port = 8766
- print(f"Starting mock HTTP+SSE server on port {port}...")
- server = run_mock_server(port)
- time.sleep(1) # Give server time to start
-
- # Run client simulation
- simulate_http_sse_client(port)
-
- # Keep server running briefly for any remaining packets
- time.sleep(1)
- print("\nDone!")
diff --git a/examples/mcp_sdk_client.py b/examples/mcp_sdk_client.py
deleted file mode 100755
index 20637fc..0000000
--- a/examples/mcp_sdk_client.py
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env python3
-"""Test MCPHawk server using the official MCP SDK client."""
-
-import asyncio
-import json
-
-from mcp import ClientSession
-from mcp.client.streamable_http import streamablehttp_client
-
-
-async def test_with_sdk_client():
- """Test using the SDK's official client."""
-
- # Connect to the server
- async with streamablehttp_client("http://localhost:8765/mcp") as (read_stream, write_stream, session_id):
- print(f"Connected with session ID: {session_id}")
-
- # Create a session
- async with ClientSession(read_stream, write_stream) as session:
- # Initialize
- print("\n1. Initializing...")
- await session.initialize()
- print("Initialized successfully")
-
- # List tools
- print("\n2. Listing tools...")
- tools_result = await session.list_tools()
- print(f"Available tools: {len(tools_result.tools)}")
- for tool in tools_result.tools:
- print(f" - {tool.name}: {tool.description}")
-
- # Call a tool
- print("\n3. Calling get_stats...")
- result = await session.call_tool("get_stats", arguments={})
- print("Result:")
- for content in result.content:
- print(f" {content.text}")
-
- # Try query_traffic
- print("\n4. Querying recent traffic...")
- result = await session.call_tool("query_traffic", arguments={"limit": 5})
- print("Result:")
- for content in result.content:
- data = json.loads(content.text)
- print(f" Found {len(data)} log entries")
-
-
-if __name__ == "__main__":
- print("Testing MCPHawk MCP Server with SDK Client")
- print("==========================================")
- print("Make sure the server is running with:")
- print(" mcphawk mcp --transport http --mcp-port 8765")
- print()
-
- try:
- asyncio.run(test_with_sdk_client())
- except Exception as e:
- print(f"Error: {type(e).__name__}: {e}")
- import traceback
- traceback.print_exc()
-
diff --git a/examples/stdio_client.py b/examples/stdio_client.py
deleted file mode 100644
index b483088..0000000
--- a/examples/stdio_client.py
+++ /dev/null
@@ -1,207 +0,0 @@
-#!/usr/bin/env python3
-"""
-Example stdio client for MCPHawk MCP server.
-
-This demonstrates how to communicate with MCPHawk's MCP server using the stdio transport.
-The MCP protocol requires:
-1. Initialize request
-2. Initialized notification
-3. Then you can make tool calls
-"""
-
-import json
-import queue
-import subprocess
-import threading
-from typing import Any, Optional
-
-
-class MCPHawkStdioClient:
- """Client for communicating with MCPHawk MCP server over stdio."""
-
- def __init__(self, debug: bool = False):
- self.debug = debug
- self.proc = None
- self.stderr_queue = queue.Queue()
- self.request_id = 0
-
- def connect(self) -> bool:
- """Start the MCP server process and initialize connection."""
- try:
- # Start the MCP server
- self.proc = subprocess.Popen(
- ["mcphawk", "mcp", "--transport", "stdio"],
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- text=True,
- bufsize=0 # Unbuffered
- )
-
- # Start stderr reader thread
- self.stderr_thread = threading.Thread(target=self._read_stderr, daemon=True)
- self.stderr_thread.start()
-
- # Send initialize request
- init_response = self._send_request({
- "method": "initialize",
- "params": {
- "protocolVersion": "2024-11-05",
- "capabilities": {},
- "clientInfo": {"name": "mcphawk-stdio-client", "version": "1.0"}
- }
- })
-
- if not init_response or "error" in init_response:
- print(f"Failed to initialize: {init_response}")
- return False
-
- # Send initialized notification
- self._send_notification({
- "method": "notifications/initialized",
- "params": {}
- })
-
- if self.debug:
- print(f"Connected to server: {init_response['result']['serverInfo']}")
-
- return True
-
- except Exception as e:
- print(f"Failed to connect: {e}")
- return False
-
- def _read_stderr(self):
- """Read stderr in a separate thread."""
- while self.proc and self.proc.poll() is None:
- line = self.proc.stderr.readline()
- if line:
- self.stderr_queue.put(line.strip())
-
- def _send_request(self, request: dict[str, Any]) -> Optional[dict[str, Any]]:
- """Send a JSON-RPC request and wait for response."""
- self.request_id += 1
- request["jsonrpc"] = "2.0"
- request["id"] = self.request_id
-
- request_str = json.dumps(request)
- if self.debug:
- print(f">>> {request_str}")
-
- self.proc.stdin.write(request_str + "\n")
- self.proc.stdin.flush()
-
- # Read response
- response_line = self.proc.stdout.readline()
- if response_line:
- try:
- response = json.loads(response_line)
- if self.debug:
- print(f"<<< {json.dumps(response, indent=2)}")
- return response
- except json.JSONDecodeError as e:
- print(f"Failed to decode response: {e}")
- print(f"Raw: {response_line}")
- return None
- return None
-
- def _send_notification(self, notification: dict[str, Any]) -> None:
- """Send a JSON-RPC notification (no response expected)."""
- notification["jsonrpc"] = "2.0"
-
- notification_str = json.dumps(notification)
- if self.debug:
- print(f">>> {notification_str}")
-
- self.proc.stdin.write(notification_str + "\n")
- self.proc.stdin.flush()
-
- def list_tools(self) -> Optional[list]:
- """Get list of available tools."""
- response = self._send_request({
- "method": "tools/list",
- "params": {}
- })
-
- if response and "result" in response:
- return response["result"]["tools"]
- return None
-
- def call_tool(self, tool_name: str, arguments: Optional[dict[str, Any]] = None) -> Optional[Any]:
- """Call a tool with given arguments."""
- response = self._send_request({
- "method": "tools/call",
- "params": {
- "name": tool_name,
- "arguments": arguments or {}
- }
- })
-
- if response and "result" in response:
- # Extract the text content from the response
- content = response["result"]["content"]
- if content and len(content) > 0:
- text = content[0]["text"]
- try:
- # Try to parse as JSON
- return json.loads(text)
- except json.JSONDecodeError:
- # Return as plain text if not JSON
- return text
- return None
-
- def close(self):
- """Close the connection."""
- if self.proc:
- self.proc.terminate()
- self.proc.wait()
- self.proc = None
-
-
-def main():
- """Example usage of the MCPHawk stdio client."""
- client = MCPHawkStdioClient(debug=True)
-
- print("Connecting to MCPHawk MCP server...")
- if not client.connect():
- print("Failed to connect!")
- return
-
- print("\n1. Listing available tools:")
- tools = client.list_tools()
- if tools:
- for tool in tools:
- print(f" - {tool['name']}: {tool['description']}")
-
- print("\n2. Getting traffic statistics:")
- stats = client.call_tool("get_stats")
- if stats:
- print(f" Total logs: {stats['total']}")
- print(f" Requests: {stats['requests']}")
- print(f" Responses: {stats['responses']}")
- print(f" Notifications: {stats['notifications']}")
- print(f" Errors: {stats['errors']}")
-
- print("\n3. Querying recent traffic:")
- logs = client.call_tool("query_traffic", {"limit": 5})
- if logs:
- print(f" Found {len(logs)} recent log entries")
- for log in logs:
- msg_preview = log['message'][:50] + "..." if len(log['message']) > 50 else log['message']
- print(f" - {log['timestamp']}: {msg_preview}")
-
- print("\n4. Listing captured methods:")
- methods = client.call_tool("list_methods")
- if methods:
- print(f" Found {len(methods)} unique methods:")
- for method in methods[:10]: # Show first 10
- print(f" - {method}")
- if len(methods) > 10:
- print(f" ... and {len(methods) - 10} more")
-
- print("\nClosing connection...")
- client.close()
-
-
-if __name__ == "__main__":
- main()
diff --git a/examples/streamable_http_example.py b/examples/streamable_http_example.py
deleted file mode 100644
index aaeaa66..0000000
--- a/examples/streamable_http_example.py
+++ /dev/null
@@ -1,115 +0,0 @@
-#!/usr/bin/env python3
-"""
-Example Streamable HTTP MCP client traffic generator.
-
-This demonstrates the Streamable HTTP transport pattern for testing MCPHawk's transport detection.
-Streamable HTTP uses:
-1. POST request with dual Accept headers (application/json, text/event-stream)
-2. Server can respond with either JSON or SSE
-"""
-
-import json
-import urllib.error
-import urllib.request
-
-
-def simulate_streamable_http_client():
- """Simulate Streamable HTTP client traffic pattern."""
-
- print("Simulating Streamable HTTP MCP Client")
- print("=" * 50)
-
- server_url = "http://localhost:8765"
-
- print("\n1. Sending request with dual Accept headers (Streamable HTTP pattern)...")
- print(f" POST {server_url}/mcp")
- print(" Accept: application/json, text/event-stream")
- print(" Content-Type: application/json")
-
- # Send a sample initialize request
- initialize_request = {
- "jsonrpc": "2.0",
- "method": "initialize",
- "params": {
- "protocolVersion": "2025-03-26", # New protocol version
- "capabilities": {},
- "clientInfo": {
- "name": "streamable-http-test",
- "version": "1.0.0"
- }
- },
- "id": 1
- }
-
- try:
- data = json.dumps(initialize_request).encode('utf-8')
- req = urllib.request.Request(
- f"{server_url}/mcp",
- data=data,
- headers={
- "Content-Type": "application/json",
- "Accept": "application/json, text/event-stream" # Key difference!
- }
- )
-
- print(f"\n Request body: {json.dumps(initialize_request, indent=2)}")
-
- with urllib.request.urlopen(req) as response:
- print(f"\n Response: {response.status}")
- content_type = response.headers.get('Content-Type', '')
- print(f" Content-Type: {content_type}")
-
- if 'text/event-stream' in content_type:
- print(" Server returned SSE response (streaming)")
- else:
- print(" Server returned JSON response")
-
- except urllib.error.URLError as e:
- print(f" Connection failed: {e}")
-
- # Send another request that might get a different response type
- print("\n2. Sending tool call request...")
- print(f" POST {server_url}/mcp")
- print(" Accept: application/json, text/event-stream")
-
- tool_request = {
- "jsonrpc": "2.0",
- "method": "tools/call",
- "params": {
- "name": "long_running_tool",
- "arguments": {}
- },
- "id": 2
- }
-
- try:
- data = json.dumps(tool_request).encode('utf-8')
- req = urllib.request.Request(
- f"{server_url}/mcp",
- data=data,
- headers={
- "Content-Type": "application/json",
- "Accept": "application/json, text/event-stream"
- }
- )
-
- with urllib.request.urlopen(req) as response:
- print(f"\n Response: {response.status}")
- content_type = response.headers.get('Content-Type', '')
- print(f" Content-Type: {content_type}")
-
- except urllib.error.URLError as e:
- print(f" Connection failed: {e}")
-
- print("\n" + "=" * 50)
- print("Streamable HTTP pattern demonstration complete")
- print("Check MCPHawk to see how it detected the transport type:")
- print("- POST with Accept: application/json, text/event-stream → Streamable HTTP")
-
-
-if __name__ == "__main__":
- print("Streamable HTTP MCP Client Example")
- print("This demonstrates the Streamable HTTP transport pattern")
- print("This should work with our MCP server\n")
-
- simulate_streamable_http_client()
diff --git a/examples/test_mcp_http.sh b/examples/test_mcp_http.sh
deleted file mode 100755
index d3f58f5..0000000
--- a/examples/test_mcp_http.sh
+++ /dev/null
@@ -1,177 +0,0 @@
-#!/bin/bash
-
-# Test MCPHawk MCP Server with various requests
-# Make sure MCPHawk is running with:
-# sudo mcphawk web --auto-detect --with-mcp --mcp-transport http --mcp-port 8765 --debug
-
-SESSION_ID="test-session-$(date +%s)"
-MCP_URL="http://localhost:8765/mcp"
-
-echo "Testing MCPHawk MCP Server with session: $SESSION_ID"
-echo "================================================"
-
-# 1. Initialize session
-echo -e "\n1. Initializing MCP session..."
-curl -X POST $MCP_URL \
- -H 'Content-Type: application/json' \
- -H 'Accept: application/json, text/event-stream' \
- -H "mcp-session-id: $SESSION_ID" \
- -d '{
- "jsonrpc": "2.0",
- "method": "initialize",
- "params": {
- "protocolVersion": "2024-11-05",
- "capabilities": {},
- "clientInfo": {
- "name": "test-client",
- "version": "1.0"
- }
- },
- "id": 1
- }' | jq .
-
-sleep 1
-
-# 2. Send initialized notification
-echo -e "\n2. Sending initialized notification..."
-curl -X POST $MCP_URL \
- -H 'Content-Type: application/json' \
- -H 'Accept: application/json, text/event-stream' \
- -H "mcp-session-id: $SESSION_ID" \
- -d '{
- "jsonrpc": "2.0",
- "method": "notifications/initialized",
- "params": {}
- }' | jq .
-
-sleep 1
-
-# 3. List available tools
-echo -e "\n3. Listing available tools..."
-curl -X POST $MCP_URL \
- -H 'Content-Type: application/json' \
- -H 'Accept: application/json, text/event-stream' \
- -H "mcp-session-id: $SESSION_ID" \
- -d '{
- "jsonrpc": "2.0",
- "method": "tools/list",
- "params": {},
- "id": 2
- }' | jq .
-
-sleep 1
-
-# 4. Get traffic statistics
-echo -e "\n4. Getting traffic statistics..."
-curl -X POST $MCP_URL \
- -H 'Content-Type: application/json' \
- -H 'Accept: application/json, text/event-stream' \
- -H "mcp-session-id: $SESSION_ID" \
- -d '{
- "jsonrpc": "2.0",
- "method": "tools/call",
- "params": {
- "name": "get_stats",
- "arguments": {}
- },
- "id": 3
- }' | jq .
-
-sleep 1
-
-# 5. Query recent traffic
-echo -e "\n5. Querying recent traffic (limit 10)..."
-curl -X POST $MCP_URL \
- -H 'Content-Type: application/json' \
- -H 'Accept: application/json, text/event-stream' \
- -H "mcp-session-id: $SESSION_ID" \
- -d '{
- "jsonrpc": "2.0",
- "method": "tools/call",
- "params": {
- "name": "query_traffic",
- "arguments": {
- "limit": 10
- }
- },
- "id": 4
- }' | jq .
-
-sleep 1
-
-# 6. List unique methods captured
-echo -e "\n6. Listing unique JSON-RPC methods..."
-curl -X POST $MCP_URL \
- -H 'Content-Type: application/json' \
- -H 'Accept: application/json, text/event-stream' \
- -H "mcp-session-id: $SESSION_ID" \
- -d '{
- "jsonrpc": "2.0",
- "method": "tools/call",
- "params": {
- "name": "list_methods",
- "arguments": {}
- },
- "id": 5
- }' | jq .
-
-sleep 1
-
-# 7. Search for specific traffic
-echo -e "\n7. Searching for 'initialize' in traffic..."
-curl -X POST $MCP_URL \
- -H 'Content-Type: application/json' \
- -H 'Accept: application/json, text/event-stream' \
- -H "mcp-session-id: $SESSION_ID" \
- -d '{
- "jsonrpc": "2.0",
- "method": "tools/call",
- "params": {
- "name": "search_traffic",
- "arguments": {
- "search_term": "initialize"
- }
- },
- "id": 6
- }' | jq .
-
-sleep 1
-
-# 8. Test error handling - call non-existent tool
-echo -e "\n8. Testing error handling (calling non-existent tool)..."
-curl -X POST $MCP_URL \
- -H 'Content-Type: application/json' \
- -H 'Accept: application/json, text/event-stream' \
- -H "mcp-session-id: $SESSION_ID" \
- -d '{
- "jsonrpc": "2.0",
- "method": "tools/call",
- "params": {
- "name": "non_existent_tool",
- "arguments": {}
- },
- "id": 7
- }' | jq .
-
-sleep 1
-
-# 9. Send a notification (no ID, no response expected)
-echo -e "\n9. Sending a notification..."
-curl -X POST $MCP_URL \
- -H 'Content-Type: application/json' \
- -H 'Accept: application/json, text/event-stream' \
- -H "mcp-session-id: $SESSION_ID" \
- -d '{
- "jsonrpc": "2.0",
- "method": "notifications/progress",
- "params": {
- "progress": 50,
- "message": "Test progress notification"
- }
- }'
-
-echo -e "\n\nAll tests completed!"
-echo "Check the MCPHawk web UI at http://localhost:8000 to see:"
-echo "- All requests marked with purple 'MCP' badges"
-echo "- Use the MCPHawk toggle button to filter these messages"
-echo "- Click on messages to see full JSON details"
\ No newline at end of file
diff --git a/examples/test_mcp_sdk.sh b/examples/test_mcp_sdk.sh
deleted file mode 100755
index e5dfc38..0000000
--- a/examples/test_mcp_sdk.sh
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/bin/bash
-
-# Test MCPHawk MCP Server (SDK version) with proper session flow
-# The SDK requires:
-# 1. First initialize request WITHOUT session ID
-# 2. Server returns session ID in response
-# 3. Use that session ID for subsequent requests
-
-MCP_URL="http://localhost:8765/mcp"
-
-echo "Testing MCPHawk MCP Server (SDK version)"
-echo "========================================"
-
-# 1. Initialize WITHOUT session ID - server will assign one
-echo -e "\n1. Initializing MCP session (no session ID)..."
-response=$(curl -s -i -X POST $MCP_URL \
- -H 'Content-Type: application/json' \
- -H 'Accept: application/json, text/event-stream' \
- -d '{
- "jsonrpc": "2.0",
- "method": "initialize",
- "params": {
- "protocolVersion": "2024-11-05",
- "capabilities": {},
- "clientInfo": {
- "name": "test-client",
- "version": "1.0"
- }
- },
- "id": 1
- }')
-
-echo "$response"
-
-# Extract session ID from response headers
-SESSION_ID=$(echo "$response" | grep -i "mcp-session-id:" | sed 's/.*: //' | tr -d '\r\n')
-
-if [ -z "$SESSION_ID" ]; then
- echo "ERROR: No session ID received from server"
- exit 1
-fi
-
-echo -e "\nReceived session ID: $SESSION_ID"
-
-sleep 1
-
-# 2. Now use the server-provided session ID for subsequent requests
-echo -e "\n2. Listing tools with session ID..."
-curl -X POST $MCP_URL \
- -H 'Content-Type: application/json' \
- -H 'Accept: application/json, text/event-stream' \
- -H "mcp-session-id: $SESSION_ID" \
- -d '{
- "jsonrpc": "2.0",
- "method": "tools/list",
- "params": {},
- "id": 2
- }' | jq .
-
-sleep 1
-
-# 3. Test a notification (should return no response)
-echo -e "\n3. Sending notification..."
-curl -i -X POST $MCP_URL \
- -H 'Content-Type: application/json' \
- -H 'Accept: application/json, text/event-stream' \
- -H "mcp-session-id: $SESSION_ID" \
- -d '{
- "jsonrpc": "2.0",
- "method": "notifications/progress",
- "params": {
- "progress": 50,
- "message": "Test progress notification"
- }
- }'
-
-echo -e "\n\nCheck http://localhost:8000 for captured traffic"
\ No newline at end of file
diff --git a/examples/test_mcp_simple.sh b/examples/test_mcp_simple.sh
deleted file mode 100755
index cb8c054..0000000
--- a/examples/test_mcp_simple.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/bin/bash
-
-# Simple test for MCPHawk MCP Server
-# Run MCPHawk with: sudo mcphawk web --auto-detect --with-mcp --mcp-transport http --mcp-port 8765 --debug
-
-SESSION_ID="test-$(date +%s)"
-echo "Testing with session: $SESSION_ID"
-
-# Single test request
-echo "Sending initialize request..."
-curl -v -X POST http://localhost:8765/mcp \
- -H 'Content-Type: application/json' \
- -H 'Accept: application/json, text/event-stream' \
- -H "mcp-session-id: $SESSION_ID" \
- -d '{
- "jsonrpc": "2.0",
- "method": "initialize",
- "params": {
- "protocolVersion": "2024-11-05",
- "capabilities": {},
- "clientInfo": {
- "name": "test-client",
- "version": "1.0"
- }
- },
- "id": 1
- }' 2>&1
-
-echo -e "\n\nCheck http://localhost:8000 for captured traffic"
\ No newline at end of file
diff --git a/frontend/index.html b/frontend/index.html
index 95cd717..2090b54 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1,13 +1,13 @@
-
+
-
-
-
- MCPHawk - Model Context Protocol Debugger
+
+
+
+ MCPHawk
-
\ No newline at end of file
+