FailEcho

BUILD WITH FAILECHO

Set up FailEcho

About two minutes. No account, no API key, nothing to pay for.

Pick the one line that matches how you run agents. The ones at the top put FailEcho in the path of your tools, which is where it works; everything talks to the same network, so an agent set up one way benefits from failures reported another way.

Pick by where your agent runs

Put FailEcho in the path of the tools your agent already uses, not in its list of tools to remember.

  • Claude Code → the plugin. A hook runs after every MCP tool call; nothing for the model to decide.
  • OpenCode → one plugin file. It runs around every tool OpenCode uses, including bash and webfetch.
  • Cursor, Claude Desktop, any other MCP client → the proxy, in front of each MCP server you already use. Advice arrives inside the failing tool’s error.
  • Your own agent code → failecho-autoreport with advice on. The answer lands on the exception you already handle.
  • Nothing can be installed → the bare MCP endpoint. The smallest option, and the least effective one.

Why the order: a tool the model has to choose to call is one it mostly does not call. In our lab, agents given FailEcho’s MCP tools used them about once every five runs. The integrations that put the answer in the tool path deliver it every time; the results we have measured come from agents that then act on it — see what it costs, measured.

Let your agent set it up

The shortest path, and the one least likely to go wrong: hand the job to the thing that will be using it.

Paste this at whatever you are running — Claude Code, Claude Desktop, Cursor, your own framework:

Read https://failecho.com/llms.txt and set yourself up to use FailEcho.

llms.txt is written for exactly this. It carries the endpoint, the config block, all four tools with when to call each, the error classes to use, and the rules about what must never be sent. An agent that can read a URL and edit its own config has everything it needs.

Prefer to do it yourself? Everything below is the same thing by hand.

Claude Code — the fast path

Installs the MCP server and a hook, so lookups and reports happen on their own.

  1. Start Claude Code, then type these two lines at its prompt, not in a terminal:
    /plugin marketplace add FailEcho/failecho
    /plugin install failecho@failecho
  2. If Claude Code says to run /reload-plugins, run it. Otherwise start a new session.
  3. Check it worked: type /plugin. FailEcho should be listed as enabled.

From then on, when an MCP tool fails, FailEcho is asked what other agents saw and the failure is reported for the next agent. Nobody has to remember to do it.

Claude Code — without the plugin

The same hook, installed by hand. One file, no dependencies beyond Python 3.

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

Then add this 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}]}]
  }
}

OpenCode — one plugin file

OpenCode runs every .js file in .opencode/plugin/ around every tool it uses. Drop this one in.

mkdir -p .opencode/plugin
curl -fsSL https://github.com/FailEcho/failecho/raw/main/opencode-plugin/plugin/failecho.js \
  -o .opencode/plugin/failecho.js

Or from npm, in opencode.json:

{ "plugin": ["failecho-opencode"] }

Nothing else to configure. When a tool fails — a curl that got a 429, a webfetch that hit a 503, an MCP tool that errored — the plugin asks the network and appends one line to the output the model is already reading:

HTTP/2 429
rate limit exceeded

FailEcho: try wait_until_reset (worked 33/40).

It reports the shape only: the API host or MCP server, the endpoint or tool, an error class and code, the latency. Never the command, the arguments, the output, a file path or a token. Local tools — read, write, grep — and shell commands with no URL in them are never reported. Advice waits at most three seconds; with the network unreachable the tool’s output comes back exactly as it was. FAILECHO_DISABLED=1 turns it off, FAILECHO_ADVISE=0 reports without annotating.

Built on the same design as the Claude Code hook. Its own comparison in the lab — OpenCode with the plugin against OpenCode without — started on 23 September 2026; until it has runs, there is no measured result for it to quote.

Cursor, Claude Desktop, any MCP client — the proxy

A model given FailEcho's tools has to think of asking, and while it is handling a failure it mostly does not. The proxy puts the answer where the model is already looking: put it in front of the MCP servers you already use, and a failed tool call comes back with one line of advice.

{
  "mcpServers": {
    "github": {
      "command": "uvx",
      "args": ["failecho-mcp", "proxy", "--",
               "npx", "-y", "@modelcontextprotocol/server-github"]
    }
  }
}

npx -y failecho-mcp proxy -- … works the same way, with no Python needed. For a remote server, give its URL instead of a command: failecho-mcp proxy --header "Authorization: Bearer $TOKEN" -- https://…/mcp (a header token works; OAuth does not, so connect those directly).

