Skip to main content
Glama
FailEcho

FailEcho

Official

FailEcho

Failure intelligence for AI agents and autonomous software. Before you retry, check the echo.

FailEcho is a live cross-agent failure intelligence network. AI agents share privacy-safe tool failures and recovery outcomes so other agents can avoid repeating the same bad retry.

Python 3.11+ FastAPI MCP License MIT

Agent A fails.
FailEcho learns.

Agent B encounters the same failure.
It sees what actually worked for other agents.

Agent B benefits from evidence it never generated itself.

Connect in one minute

MCP endpoint

https://failecho.com/mcp
claude mcp add --transport http failecho https://failecho.com/mcp
{
  "mcpServers": {
    "failecho": { "type": "http", "url": "https://failecho.com/mcp" }
  }
}

Python, if you want failures and successes reported automatically:

from failecho import FailEcho

echo = FailEcho("https://failecho.com", reporter_id="my-agent-1")

outcome = await echo.observe_tool_call(
    service="github-mcp",
    operation="create_issue",
    call=lambda: github.create_issue(**args),
)

if outcome.failed and outcome.decision.actionable:
    do(outcome.decision.recommendation)   # your code decides, never FailEcho

No account. No API key. Free during the public MVP. Full integration guide: Connect an agent.

Related MCP server: Casebook MCP

What it does

See whether other AI agents are hitting the same tool failure right now — and which recovery actions actually worked. FailEcho exposes a Model Context Protocol (MCP) endpoint that agents can query after a tool failure, plus a REST API.

Tool

When the agent calls it

check_tool_failure

a tool failed — before retrying

report_tool_failure

contribute the failure

report_tool_success

contribute a success (the denominator)

report_recovery_outcome

say whether the fix worked

FailEcho normalizes error text deterministically (no model) into a fingerprint, accumulates recovery outcomes against it, and returns a recommendation only when independent reporters agree. Thin evidence returns INSUFFICIENT_DATA rather than a guess. Confidence is a Wilson score lower bound you can recompute from the counts returned beside it.

It stores failure metadata only: no prompts, tool arguments, tool results, request or response bodies, headers, keys or user content. Raw error text is discarded after normalization.

Live: https://failecho.com · /docs · /openapi.json · /llms.txt

This is not an observability platform, an error database, an uptime monitor or an LLM debugger. The unit of the system is:

service + operation + version + schema_hash + failure fingerprint
                     + observed recovery outcomes

Vocabulary

Term

Meaning

FailEcho Network

the whole system

Failure Echo

a normalized observed failure, shared by fingerprint

Recovery Echo

evidence that a recovery action worked

Incident

a sudden abnormal failure increase

Reporter

an agent or runtime sending telemetry

Fingerprint

the canonical normalized error identity

The brand vocabulary is for humans. Wire formats are deliberately unbranded: endpoint paths, MCP tool names and field names (fingerprint, recommendation, recovery_actions) stay exactly as they are, because machine clarity outranks naming purity.


See the network effect locally

Two terminals, about a minute.

# 1. the network
uv run uvicorn app.main:app --reload
#    or: .venv/bin/python -m uvicorn app.main:app --reload

# 2. six independent agents hitting the same broken tool
uv run python examples/live_agent/run_demo.py
#    or: .venv/bin/python examples/live_agent/run_demo.py

The demo starts a small local tool server, then runs six logically independent agents against it. Every network call goes over MCP, from an external process, using the official MCP SDK.

Agent A calls a tool. It fails: the provider renamed a field.
        |
        v
Agent A reports the failure          -> the network records it
Agent A has no evidence to go on, so it retries (fails),
        refreshes the tool schema (works), and reports both outcomes
        |
        v
Agents C, D, E, F hit the same failure with different repository ids
        -> normalization collapses all of them onto ONE fingerprint
        -> the network accumulates evidence from 5 independent reporters
        |
        v
Agent B hits the same failure with yet another id, and asks first
        -> the network recognises the fingerprint
        -> "refresh_schema: 5/5 successes, 5 reporters, confidence 0.57"
        -> "retry: 0/5. Do not bother."
        |
        v
Agent B skips the retry the others wasted a call on, refreshes, succeeds,
and reports its outcome -- which makes the next agent's answer better.

Agent B never met Agent A. It only met the network. That is the entire product.

Real output from the sixth agent, which had reported nothing before it asked:

Calling tool...
x tool failed

  422 validation_error
  Repository 987654 rejected field body: field "body" is no longer accepted, use "content"

Checking shared failure intelligence...

  Fingerprint:            6ed9ef705ff4037af2c977306b8b9f92
  Known failure:          YES
  Observed failures:      11
  Independent reporters:  6
  Service status:         MAJOR

  Recovery actions others reported:
    refresh_schema        5/5 (100.0%) confidence 0.57 reporters 5
    retry                 0/5 (0.0%) confidence 0.00 reporters 5

Best observed recovery:
  refresh_schema
  Skipping retry: other agents already proved it does not work here.

Applying recovery: refresh_schema
  Refreshed tool schema -> v3.0.0, field 'content'
  Retrying tool call...
  + tool call succeeded

Reporting recovery outcome...
+ accepted   (refresh_schema -> success)

Watch it land on the homepage at http://localhost:8000 while the demo runs. Demo agents label themselves with X-Reporter-Kind: demo, so their traffic is real evidence but is never counted as adoption — see Demo data.

Details, including how to run the tool server separately, are in examples/live_agent.


Connect an agent

Two ways in, and the difference matters.

MCP lets an agent explicitly ask and report — the model decides when to call check_tool_failure, so you get intelligence exactly where the agent reasons about a failure, and nothing else.

SDK instrumentation reports success and failure telemetry automatically for every tool call, without the model deciding anything. That is what produces denominators, and without denominators every failure rate in the network is meaningless.

Most deployments want both.

1. MCP

claude mcp add --transport http failecho https://failecho.com/mcp
{
  "mcpServers": {
    "failecho": {
      "type": "http",
      "url": "https://failecho.com/mcp"
    }
  }
}

Tool

When the agent calls it

check_tool_failure

a tool failed — before retrying

report_tool_failure

contribute the failure

report_tool_success

contribute a success (the denominator)

report_recovery_outcome

say whether the fix worked

2. Python

Copy client/ into your project (not published to PyPI yet), then:

from failecho import FailEcho

echo = FailEcho(
    endpoint="https://failecho.com",
    reporter_id="my-agent-1",       # optional, hashed server-side
)

outcome = await echo.observe_tool_call(
    service="github-mcp",
    operation="create_issue",
    version="2.8.1",
    schema_hash="a817ce",
    call=lambda: github.create_issue(**args),
)

if outcome.failed and outcome.decision.actionable:
    # YOUR code decides. FailEcho never acts on your behalf.
    if outcome.decision.confidence > 0.8:
        refresh_schema()
        await echo.report_recovery(
            fingerprint=outcome.decision.fingerprint,
            action="refresh_schema",
            successful=True,
        )

observe_tool_call reports the success or the failure, queries FailEcho when the call failed, and hands you a FailureDecision. It never retries, never refreshes and never falls back — executing a recovery can double-post or double-charge, so that decision stays yours.

It cannot break your agent. Every call is fail-soft: a timeout or an unreachable host is swallowed and your tool result is returned anyway. Set FAILECHO_DISABLED=1 and the whole client becomes a no-op.

3. Framework instrumentation

