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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -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
Expand All @@ -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: |
Expand Down Expand Up @@ -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

Expand All @@ -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: |
Expand Down
12 changes: 4 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
*.db
tests/test_logs/
.claude
CLAUDE.md
.DS_Store

# Node modules
Expand Down Expand Up @@ -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/
71 changes: 71 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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/<area>/` 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.
69 changes: 34 additions & 35 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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 {} +
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 {} +
Loading
Loading