FailEcho

MCP serverEverything else

Check what other agents hit the same tool failure — and what recovery worked. Ask before retrying.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use FailEcho

From the project's README

As published by FailEcho/failecho in README.md.

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.

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.

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.

ToolWhen the agent calls it
check_tool_failurea tool failed — before retrying
report_tool_failurecontribute the failure
report_tool_successcontribute a success (the denominator)
report_recovery_outcomesay 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

TermMeaning
FailEcho Networkthe whole system
Failure Echoa normalized observed failure, shared by fingerprint
Recovery Echoevidence that a recovery action worked
Incidenta sudden abnormal failure increase
Reporteran agent or runtime sending telemetry
Fingerprintthe 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"
    }
  }
}
ToolWhen the agent calls it
check_tool_failurea tool failed — before retrying
report_tool_failurecontribute the failure
report_tool_successcontribute a success (the denominator)
report_recovery_outcomesay 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"
  }'

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.


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"}'

Tools

ToolPurpose
check_tool_failureCall before retrying. What is happening with this failure right now, and what recovery actually worked?
report_tool_failureContribute a failure observation. Returns its fingerprint.
report_tool_successContribute a success, so failure rates have a denominator.
report_recovery_outcomeReport 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.

EndpointWhen to call it
POST /v1/observeafter every tool call — successes and failures
POST /v1/querywhen a call fails, before you retry
POST /v1/outcomeafter 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:

FieldNotes
service, operation, version, schema_hashwhat was called
outcomesuccess or failure
error_type, error_codeshort classifiers
normalized_erroridentifiers replaced, secrets redacted
latency_ms
fingerprintSHA-256 digest
reporter_hashsalted 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:

Shortened here. Read the whole README on GitHub.

Advanced
Delivery
failecho MCP server → your ahel gateway (mcp.ahel.ai) → every connected AI client.
Catalog kind
mcp-server
Gateway key
com-failecho-failecho
Source
github.com/FailEcho/failecho
Hosted endpoint
https://failecho.com/mcp
FailEcho: MCP server · ahel