Skip to content

Only serve libraries over MCP that actually publish a llms-full.txt - #555

Merged
abernier merged 5 commits into
mainfrom
fix/mcp-unserved-libraries
Aug 11, 2026
Merged

Only serve libraries over MCP that actually publish a llms-full.txt#555
abernier merged 5 commits into
mainfrom
fix/mcp-unserved-libraries

Conversation

@abernier

@abernier abernier commented Aug 11, 2026

Copy link
Copy Markdown
Member

Found while wiring https://docs.pmnd.rs/api/mcp into a Claude Code plugin: the server advertises 11 libraries and can only serve 4.

The bug

Libraries were selected by testing whether docs_url is on pmndrs.github.io, as a proxy for "built with this generator, therefore ships a /llms-full.txt". The proxy is wrong for 7 of the 11 it lets through — probed just now against production:

library ${docs_url}/llms-full.txt <page> elements
react-three-fiber 200 20
drei 200 134
zustand 200 42
docs 200 16
a11y 404 0
react-postprocessing 404 0
uikit 404 0
xr 404 0
prai 404 0
viverse 404 0
leva 404 0

Those 7 are non-functional today on both entry points — but only one of them says so:

get_page_content(lib="leva", …)  →  MCP server error: Failed to fetch llms-full.txt: Not Found
read docs://leva/index           →  ""     ← silent

get_page_content checks response.ok. The index resource did not, so the 404 body parsed to zero <page> elements and shipped an empty index. A client reads that as "this library has no documentation pages" — which is an invitation to guess paths. Every guess then fails with Page not found: /x, which points at the path rather than at the missing dump. That is a long way to walk from a 404.

The fix

  • page.tsx — an explicit llms_full?: boolean on the four libraries whose sites publish a dump. Flip it on as each of the others ships one; that is a fact maintainers know and the URL cannot tell you. No second list: it is one field on the existing libs, which stays the single source of truth.
  • route.ts — select on that flag, and check response.ok in the index resource too, so a missing dump fails loudly on both paths.

Nothing is lost: the 7 dropped libraries return errors or empty strings today.

libs was also declared as const with no annotation, so Library was decorative — llms_ful: true would have compiled and silently dropped a library from MCP. It is now as const satisfies Record<string, Library>, which keeps the literal types and makes that a type error.

On the home page

The page lists fourteen libraries and the server can serve four; nothing said which, so the only way to find out was to ask and get an empty index back. Each servable card now carries an MCP badge, read from the same llms_full flag the enum filters on — so the page and the server cannot disagree, and a library gets its badge on the commit that makes it servable.

Uses shadcn's Badge, which the repo was already set up for (components.json, the token block in globals.css, cn() in lib/utils), so it pulls in no new dependency. Checked in both colour schemes.

⚠️ This changes the home page snapshot — the Chromatic baselines will need accepting.

Manifest

docs://pmndrs/manifest was sending clients down the same road, so it is corrected in the same pass:

  • advertised docs://jotai/index and docs://valtio/index — neither is served (both are filtered out; their docs live off-pmndrs)
  • every example used zustand paths under /docs/... that 404. The real ones are /learn/... and /reference/.... Path shapes differ per library — r3f uses /api and /tutorials, drei /abstractions and /staging — so the manifest now says to read the index rather than extrapolate from another library
  • claimed local docs_urls resolve to https://docs.pmnd.rs/<lib>; the code discards the path and uses the host
  • pointed "Getting Started" at /api/sse while the homepage advertises /api/mcp

Tests

vitest.config.ts was missing the @/package.json alias that tsconfig.json has, so import('./route') threw Cannot find packagethe one test that actually imports the route has been failing on main, and passing anyway, because it only asserts toBeDefined(). Alias added; that test now genuinely runs.

Which exposed the next layer: once it ran, it went to the real pmndrs.github.io. The MSW handlers mock r3f.docs.pmnd.rs and zustand.docs.pmnd.rs — hosts the route never requests — and onUnhandledRequest was 'warn'. The suite is now hermetic:

  • handlers derived from libs, the same source of truth the route reads, so mocks cannot drift from the real docs_urls
  • onUnhandledRequest: 'error' — any future unmocked call fails instead of opening a socket
  • that test asserts on the returned page content, not toBeDefined(), which an error response satisfies just as well. Comment out the handlers and it goes red, which is the point

On top of that, Library Filtering re-implemented the old filter inline and asserted on the copy, so it would have kept passing after this change. It now asserts against the real libs export, including the 7 pmndrs.github.io libraries that must stay excluded.

New regression test: a 404 on llms-full.txt must produce an error, not an empty index.

Before (main):  Tests  1 failed | 76 passed (77)
After:          Tests  78 passed (78)