Reference integration, Pydantic AI:

from failecho import FailEcho
from failecho.integrations.pydantic_ai import instrument_toolset

echo = FailEcho("https://failecho.com", reporter_id="my-agent-1")
agent = Agent("openai:gpt-4o", toolsets=[instrument_toolset(my_toolset, echo)])

Every tool call now reports its outcome. The wrapper is behaviourally invisible: same results, same exceptions, same control flow. Tool arguments are never read and never sent.

Other frameworks (LangChain, LlamaIndex, CrewAI, OpenAI Agents SDK, Claude Code hooks) are not built yet. They should implement failecho.adapters.ToolTelemetrySink — four events, one direction — rather than touch FailEcho's core. See client/failecho/adapters.py.

4. REST

curl -X POST https://failecho.com/v1/query \
  -H "Content-Type: application/json" \
  -H "X-Reporter-ID: my-agent-1" \
  -d '{
    "service": "github-mcp",
    "operation": "create_issue",
    "error_type": "validation_error",
    "error_code": "422",
    "error_message": "Repository 555812 was not found"
  }'

5. Claude Code plugin (automatic)

Connecting the MCP server leaves it to the model to call FailEcho when a tool fails, and models forget. The plugin removes the decision:

/plugin marketplace add FailEcho/failecho
/plugin install failecho@failecho

That installs the MCP server and a hook Claude Code runs after every MCP tool call, so every failure is reported, successes give the failure rates their denominator, and a second attempt is recorded as a recovery (retry with the same arguments, adjust_arguments with new ones). When the network already knows a failure, the hook hands Claude a short note -- how often others hit it and which recovery worked -- before it retries.

Without the plugin, the hook is one file with no dependencies beyond Python 3:

mkdir -p ~/.claude/hooks
curl -fsSL https://raw.githubusercontent.com/FailEcho/failecho/main/plugin/hooks/failecho_hook.py \
  -o ~/.claude/hooks/failecho_hook.py

Then add to ~/.claude/settings.json:

{
  "hooks": {
    "PostToolUseFailure": [{"matcher": "mcp__.*", "hooks": [
      {"type": "command", "command": "python3 ~/.claude/hooks/failecho_hook.py", "timeout": 10}]}],
    "PostToolUse": [{"matcher": "mcp__.*", "hooks": [
      {"type": "command", "command": "python3 ~/.claude/hooks/failecho_hook.py", "timeout": 10}]}]
  }
}

What leaves your machine: the server's public name and the tool name, a coarse error class and code (rate_limit / 429), and the call's latency. Never tool arguments, tool results, prompts, file paths or session ids, and the error text only if you set FAILECHO_HOOK_SEND_ERRORS=1. A server is named by its public package (npx @scope/server, uvx server) or its public host; local scripts and private hosts are skipped entirely. Name one yourself with FAILECHO_HOOK_SERVICE_NAMES='{"alias": "public-name"}'. If FailEcho is unreachable, the hook gives up after one short timeout and Claude carries on.

Variable

Default

Purpose

FAILECHO_DISABLED

unset

1 turns the hook off

FAILECHO_HOOK_SEND_ERRORS

unset

1 also sends the error text, normalized server-side

FAILECHO_HOOK_REPORT_SUCCESS

1

0 stops success reports

FAILECHO_HOOK_SERVICE_NAMES

unset

JSON map from a server alias to a public name

FAILECHO_ENDPOINT

https://failecho.com

your own server, if you self-host

About reporter IDs

Optional, and never required. A stable one is salted and hashed on arrival — the raw value is never stored — and it improves three things: independent reporter counting, poisoning resistance, and FailEcho's ability to tell you that a recommendation came from somebody other than you. Anonymous reporting stays fully supported.


Your own agents (first-party)

While the network bootstraps, the operator's own agents report real failures too. That data is real field evidence, but it is not independent and it is not adoption, so it carries its own label everywhere it appears:

Source

Who

Counts as adoption

Shown to agents as

agent

any real agent

yes

agent

first_party

FailEcho's own agents

no

first_party

demo_agent

agents sending X-Reporter-Kind: demo

no

demo data

synthetic

scripts/seed_demo.py

no

demo data

first_party is a claim about who is reporting, so it has to be proven: send X-FailEcho-Operator: <FIN_FIRST_PARTY_TOKEN>. A wrong or missing token is stored as demo, which keeps it out of adoption and never shows it to anyone as operator evidence. Every query answer lists evidence_sources, so an agent can tell an answer backed only by first_party from one that independent agents back.

Generate the token once, on the server:

echo "FIN_FIRST_PARTY_TOKEN=$(openssl rand -hex 32)" >> /etc/failecho.env

Then give it to your own agents, and nobody else:

# Claude Code
claude mcp add --transport http failecho https://failecho.com/mcp \
  --header "X-FailEcho-Operator: <token>"

# stdio relay
FAILECHO_OPERATOR_TOKEN=<token> failecho-mcp

The Python client takes operator_token="<token>", or reads FAILECHO_OPERATOR_TOKEN.

Naming what failed

The name is part of the fingerprint, so evidence is only shared when agents name the same thing the same way. Use the MCP server's own name (its serverInfo.name) or the HTTP API's host as service, and the tool name exactly as the server defines it as operation: create_issue, not mcp__github__create_issue.

Concept

Agent A fails
    |
    v
reports anonymously  ---------> network learns
                                    |
Agent B hits the same problem       |
    |                               |
    v                               v
queries the network  <---------  what happened to others
    |
    v
skips the useless retry, uses the recovery that works

Run locally

Python 3.11+.

# with uv
uv venv
uv pip install -r requirements.txt
uv run uvicorn app.main:app --reload

# or plain venv + pip
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python -m uvicorn app.main:app --reload

Seed synthetic demo data so the homepage has something to show:

.venv/bin/python scripts/seed_demo.py            # add demo data
.venv/bin/python scripts/seed_demo.py --reset    # replace existing demo data
.venv/bin/python scripts/seed_demo.py --purge    # remove demo data

Then:

Run the tests:

.venv/bin/python -m pytest

Fold expired raw observations into hourly aggregates (safe to run any time):

.venv/bin/python scripts/prune.py --dry-run
.venv/bin/python scripts/prune.py

End-to-end examples (server must be running):

.venv/bin/python client/example_agent.py            # REST, single agent
.venv/bin/python examples/live_agent/run_demo.py    # MCP, six agents, network effect

The demo runs its tool server in a background thread. To run it separately (two terminals) instead:

.venv/bin/python examples/live_agent/tool_server.py
.venv/bin/python examples/live_agent/run_demo.py --no-tool-server

MCP

The MCP server runs inside the same FastAPI process — no second service to deploy or supervise — and speaks Streamable HTTP at /mcp. It is stateless with JSON responses: no per-session memory, no long-lived streams, which is what keeps it viable on a small VPS.

Connect

Claude Code:

claude mcp add --transport http failecho https://failecho.com/mcp
# local:
claude mcp add --transport http failecho http://localhost:8000/mcp

Generic MCP client config (mcpServers style):

{
  "mcpServers": {
    "failecho": {
      "type": "http",
      "url": "https://failecho.com/mcp"
    }
  }
}

Raw JSON-RPC, if you want to see it work:

curl -s localhost:8000/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Local stdio server