429 rate limit exceeded
FailEcho: try backoff, worked n/m (confidence 0.61).

A server that wraps an API reports under its own name, so tell the proxy which host each tool calls: --upstream 'github_*=api.github.com' (or --upstream api.github.com for every tool). When the server's own name has no advice yet, the proxy asks under that host and says so — FailEcho (evidence from api.github.com): …. Reports keep the server's name. The GitHub MCP server's own names map to api.github.com without being told.

Every other message passes through unchanged — byte for byte for a server started locally; a remote server's messages are parsed and re-serialised. Each call's outcome is reported as its shape only — the server's name, the tool, an error class and code, the latency — never arguments, never results, and never the error text: the proxy reads it to pick an error class and drops it, ignoring FAILECHO_SEND_ERRORS even when you have set it for the wrapper (0.2.2 and newer). A failed call repeated with the same arguments counts as a retry, compared as a hash that never leaves your machine. Advice waits at most 3 seconds, on failures only; if FailEcho is unreachable the error passes through as it was. FAILECHO_DISABLED=1 makes it a plain pipe, FAILECHO_ADVISE=0 reports without adding advice.

Your own code, no MCP

One HTTP call. It writes no observation and keeps no record of the question, and is never rate limited; it adds 1 to a daily counter of answered/unanswered queries.

curl -X POST https://failecho.com/v1/query \
  -H "Content-Type: application/json" \
  -d '{"service": "api.github.com", "operation": "create_issue",
       "error_type": "rate_limit", "error_code": "429"}'

Reporting takes two more calls. /v1/observe after every call, success or failure — failure rates need a denominator:

curl -X POST https://failecho.com/v1/observe \
  -H "Content-Type: application/json" \
  -H "X-Reporter-ID: my-agent-1" \
  -d '{"service": "github-mcp", "operation": "create_issue",
       "outcome": "failure", "error_type": "validation_error",
       "error_code": "422"}'

That returns the fingerprint. If you then try something and it works, send the outcome — this is the scarcest and most useful thing in the network, because it is the only call that records what actually fixed a failure:

curl -X POST https://failecho.com/v1/outcome \
  -H "Content-Type: application/json" \
  -H "X-Reporter-ID: my-agent-1" \
  -d '{"fingerprint": "<from /v1/observe>",
       "action": "refresh_schema", "successful": true}'

Those three field names are exact: fingerprint, action, successful. Anything else returns 422 with the missing field named. Full schemas in the API reference.

The bare MCP endpoint — smallest, and least effective

For when nothing can be installed: paste the endpoint into the client's MCP settings, as type http. Nothing runs on your machine and reads store nothing — which makes it the safest thing to add, and the weakest. The model has to choose to call check_tool_failure while it is busy failing, and in our lab it did about once every five runs. If you can, put the proxy in front of your servers instead.

MCP endpoint https://failecho.com/mcp Streamable HTTP · no auth · no key

Config-file clients usually want this shape:

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

Where it goes, and what changes, in the clients we have checked:

Claude Code   .mcp.json               "mcpServers"   type + url
Cursor        .cursor/mcp.json        "mcpServers"   url only, no type
VS Code       .vscode/mcp.json        "servers"      type + url

The endpoint is the same in all of them. Only the file name and the top-level key move, so if your client is not listed, look up where it keeps its MCP config and use the shape above.

Agent frameworks take the URL directly — neither of these has a registry to be listed in, so there is nothing to wait for. Both of the snippets below were run against this endpoint before being written here.

# LlamaIndex — pip install llama-index-tools-mcp
from llama_index.tools.mcp import BasicMCPClient

client = BasicMCPClient("https://failecho.com/mcp")
result = await client.list_tools()   # result.tools holds the four tools
# LangChain — pip install langchain fastmcp
from langchain.mcp import MCPAdapter

async with MCPAdapter("https://failecho.com/mcp") as adapter:
    tools = await adapter.list_tools()

Connecting makes the four tools available. It does not make an agent use them — that stays the model's decision, one call at a time, and a small local model reliably will not make it. If you want reporting to happen whether or not the model thinks of it, wrap the call site instead. One file, standard library only, so it brings none of our dependencies with it:

pip install failecho-autoreport

# no code changes at all: run your script with every outbound HTTP call observed
python -m failecho_autoreport run my_agent.py

# or ask the network what it knows, from a shell
python -m failecho_autoreport check api.github.com "GET /repos" not_found 404

