Atenea documentation
When a provider looks flaky
On this page
Historical evidence: any Serena references below describe a provider retired in contract 4.0.0. They are not operational setup instructions.
When a provider looks flaky#
A report that reads “Serena is unstable” is almost never about Serena.
It is the shape Atenea’s own funnel gives to a fault underneath every provider on the same transport. The funnel hides the fault wherever it has somewhere else to go and exposes it only where it does not, so a whole-seam outage arrives on your desk named after the one capability with no alternative.
This page is the write-up of a real instance. The specific bug is worth knowing; the shape is worth knowing better, because the next one will wear a different name.
Why one name comes back#
Two different questions decide whether an implementation can answer, and the funnel keeps them apart on purpose. Reach is a wiring decision made before anything ran: is an adapter attached that claims this implementation? Health is what a probe found. An implementation nobody wired up and one that is wired and broken are different findings, and only the second is worth debugging.
atenea status prints both, in the orchestrator block:
serves ripgrep
no runner claude.search, codex.search, kivgraph.*, serena.*, tokensave.*
declared symbol.unresolved (not offered: no implementation)The current stock catalogue is 38 implementations across 28 capabilities.
With only the local runner attached, one implementation is reachable and
symbol.unresolved remains deliberately dormant: it is a useful catalog
contract, but Kivgraph no longer exposes the old unresolved-reference MCP
tool. The MCP surface applies the same rule to every unoffered implementation:
it is absent from tools/list, and a direct call returns a clear
not_offered diagnostic. An attached runner that is temporarily down is still
offered, so health remains an outage rather than a wiring error.
Attaching Serena — the configuration this write-up is about — adds its five implementations:
| Capability | Reachable with Serena attached | Over |
|---|---|---|
code.search | ripgrep | a local binary |
symbol.definition | serena.definition | local HTTP |
symbol.references | serena.references | local HTTP |
symbol.implementations | serena.implementations | local HTTP |
symbol.overview | serena.overview | local HTTP |
symbol.search | serena.symbol_search | local HTTP |
The remaining implementations do not buy you here.
code.search declares three — claude.search, codex.search and ripgrep —
and Serena is in none of them: the Serena adapter’s fifth implementation is
serena.symbol_search, and it answers symbol.search, a structural query
against Serena’s own index that returns declarations. That is a different
capability from the literal-text one, so attaching Serena adds nothing to text
search and everything to the symbol half.
The symbol half is where the coverage is thinner than the catalogue makes it
look. symbol.definition, symbol.references and symbol.overview each
declare a kivgraph sibling, and symbol.overview declares a tokensave one on
top of that — but both of those adapters are stdio child processes that this
configuration never attaches, and an implementation nobody wired up is not a
fallback. symbol.implementations and symbol.search do not even have a
sibling to leave unattached: one declared implementation each, both Serena’s.
So text search stands on a local binary, and all five symbol capabilities
stand on one transport.
Now break that transport.
code.search never touched it. ripgrep answers, commissions finish, and
nothing is reported — correctly, because nothing failed.
All five symbol capabilities lose their only reachable implementation at the
same instant. The three with a kivgraph or tokensave sibling declared do not
survive on it — the funnel drops those at reach, before health is ever
consulted, and it drops them exactly as fast whether the transport is up or
down. No survivor, so all five fail out loud, every time.
The operator sees: symbols are broken, search is fine. The only name attached to the broken half is Serena. So the report says Serena — and the next hour goes into a component that is running perfectly.
The blast radius of a transport fault is every capability whose whole implementation set sits on that transport. That set has a provider’s name on it, and the transport does not, so the report comes back naming the provider. Before believing it, look at what still works and ask what it reaches over.
The specific bug: localhost is not an address#
Servers behind a local proxy bind an address, and clients are configured with a name. Those are only the same thing while name resolution agrees with them.
Observed on one machine in July 2026, when four MCP servers ran behind local proxies:
$ ss -lntp
LISTEN 127.0.0.1:40010 thv # serena
LISTEN 127.0.0.1:40011 thv # context7
LISTEN 127.0.0.1:40020 thv # semgrep
LISTEN 127.0.0.1:40021 thv # chrome-devtoolsThat machine now runs two of the four as stdio-per-client, with no proxy and no port, so the capture is history rather than an inventory – read the ports as an example, never as a list to copy. The failure it demonstrates is not history: it applies to every proxied server, and it gets worse as the count goes up, because they all share the one name that broke.
Every one of them is bound to 127.0.0.1 — IPv4, and only IPv4. Nothing is
listening on ::1.
The client configuration had been rewritten to point at http://localhost:PORT.
That works, right up until the moment localhost resolves to ::1 first —
which is the default nearly everywhere, and arrives on its own with a resolver
update, an /etc/hosts edit, a container network change, or a distribution
bumping nsswitch.conf. Nobody has to do anything wrong. The machine simply
starts answering a question differently one morning.
When it flips, the client tries ::1, gets connection refused, and all four
servers become unreachable in the same instant. Not degraded. Not slow. Gone,
together, for a reason that is nowhere near any of them.
The nasty part is the state in between. A resolver that returns both families and a client that retries can produce a connection that works most of the time and fails some of the time — which is the literal definition of the word in the original report.
Why Atenea is not exposed to it#
Atenea’s shipped endpoint is a literal address:
[orchestrator.serena]
endpoint = "http://127.0.0.1:40010/mcp"Not localhost. That is deliberate and it is load-bearing: the proxy binds
127.0.0.1, so naming 127.0.0.1 is the only spelling that cannot be broken by
something outside Atenea changing its mind about what a name means. There is no
lookup to go wrong, no address family to guess, and no failure that arrives
without anybody touching Atenea.
It reads like a tidiness wart. localhost is friendlier, it is what everybody
writes, and swapping it in would pass every test in this repository. It would
also hand the whole class of failure above to a component that currently cannot
have it.
Do not change it to a name. If a future proxy binds something else, pin the new address — not a name that resolves to it today.
The practical consequence during the incident: Atenea kept answering symbol questions while the CLIs on the same machine could not reach a single MCP server. Which, if you are not expecting it, makes the fault look even more like Serena misbehaving — same server, working for one caller and not another, minutes apart.
Telling the two apart#
The question is always: is one provider down, or is the ground under several of them gone? Two commands answer it.
atenea statusRead the implementation lines, not the capability names. One provider having a
bad day looks like one line changing. A transport fault looks like every
implementation that reaches the same way going at once, while the local ones
stay green. If ripgrep is fine and everything behind a proxy is not, stop
reading the provider list — the answer is not in it.
atenea select code.search --repo currentselect asks the funnel who would answer and prints every stage with what it
dropped and why. This is how you see a capability that is still answering while
its alternatives quietly went away — the answer comes back, and the trace names
everybody who did not give it:
capability code.search
repository current
chosen ripgrep (the only surviving implementation)
funnel
constraints 3 in -> 3 out: claude.search, codex.search, ripgrep
reach 3 in -> 1 out: ripgrep
dropped claude.search: no attached runner serves it
dropped codex.search: no attached runner serves it
health 1 in -> 1 out: ripgrep
choice 1 in -> 1 out: ripgrepRead the stage a name was dropped at, not just that it was dropped. reach
means nobody wired it up — a settings question, and no reason to touch the
provider. constraints means it was wired but does not fit this repository,
which is usually a missing index and never an outage. health means it was
wired, it fit, and it did not answer, which is the only one of the three worth
paging anybody over.
Asking the same repository for symbol.overview puts the first two verdicts on
one screen, and shows that three declared implementations can still produce zero
answers:
atenea select symbol.overview --repo currentcapability symbol.overview
repository current
funnel
constraints 3 in -> 1 out: tokensave.overview
dropped kivgraph.overview: needs an index from provider kivgraph, repository has none -- atenea detect looks for one; the provider must be indexed externally
dropped serena.overview: needs an index from provider serena, repository has none -- atenea detect looks for one; the provider must be indexed externally
reach 1 in -> 0 out: -
dropped tokensave.overview: no attached runner serves it
atenea: unavailable: no attached runner serves any implementation of symbol.overviewNothing here is Serena’s fault, or kivgraph’s, or tokensave’s. Two providers
hold no index for this repository and the third was never attached, so the
funnel empties before it reaches health and no provider is asked a single
question. A report written from the failure alone would still name one of the
three.
health is narrower than “it failed”, and deliberately so. Four bins never
reach the record at all: not_found, permission_denied, invalid_input and
canceled are facts about the request, not evidence about who was asked, so a
run of them neither condemns a provider nor exonerates one — the streak stays
exactly where the last real attempt left it. Only a provider’s own failures,
three of a kind inside five minutes, mark it down.
This is the same wrong-culprit trap as the resolver above, one layer in. A
sweep of thirty-four TypeScript files hit three generated ones that no graph
contains; each returned an honest not_found, three in a row read as an
outage, and Atenea marked every implementation of symbol.overview down for
the whole repository. Every real file after that point failed, and the report
named the provider — which had answered correctly every single time.
Then check the seam itself, from outside Atenea:
ss -lntp | grep :PORT # what address is the proxy actually bound to?
getent ahosts localhost # what does the name resolve to, in what order?If the first says 127.0.0.1 and the second lists ::1 first, you have found
it, and you have found it for every server on that host — not just the one in
the report.
What not to do about it#
Editing the client configuration by hand is the obvious fix and it does not last. Tooling that registers MCP servers rewrites those entries, so a hand-edit survives until the next time a workload starts and then quietly reverts. The symptom returns weeks later looking like a new bug.
The fix belongs wherever the registration happens, so that the address is written correctly every time rather than corrected after the fact.
And resist the tempting one-liner: do not add ::1 localhost to /etc/hosts
to “make both work”. It changes the answer for every program on the machine to
solve a problem in three configuration files, and the next thing that breaks
will have nothing to do with MCP.
The general lesson#
Three things worth carrying forward:
- A fault reports under the name of whatever had no fallback. Before trusting the name in a bug report, count the implementations behind each capability that is failing and each one that is not. The pattern of what still works locates the fault far better than the failure itself.
- Bind addresses and configured names are two different facts. They agree until something outside your project decides otherwise, and the failure arrives with no change on your side to point at.
- A health check nobody reads is not a health check. The invariant that caught this had been failing every fifteen minutes, on a timer, for as long as anyone cared to look back. The information existed the whole time. What was missing was somebody reading it.