Metadata-Version: 2.4
Name: tm-mcp
Version: 1.2.1
Summary: TrafficMorph MCP server — drive load tests and triage regressions from Claude / Cursor / Continue.dev.
Project-URL: Homepage, https://github.com/trafficmorph-gif/tm-mcp
Project-URL: Documentation, https://github.com/trafficmorph-gif/tm-mcp#readme
Project-URL: Repository, https://github.com/trafficmorph-gif/tm-mcp
Project-URL: Issues, https://github.com/trafficmorph-gif/tm-mcp/issues
Author-email: TrafficMorph <support@trafficmorph.io>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: claude,load-testing,mcp,model-context-protocol,trafficmorph
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Requires-Dist: mcp>=1.0
Requires-Dist: trafficmorph<0.4.0,>=0.3.2
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'dev'
Description-Content-Type: text/markdown

# TrafficMorph MCP Server

```
████████╗██████╗  █████╗ ███████╗███████╗██╗ ██████╗
╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝██║██╔════╝
   ██║   ██████╔╝███████║█████╗  █████╗  ██║██║
   ██║   ██╔══██╗██╔══██║██╔══╝  ██╔══╝  ██║██║
   ██║   ██║  ██║██║  ██║██║     ██║     ██║╚██████╗
   ╚═╝   ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝     ╚═╝     ╚═╝ ╚═════╝
        ███╗   ███╗ ██████╗ ██████╗ ██████╗ ██╗  ██╗
        ████╗ ████║██╔═══██╗██╔══██╗██╔══██╗██║  ██║
        ██╔████╔██║██║   ██║██████╔╝██████╔╝███████║
        ██║╚██╔╝██║██║   ██║██╔══██╗██╔═══╝ ██╔══██║
        ██║ ╚═╝ ██║╚██████╔╝██║  ██║██║     ██║  ██║
        ╚═╝     ╚═╝ ╚═════╝ ╚═╝  ╚═╝╚═╝     ╚═╝  ╚═╝
```

[![PyPI](https://img.shields.io/pypi/v/tm-mcp)](https://pypi.org/project/tm-mcp/)
[![Python versions](https://img.shields.io/pypi/pyversions/tm-mcp)](https://pypi.org/project/tm-mcp/)
[![License](https://img.shields.io/pypi/l/tm-mcp)](LICENSE)

Drive TrafficMorph from Claude Desktop, Claude Code, 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.

**Current release**: 1.2.0 — **25 tools + 4 prompts + 5 resources**
(34 catalog entries total). 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

Both Claude Code and Claude Desktop use 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:8080`
  - **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"
      }
    }
  }
}
```

### 4. Verify

In Claude Code:

```
> /mcp
```

You should see `trafficmorph` listed with 25 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).

## 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 (25) — 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 |
| `tm_list_domains` | All registered domains + verification status |
| `tm_compare_runs` | Side-by-side metric diff of two runs (synthetic) |
| `tm_analyse_capture` | Per-endpoint analysis of a JSONL capture file |
| **Run control** | |
| `tm_start_run` | Start a run; 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 (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 |
| **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` first) |

### 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:8080` 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 the existing profile (including scripts /
callbacks / alerts the MCP tool surface doesn't expose). 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.

**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