# or wrap exactly the calls you choose
from failecho_autoreport import FailEcho
fe = FailEcho()

tools = fe.wrap(tools, service="github-mcp")   # a framework's tool list

@fe.watch(service="api.github.com", operation="create_issue")
def create_issue(...):                          # or one call site
    ...

fe.recovered("github-mcp", "create_issue", "refresh_schema")  # what fixed it

Reporting helps the next agent. To help this one, turn on advice: when a watched call fails, the wrapper asks the network what worked (a read that writes no observation; it adds 1 to a daily query counter) and attaches the answer to the exception before it goes on up. Nothing is acted on for you, and the exception's type and message are unchanged.

fe = FailEcho(advise=True)          # or FAILECHO_ADVISE=1

@fe.watch(service="api.groq.com", operation="chat.completions")
def ask_model(model, messages):
    return client.chat.completions.create(model=model, messages=messages)

try:
    reply = ask_model("llama-3.3-70b-versatile", messages)
except Exception as exc:
    print(fe.advice_text(exc))      # e.g. "FailEcho: try switch_model, worked 310/341 ..."
    action = ((getattr(exc, "failecho", None) or {}).get("recommendation") or {}).get("action")
    if action == "switch_model":
        reply = ask_model("llama-3.1-8b-instant", messages)   # what fixed it for others
    elif action == "skip":
        raise                       # nothing has worked for anyone lately: stop retrying
    else:
        ...                         # your usual retry

The gains we have measured come from acting on the advice, as above: in our lab, agents that switched model when told recovered 74% of model-provider rate-limit failures, against 31% for twins that retried once. When the advice was only put in front of the model and the model decided, we have measured no difference yet. Name the service and operation the way the API does — its host and its endpoint — so your failure meets everyone else’s. The read waits at most 3 seconds, on failures only.

It never raises, never blocks (reports go to a worker thread, so an unreachable FailEcho costs the caller about 3ms rather than a timeout), and returns and raises exactly what your code did. It sends the failure's shape only — service, operation, error class, code, duration — never arguments, results or the error text, unless you set FAILECHO_SEND_ERRORS=1. FAILECHO_DISABLED=1 turns it off. Zero dependencies: the wheel is 8KB and pulls in nothing. Declare mutates=True on anything that is not a read, so a write that has never failed can be reported as unverified rather than as success. The run form patches urllib, requests and httpx for that process and reports each call as host plus method and first path segment — GET /repos — with everything after the first segment, the query string, headers and body never read. On hosts where the first segment is the resource itself (the npm registry), that name is what gets reported; use the decorator there.

The recovered line is the one worth bothering with. Failures alone give the network a failure rate; only an outcome records what fixed it, which is the half another agent can act on. From the decorator it stays one explicit call, because the wrapper cannot know what your code did between two calls. The run form can see one pattern for itself: the same URL fails with a 429, a 5xx, a timeout or a connection error and then succeeds within two minutes. It reports that as a retry, or a backoff if your code waited a second or more first, and a second failure as the retry not working. Nothing is inferred for 404s, auth or validation errors, and the URL it matches on never leaves the process. FAILECHO_INFER_RECOVERY=0 turns it off.

Two things we hit doing that, so you do not have to: langchain.mcp is beta as of langchain 1.4.0 and says its API may change, and importing it needs fastmcp present even though installing langchain does not bring it in.

If your client can only start a local process and cannot speak HTTP, run the relay. It is on PyPI, imports only the MCP SDK, and keeps nothing itself — every call is forwarded to the same network:

{
  "mcpServers": {
    "failecho": {
      "command": "uvx",
      "args": ["failecho-mcp"]
    }
  }
}

Or the same relay in Node, if that is what you have:

{
  "mcpServers": {
    "failecho": {
      "command": "npx",
      "args": ["-y", "failecho-mcp"]
    }
  }
}

Both are ours, both store nothing, and both forward to the same network. The Node one has no dependencies and starts in about a second. Prefer the direct endpoint above when your client speaks Streamable HTTP: one less moving part.

The client then has four tools: check_tool_failure before a retry, and report_tool_failure, report_tool_success and report_recovery_outcome to contribute. Without the hook, your agent has to call them itself, so say so in its instructions.

Already on OpenTelemetry? Point the traces here

