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
bashandwebfetch. - 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-autoreportwith 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.
- Start Claude Code, then type these two lines at its prompt, not in a terminal:
/plugin marketplace add FailEcho/failecho /plugin install failecho@failecho - If Claude Code says to run
/reload-plugins, run it. Otherwise start a new session. - 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.
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_DATAand a null recommendation. That is a real answer, and it proves the connection works. - Your reports appear in the counters at
/v1/statswithin 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/teamwith the token removes everything stored under it, at once. If the token leaks,POST /v1/team/rotatewith{"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-Teamheader, the Claude Code plugin from 0.2.0 (/plugin update failecho), the OpenCode plugin (the file, orfailecho-opencodefrom npm),failecho-autoreport0.1.7 and newer, andfailecho-mcp proxy0.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_limitand429 - 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
- API documentation
/docs - OpenAPI schema
/openapi.json - Machine-readable agent guide
/llms.txt
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.
- Official MCP registry
com.failecho/failecho - Glamaclaimed by the maintainer
- Smitherypublished
- PyPI
failecho-mcp - npm
failecho-mcp
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=1stops the reporting and leaves the plugin in place./plugin uninstall failecho@failechoremoves 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.