Some hosts can only start a local process and talk to it over stdin/stdout. failecho-mcp is for them. It is a relay, not a second FailEcho: it has no database and stores nothing. Every tools/list and tools/call is forwarded to the shared network, so it serves the same four tools, with the same descriptions and the same evidence, as the URL above.

uvx --from git+https://github.com/FailEcho/failecho failecho-mcp

It is not on PyPI yet, so uvx installs it from the repository. That pulls in the server's dependencies too; the relay itself imports only the MCP SDK.

Client config (mcpServers style):

{
  "mcpServers": {
    "failecho": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/FailEcho/failecho", "failecho-mcp"]
    }
  }
}

Variable

Default

Purpose

FAILECHO_URL

https://failecho.com/mcp

Network to relay to. Point it at your own server if you self-host.

FAILECHO_REPORTER_KIND

unset

Set to demo for demo agents, so their reports stay out of adoption numbers.

If the network is unreachable, a tool call returns an error result that says so and records nothing, and the agent falls back to its own retry policy instead of hanging.

Prefer the URL when your client supports it: one hop fewer, nothing to install.

Tools

Tool

Purpose

check_tool_failure

Call before retrying. What is happening with this failure right now, and what recovery actually worked?

report_tool_failure

Contribute a failure observation. Returns its fingerprint.

report_tool_success

Contribute a success, so failure rates have a denominator.

report_recovery_outcome

Report whether a recovery action worked.

All four call the same functions as the REST endpoints (app/core/service.py), so an MCP client and a curl user can never disagree about what a failure means — there is one normalizer, one fingerprint function, one intelligence layer.

Example check_tool_failure result:

{
  "known": true,
  "fingerprint": "01ae47053fbb3eabf8f3e480cba45ba8",
  "status": "MAJOR",
  "observations": { "total": 418, "last_5m": 81, "last_1h": 201, "unique_reporters": 47 },
  "failure_rate": { "last_5m": 0.73, "last_1h": 0.31 },
  "recovery_actions": [
    { "action": "refresh_schema", "attempts": 124, "successes": 117,
      "success_rate": 0.9435, "effective_attempts": 124, "unique_reporters": 45,
      "confidence": 0.8881 }
  ],
  "recommendation": { "action": "refresh_schema", "confidence": 0.8881 },
  "demo_data_included": false
}

demo_data_included tells an agent when synthetic demo rows are part of the numbers. Disable MCP entirely with FIN_MCP_ENABLED=0.


REST API

Three calls. No account, no API key, no payment.

Endpoint

When to call it

POST /v1/observe

after every tool call — successes and failures

POST /v1/query

when a call fails, before you retry

POST /v1/outcome

after you tried a recovery action

Report a failure

curl -s localhost:8000/v1/observe \
  -H 'content-type: application/json' \
  -H 'X-Reporter-ID: my-agent-1' \
  -d '{
    "service": "github-mcp",
    "operation": "create_issue",
    "version": "2.8.1",
    "schema_hash": "a817ce",
    "outcome": "failure",
    "error_type": "validation_error",
    "error_code": "422",
    "error_message": "Repository 918272 was not found",
    "latency_ms": 421
  }'
{
  "accepted": true,
  "fingerprint": "01ae47053fbb3eabf8f3e480cba45ba8",
  "known": true,
  "observations": 143,
  "normalized_error": "Repository <N> was not found"
}

The message is normalized before anything is stored: Repository 918272 was not foundRepository <N> was not found. The fingerprint is sha256(service | operation | version | schema_hash | error_type | error_code | normalized_error), truncated to 32 hex chars.

Report a success

Failure rates need a denominator, so send successes too:

curl -s localhost:8000/v1/observe \
  -H 'content-type: application/json' \
  -d '{
    "service": "github-mcp", "operation": "create_issue",
    "version": "2.8.1", "schema_hash": "a817ce",
    "outcome": "success", "latency_ms": 318
  }'

Query the network

curl -s localhost:8000/v1/query \
  -H 'content-type: application/json' \
  -d '{
    "service": "github-mcp",
    "operation": "create_issue",
    "version": "2.8.1",
    "schema_hash": "a817ce",
    "error_type": "validation_error",
    "error_code": "422",
    "error_message": "Repository 555812 was not found"
  }'
{
  "known": true,
  "fingerprint": "01ae47053fbb3eabf8f3e480cba45ba8",
  "status": "MAJOR",
  "looks_new": false,
  "observations": { "total": 418, "last_5m": 81, "last_1h": 201, "unique_reporters": 47 },
  "failure_rate": { "last_5m": 0.73, "last_1h": 0.31 },
  "recovery_actions": [
    { "action": "refresh_schema", "attempts": 124, "successes": 117,
      "success_rate": 0.9435, "confidence": 0.8881 },
    { "action": "retry", "attempts": 91, "successes": 17,
      "success_rate": 0.1868, "confidence": 0.12 }
  ],
  "recommendation": {
    "action": "refresh_schema", "confidence": 0.8881,
    "based_on_attempts": 124, "based_on_successes": 117
  }
}

When the network has nothing useful:

{ "known": false, "status": "INSUFFICIENT_DATA", "recommendation": null }

/v1/query is read-only. It stores nothing.

Report a recovery outcome

curl -s localhost:8000/v1/outcome \
  -H 'content-type: application/json' \
  -d '{
    "fingerprint": "01ae47053fbb3eabf8f3e480cba45ba8",
    "action": "refresh_schema",
    "successful": true
  }'
{ "accepted": true }

Actions are free-form strings in V1. Common ones: retry, wait, refresh_schema, remove_optional_field, reconnect, use_fallback, reauthenticate, abort.

Status

curl -s localhost:8000/v1/services              # per service/operation health, worst first
curl -s localhost:8000/v1/stats                 # counters; real and synthetic kept separate
curl -s localhost:8000/v1/recovery-intelligence # best evidenced recovery actions
curl -s localhost:8000/health                   # {"status":"ok"}
curl -s localhost:8000/llms.txt                 # agent-readable description of the service

Python client

Zero dependencies — standard library only. Copy client/failure_network.py and client/failecho.py into your agent (the package is not published yet).

failecho is the preferred import name and simply re-exports failure_network, which keeps working unchanged — the rename is additive, so no existing code breaks.

from failecho import Client   # or: from failure_network import Client

client = Client("http://localhost:8000", reporter_id="my-agent-1")

client.observe_failure(
    service="github-mcp",
    operation="create_issue",
    version="2.8.1",
    schema_hash="abc",
    error_type="validation_error",
    error_code="422",
    error_message="Repository 91827 not found",
)

intel = client.query(
    service="github-mcp",
    operation="create_issue",
    version="2.8.1",
    schema_hash="abc",
    error_type="validation_error",
    error_code="422",
    error_message="Repository 12345 not found",
)

if intel["recommendation"]:
    action = intel["recommendation"]["action"]     # e.g. "refresh_schema"
    client.report_recovery(
        fingerprint=intel["fingerprint"], action=action, successful=True
    )

client.observe_success(service="github-mcp", operation="create_issue", latency_ms=318)

Every call is fail-soft: a timeout or an unreachable server returns None (or a neutral INSUFFICIENT_DATA dict from query) instead of raising. Telemetry must never break the agent it observes.


Privacy

Privacy is a product feature, not a setting.

Collected — structured failure metadata only:

Field

Notes

service, operation, version, schema_hash

what was called

outcome

success or failure

error_type, error_code

short classifiers

normalized_error

identifiers replaced, secrets redacted

latency_ms

fingerprint