An agent gateway, a Collector, or your own SDK exporter can send traces to https://failecho.com/v1/otlp/traces — protobuf or JSON, the standard OTLP/HTTP shapes. Client spans become observations: server.address (or rpc.service) is the service, rpc.method or the HTTP method plus the first segment of http.route is the operation — GET /repos, the same name the zero-code wrapper uses, so the evidence joins. Every other span is rejected and counted in partialSuccess, which is normal: most spans are not outbound calls.

# OpenTelemetry Collector: send only errors, strip anything payload-shaped first
processors:
  filter/errors:
    error_mode: ignore
    traces:
      span: ['status.code != STATUS_CODE_ERROR']
  attributes/strip:
    actions:
      - { key: url.full, action: delete }
      - { key: url.query, action: delete }
      - { key: db.statement, action: delete }
      - { pattern: 'http\.(request|response)\.(header|body).*', action: delete }
      - { pattern: 'gen_ai\..*', action: delete }
exporters:
  otlphttp/failecho:
    traces_endpoint: https://failecho.com/v1/otlp/traces
    headers: { X-Reporter-ID: my-gateway }
service:
  pipelines:
    traces:
      processors: [filter/errors, attributes/strip]
      exporters: [otlphttp/failecho]
# Python SDK, no gateway: pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace.export import BatchSpanProcessor

provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
    endpoint="https://failecho.com/v1/otlp/traces",
    headers={"X-Reporter-ID": "my-agent"})))

# then, per outbound call, a CLIENT span with server.address, http.request.method,
# http.route and http.response.status_code -- the semantic conventions you already set

Only a fixed set of attribute keys is read. url.full, headers, bodies, db.statement, exception.*, gen_ai.*, events and links are never read, under any setting; the Collector snippet strips them anyway, so control stays on your side. At most 500 spans and 256KB per request, rate limited per IP like every write. The Python snippet was run before it was written here: one 503 and one 200 from httpbingo.org, exported with the SDK's default protobuf, arrived as httpbingo.org GET /status server_error/503 and one success. The Collector snippet was not run — this box has no room for a Collector — and is the documented shape of the two processors; tell us if it needs a fix.

What traces cannot give: recovery outcomes. A span says a call failed, and later that one succeeded; it does not say what anyone decided in between. Failure rates and the skip verdict come from traces; what fixed it still needs POST /v1/outcome, the wrapper's recovered(), or the MCP tool.

How to tell it is working

  • Ask your agent to check any failure. A signature nobody has reported returns INSUFFICIENT_DATA and a null recommendation. That is a real answer, and it proves the connection works.
  • Your reports appear in the counters at /v1/stats within seconds, and on the live page.
  • Every answer carries evidence_sources, so you can see whether independent agents saw the same failure or only FailEcho's own agents did.

Private mode — free for early teams

A shared network needs other agents. Your own evidence does not.

Send a secret your team generates and every report is stored for your team alone. Never pooled, never counted as adoption, never rolled into any aggregate, never shown to another team, never part of any public number — and not by policy: private rows live in a different table from everything the public network is computed from.

export FAILECHO_TEAM="$(python3 -c 'import secrets;print(secrets.token_urlsafe(32))')"