tsc --noEmit and eslint are clean.

No changeset

Per the README policy this is a site-only change — it touches the deployed API route, not the reusable workflow, build behavior or templates that consumers pin @v3 for.


🤖 Generated with Claude Code

The MCP server selected libraries by asking whether their docs_url is on
pmndrs.github.io, as a proxy for "this site is built with this generator and
therefore ships a /llms-full.txt dump". That proxy is wrong for 7 of the 11
libraries it let through: a11y, react-postprocessing, uikit, xr, prai, viverse
and leva all 404 on that file.

Both entry points were affected, but only one said so. get_page_content checks
response.ok and fails cleanly; the index resource did not, so a 404 body parsed
to zero <page> elements and shipped an empty index. To a client that reads as
"this library has no documentation pages", which is an invitation to guess
paths -- and every guess then fails with "Page not found", pointing at the path
rather than at the missing dump.

So: replace the URL heuristic with an explicit llms_full flag on the four
libraries whose sites do publish a dump, and check response.ok in the index
resource too. Nothing is lost -- those 7 libraries are non-functional today on
both the resource and the tool. Flip the flag on as each site ships its dump.

Also fix the manifest, which sent clients down the same path: it advertised
docs://jotai/index and docs://valtio/index (neither is served), and every
example used zustand paths under /docs/... that 404 -- the real ones live under
/learn and /reference. Since path shapes differ per library, the manifest now
says to read the index rather than extrapolate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hape

Unblocking the @/package.json alias woke up the one test that imports ./route,
and it went straight to the real pmndrs.github.io: the MSW handlers mock
r3f.docs.pmnd.rs and zustand.docs.pmnd.rs, hosts the route never requests, and
onUnhandledRequest was 'warn'. A dead test hid that; a live one would have paid
for it on every run.

So derive the handlers from libs -- same source of truth as the route, so the
mocks cannot drift from the real docs_urls -- and set onUnhandledRequest to
'error', which turns any future unmocked call into a failure rather than a
socket.

That alone would not have caught it, because the test asserted toBeDefined(),
which an error response satisfies as well as a real one. It now asserts on the
page content, so removing the handlers turns it red.

Separately, `libs` was declared `as const` with no annotation, so the Library
interface was decorative: llms_ful: true would have compiled and silently
dropped the library from MCP. `as const satisfies Record<string, Library>` keeps
the literal types and makes that a type error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
abernier added a commit to pmndrs/claude-code-plugin that referenced this pull request Aug 11, 2026
UNSERVED was a hand-maintained mirror of SERVED, and it only covered the seven
libraries that happened to be broken the day it was written. A library that
started working was caught; one the server newly advertised was not, and neither
was the pair silently disagreeing.

The server publishes its own universe in the get_page_content `lib` enum, so the
check now reads it, probes every entry, and asserts that the set which actually
works equals SERVED. One list to maintain, and it fails in both directions --
a library that starts working reads as loudly as one that stops.

The skill stops enumerating them too, for the same reason: it states the rule
("anything not listed above is unserved, use WebFetch") rather than seven names
that rot the day pmndrs/docs#555 lands. The offline suite pins its served list to
SERVED by set equality, so a name added to the prose alone is the same failure as
one dropped.

Moves to vitest and .mts along the way. The suites split by config rather than by
directory -- vitest globs are explicit, so the .live.mts suffix genuinely excludes
the network check, which is what `node --test` walking all of test/ would not do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The home page lists fourteen libraries and the MCP server can serve four of
them. Nothing on the page said which, so the only way to find out was to ask the
server and get an empty index back.

The badge reads from the same llms_full flag the tool enum filters on, so the
page and the server cannot disagree: a library gains its badge on the commit that
makes it servable, not on a separate one someone remembers to write.

Adds shadcn's Badge, which the repo was already configured for -- components.json,
the token block in globals.css and cn() in lib/utils were all in place, so this
pulls in no new dependency. Checked in both colour schemes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CLI served the legacy new-york badge, which is rounded-md. Current upstream
is rounded-full -- match it, so the one shadcn component in the repo does not
look like a different design system.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The badge is a label, not a control, so the pointer has no business changing it.
Scoped the hover states to [a&]: like shadcn does upstream, which keeps them for
a badge rendered as a link and drops them for this one. Verified by hovering it:
same background before and during.

Also switches the card header to items-start, so the title block sits at the top
rather than being centred against a tall icon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abernier abernier reopened this Aug 11, 2026
@abernier
abernier merged commit 2a7cbc7 into main Aug 11, 2026
9 of 10 checks passed
@abernier
abernier deleted the fix/mcp-unserved-libraries branch August 11, 2026 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant