tm-mcp
# TrafficMorph MCP Server
```
████████╗██████╗ █████╗ ███████╗███████╗██╗ ██████╗
╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝██║██╔════╝
██║ ██████╔╝███████║█████╗ █████╗ ██║██║
██║ ██╔══██╗██╔══██║██╔══╝ ██╔══╝ ██║██║
██║ ██║ ██║██║ ██║██║ ██║ ██║╚██████╗
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝
███╗ ███╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗
████╗ ████║██╔═══██╗██╔══██╗██╔══██╗██║ ██║
██╔████╔██║██║ ██║██████╔╝██████╔╝███████║
██║╚██╔╝██║██║ ██║██╔══██╗██╔═══╝ ██╔══██║
██║ ╚═╝ ██║╚██████╔╝██║ ██║██║ ██║ ██║
╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
```
[](https://pypi.org/project/tm-mcp/)
[](https://pypi.org/project/tm-mcp/)
[](LICENSE)
Drive TrafficMorph from Claude Code, Claude Desktop, Codex, Cursor, or
any other host that speaks the [Model Context Protocol](https://modelcontextprotocol.io/).
The flagship use case is **CI-failure triage**:
> *"My TrafficMorph CI step just failed on run 1234 — what
> regressed vs the baseline?"*
→ Claude calls the right tools, fetches the relevant runs, computes
the per-metric delta, and produces a human-readable triage report
in seconds.
**Catalog**: **37 tools + 4 prompts + 5 resources** (46 entries). See [CHANGELOG.md](CHANGELOG.md) for
the per-release history and [STABILITY.md](STABILITY.md) for what
1.x commits to keeping stable. See [MCP-USAGE.md](MCP-USAGE.md) for
worked example conversations.
## Quick start
Four steps from zero to *"Claude is driving my TrafficMorph
account"*:
### 1. Get an API key
Open the TrafficMorph app → **Settings → API Keys** → click
**Generate**. Copy the `tm_…` value.
### 2. Install (or skip — `uvx` runs without install)
```bash
# Permanent install:
pip install tm-mcp
# OR — no install at all. The MCP host config below uses `uvx`,
# which downloads + caches the package on first run. Confirm
# uvx itself is on your PATH:
uvx --version
```
> **Note**: `tm-mcp` is a long-running MCP server, not a CLI with
> a `--help` flag. Invoking it directly without env vars exits 2
> with a configuration error. The host (Claude Code / Desktop)
> subprocesses it and pipes JSON-RPC over stdio — you don't run
> it yourself unless you're debugging.
### 3. Register with your MCP host
Every supported host uses the same env-var protocol:
`TM_API_KEY` for the API key, `TM_BASE_URL` for the base URL. The
server reads both at startup and fails fast with a clean
"couldn't start" message if either is missing — your host's log
shows that instead of opaque "tool call failed" errors later.
**In all snippets below, replace two placeholders with your own values:**
- `TM_API_KEY=tm_xxxxxxxxxxxxxxxx` → the API key from Step 1.
- `TM_BASE_URL=https://YOUR-TRAFFICMORPH-HOST` → the URL of your
TrafficMorph server. Common values:
- **Local dev**: `http://localhost:8092`
- **Self-hosted prod**: `https://trafficmorph.your-company.com`
(or whatever URL your install lives at)
- **Cloud SaaS**: the URL shown in your TrafficMorph app's
browser address bar
- **Do not** copy the literal `YOUR-TRAFFICMORPH-HOST` placeholder
— it won't resolve and every tool call will fail.
**Claude Code** — one command:
```bash
claude mcp add trafficmorph \
-e TM_API_KEY=tm_xxxxxxxxxxxxxxxx \
-e TM_BASE_URL=https://YOUR-TRAFFICMORPH-HOST \
-- uvx tm-mcp
```
Project-scope alternative — drop this at the repo root:
```jsonc
// .mcp.json
{
"mcpServers": {
"trafficmorph": {
"command": "uvx",
"args": ["tm-mcp"],
"env": {
"TM_API_KEY": "tm_xxxxxxxxxxxxxxxx",
"TM_BASE_URL": "https://YOUR-TRAFFICMORPH-HOST"
}
}
}
}
```
**Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json`
(macOS) or the equivalent on your OS, then restart Claude Desktop:
```jsonc
{
"mcpServers": {
"trafficmorph": {
"command": "uvx",
"args": ["tm-mcp"],
"env": {
"TM_API_KEY": "tm_xxxxxxxxxxxxxxxx",
"TM_BASE_URL": "https://YOUR-TRAFFICMORPH-HOST"
}
}
}
}
```
**Codex** — add to `~/.codex/config.toml` (the Codex CLI reads MCP
servers from this file at startup), then restart Codex. Note the TOML
shape: a `[mcp_servers.<name>]` table with `command` / `args` and an
inline `env` table.
```toml
[mcp_servers.trafficmorph]
command = "uvx"
args = ["tm-mcp"]
env = { TM_API_KEY = "tm_xxxxxxxxxxxxxxxx", TM_BASE_URL = "https://YOUR-TRAFFICMORPH-HOST" }
```
### 4. Verify
In Claude Code:
```
> /mcp
```
You should see `trafficmorph` listed with 37 tools, 4 prompts,
and 5 resources. If you see red / error, check:
```bash
claude mcp list # is `trafficmorph` registered?
claude mcp get trafficmorph # what env vars + command?
```
The most common gotcha: forgetting `TM_BASE_URL`. The server
refuses to start without it and surfaces a clear "`$TM_BASE_URL`
is not set" error in your MCP host's log. Set it in the host
config (see step 3).
## Security
You've just put a long-lived API key — full access to your
TrafficMorph account — into a config file on disk. A few real
gotchas worth knowing about:
### Don't commit / share / paste the host config file
The token sits **in plaintext** in your MCP host's config:
* Claude Desktop — `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) / `%AppData%\Claude\claude_desktop_config.json` (Windows)
* Claude Code — managed via `claude mcp add`, stored in `~/.claude/`
* Codex — `~/.codex/config.toml`
* Cursor — `~/.cursor/mcp.json`
If you sync dotfiles to a public repo, paste your config into a
support ticket, or DM it to a colleague to help them set up
`tm-mcp`, **you've leaked the token**. Treat the file like an SSH
private key. If it does leak, rotate immediately in the
TrafficMorph app (**Settings → API Keys → Regenerate**) — the old
key is revoked the moment the new one is generated.
### Use env vars in the host config, never CLI args
`tm-mcp` hard-rejects any command-line argument other than
`--help` / `-h`. This is deliberate — credentials passed via
argv would show up in `ps -ef`, system audit logs, crash dumps,
and any monitoring agent that records process command lines.
**Right** (what the Quick start above shows):
```json
"trafficmorph": {
"command": "uvx",
"args": ["tm-mcp"],
"env": {
"TM_API_KEY": "tm_xxxxxxxxxxxxxxxx",
"TM_BASE_URL": "https://YOUR-TRAFFICMORPH-HOST"
}
}
```
**Wrong** (will refuse to start with `command-line arguments are
not supported`):
```json
"trafficmorph": {
"command": "uvx",
"args": ["tm-mcp", "--api-key=tm_xxxxxxxxxxxxxxxx"]
}
```
### Verbose MCP-host logging can capture credentials
Most MCP hosts have a debug / verbose mode that logs every
subprocess startup including its env block. If you enable that
to troubleshoot something else, your `TM_API_KEY` will appear in
the host's log file. Disable verbose logging once you're done
debugging, and consider rotating the key if the log file ever
left your machine.
### Trust boundary is the MCP host itself
Any MCP host you configure `tm-mcp` into has full read access to
the env vars you pass it (that's what makes it work). Only
configure `tm-mcp` in hosts you trust — same posture as
installing any local CLI tool or IDE extension.
### Server-side defences (already in place)
* Network leg uses **HTTPS** to your TrafficMorph deployment (the
underlying `trafficmorph` SDK validates the base URL on
startup and refuses non-`http`/`https` schemes).
* Tokens are **hashed at rest** server-side, never stored in
plaintext in the database.
* Every `tm_…` token is **revocable** independently — rotating
one key doesn't affect the rest of your account.
## What you can do
Conversational examples — try any of these once the server is wired up:
- *"List my TrafficMorph profiles."*
- *"Show me the last 5 runs for profile 42."*
- *"Start a run on profile 42 and wait for the verdict."*
- *"Create a profile 'smoke-test' hitting https://api.example.com/health at 50 RPS for 60s."*
- *"What domains am I cleared to load-test against?"*
- *"Add api.example.com as a new domain and walk me through verification."*
- *"Compare run 1234 against run 1198."*
Or invoke a **slash-command prompt** for a guided workflow:
- `/tm_triage 42` — find the most recent failed run for profile 42, diff against the latest PASS baseline, narrate the regression.
- `/tm_setup_loadtest https://api.example.com 100 60` — handle domain verification + profile creation + optional immediate run.
- `/tm_compare_baseline 42` — quick regression check vs the last green.
- `/tm_import_capture_guided ~/.trafficmorph/captures/my.jsonl` — analyse → preview → import workflow.
Or @-mention a **resource** to pull pre-baked context into the chat:
- `@tm://profiles` — your full profile list as session-start context.
- `@tm://history/recent` — the last 20 runs across all profiles.
- `@tm://domains` — verified domain list.
- `@tm://profiles/42` or `@tm://history/1234` — one specific entity by id.
See [MCP-USAGE.md](MCP-USAGE.md) for end-to-end worked conversations
including failure triage, new-test setup, and capture-driven profile
import.
## Full catalog
### Tools (37) — AI-invoked actions
| Tool | Action |
|---|---|
| **Read** | |
| `tm_list_profiles` | List all profiles owned by the authenticated user |
| `tm_get_profile` | Full config + run status for one profile |
| `tm_list_history` | Paginated past runs with filters (`auto_verdict=FAIL` is the CI-triage filter) |
| `tm_get_run` | Full metric set + verdict for one run (`include_series=False` drops the per-bucket series) |
| `tm_list_domains` | All registered domains + verification status |
| `tm_compare_runs` | Metric-by-metric diff of two runs plus the server's significance tests |
| `tm_get_run_status` | Live progress of a profile's current run (RPS, response codes, last minute of latency) |
| `tm_list_regions` | Dispatch regions available to the account |
| `tm_get_account` | Plan, limits (RPS, concurrent runs, profiles, quotas) and this month's usage |
| `tm_list_history_filters` | Profiles, regions, tags and verdicts the history filters accept |
| `tm_export_profile` | Portable `trafficmorph-profile-v1` document (same as the web UI's Export) |
| `tm_analyse_capture` | Per-endpoint analysis of a JSONL capture file |
| `tm_list_capture_sources` | Remote capture sources (SFTP / S3) configured in the web UI; no credentials |
| `tm_list_capture_source_files` | Capture files in one remote source, newest first |
| `tm_analyse_remote_capture` | Per-endpoint analysis of a capture file in a remote source |
| **Run control** | |
| `tm_start_run` | Start a run, optionally in a given `region` and with history `tags`; with `wait=True` + `fail_on_verdict=["FAIL","WARN"]` mirrors `tm runs start --wait` in the CLI |
| `tm_stop_run` | Stop the in-flight run for a profile (idempotent) |
| `tm_pause_run` | Pause without losing position (idempotent) |
| `tm_resume_run` | Resume a paused run from where it left off |
| **Profile lifecycle + capture import** | |
| `tm_create_profile` | Create a new profile, optionally with scripts, callback URL, alert policy, weekly schedule, variables sets, default region, readiness URL and interpolation mode (fails fast on name collision to prevent silent upsert wipe) |
| `tm_update_profile` | Partial update by id (read-modify-write internally — only pass the fields you want to change) |
| `tm_delete_profile` | Remove a profile |
| `tm_import_capture` | Persist analysed-capture groups as profiles |
| `tm_import_remote_capture` | Same, for a capture file in a remote source |
| `tm_import_profile` | Create a profile from a portable document; refuses to overwrite unless `overwrite=True` |
| `tm_delete_schedule` | Remove a profile's schedule (idempotent) |
| `tm_delete_run` | Delete one run from history |
| **Domain management** | |
| `tm_add_domain` | Register a domain for verification (idempotent) |
| `tm_verify_domain_dns` | Check the TXT challenge record |
| `tm_verify_domain_http` | Check the `/.well-known/trafficmorph-verify.txt` file |
| `tm_delete_domain` | Remove a domain |
| **Variables-set lifecycle** | |
| `tm_list_variables_sets` | List all variables sets owned by the user |
| `tm_get_variables_set` | Single set's metadata (id, name, mode, columns, row count) |
| `tm_create_variables_set` | Upload a CSV-style set (inline `csv_content` string, `mode` is one of `"ROW"` / `"COLUMN"` / `"SEQUENTIAL"`) |
| `tm_rename_variables_set` | Rename without touching content (idempotent) |
| `tm_change_variables_set_mode` | Switch between ROW / COLUMN / SEQUENTIAL without re-uploading |
| `tm_delete_variables_set` | Remove a set; **400s if still attached** to any profile (detach via `tm_update_profile(variables_set_ids=...)` first) |
Read tools carry `readOnlyHint=true`, so hosts can run them without a
confirmation prompt. Run-history payloads come back with their metric
maps (`latencyQuantiles`, `responseCodes`, series, verdict reasons)
decoded into JSON objects.
**Server compatibility.** `tm_get_run_status`, `tm_list_regions`,
`tm_start_run(region=/tags=)`, the significance part of
`tm_compare_runs`, the variables-set / region / readiness /
interpolation profile fields, and the account, history-filter,
schedule-delete, run-delete, export / import and remote-capture tools
need a TrafficMorph server that has them in `/api/v1`. Against an older server those calls fail (compare
falls back to its client-side diff), and `tm_update_profile` still
resets those four profile settings.
**Known limitations**:
- `tm_start_run(wait=True)` no longer blocks other calls and sends
progress notifications, but the MCP host's own tool-call timeout
still applies; for runs longer than that, use `wait=False` and poll
`tm_get_run_status`.
### Prompts (4) — user-invoked slash commands
| Slash command | Workflow |
|---|---|
| `/tm_triage <profile_id>` | Find the most recent FAIL → diff vs latest PASS → narrate the regression |
| `/tm_setup_loadtest <url> <rps> <duration_seconds>` | Domain verification (if needed) + profile creation + optional run |
| `/tm_compare_baseline <profile_id>` | Quick regression check: latest run vs latest PASS |
| `/tm_import_capture_guided <path>` | Analyse → present groups → user picks → import |
Prompts return a templated user message that steers the AI through
a specific tool sequence. They're how you kick off a known workflow
without typing the full natural-language description every time.
### Resources (5) — @-mention URIs
| URI | What it returns |
|---|---|
| `tm://profiles` | All your profiles, JSON |
| `tm://profiles/{id}` | One profile's full config |
| `tm://history/recent` | Last 20 runs across all profiles |
| `tm://history/{run_id}` | One run's full metrics |
| `tm://domains` | All registered domains + verification status |
Resources are read-only data the host pulls into context — usually
at session start via @-mention. They wrap the corresponding read
tools 1:1; the difference is *who decides when to read* (AI for
tools, host for resources).
## Configuration
| Env var | Required | Notes |
|---|---|---|
| `TM_API_KEY` | yes | Full `tm_…` value provisioned from in-app Settings → API keys |
| `TM_BASE_URL` | yes | URL of your TrafficMorph install (`http://localhost:8092` for local dev, your hosted URL otherwise). No built-in default — the server refuses to start without it |
| `TM_MCP_CAPTURE_ROOT` | no | Defaults to `~/.trafficmorph/captures/`. Allow-listed root for `tm_analyse_capture` + `tm_import_capture` file paths |
## Capture-file path validation
`tm_analyse_capture` and `tm_import_capture` accept a server-side
file path. The MCP server validates each path before passing it
through:
| Rule | Why |
|---|---|
| Must resolve inside `$TM_MCP_CAPTURE_ROOT` (default `~/.trafficmorph/captures/`) | Prevents AI invocation from probing `~/.ssh/`, `~/.aws/`, `~/Documents/…` |
| Symlinks resolving outside the root are rejected | Classic symlink-escape defense |
| `..` segments rejected at parse time | Path-traversal defense |
| Only `.jsonl` extension | The capture parser reads plain JSONL (no gzip wrapping) |
Override the root via `TM_MCP_CAPTURE_ROOT` in your MCP host's
server config:
```jsonc
"env": {
"TM_API_KEY": "...",
"TM_BASE_URL": "...",
"TM_MCP_CAPTURE_ROOT": "/path/to/your/captures"
}
```
## Troubleshooting
**Server fails to start with `$TM_API_KEY is not set` or
`$TM_BASE_URL is not set`.** One of the required env vars wasn't
delivered to the subprocess by your MCP host. The error names the
missing variable; add it to the `env` block in your host config
(see [Step 3](#3-register-with-your-mcp-host)).
**Server starts, but every tool call hits a network / DNS error.**
`TM_BASE_URL` is set to something that doesn't resolve — typically
a placeholder like `https://YOUR-TRAFFICMORPH-HOST` that wasn't
edited, a typo in the hostname, or an internal URL not reachable
from where the MCP host runs. Read the URL back from
`claude mcp get trafficmorph` and confirm it resolves with
`curl -I "$TM_BASE_URL/api/v1/profiles"`.
**Tools work, but specific ones return 404.** Your TrafficMorph
server is running an older build that doesn't expose those
endpoints yet. Upgrade the server.
**`PLAN_UPGRADE_REQUIRED` on every call.** Your TrafficMorph
account doesn't have API access enabled. Check your account
settings, or point the MCP server at a deployment where your
account has API access.
**`tm_create_profile` refuses with "A profile named X already
exists".** The server's POST endpoint is upsert-by-name and would
silently replace every field of the existing profile, including
settings you didn't pass. Use
`tm_update_profile(profile_id=<id from error>)` instead, or pick
a unique name.
**`tm_update_profile` refuses to rename.** A rename would collide
with another profile under your account. The error names both
ids; either pick a unique new name OR call `tm_update_profile`
against the OTHER profile if you actually meant to edit that one.
**"Could not reach the TrafficMorph server at …".** No HTTP response
came back: the server isn't running, or `TM_BASE_URL` in the MCP host
config points at the wrong place. The error names the URL it tried.
**Domain verify returns 400 immediately.** That's the fail-fast
contract — verification is NOT polling-style. The 400 message
includes the expected TXT record / URL + token; read it back to
the user, wait for them to install the record / file, then retry.
## Versioning
| | Source | Notes |
|---|---|---|
| MCP server release | `tm-mcp` on PyPI | Pin via `pip install 'tm-mcp==X.Y.Z'`. See [CHANGELOG.md](CHANGELOG.md). |
| API version | `tm_mcp.__version__` matches the PyPI version | Use to identify what's installed in support tickets |
The MCP server is a thin layer over the `trafficmorph` Python SDK.
The dependency pin in `pyproject.toml` constrains the SDK range
this release is tested against; updating the SDK ships as a new
`tm-mcp` release.
## See also
- [MCP-USAGE.md](MCP-USAGE.md) — comprehensive user guide with worked example conversations
- [CHANGELOG.md](CHANGELOG.md) — per-release history
- [STABILITY.md](STABILITY.md) — v1.0 stability promise (what tools / prompts / resources stay stable across 1.x)
- [examples/](examples/) — full conversation transcripts (triage, setup, capture import)
- [TrafficMorph Python SDK](https://pypi.org/project/trafficmorph/) — the HTTP layer this MCP server uses
TDQS
Scored across 25 tools
Each tool targets a distinct operation (domain management, profile CRUD, run lifecycle, capture analysis/import, variables set management) with no overlapping responsibilities. Even similar tools like tm_verify_domain_dns and tm_verify_domain_http are clearly differentiated by verification method.
All tools follow a consistent 'tm_verb_noun' pattern (e.g., tm_add_domain, tm_get_run, tm_delete_variables_set). No mixing of naming conventions, and verbs are appropriately concise.
25 tools is on the higher side but still well-scoped for a comprehensive traffic testing service covering domains, profiles, runs, captures, and variables sets. Each tool has a clear purpose, and no redundancy is apparent.
The tool surface covers core CRUD and lifecycle operations for all major entities. Minor gaps exist, such as no tool for updating variables set CSV content or managing alert policies, but these are mentioned as deliberately not exposed.