~bigbes/sr-ht-spec: 824788ab

mcpsrv: mark /mcp uncacheable, fail closed on origin, split tool errors from faults

Three gaps between this surface and the cov/dolt pattern the siblings settled
on. The write tools are untouched, the per-tool grant scheme is untouched, and
the transport stays stateful.

The endpoint set no Cache-Control and no Vary at all. Every answer here depends
entirely on the credential the request carried and says nothing about it in its
URL, and some of them are the whole approved corpus, so a shared cache was free
to keep one and replay it to the next caller. cache.go is a local copy of
ecore/mcphttp.PrivateCache, byte for byte on the header values so the retrofit
is a delete and an import once that commit is published. It commits the headers
on Write and Flush as well as WriteHeader: the SDK answers a POST with an event
stream that never calls WriteHeader, so a wrapper hooking only that one sets
nothing on the response an agent actually gets, while passing every other header
test. Measured — with only WriteHeader hooked the streamed answer leaves with
the SDK's own `no-cache, no-transform` and no Vary.

Vary names both planes, where dolt.sr.ht names Authorization alone. dolt is
right for dolt: its /mcp is bearer-only, so naming Cookie would promise a cache
a dependency the surface never reads. It is wrong here. authn.Resolver.Resolve
prefers a bearer token when one is present but falls through to
login.UsernameFromRequest when none is, and an owner cookie resolves to
KindOwner — which is exactly what Gate admits. On this service the cookie is
the difference between the whole corpus and a 401. This is the one string a
future mcphttp retrofit has to reconcile between the two services.

An origin with no host in it was a warning and then an unguarded endpoint. The
Host allowlist is the only thing protecting /mcp once the SDK's own rebinding
guard is disabled, so that path turned one unparseable config value into a
silently open endpoint indistinguishable in every functional test from a
correctly guarded one. It is a construction error now. The daemon cannot reach
it either way: service.Config.Validate already refuses to start unless the
origin parses and carries a host.

Errors from below travelled to the agent as tool results carrying their own
text, so a dead git object store and a missing document were the same kind of
answer. A tool result means "the call was understood and the thing you asked
for is not there", so an agent reading one for a store outage concludes the
document does not exist and rewrites a specification around a document that is
perfectly real — and the store's own words reached it. errors.go splits the
two: service.ErrNotFound is a tool result whose sentence is built from the
call's own arguments, and everything else is a jsonrpc protocol error saying
"internal server error" with the detail logged. The old tests asserted the
behaviour being removed — that the agent was shown the words "on fire" — and
are replaced by ones that pin the split in both directions.

Eugene Blikh <bigbes@gmail.com> — 2026-08-16 08:23:31 UTC

Commit 824788ab8269bd6c58de5848bc4545450fac7aafview raw patch

Parent(s): bd5b615a

10 changed file(s)

FileStatus+
mcpsrv/cache.go A +133
mcpsrv/cache_internal_test.go A +168
mcpsrv/cache_test.go A +152
mcpsrv/errors.go A +90
mcpsrv/hostguard_test.go M +36 -21
mcpsrv/list.go M +5 -3
mcpsrv/mcpsrv.go M +41 -17
mcpsrv/mcpsrv_test.go M +62 -17
mcpsrv/read.go M +8 -6
mcpsrv/search.go M +5 -1