| tm_list_profilesA | List all traffic profiles owned by the authenticated user. Returns a list of lightweight summaries — one per profile —
with id, name, and createdAt. Use tm_get_profile(id) for
the full configuration of any single profile. Ordering: server-default (typically most-recently-updated
first). The endpoint doesn't expose a sort parameter. |
| tm_get_profileA | Get the full configuration of a single traffic profile by id. Returns: name, RPS curve points, target URL, HTTP method,
request body / headers, optional init/cleanup/response scripts,
callback config, auto-alert settings, schedule (if any), AND
the current run status (IDLE / RUNNING / PAUSED /
COMPLETED). 400 if the profile isn't found or doesn't belong to this user
(per the v1 API contract — IllegalArgumentException → 400). |
| tm_list_historyA | List past runs with optional filters. The killer use case for
CI-failure triage: tm_list_history(auto_verdict="FAIL", size=10)
returns the 10 most recent failing runs across all profiles. Filters are AND-combined: profile_id: narrow to one profile.
triggered_by: api / ui / scheduled.
region: region code (e.g. us-east-1).
auto_verdict: PASS / WARN / FAIL / NO_BASELINE.
tag: arbitrary user tag.
from_ / to: ISO-8601 timestamps bounding startedAt.
from_ is named with a trailing underscore because from
is a Python keyword.
page / size: zero-based pagination, size capped at 100
by the server.
Returns the standard pagination envelope:
{content: [...summaries...], page, size, totalElements, totalPages}. |
| tm_get_runA | Get one run's full detail — the CI post-run inspection
endpoint. Returns the complete metric set: response-code distribution,
latency quantiles (p50/p95/p99/...), RPS / latency time series,
secondary stats from response scripts, the auto-comparison
snapshot against the configured baseline, AND the auto-verdict For comparing two runs side-by-side, use tm_compare_runs
— it does the metric-by-metric diff so the AI doesn't have to. |
| tm_compare_runsA | Synthetic side-by-side diff of two runs. No equivalent /api/v1 endpoint exists — this tool fetches both
runs and computes the delta client-side. Note: the response
shape here is the MCP server's own — distinct from the native
RunComparisonService's RunComparisonResponse. The MCP
shape is leaner (no full run summaries embedded) because the AI
rarely needs every field, and a separate tm_get_run call
surfaces the full per-run detail if needed. Return shape: .. code-block:: python {
"run_id": <int>, # the newer run's id (post-swap)
"baseline_run_id": <int>, # the older run's id (post-swap)
"verdict_change": {
"run": <str|None>, # e.g. "FAIL"
"baseline": <str|None>, # e.g. "PASS"
},
"deltas": {
"<metric_name>": {
"a": <number>, # value from `run`
"b": <number>, # value from `baseline`
"delta_pct": <float|None>, # None when baseline is 0
"delta_abs": <number>, # only when delta_pct is None
"regression": True, # only when above +10% in a "bad" direction
},
...
},
}
Metrics covered: totalRequests, totalErrors, avgRps,
peakRps, successRate, durationSeconds (top-level),
plus latency_<quantile> rows for every quantile present in
both runs (latency_p50, latency_p95, etc.). Regression flag fires only for metrics with a known
"bad direction": totalErrors UP, successRate DOWN, any
latency quantile UP. Throughput / duration deltas are reported
without regression flags — the AI host interprets direction
based on context. Threshold is intentionally generous (10%) — the server's
auto-comparison uses tighter thresholds for the official
verdict; this tool is a "draw attention to these metrics"
surface for the AI to narrate alongside the verdict. Invariants enforced — mirror the native
RunComparisonService.compare(...) server method
(RunComparisonService.java line ~61): run_id != baseline_run_id (comparing a run to itself
produces a useless all-zero diff that would mislead the AI).
Both runs belong to the same profile (cross-profile
compares mix incomparable workloads — e.g. checkout-flow
vs login-flow latency — and produce % deltas that look
valid but answer no real question). Most-recent run is returned as run when createdAt
is present and parseable on both runs. The tool
compares parsed datetimes (UTC-aware, normalized from
naive input) and swaps the caller's arguments if it
detects "older first" — the result narrative then reads
"newer run regressed vs older baseline" consistently.
Order detection uses parsed-datetime comparison, not raw
string compare, so ISO-8601 fractional-seconds and offset
variants behave correctly. Fallback: when either run is missing createdAt or
the value can't be parsed, the tool keeps the caller-
supplied order rather than guessing. That's an intentional
safety net — better to surface the diff in the order the
AI asked for than to silently rearrange based on weak
signals — but it means the "newer = run" invariant only
holds when timestamps are usable. Mirrors the native
service's own defensive null-handling.
|
| tm_list_domainsA | List all domains registered by the authenticated user with
their verification status. Each entry carries id, domain, verified flag, and the DNS /
HTTP challenge tokens that are still in play. Useful for
answering "what domains am I allowed to load-test?" and for
walking a user through verification. |
| tm_add_domainA | Register a new domain for verification. Returns the
VerifiedDomainResponse record carrying a single
verificationToken plus pre-formatted setup instructions
for both verification methods. Verification is required before TrafficMorph will run traffic
against the domain — it's the gate that prevents arbitrary
abuse-as-a-service. Two verification methods are offered;
the same token is used for both — pick whichever method
is easier to set up: DNS — install the token as a TXT record at
_trafficmorph-verify.<domain>. Best for users who
control DNS. Use :func:tm_verify_domain_dns after the
record propagates. HTTP — serve the token at
https://<domain>/.well-known/trafficmorph-verify.txt.
Best when DNS access is restricted. Use
:func:tm_verify_domain_http once the file is live.
Response shape:: {
"id": 7,
"domain": "api.example.com",
"verificationToken": "tm-verify-abc123…",
"verificationMethod": null, # set after verification
"verified": false,
"verifiedAt": null,
"createdAt": "2026-05-16T12:00:00Z",
"dnsInstruction": "Add a TXT record for _trafficmorph-verify.api.example.com with value: tm-verify-abc123…",
"httpInstruction": "Place a file at https://api.example.com/.well-known/trafficmorph-verify.txt containing: tm-verify-abc123…"
}
The dnsInstruction / httpInstruction strings are
pre-formatted by the server for verbatim display — the AI
host can read either back to the user without composing the
setup steps itself. Idempotent: calling again with the same domain returns the
existing record with the same token (the server treats repeat
registrations as no-ops). |
| tm_verify_domain_dnsA | Run the DNS-TXT verification check on a previously-added
domain. The server looks up the TXT record at
_trafficmorph-verify.<domain> and matches it against the
verificationToken issued by :func:tm_add_domain. On
match, the domain flips to verified=true and becomes
usable as a profile target. On miss the server returns 400 (rather than a polling-
style 200 "still pending"). That's deliberate — CI flows fail
fast on misconfigured DNS instead of looping forever. The 400
error message includes the expected TXT name + token, so the
AI host can read it back to the user verbatim for them to
copy into their DNS provider. Success response is the same VerifiedDomainResponse shape
as :func:tm_add_domain, now with verified=true,
verifiedAt populated, and verificationMethod="DNS"
(the server's stored value — uppercase per
DomainVerificationService.markVerified L112/L148/L183;
other documented values are "HTTP" and "ADMIN").
Pair with :func:tm_list_domains after a successful call to
confirm the flag flipped. |
| tm_verify_domain_httpA | Run the HTTP-token verification check on a previously-added
domain. The server fetches
https://<domain>/.well-known/trafficmorph-verify.txt
(falling back to plain HTTP if HTTPS fails) and matches the
file body against the verificationToken issued by
:func:tm_add_domain — the same token DNS verification
uses; there's only one token per domain. On match, the
domain flips to verified=true. Alternative to :func:tm_verify_domain_dns for users who can
serve a static file at the target host but can't edit DNS. Same fail-fast contract: misses return 400 with the expected
URL + token in the message, not a "still pending" 200. The
error message is constructed to be readable directly to the
user (no JSON parsing required by the AI host). Success response: VerifiedDomainResponse with
verified=true, verifiedAt populated, and
verificationMethod="HTTP" (uppercase — matches the
server's stored value, see :func:tm_verify_domain_dns for
the full set of method tokens). |
| tm_delete_domainA | Remove a registered domain. Profiles targeting this domain remain saved but won't be
runnable until another verified domain covers the target host
(see :func:tm_list_domains to find what's covered). Returns {"domain_id": <id>, "deleted": True} on success.
A 400 / "Domain not found" surfaces as a typed
:class:ToolError — caller can react to it without parsing
message text. |
| tm_list_variables_setsA | List all variables sets owned by the authenticated user. Returns metadata only — id, name, mode, macro columns, optional
weight column, row count, byte size, timestamps. The raw CSV
content is NOT included (it's downloaded via a separate UI-only
endpoint when needed). For a single set's full metadata use
:func:tm_get_variables_set. Useful for answering "what variables sets do I have available
to attach to a profile?" — common follow-up when the AI is
asked to set up a parameterized load test. |
| tm_get_variables_setA | Get a single variables set's metadata. Returns the same shape as one element of :func:tm_list_variables_sets:
id, name, mode, macroColumns, weightColumn, rowCount, byteSize,
createdAt, updatedAt. Raw CSV omitted; use the in-app UI if you
need row-level inspection. |
| tm_create_variables_setA | Create a new variables set from inline CSV content. csv_content is the full CSV as a string — header row plus
data rows. The server parses + validates at upload time and
rejects malformed or oversized inputs with a typed
:class:ToolError.
The header row's column names define which $$macro$$
placeholders this set substitutes when attached to a profile.
Example:: userId,token,weight
u-1001,tk-abc,5
u-1002,tk-def,1
Attached to a profile whose URL is
https://api.example.com/users/$$userId$$ and which sends
header Authorization: Bearer $$token$$, each request: Picks a row (weighted random in ROW mode — row 1 with
5/6 probability, row 2 with 1/6; sequential in
SEQUENTIAL mode). Substitutes the row's userId and token columns into
the matching placeholders. Sends the resulting request.
mode defaults to "ROW" (weighted per-row sampling).
Other accepted values: "COLUMN" (per-column independent
sampling) and "SEQUENTIAL" (ordered walk, for captured-
traffic replay). ROW mode REQUIRES a weight column
(case-insensitive header); COLUMN accepts an optional shared
weight column AND per-column {col}_weight overrides;
SEQUENTIAL ignores weights. See the helper docstring in
_validate_variables_set_mode for the full mode contract.
Returns the created set's metadata (id, name, mode,
macroColumns, weightColumn, rowCount, byteSize). The AI host
typically follows with attaching the new set to a profile —
that's handled via tm_update_profile on the profile
side (variables-set attachment is on the profile, not the
set). Quota: each account has a per-user variables-set quota.
A 400 with Variables-set quota reached means delete an
existing set first. Duplicate names rejected (strict-uniqueness semantics).
Per-user, names must be unique — a second create call with
a name that already exists 400s with A variables set named 'X' already exists. The endpoint deliberately doesn't
auto-suffix (unlike the capture-import path which does
(2), (3), …). Pick a fresh name, or
:func:tm_rename_variables_set / :func:tm_delete_variables_set
the existing one before re-creating. |
| tm_rename_variables_setA | Rename a variables set. Changes only the display name; the
CSV content, sampling mode, and attached-profile relationships
are untouched. Returns the updated metadata (same shape as :func:tm_get_variables_set). |
| tm_change_variables_set_modeA | Switch sampling mode on an existing set without re-uploading
the CSV. Accepts any of the three modes: ROW, COLUMN,
SEQUENTIAL. Common use cases: A capture-import-created set lands in ROW mode by
default; flip to SEQUENTIAL for ordered replay that
reproduces the original traffic order. A set originally created with ROW (correlated per-row
sampling) can be flipped to COLUMN if you want
uncorrelated combinations across columns instead.
The server re-parses the stored CSV with the target mode and
400s if the CSV is incompatible — e.g. switching to ROW
when the CSV has no weight column. The error message names
the actual cause so the AI host can either: Returns the updated metadata. |
| tm_delete_variables_setA | Delete a variables set. Fails fast (HTTP 400) if the set is
still attached to any profile — the server does NOT
auto-detach. The server-side error message names the referencing profile(s)
so the caller can detach them first. The MCP layer preserves
that message verbatim in the typed ToolError so the AI
host can read it back to the user. Example error text:: Can't delete 'users-fixture' — still attached to profiles:
loadtest-api.example.com, smoke-test. Detach it from those
profiles first.
To delete an attached set, the typical recovery is to call
:func:tm_update_profile on each referencing profile with an
updated variables-set attachment list that omits this set's
id, then retry the delete. (The variables-set attachment is
a property of the profile, not of the set — managed via the
profile's update path, not via a separate endpoint here.) Returns {"variables_set_id": <id>, "deleted": True} on
success. A 400 / "Variables set not found" or
"still attached" surfaces as a typed :class:ToolError —
caller can react without parsing message text beyond the
error string itself. |
| tm_start_runA | Start a traffic run for the given profile. Default behavior (wait=False) is fire-and-return: POST to
/api/v1/profiles/{id}/start, return the run id + initial
status, exit. Useful for "kick this off and tell me when it's
done" UI flows. Set wait=True to drive the CI-gate shape (mirrors the
tm runs start --wait CLI flow): Snapshot the profile's current top history id (so we can
later distinguish "my run's row landed" from "a previous
run's row is still there"). Start the run. Poll /api/v1/profiles/{id} every poll_interval_seconds
until status leaves {"RUNNING", "PAUSED"}. Fetch the post-run history row with bounded exponential
backoff up to verdict_timeout_seconds — closes three
race windows (terminal-status vs row-insert vs verdict
worker; see _fetch_post_run_history for the full
rationale).
fail_on_verdict triggers the gate evaluation. Accepts a
list of verdict tokens — any of "FAIL", "WARN",
"NO_BASELINE". PASS is deliberately not accepted. Requires
wait=True (the gate needs the verdict, which requires
waiting). Unknown tokens raise ToolError before any HTTP call —
a typo like ["FAILL"] would otherwise produce a silent
false-pass.
Return shape: .. code-block:: python # Without wait:
{
"run_id": <str>, # in-memory run id
"profile_id": <int>,
"status": <str>, # initial — typically "RUNNING"
"waited": False,
}
# With wait:
{
"run_id": <int>, # history row id (persisted; use with tm_get_run)
"started_run_id": <str>, # in-memory runId from /start (informational)
"profile_id": <int>,
"status": <str>, # terminal — e.g. "COMPLETED" or "IDLE"
"verdict": <str|None>, # e.g. "FAIL", "PASS", "NO_BASELINE"
"verdict_reasons": <str|None>,
"fail_on_match": <bool|None>, # True when verdict ∈ fail_on_verdict, OR
# when gate is set but verdict is None
# (fail-closed). None when fail_on_verdict
# was not provided.
"metrics": {
"totalRequests": <int>,
"totalErrors": <int>,
"avgRps": <float>,
"peakRps": <float>,
"successRate": <float>,
"latencyQuantiles": {<quantile>: <ms>, ...},
},
"verdict_pending": <bool>, # True iff row appeared but verdict never
# populated within verdict_timeout_seconds
"waited": True,
}
Run-correlation defense. Exact runId match. The server
persists the in-memory runId on every RunHistory row (see
RunHistory.runId field). The wait flow compares the row's
runId to the one returned by /start; mismatch → fail
closed with ToolError. This is the definitive correlation —
no time-drift ambiguity, no race window where two starts
within seconds of each other false-negative. Additional defenses kept as belt-and-suspenders: triggered_by filter on history fetches: both the
pre-start snapshot and the post-run fetch filter
triggered_by="api". Narrows the candidate set to rows
we could plausibly have produced; protects the anchor logic
from cross-channel collisions. Time-drift fallback (transitional): if a row's
runId is null (legacy data persisted before the column
was added), falls back to the 30s forward / 5s backward
drift check. Removable once production rows all carry the
field.
Fail closed when ambiguous. Row with neither a runId nor
a parseable startedAt → ToolError. Better to surface "can't
verify identity" than silently return a possibly-wrong
verdict. |
| tm_stop_runA | Stop the in-flight run for a profile. Fully idempotent —
succeeds with status="IDLE" even when nothing is running.
Any in-flight dispatch loops cancel within a tick on the
server side. |
| tm_pause_runA | Pause the in-flight run for a profile. Idempotent — when no
run is active, returns status="IDLE" rather than erroring.
Useful when a workflow wants to inspect intermediate metrics
without losing progress. |
| tm_resume_runA | Resume a paused run. Continues from the pre-pause position
on the RPS curve and any SEQUENTIAL variables-set cursors.
Not idempotent in the no-state case — unlike stop/pause,
calling resume without an in-flight or paused run is a 400. |
| tm_analyse_captureA | Analyse a JSONL traffic capture and return the proposed
profile structure. capture_path must point to a .jsonl file under
$TM_MCP_CAPTURE_ROOT (defaults to
~/.trafficmorph/captures/). .jsonl.gz and other
compressed forms are NOT supported — the server-side parser
reads plain text only. Symlinks resolving outside the root,
path-traversal sequences, and other extensions are rejected
by the path validator in :mod:tm_mcp.capture_path — see its
module docstring for the security rationale.
Returns per-endpoint analysis: URL skeleton with $$macro$$
placeholders, derived RPS curve, extracted variables, sample
URLs. No state is persisted. Pair with
:func:tm_import_capture to actually create profiles from
the analysis. |
| tm_import_captureA | Import the chosen groups from an analysed JSONL capture as
real traffic profiles. Workflow: call :func:tm_analyse_capture first to inspect the
derived groups (method + URL skeleton + variables + RPS curve);
pick the ones you want to persist; pass them here as
selections. capture_path must be the SAME JSONL file you analysed —
server re-derives the analysis on commit (the preview response
deliberately doesn't carry full per-row values, so trusting
client-supplied preview data would be both heavy and tamper-
able). The path resolves under $TM_MCP_CAPTURE_ROOT with
the same security checks as tm_analyse_capture.
selections is a dict of shape::
{
"groups": [
{
"method": "POST",
"urlSkeleton": "https://api.example.com/api/x",
"profileName": "load-test users" # optional
},
...
]
}
Each (method, urlSkeleton) pair must match a group returned
by the prior tm_analyse_capture call exactly — index-based
references would be fragile across re-analysis (groups sort by
row count, ties break alphabetically). profileName is
optional; omit / null lets the server pick a default like
"[capture] POST /api/x". Empty groups list is allowed
(no-op; result reports zero profiles created). Returns the server's import result: createdProfiles (array
of {profileId, name}), createdVariablesSetCount,
skippedSelections (with reasons), warnings. |
| tm_create_profileA | Create a new traffic profile (upsert by name — see below). Minimum viable profile = name + target_url +
duration_seconds + points. The points array is the
RPS curve as a list of {"x": seconds, "y": rps} dicts —
e.g. [{"x": 0, "y": 10}, {"x": 60, "y": 100}] ramps from
10 to 100 RPS over the first minute. Optional shape knobs: http_method — defaults server-side to POST if omitted.
request_body — string body sent on every request.
request_headers — JSON-encoded header list, e.g.
'[{"name":"Authorization","value":"Bearer ..."}]'.
Must be a string (the API accepts a string for round-tripping
via the UI's form layer).
loop — repeat the curve indefinitely while the run is
active. Defaults to False.
Not yet exposed via MCP: init / response / cleanup scripts,
alert policy, callback URL, schedule. Use the web UI or the
public REST API directly for those. Name-collision behavior: the server's create endpoint is
upsert-by-name, which would silently REPLACE every field
of an existing profile from the request body — including
advanced fields (scripts, callback, alerts) this tool doesn't
expose. To prevent that data loss, this tool fails fast on
collision: if a profile with this name already exists,
you get a typed :class:ToolError naming the existing
profile's id, and no write is attempted. Name comparison matches the server's normalization: whitespace
is trimmed and the match is per-character case-insensitive
(mirrors the JDBC LOWER() lookup used by Spring Data's
IgnoreCase derived query). " Existing ", "EXISTING",
and "existing" all collide with an existing "Existing"
profile. ASCII names recommended. Non-ASCII names (e.g. "Straße",
"İstanbul") work, but the case-fold semantics on the JVM
and PostgreSQL may differ subtly from a Python-side
re-implementation. The MCP guard uses str.lower() to match
the server's per-character behavior (e.g. "Straße" and
"STRASSE" are treated as DISTINCT names by both sides),
not the more aggressive Python casefold(). For names with
locale-sensitive characters, ASCII spellings give predictable
cross-database behavior. To modify an existing profile, use :func:tm_update_profile
(which preserves advanced fields on partial updates). To
create a fresh profile, pick a name that doesn't collide. Concurrency caveat: the collision check is a client-side
preflight against GET /profiles immediately before the
POST. A concurrent POST from another session for the
same user can land in the gap. For single-user MCP usage the
race is sub-millisecond; for higher-concurrency scenarios the
structural fix is a server-side create-if-absent contract
(tracked separately). Returns the full saved profile dict (id, name, duration, …). |
| tm_update_profileA | Update an existing profile by id. Fields you omit are kept
as-is. The underlying API endpoint is a full PUT (the request body
replaces every field of the profile), but exposing that
directly would force the AI to GET-then-PUT in two calls for
every tweak. This tool does the read-merge-write internally:
fetches the current profile, overlays the kwargs you provided,
PUTs the merged body back. Use this when you know the profile's id. Examples: "Make this profile last 5 minutes instead of 1" → call with
profile_id=42, duration_seconds=300. "Switch the target to staging" → profile_id=42, target_url='https://staging.example.com/api'. "Replace the RPS curve" → profile_id=42, points=[...].
Fields not yet exposed (scripts / alerts / schedule) are
preserved verbatim from the existing profile. Rename caveat: the server's PUT endpoint correctly routes
by path id (it pins the request body's profile id to the
path id, so the save path takes its id-based update branch).
But the server does NOT enforce name uniqueness, so a
rename to another profile's name would create a duplicate-
named pair. To keep the data model clean and AI workflows
predictable, this tool still pre-flights a GET /profiles
whenever the name kwarg changes the current value, and
raises :class:ToolError if the new name collides with any
other profile (whitespace + case-insensitive match, mirroring
the server's normalization). Updates that don't change the
name skip this check entirely (no extra roundtrip). Returns the full updated profile dict. |
| tm_delete_profileA | Delete a profile by id. Any in-flight run is cancelled
server-side; run history rows are retained (only the profile
definition is removed). Returns {"profile_id": <id>, "deleted": True} on success.
A 400 / "Profile not found" surfaces as a typed ToolError
— caller can react to it without parsing message text. |