SHA-256 digest

reporter_hash

salted hash of an optional header, or NULL

created_at, source

We do not want, and never store:

  • prompts

  • model messages

  • tool arguments

  • tool results

  • request bodies and response bodies

  • HTTP headers and cookies

  • API keys, tokens and secrets

  • customer names, emails and any user content

  • credit-card data

Metadata only. If a field is not in the table above, this network does not want it — and the schemas give it nowhere to land.

How that is enforced:

  1. The request schemas have no fields for any of it. Unknown JSON keys are dropped by Pydantic before the handler runs, so an agent that accidentally sends {"prompt": ...} cannot persist it here.

  2. The raw error_message is normalized at the edge and the raw string is discarded — never written to a column, never logged. Only normalized_error survives.

  3. Normalization runs a redaction pass first: credential-shaped substrings (bearer tokens, API keys, JWTs, card-shaped digit groups) become <REDACTED> rather than being categorised and kept.

  4. X-Reporter-ID is optional, salted with FIN_REPORTER_SALT and hashed on arrival. The raw value is never stored. Rotating the salt makes existing hashes unlinkable.

  5. There is no authentication, so there is no account, email or billing identity to leak in the first place.

Normalization examples:

Repository 918272 was not found              -> Repository <N> was not found
User carol@acme.com at 10.0.12.7 failed      -> User <EMAIL> at <IP> failed
GET https://api.example.com/v1/x?y=2 failed  -> GET <URL> failed
token=sk_live_9aBc12345678xyz rejected       -> <REDACTED> rejected
HTTP 422 unprocessable                       -> HTTP 422 unprocessable   (unchanged)

Small numbers survive on purpose: 422 and 500 are semantics, not identifiers. See app/core/normalize.py and app/core/privacy.py.


How the numbers are produced

Everything is deterministic arithmetic over observation counts. No model, no learned parameter, nothing you cannot recompute yourself.

Incident status (MVP heuristic, constants in app/core/config.py):

< 10 observations in the last hour        -> INSUFFICIENT_DATA
failure rate < 5%                         -> HEALTHY
failure rate >= 5%  and < 30%             -> DEGRADED
failure rate >= 30%                       -> MAJOR

The 5-minute window takes over from the 1-hour window once it holds at least 5 observations, so a fresh incident is not diluted by an hour of healthy history. This is a threshold on a ratio — not change-point detection, not seasonality aware, not statistically calibrated. It is labelled MVP logic on purpose.

Recovery confidence is the lower bound of the 95% Wilson score interval for that action's success rate. It folds sample size into the number, so 5/5 successes ranks below 117/124 successes. An action is only recommended with at least 5 attempts and a 60% success rate, and confidence is capped below 1.0. Thin evidence returns "recommendation": null. The network never fabricates confidence.

Unique reporters counts distinct non-null reporter hashes, so one agent sending 1000 events does not look like 1000 independent reporters. Anonymous observations are excluded from that count, making it a lower bound.


Abuse floor (V1)

No accounts, so the defences are structural rather than identity-based. Two independent layers, both transparent:

Per-reporter evidence cap. For confidence and recommendations, one reporter contributes at most FIN_MAX_REPORTER_WEIGHT_PER_HOUR (default 5) attempts per fingerprint + action + hour. Raw counts are still reported verbatim — the API returns attempts alongside effective_attempts, so you can see both what was reported and what actually counted. Successes are scaled down proportionally when a bucket is capped, so trimming volume never invents a better success rate. All anonymous reports in a bucket are treated as one reporter: unattributed evidence cannot prove it is independent.

Reporter diversity. A recommendation needs 5 effective attempts and a 60% success rate. Evidence backed by fewer than FIN_MIN_UNIQUE_REPORTERS (default 3) distinct reporters is not blocked — anonymous reporting is a supported mode — but its confidence is multiplied by FIN_LOW_DIVERSITY_CONFIDENCE_FACTOR (default 0.7).

Write rate limiting. POST /v1/observe, POST /v1/outcome and the MCP reporting tools share one budget of FIN_RATE_LIMIT_WRITES_PER_MINUTE (default 120) per client IP — switching transport does not buy a second budget. Reads are never rate limited; querying is the product. The limiter is an in-process dict: it is not distributed, so a second worker would get its own budget, and it does not stop a distributed flood. The evidence cap is the defence that survives an attacker who changes IP, because it limits influence rather than requests.

Behind Cloudflare or nginx, set FIN_TRUST_PROXY=1 so the limiter reads CF-Connecting-IP / X-Forwarded-For instead of the proxy's own address. Leave it off when the server is directly exposed: trusting those headers would let any client forge its own rate-limit identity.

Reporter identity is still optional and still hashed with a salt before storage. Raw identifiers are never written anywhere.


Retention and pruning

Raw observations are the hot path (the 5-minute and 1-hour windows read them directly) and also the thing that grows without bound. So:

raw observations   kept FIN_RETENTION_HOURS (default 48h)
                   then folded into hourly aggregates and deleted
hourly aggregates  kept indefinitely

Two aggregate tables: hourly_stats (successes, failures, unique reporters, latency sum/count per hour × service × operation × version × schema × source) and hourly_recovery_stats (attempts, successes, and the capped effective counts per hour × fingerprint × action).

The invariant: a raw row is aggregated and deleted inside one transaction, so aggregates only ever describe rows that no longer exist. "Raw + aggregates" is a total, never a double count — and re-running the pruner is a no-op, because what it already folded is gone. Short windows (5m, 1h) always read raw rows only, so pruning can never change a live status. The recovery cap is applied per hour bucket, which is exactly the grain the aggregates use, so pruning cannot change a recommendation either.

python scripts/prune.py                # use FIN_RETENTION_HOURS
python scripts/prune.py --hours 24     # override the window
python scripts/prune.py --dry-run      # report only, change nothing
python scripts/prune.py --vacuum       # also reclaim file space (briefly locks)
Retention window: 48h
Cutoff:           2026-09-08T09:51:18Z
Aggregated 18429 observations into 96 hourly buckets
Aggregated 812 recovery outcomes into 41 hourly buckets
Deleted 18429 raw observations
Deleted 812 raw recovery outcomes
Database size:    4.21 MB

Recommended cron (hourly, at :15) — not needed for local development:

15 * * * * /srv/failure-network/.venv/bin/python /srv/failure-network/scripts/prune.py >> /var/log/failure-network-prune.log 2>&1

Or use the bundled systemd timer: deploy/failure-network-prune.timer.


Project layout

app/
  main.py               FastAPI app, CORS, static homepage, /health, /llms.txt
  mcp_server.py         MCP tools + Streamable HTTP endpoint (same process)
  api/                  observe.py  query.py  outcome.py  services.py  deps.py
  core/                 normalize.py  fingerprint.py  intelligence.py
                        service.py  retention.py  ratelimit.py
                        privacy.py  config.py  clock.py
  db/                   database.py (async engine)  models.py
  schemas/              Pydantic request/response models with agent-readable docs
  web/static/           index.html  style.css  app.js   (no framework, no build)
                        logo.svg  favicon.svg  og-image.svg
client/
  failecho/             the public client package
    __init__.py         FailEcho: observe_tool_call, report_*, query
    adapters.py         ToolTelemetrySink -- the framework seam
    integrations/
      pydantic_ai.py    reference integration (optional dependency)
  failure_network.py    zero-dependency REST client (still supported)
  auto_recovery.py      the passive wrapper FailEcho is built on
  auto_recovery.py      failure-aware tool wrapper (reports + asks, never acts)
  example_agent.py      end-to-end REST usage example
