Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
TM_API_KEYYesFull tm_… value provisioned from in-app Settings → API keys
TM_BASE_URLYesURL 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_ROOTNoDefault root for capture file paths. Allow-listed root for tm_analyse_capture + tm_import_capture file paths~/.trafficmorph/captures/

Capabilities

Features and capabilities supported by this server

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
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

  • reasoning (autoVerdict, autoVerdictReasons).

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):

  1. run_id != baseline_run_id (comparing a run to itself produces a useless all-zero diff that would mislead the AI).

  2. 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).

  3. 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_domainthe 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:

  • Fix the underlying CSV (delete + re-upload with the right shape).

  • Stay in the current mode.

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):

  1. 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").

  2. Start the run.

  3. Poll /api/v1/profiles/{id} every poll_interval_seconds until status leaves {"RUNNING", "PAUSED"}.

  4. 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.

Prompts

Interactive templates invoked by user choice

NameDescription
tm_triageFind the most recent FAIL run for a profile, compare against the most recent PASS baseline, and explain what regressed. Returns a narrative the AI can read back to the user.
tm_setup_loadtestWalk through: domain verification (if needed), profile creation with a flat-RPS curve, optional immediate run. Handles the domain-not-verified case gracefully by walking the user through DNS/HTTP setup.
tm_compare_baselineQuick regression check: fetch the most recent run and most recent PASS for a profile, then diff them. Useful between CI runs or after a deploy.
tm_import_capture_guidedAnalyse a capture file, present the discovered groups in a compact table, ask the user which groups to import as profiles, then commit. Handles the analyse → review → import loop that's otherwise awkward over a single chat turn.

Resources

Contextual data attached and managed by the client

NameDescription
profilesLightweight summary of every traffic profile owned by the authenticated user. Same payload as `tm_list_profiles` — use this resource when you want to ground a conversation in 'what profiles do I have' without an explicit tool call.
history_recentMost-recent 20 traffic runs across all your profiles, newest first. Useful as session-start context for questions like 'what failed recently' or 'show me the last week's traffic'. For filtered / paginated history use the `tm_list_history` tool.
domainsAll domains registered by the authenticated user, with verification status (verified / pending) and the challenge tokens still in play. Same payload as `tm_list_domains`. Pull this when answering 'what domains am I cleared to load-test' or while diagnosing a domain-not-verified block at run-start.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/trafficmorph-gif/tm-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server