Then use FailEcho exactly as before. A query with the same token returns the public answer plus team_evidence: what your own agents have seen, and your own recommendation once your own history clears the same bar the public one does — five attempts at 60%. Your own history does not lower that bar. When five of your agents’ recent attempts on a failure have all failed, it tells your team to stop retrying it, exactly as the public network does.

  • No account. The token is self-chosen, like reporter_id, only secret. It is stored as a salted hash and never in the clear, so there is nothing here to leak — and losing it loses access to that evidence. Keep it like a credential.
  • Kept for 30 days, rather than the public 48 hours, because your own history is the point of it.
  • Yours to delete or move. DELETE /v1/team with the token removes everything stored under it, at once. If the token leaks, POST /v1/team/rotate with {"new_token": "…"} moves your history to a new one and the old one reads nothing afterwards. Whoever holds the token can do both, so treat it like a password.
  • Where it works today: the REST API with the X-FailEcho-Team header, the Claude Code plugin from 0.2.0 (/plugin update failecho), the OpenCode plugin (the file, or failecho-opencode from npm), failecho-autoreport 0.1.7 and newer, and failecho-mcp proxy 0.2.3 and newer, from PyPI or npm. Older versions of those two ignore the variable and report publicly: upgrade first. It is never an MCP tool argument: a token is a secret, and tool arguments are visible to the model and to anything that logs the conversation.
  • Your own fixes come back. When the public network has no answer and your team does, the advice line says FailEcho (your team's own history): try … — always labelled as yours, never as other agents'.
  • Free while the network is bootstrapping. This is the first thing here that would ever be paid for. It works at zero independent reporters, which is exactly where FailEcho is today, and early teams get it for nothing.

What it costs, measured

Two questions worth asking before you add anything to an agent loop: how much slower does it get, and does it pay for itself.

Our own agents run the same tasks twice, one side asking FailEcho and acting on the answer, one side not, against the same live APIs. Measured 22–24 September 2026; every number, its sample and its significance test are in docs/claims.md.

  • Model-provider rate limits: 74% of failures recovered, against 31%, and 91% against 80% of runs finished (416 runs a side, p < 0.0001). The move that does it is switch_model, which on Groq’s 429 had worked 310 times in 341.
  • Flaky endpoints: 12.2 s lost inside failures per run, against 19.4 s, and 1.5 attempts per failure against 2.0: the time saved is retries that were never going to work.
  • With advice read from production — what a new user gets — 1.43 attempts per failure against 1.99, and 1.1 s lost per run against 2.1 s.
  • When it says stop, it is right. Agents told to skip an exhausted quota that retried anyway recovered 0 times in 404.
  • No extra tokens (1,495 against 1,536 per finished run, no difference), and one round trip per failure, never on a success.
  • No gain for an agent that already recovers carefully (9.8 s against 10.1 s lost per run: no difference).
  • No difference in answer correctness (99.3% against 99.2%) or on coding tasks.
  • No effect measured when the model alone decides what to do with the advice.

All of it is our own agents: production has 0 independent reporters, and the control side retries once after three seconds rather than recovering competently. OpenAI and Anthropic are not in the lab yet. Live numbers are on the lab scoreboard and on the live page.

What leaves your machine

Sent

  • the service and tool that failed
  • a coarse error class and code, such as rate_limit and 429
  • how long the call took

Never sent

  • prompts
  • tool arguments
  • tool results
  • file paths or session ids
  • API keys or secrets

The error text is only sent if you set FAILECHO_HOOK_SEND_ERRORS=1, and even then it is normalized and the raw string discarded. Servers the hook cannot name the way other users would, such as local scripts and private hosts, are skipped entirely. The full contract is on the about page.

Python client

A wrapper that reports and asks around one call. Not on PyPI yet.

# Not published to PyPI yet: copy client/ from the repository.
import sys; sys.path.insert(0, "client")
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=make_issue)

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

It works before anyone else joins

A shared network is the goal. It is not the requirement.

A recovery action is recommended once five recovery attempts back it — not five failures. Reporting the same failure five times and nothing else leaves the recommendation null, correctly: failures cannot prove a fix. What counts is the outcome afterwards, and those five can all be yours. Hit the failure five times, let the hook record what fixed it each time, and the sixth time FailEcho tells you — before a single other agent has ever connected.

Every recommendation says which it is. from_other_agents is false when the evidence is your own history coming back to you, true when somebody else paid for it, and null if you did not send a reporter_id, because then it cannot be known. Confidence is discounted below three distinct reporters, so your own evidence counts for less than a crowd's — it just does not count for nothing.

Which is also the honest reason to connect one now: the thing that makes it useful to you is the same thing that makes it useful to everyone else.

Where FailEcho is listed

If you would rather install from a directory than from this page. Being listed is not a measure of use, and none of these say anything about how many agents are reporting.

If nothing shows up

Four things account for almost every quiet install. If none of them is it, that is a bug and I want to hear about it.

The plugin went in mid-session
Claude Code reads its hooks when a session starts, so an install made during this one is not live yet. Start a new session, then let an MCP tool fail.
Your MCP servers are local scripts
Those are skipped on purpose: a failure in a server only you run is no use to anyone else. To share one, give it a public name with FAILECHO_HOOK_SERVICE_NAMES='{"alias": "public-name"}'.
The failures are Bash, curl or web calls
The hook only watches MCP tools. Report the rest yourself with one POST /v1/observe — the shapes are in the API reference.
You want it off
FAILECHO_DISABLED=1 stops the reporting and leaves the plugin in place. /plugin uninstall failecho@failecho removes it. Either way, nothing is stored about you.

Still nothing? Email contact@failecho.com with what you are running and what you expected. FailEcho is new and the quiet failures are the ones I cannot see from here — no report reaching the network looks exactly like nobody trying. A one-line message is genuinely useful, and you will get an answer from a person.

Something that looks like a security problem instead: security@failecho.com.