fix(api): four leftovers from the 49-check live-verification sweep - #10490
Conversation
- og cards send Access-Control-Allow-Origin: * to foreign origins (the
global CORSMiddleware only answers the site's own allowlist, so chat
UIs fetching the card in-page were blocked - the same failure class
the GCS bucket CORS fix removed)
- llms-full.txt lines list implementations as {language}/{library} so
the render URL template is instantiable from the file alone (the bare
library id forced a second /specs/{id} call per image)
- error responses on prerendered pages echo the public path, never the
internal /seo-proxy prefix (crawlers saw the internal routing on every
dead spec URL)
- MCP serverInfo reports the app version via FastMCP(version=...) - it
showed fastmcp's package version 3.4.5 while /health said 3.1.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR applies four small API fixes discovered during a post-deploy live-verification sweep, improving cross-origin OG image consumption, the standalone usability of llms-full.txt, the correctness of error-path reporting for prerendered routes, and MCP server version reporting.
Changes:
- Add
Access-Control-Allow-Originhandling for/og/*responses to support cross-origin in-page fetches. - Update
llms-full.txtto list implementations as{language}/{library}so render URLs can be derived without extra API calls. - Ensure JSON error bodies reflect the public (non-
/seo-proxy) path, and set MCPserverInfo.versionto the app version.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
api/main.py |
Adds OG CORS header logic in the cache-header middleware. |
api/routers/seo.py |
Changes llms-full.txt format to {language}/{library} and updates header docs accordingly. |
api/exceptions.py |
Translates /seo-proxy/* internal paths to public paths in JSON error responses. |
api/mcp/server.py |
Sets FastMCP server version to APP_VERSION for correct MCP serverInfo. |
tests/unit/api/test_seo_helpers.py |
Updates mocks and asserts for new llms-full.txt format. |
tests/unit/api/test_routers.py |
Adds/updates tests for llms-full.txt, OG CORS header, and public-path error bodies. |
tests/unit/api/mcp/test_tools.py |
Adds protocol-level test asserting MCP server version matches APP_VERSION. |
CHANGELOG.md |
Documents the live-verification follow-up fixes under Unreleased. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Copilot review on #10490: the ACAO block sat behind the method/status guard, so a cross-origin caller of a 404 og card got an opaque response and could not even read the status. Moved before the guard, with a test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (1)
api/exceptions.py:112
- _public_path() treats any path starting with "/seo-proxy" as internal. That will also rewrite unrelated paths like "/seo-proxy-foo" (if they ever exist), which would produce an incorrect public path in error JSON. Since all intended internal routes are either exactly "/seo-proxy" or start with "/seo-proxy/", make the prefix check strict to avoid accidental rewrites.
path = request.url.path
if path.startswith("/seo-proxy"):
return path.removeprefix("/seo-proxy") or "/"
return path
## Summary
- The bot renderings of `/libraries`, `/stats`, `/about`, `/legal` and
`/palette` were 29–59-word title+description stubs — a crawler following
llms.txt's own link descriptions ("the fifteen supported plotting
libraries", …) found none of the promised content (AI-access audit
2026-08-19; confirmed by the machine-files verifier in the post-deploy
sweep).
- Each page now derives its body from its single source of truth, so
none can drift: `/libraries` renders the full registry from
`core/constants.py` (grouped by language, with versions, descriptions,
doc links); `/stats` shows the live catalogue counts via the same cached
`/stats` data the startup prewarm fills (registry-derived fallback
without a DB); `/about` carries the pipeline story mirroring
`AboutPage.tsx` with registry-derived counts; `/legal` carries the
operator/privacy/hosting facts mirroring `LegalPage.tsx`; `/palette`
lists all 8 categorical hexes plus the amber/neutral/muted anchors
straight from `core/palette.py`.
## Plan
One of the three "for later" items from the AI-access verification
sweep, done on request. Independent of #10490 (different `seo.py` hunks)
and of the nginx charset PR.
## Test plan
- [x] `ruff check` + `ruff format --check` clean; `mypy api core` clean;
1,751 unit + 67 integration tests pass (new/updated tests assert the
registry list per language, the palette hexes from the module,
operator/privacy facts, the pipeline copy, and both stats bodies — live
counts with DB, registry fallback without)
- [ ] After merge + deploy: `curl -A Googlebot
https://anyplot.ai/libraries` lists all 15 libraries; `/stats` shows the
live counts; `/palette` carries `#009E73`
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary - `llms.txt` and `robots.txt` carry UTF-8 punctuation (em dashes, arrows) but were served as bare `text/plain` without a charset — a strict client decodes them as Latin-1 mojibake (found by the machine-files verifier in the 2026-08-19 live sweep; `llms-full.txt`, generated by the API, already declares `charset=utf-8`). - `charset utf-8;` in both nginx server blocks (main + python.anyplot.ai). It applies to nginx's default `charset_types`; proxied responses that already declare a charset keep theirs. - The daily bot-serving monitor now asserts the charset on `llms.txt` (timeout comment recomputed: 22 retried checks + two non-retried probes). ## Plan One of the three "for later" items from the AI-access verification sweep, done on request. Independent of #10490 and #10491. ## Test plan - [x] Config-only change following the existing block structure; no local nginx syntax check possible here (no docker) — same known verification gap as #10488, flagged per CLAUDE.md - [ ] After merge + deploy: `curl -sI https://anyplot.ai/llms.txt | grep -i content-type` → `text/plain; charset=utf-8`; same for `robots.txt`; the extended daily monitor guards it from then on --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
Access-Control-Allow-Origin: *to foreign origins — the global CORSMiddleware answers only the site's own allowlist, so chat UIs fetching a card in-page were blocked (the same in-page-fetch failure class the GCS bucket CORS fix removed).setdefaultkeeps CORSMiddleware's own header for allowlisted origins.llms-full.txtlines list implementations as{language}/{library}so the GCS render URL template is instantiable from the file alone; the bare library id forced a second/specs/{id}call per image./seo-proxy/{slug}routing prefix in the JSON body.serverInforeports the app version viaFastMCP(version=APP_VERSION)— it showed fastmcp's package version 3.4.5 while/healthsaid 3.1.0.Plan
Follow-ups found by the post-deploy live-verification sweep (49 checks across crawler UAs, machine files, REST, images/CORS, JSON-LD, MCP; zero failures on everything already deployed). This commit was originally pushed to the #10489 branch but raced its squash-merge — recovered per the repo's merge-race procedure by cherry-picking onto fresh
main; the recreated stale branch was deleted.Test plan
ruff check+ruff format --checkclean;mypy api coreclean; 1,750 unit + 67 integration tests pass (new tests: og ACAO for a foreign Origin, public path in 404 bodies, llms-full{language}/{library}format, MCP version == APP_VERSION)curl -sI -H "Origin: https://chat.openai.com" https://api.anyplot.ai/og/home.png | grep -i access-control-allow-origin→*;curl https://anyplot.ai/llms-full.txt | headshows{language}/{library}entries; MCPinitializeserverInfo.version == 3.1.0