examples/live_agent/
  tool_server.py        local tool that just shipped a breaking change
  tool_client.py        agent-side tool client with a stale cached schema
  network.py            MCP client (official SDK) for the four network tools
  agents.py             the autonomous loop: fail -> report -> ask -> recover
  run_demo.py           one command, six independent agents
scripts/
  seed_demo.py          synthetic demo telemetry (source='synthetic')
  prune.py              aggregate + delete expired raw rows
deploy/
  failure-network.service        systemd unit
  failure-network-prune.timer    hourly retention timer
  Caddyfile.failecho-dev         optional origin-level .dev redirect
LICENSE  SECURITY.md  CONTRIBUTING.md  .env.example
tests/                  the suite

app/core/service.py is the seam that keeps transports honest: REST handlers and MCP tools both call record_observation, query_intelligence and record_recovery_outcome. Nothing in app/core/ knows what HTTP is, so the next transport (OTel receiver, worker, CLI) plugs in the same way.

Demo data

scripts/seed_demo.py writes ~2000 observations and ~300 recovery outcomes across four services, every row tagged source='synthetic':

Service

Operation

Scenario

github-mcp

create_issue

MAJOR — schema drift; refresh_schema fixes it, retry does not

search-api

search

DEGRADED — upstream timeouts; use_fallback works

stripe-mcp

create_refund

HEALTHY — occasional rate limiting

example-agent-tool

run

HEALTHY — rare crash, only 3 recovery attempts, so no recommendation is given

There are two kinds of non-real telemetry, and both are labelled at the row level by a source column:

source

Where it comes from

Counted as adoption

agent

a real autonomous system

yes

demo_agent

a caller that sent X-Reporter-Kind: demo (the demo agents)

no

synthetic

scripts/seed_demo.py

no

demo_agent rows are real observations from real tool calls — the demo genuinely breaks a tool and genuinely recovers — but they are demonstrations, so they stay out of adoption metrics. Self-labelling can only ever downgrade a report: nothing a caller sends can promote a row to real telemetry, which is why trusting the header is safe.

FIN_DEMO_MODE=1 marks a deployment as a demonstration instance: /v1/stats returns demo_mode: true and the homepage shows a DEMO MODE badge. It never generates traffic — it only labels what is already stored. Nothing in this project fabricates telemetry at startup.

Both kinds are tracked separately everywhere they surface:

  • /v1/stats reports real_observations_total, real_observations_24h, real_reporters_24h and real_failure_fingerprints excluding all demo rows, plus synthetic_observations and demo_agent_observations separately. They are never summed into one adoption number.

  • the homepage renders real telemetry in the headline block and synthetic counters in a separate, visibly labelled block;

  • /v1/recovery-intelligence flags every entry with demo_data: true|false (?include_demo=false hides them);

  • POST /v1/query and the MCP check_tool_failure tool return demo_data_included, so an autonomous caller knows when it is acting on demo evidence.

Remove it all with python scripts/seed_demo.py --purge.


Configuration

Every setting is an environment variable; defaults are in app/core/config.py.

Variable

Default

Meaning

FIN_DATABASE_URL

sqlite+aiosqlite:///./data/failure_network.db

swap for postgresql+asyncpg://... later

FIN_REPORTER_SALT

dev-salt-change-me

change in production; rotating it unlinks old hashes

FIN_WINDOW_SHORT_SECONDS

300

short window

FIN_WINDOW_LONG_SECONDS

3600

long window

FIN_MIN_OBSERVATIONS_FOR_STATUS

10

below this: INSUFFICIENT_DATA

FIN_HEALTHY_MAX_FAILURE_RATE

0.05

FIN_DEGRADED_MAX_FAILURE_RATE

0.30

FIN_MIN_RECOVERY_ATTEMPTS

5

evidence floor for a recommendation

FIN_MIN_RECOVERY_SUCCESS_RATE

0.60

FIN_MAX_CONFIDENCE

0.99

never claim certainty

FIN_MAX_REPORTER_WEIGHT_PER_HOUR

5

max attempts one reporter contributes per fingerprint+action+hour

FIN_MIN_UNIQUE_REPORTERS

3

below this, confidence is discounted (never blocked)

FIN_LOW_DIVERSITY_CONFIDENCE_FACTOR

0.7

the discount

FIN_RATE_LIMIT_ENABLED

1

write rate limiting on/off

FIN_RATE_LIMIT_WRITES_PER_MINUTE

120

per client IP, REST + MCP combined

FIN_TRUST_PROXY

0

read CF-Connecting-IP / X-Forwarded-For; only behind a real proxy

FIN_RETENTION_HOURS

48

raw observations older than this are aggregated and deleted

FIN_MCP_ENABLED

1

mount the MCP endpoint

FIN_MCP_PATH

/mcp

where to mount it

FIN_MCP_ALLOWED_HOSTS

(empty)

comma list; enables DNS-rebinding protection when set

FIN_MCP_ALLOWED_ORIGINS

(empty)

comma list; same

FIN_ALLOWED_ORIGINS

*

CORS origins for browsers (comma-separated)

FIN_PUBLIC_URL

http://localhost:8000

canonical public origin; drives canonical/OG tags, /llms.txt and every on-page example

FIN_GITHUB_URL

(empty)

repository link (https://github.com/FailEcho/failecho in production); while empty, no GitHub link is rendered anywhere

FIN_DEMO_MODE

0

label this deployment as a demo instance (generates nothing)

Deploying on a small VPS

Brand assets

app/web/static/logo.svg       the mark, inherits surrounding text colour
app/web/static/favicon.svg    the mark with fixed neutrals, for tab bars
app/web/static/og-image.svg   1200x630 social card

The mark is a failure event and its echo: one tall stroke in signal red, repeating outward and decaying. It carries no baked-in wordmark — "FailEcho" is always HTML text beside it, so the mark stays usable at 16px and as an avatar.

og-image.svg is served as-is. Most social platforms do not render SVG previews; when a PNG becomes necessary, export it once with any tool and drop it next to the SVG rather than adding a rendering dependency to the service.

Domains

failecho.com is the canonical public origin. Everything an agent or a human needs lives on it:

https://failecho.com/              homepage
https://failecho.com/mcp           MCP endpoint (Streamable HTTP)
https://failecho.com/docs          API reference
https://failecho.com/openapi.json  machine-readable schema
https://failecho.com/llms.txt      plain-text summary for agents

failecho.dev is a secondary domain and redirects permanently to failecho.com, preserving the path:

https://failecho.dev/*      ->  301  ->  https://failecho.com/*
https://failecho.dev/docs   ->  301  ->  https://failecho.com/docs
https://failecho.dev/mcp    ->  301  ->  https://failecho.com/mcp

Do this at the edge, not in the application. The app has no notion of a second domain and should not grow one.

www.failecho.comfailecho.com is handled at the origin by Caddy (redir https://failecho.com{uri} permanent), so it needs no Cloudflare rule — only a proxied DNS record for www.

Cloudflare (preferred) for the .dev domain. Add failecho.dev to the same account, then Rules → Redirect Rules → Create rule:

Field

Value

When incoming requests match

Hostname contains failecho.dev

Then

Dynamic redirect

Expression

concat("https://failecho.com", http.request.uri.path)

Status

301

Preserve query string

on

One rule covers both failecho.dev and www.failecho.dev — the Hostname contains match catches each — and it costs nothing on the free plan. Never serve a copy of the site from .dev: two origins with the same content is the classic way to have Google pick the wrong canonical. Both hostnames still need proxied DNS records (an A to the origin, or an AAAA to 100:: if you would rather the origin never see the request at all).

Caddy fallback, if you ever serve .dev from the origin instead — the config ships in deploy/Caddyfile.failecho-dev:

failecho.dev, www.failecho.dev {
	redir https://failecho.com{uri} permanent
}

api.failecho.com is deliberately not used in this MVP: a second origin would mean a second certificate, a second CORS surface and a second thing to explain, for no benefit while the API and the site are the same process.

Set the origin once, in one place:

FIN_PUBLIC_URL=https://failecho.com

It drives the canonical tag, Open Graph URLs, /llms.txt, the MCP endpoint shown on the homepage and every copyable example. No file in the codebase hardcodes the domain. Left unset, everything falls back to the request's own origin, so local development and IP-address access both stay correct.

Cloudflare checklist

DNS

Name

Type

Value

Proxy

failecho.com

A

origin IP

proxied

www.failecho.com

A

origin IP

proxied

failecho.dev

A

origin IP

proxied

www.failecho.dev

A

origin IP

proxied

www.failecho.comfailecho.com is handled by Caddy (redir ... permanent). The .dev hostnames are handled by the redirect rule above.

Order matters on first setup: leave the records unproxied (grey cloud) until Caddy has obtained its Let's Encrypt certificate, then switch to proxied and set SSL/TLS → Overview → Full (strict). Turning the proxy on first, or leaving the mode on "Flexible", is the usual way this goes wrong.

Caching. Never cache the live surfaces. Caddy already sends Cache-Control: no-store for /v1/*, /health and /mcp, and max-age=3600 for /static/*; leave Cloudflare on "Respect origin headers" rather than adding a blanket cache rule. Caching /mcp would break MCP sessions, and caching /v1/stats would make the live network look frozen.

Rate limiting. Cloudflare rate limiting is a supplement, not a replacement: FailEcho's own per-IP write limit and per-reporter evidence cap must keep working with the proxy off, because they are what stop poisoning, and poisoning does not care about your CDN. Nothing here requires a paid Cloudflare plan.

Deployment topology

Intended topology. The app binds to loopback only; TLS and the public address belong to Cloudflare and a local reverse proxy:

   internet
      |
      v
  Cloudflare (TLS, DNS, DDoS)
      |
      v
  nginx / caddy on the VPS  (:443 -> :8000)
      |
      v
  uvicorn 127.0.0.1:8000    FastAPI + SQLite (WAL)

Do not bind uvicorn to 0.0.0.0 in this topology. Binding publicly skips the proxy, exposes the origin directly, and makes FIN_TRUST_PROXY=1 unsafe (any client could then forge X-Forwarded-For and bypass the rate limit). Docker is optional and not required.

Step 1 — generate the reporter salt once and keep it.

sudo install -d -o failurenet -g failurenet /srv/failure-network/data
printf 'FIN_REPORTER_SALT=%s\n' "$(openssl rand -hex 16)" \
  | sudo tee /etc/failure-network.env > /dev/null
sudo chmod 600 /etc/failure-network.env

Generating it inline on the command line would mint a new salt on every restart, which silently resets every reporter hash and every unique-reporter count. Generate once, store once.

Step 2 — production command (what the systemd unit runs):

set -a; . /etc/failure-network.env; set +a
export FIN_DATABASE_URL="sqlite+aiosqlite:////srv/failure-network/data/failure_network.db"
export FIN_PUBLIC_URL="https://failecho.com"
export FIN_TRUST_PROXY=1
export FIN_ALLOWED_ORIGINS='*'
export FIN_RETENTION_HOURS=48

/srv/failure-network/.venv/bin/python -m uvicorn app.main:app \
  --host 127.0.0.1 --port 8000 --workers 1 \
  --proxy-headers --forwarded-allow-ips '127.0.0.1' --no-server-header

Note the four slashes in the SQLite URL: sqlite+aiosqlite:/// plus the absolute path /srv/.... Three slashes would make it relative to the working directory.

Step 3 — reverse proxy. Caddy:

failures.example.com {
    reverse_proxy 127.0.0.1:8000
}

nginx:

location / {
    proxy_pass         http://127.0.0.1:8000;
    proxy_set_header   Host $host;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_buffering    off;   # keeps /mcp responsive
}

Then sudo cp deploy/failure-network.service /etc/systemd/system/ and sudo systemctl enable --now failure-network.

Environment checklist

Variable

Production value

Why

FIN_REPORTER_SALT

32 hex chars from /etc/failure-network.env

set once; rotating it unlinks existing reporter hashes

FIN_DATABASE_URL

sqlite+aiosqlite:////srv/failure-network/data/failure_network.db

absolute path, four slashes

FIN_TRUST_PROXY

1 only behind the proxy above

otherwise clients forge their own rate-limit identity

FIN_ALLOWED_ORIGINS

*, or https://yourdomain

comma-separated; * keeps the public API browser-callable

FIN_RETENTION_HOURS

48

raw rows older than this become hourly aggregates

FIN_DEMO_MODE

0 in production

1 only for a demonstration instance

FIN_PUBLIC_URL

https://failecho.com

canonical origin for links, tags and examples

FIN_GITHUB_URL

repository URL, or unset

no link is rendered while unset

Other notes

  • One worker. SQLite serialises writes anyway, and the rate limiter and MCP session manager are per-process — two workers would mean two independent rate-limit budgets. Scale out only after moving to PostgreSQL.

  • MCP is served from the same process at /mcp; it answers with plain JSON, so no SSE-specific proxy tuning is needed beyond disabling buffering.

  • Retention: deploy/failure-network-prune.timer, or the cron line above.

  • Backups: copy data/ (including -wal/-shm) or run sqlite3 data/failure_network.db ".backup backup.db". No downtime needed.

Resident memory is well under 150 MB with the MCP server mounted; SQLite runs in WAL mode with synchronous=NORMAL and a 5 s busy timeout, so readers are not blocked by writers.

Migrating to PostgreSQL later

Every column type is portable, timestamps are naive UTC, there are no SQLite-specific types and no expression indexes. Migration is FIN_DATABASE_URL=postgresql+asyncpg://... plus pip install asyncpg and one Alembic baseline.


Is it working?

FailEcho publishes the numbers that decide whether the idea holds, on /v1/stats. They are deliberately unflattering.

Metric

What it answers

real_observations_24h

is anything real arriving?

real_successes_24h

do we have denominators, or only complaints?

real_reporters_24h

how many independent systems?

known_hit_rate_24h

when an agent asks, does FailEcho know anything?

recovery_outcome_ratio_24h

do agents say whether the fix worked?

cross_agent_help_24h

did an agent use evidence it did not generate?

cross_agent_help_24h is the one that matters. It counts a query only when the caller identified itself, a recommendation was returned, and at least one reporter behind that recommendation was somebody else. Anonymous callers and single-reporter evidence are not counted — undercounting the effect is honest, overcounting it is not.

recovery_outcome_ratio_24h is the fragile one. Reporting a failure is automatic; reporting whether the fix worked requires the agent to come back afterwards. Without those reports FailEcho is an error counter.

Launch milestones

Internal experiment markers, not marketing claims:

1   one real reporter
2   ten independent real reporters
3   100+ real observations per day
4   the first repeated real fingerprint
5   the first real cross-agent recovery benefit

Milestone 5 is the hypothesis: an agent hits a failure, queries FailEcho, receives evidence generated by unrelated agents, changes behaviour, and recovers. Everything before it is plumbing.


MVP limitations

Stated plainly, because pretending otherwise would make the network less useful:

  • No authentication. Anyone can report anything. The per-reporter evidence cap and rate limiter raise the cost of poisoning the statistics; they do not make it impossible, and a distributed flood from many IPs would still get through.

  • No reputation scoring. Reporters are counted, not ranked. A reporter that has been right a thousand times counts the same as a fresh one.

  • The rate limiter is in-process and not distributed. One uvicorn worker, one budget. It resets on restart.

  • No sophisticated anomaly detection. Status is a fixed threshold on a failure ratio over two fixed windows.

  • Unique-reporter counts in aggregates are lower bounds. Reporter identities are not retained past pruning, so merged buckets keep the maximum per-bucket count rather than a true distinct count.

  • No OpenTelemetry ingestion yet, no TypeScript SDK yet, no payments (x402 or otherwise). Everything is free.

  • SQLite is a prototype-stage choice. Retention keeps the file small, but a busy network will eventually want PostgreSQL (a URL swap plus asyncpg).

  • Recovery actions are free-form strings, so refresh_schema and refreshSchema would be counted separately if agents disagree on spelling (input is lowercased and space-normalized, which handles the common cases).

Launch documentation

  • docs/marketing.md — approved messaging, launch posts, and the claims that must never be made

  • docs/launch-plan.md — distribution sequence, experiment metrics and the milestones that decide whether this works

  • docs/search-console.md — indexing checklist for Google Search Console and Bing, and what actually moves brand search

Contact

Licence

MIT.

Available Tools

4 tools
check_tool_failureCheck what the network knows about a tool failureAInspect

Use FailEcho when another tool fails, before retrying blindly.

Use this tool when another tool, API call, or MCP server operation fails. It checks whether other autonomous systems recently experienced the same failure and returns current failure intelligence, known recovery actions, and confidence based on observed outcomes.

Call it BEFORE retrying. A retry that is failing for every other agent right now is a retry you can skip, and the network often knows a specific action that works instead (refresh a stale tool schema, fall back to another provider, reconnect, wait).

Returns: status (HEALTHY / DEGRADED / MAJOR / INSUFFICIENT_DATA), how many observations and distinct reporters have seen this exact failure in the last 5 minutes and hour, the current failure rate for the service+operation, every recovery action other agents tried with its success rate, and a single recommendation when the evidence supports one.

recommendation is null when evidence is insufficient -- that is a real answer, not an error. Confidence is a Wilson score lower bound computed from observed attempts; it is never generated by a model. Check demo_data_included: when true, synthetic demo rows are part of the numbers.

Reading is free, anonymous, unauthenticated and never rate limited, and this call stores nothing.

ParametersJSON Schema
NameRequiredDescriptionDefault
serviceYesTool/service identifier, e.g. 'github-mcp'.
versionNoVersion of the failing service/tool, if known.
operationYesOperation/tool name that failed, e.g. 'create_issue'.
error_codeNoProtocol/vendor code, e.g. '422', 'ECONNRESET'.
error_typeNoShort failure class, e.g. 'validation_error', 'timeout', 'rate_limit', 'auth_error'.
reporter_idNoOptional stable identifier for your agent. Salted and hashed on arrival and never stored by this call; it only lets FailEcho tell whether the evidence it just gave you came from a different reporter.
schema_hashNoShort hash of the tool schema you used. Lets FailEcho separate 'the API broke' from 'your tool schema is stale'.
error_messageNoThe error text you received. Normalized server-side (identifiers replaced, credential-shaped substrings redacted) and never stored by this call. Do not include prompts, tool arguments, secrets or user content.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are present, so the description carries the full burden of behavioral disclosure, and it delivers comprehensively: the call is 'free, anonymous, unauthenticated and never rate limited, and this call stores nothing.' It also discloses semantic edge cases ('recommendation is null when evidence is insufficient -- that is a real answer, not an error'), statistical methodology ('Wilson score lower bound... never generated by a model'), and the demo-data caveat ('synthetic demo rows are part of the numbers').

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with purpose and trigger conditions, then organized into logical blocks (when to use, what it returns, semantics of null/confidence/demo data, privacy guarantees). It is longer than typical, and the Returns paragraph partially re-describes what the output schema already enumerates, but nearly every sentence carries distinct information, and the length is justified by the tool's trust-sensitive nature.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For an 8-parameter, no-annotation tool accepting error messages and reporter identifiers from an external network, nothing needed for correct invocation is missing: trigger conditions, return contract, null semantics, statistical integrity, demo-data contamination, and privacy/rate-limit guarantees are all covered. The presence of an output schema further covers return structure, and even so the description explains the non-obvious interpretation rules.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all 8 parameters, warranting the baseline of 3. The description adds some contextual framing (e.g., schema_hash lets FailEcho separate 'the API broke' from 'your tool schema is stale', and error text is normalized and redacted server-side), but these details are also present in the schema itself. The description does not compensate for any gap because there is no gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific, concrete definition: it 'checks whether other autonomous systems recently experienced the same failure and returns current failure intelligence, known recovery actions, and confidence based on observed outcomes.' The verb+resource is precise, and the tool is clearly differentiated from the report_* siblings by being the pre-retry query tool versus the post-hoc reporting tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage guidance is explicit and actionable: 'Use this tool when another tool, API call, or MCP server operation fails. Call it BEFORE retrying.' It also names the alternative behavior it replaces ('a retry that is failing for every other agent right now is a retry you can skip') and the recovery actions the network may suggest. There is no ambiguity about when to invoke it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

report_recovery_outcomeReport whether a recovery action workedAInspect

After you acted on a known failure -- retried, waited, refreshed a stale schema, reconnected, fell back to another provider -- report whether it actually resolved the problem.

This is the highest-value telemetry in the network: it is the difference between 'everyone is failing' and 'everyone is failing, and refreshing the schema fixes it'. Every recommendation other agents receive is built from these reports.

Pass the fingerprint returned by check_tool_failure or report_tool_failure. Common actions: retry, wait, refresh_schema, remove_optional_field, reconnect, use_fallback, reauthenticate, abort. Report one outcome per attempt, not one per retry-loop iteration: a single reporter contributes at most 5 attempts per hour to any action's confidence.

ParametersJSON Schema
NameRequiredDescriptionDefault
actionYesWhat you tried, e.g. 'refresh_schema'. Lowercase, [a-z0-9_.-], spaces become underscores.
successfulYesTrue when the action resolved the failure.
fingerprintYesFingerprint from check_tool_failure or report_tool_failure (32 lowercase hex characters).
reporter_idNoOptional stable agent id; hashed, never stored raw.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden, and it delivers: it discloses that reports feed downstream recommendations, that each attempt should be reported separately, and that contributions are rate-limited per action. It also frames the tool as telemetry, which makes the side effect of calling it clear.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the tool's purpose and remains focused: the first sentence states exactly when to call it, and later sentences justify the telemetry value, enumerate common actions, and set rate expectations. It is longer than a minimal description, but the added sentences all carry operational guidance rather than filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a four-parameter telemetry tool with full schema coverage and an output schema, this description is complete: an agent knows when to call it, what to pass, what action values exist, how to avoid duplicate reports, and the rate limit. The only minor omission is an explicit alternative to report_tool_success, but that does not block correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3; the description adds value above the schema by listing canonical action values, explaining that the fingerprint must come from check_tool_failure or report_tool_failure, and linking the action parameter to per-hour confidence accumulation. It does not duplicate parameter formats, letting the schema handle syntax.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb and resource: it instructs the agent to report whether a recovery action resolved a known failure, and it ties the report to a fingerprint from check_tool_failure or report_tool_failure. This clearly differentiates the tool from the sibling failure-reporting tools: check/report_tool_failure record the failure itself, while this records the outcome of the attempted fix.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It specifies the exact trigger ('After you acted on a known failure'), what to pass (the fingerprint), and common action values to report. It also sets rate and granularity guidance ('one outcome per attempt', 'at most 5 attempts per hour'), though it does not explicitly state when to prefer report_tool_success over this tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

report_tool_failureReport a failed tool call to the networkAInspect

Anonymously contribute a tool/API/MCP failure to FailEcho so other autonomous systems can recognise it. Call this whenever a tool call fails, after (or alongside) check_tool_failure.

PRIVACY -- this is a shared public network. Send failure metadata ONLY. Never include prompts, model messages, tool arguments, tool results, request or response bodies, HTTP headers, cookies, API keys, tokens, customer names, emails or any user content. The error message is normalized server-side (numbers, UUIDs, emails, URLs, IPs and tokens replaced with placeholders; credential-shaped substrings redacted) and the raw text is discarded, but that is a safety net, not a licence to send sensitive data.

Pass a stable reporter_id if you can: it is salted and hashed before storage, is never stored raw, and lets the network count you as one independent reporter instead of anonymous noise. Writes are rate limited per client.

ParametersJSON Schema
NameRequiredDescriptionDefault
serviceYesTool/service identifier.
versionNoVersion of the service/tool.
operationYesOperation that failed.
error_codeNoProtocol/vendor code, e.g. '422'.
error_typeNoShort failure class, e.g. 'validation_error'.
latency_msNoObserved call latency in milliseconds.
reporter_idNoOptional stable identifier for your agent. Salted and hashed on arrival; never stored raw.
schema_hashNoShort hash of the tool schema used.
error_messageNoError text. Normalized before storage; no secrets please.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full behavioral disclosure burden and succeeds. It reveals that the network is shared/public, that only failure metadata should be sent, that error messages are normalized server-side (with raw text discarded), that reporter_id is salted/hashed, that writes are rate limited, and the privacy redaction heuristics. All of this goes well beyond a bare name and would materially change how an agent uses the tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is verbose but every block earns its place: primary action, when-to-call, privacy restrictions, normalization details, reporter_id guidance, and rate limiting. The most important call/decide information (purpose and when to use) is front-loaded, while deep privacy details appear later. Slightly longer than minimal, but justified by the sensitive nature of the tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Complete for a 9-parameter tool with no annotations and a nontrivial privacy model. The description covers when to call, what data may/must not be sent, server-side normalization guarantees, reporter identity semantics, and rate limiting. With an output schema present, return-value documentation is not required. No critical gap remains.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and parameters already have clear descriptions, so the baseline is 3. The description adds value beyond the schema by explaining why reporter_id matters (counts as one independent reporter instead of anonymous noise) and how the error_message normalization process works (numbers, UUIDs, emails, URLs, IPs, tokens replaced; credential-shaped substrings redacted) — details absent from the property definitions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb and resource ('anonymously contribute a tool/API/MCP failure to FailEcho') and states the purpose of helping other autonomous systems recognize failures. It clearly distinguishes itself from siblings like report_tool_success and report_recovery_outcome by focusing on failures.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says 'Call this whenever a tool call fails' and specifies the order relative to the sibling tool check_tool_failure ('after or alongside'). This leaves no ambiguity about when the tool should be invoked.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

report_tool_successReport a successful tool call to the networkAInspect

Report that a tool call SUCCEEDED. This matters more than it sounds: a failure rate is failures divided by total calls, so a network that only hears about failures cannot tell a broken service from a busy one, and every status it reports would be wrong.

Cheap to call and carries no error data at all -- just which service/operation/version succeeded and how long it took. Same privacy rules apply: metadata only, never arguments or results.

ParametersJSON Schema
NameRequiredDescriptionDefault
serviceYesTool/service identifier.
versionNoVersion of the service/tool.
operationYesOperation that succeeded.
latency_msNoObserved call latency in milliseconds.
reporter_idNoOptional stable agent id; hashed, never stored raw.
schema_hashNoShort hash of the tool schema used.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.1/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden. It does well: it discloses that the call is cheap, contains only metadata (service/operation/version/latency), never arguments or results, and follows the same privacy rules. It does not discuss idempotency or rate limits, but these are not critical for this simple reporting tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and front-loaded with the core action. The rationale paragraph, while longer than strictly necessary, earns its place by motivating why success reports are important. There is no filler or repetition of schema content.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 6-parameter tool with full schema coverage and an output schema, the description covers the essential behavioral context: what is reported, what is excluded, and privacy expectations. It is complete enough for an agent to decide when and how to invoke it, though it could have named the failure-report sibling explicitly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3. The description restates the main payload fields (service, operation, version, latency) but adds no new per-parameter semantics beyond what the input schema already documents. The privacy note is behavioral context rather than parameter-level meaning.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Report that a tool call SUCCEEDED.' It also differentiates itself from siblings by emphasizing that it carries no error data, making it clearly distinct from report_tool_failure and check_tool_failure.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives clear context for when to call it: after a tool call succeeds, and it explains why success reports matter (failure rate denominators). It implies the exclusion of failures via 'carries no error data,' but it does not explicitly name the failure-reporting sibling as the alternative, so it falls just short of a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 4 tool updatesv0.1.0
    • First observedcheck_tool_failure
    • First observedreport_recovery_outcome
    • First observedreport_tool_failure
    • First observedreport_tool_success

TDQS

A4.5/5.0

Scored across 4 tools

Disambiguation5/5

Each tool targets a distinct part of the failure intelligence lifecycle: contributing a failure, checking known failures, reporting a recovery outcome, and reporting a success. The descriptions make the boundaries explicit, including when to use each (before retrying, after acting, etc.). No two tools appear interchangeable.

Naming Consistency5/5

All four tool names follow a consistent snake_case verb_noun pattern: report_tool_failure, check_tool_failure, report_recovery_outcome, report_tool_success. The prefixes report_ and check_ clearly distinguish write versus read operations.

Tool Count5/5

Four tools is well-scoped for a focused failure telemetry network. Each tool earns its place by covering a distinct telemetry action: failure report, failure lookup, recovery outcome, and success report. Nothing feels redundant or missing from the minimal viable set.

Completeness5/5

The server covers the full loop for failure intelligence: reporting failures, checking them, reporting recovery outcomes, and reporting successes to compute accurate failure rates. The presence of report_tool_success is especially important for denominator data. No obvious lifecycle gap remains for the stated purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Enables AI agents to semantically search and contribute insights to a shared knowledge base built from other agents' experiences.
    5
    13
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables coding agents to query and commit to a research graph that remembers failed experiments, ensuring reproducibility and preventing redundant work.
    3
    MIT