fix(api): open the machine interface AI agents were locked out of - #10487
Conversation
The 2026-08-19 AI-access audit found the AI-facing infrastructure largely
exists but hides itself from the clients it was built for:
- robots.txt on api.anyplot.ai disallowed the whole read API + MCP
- /specs/{id}/{lib}/code 404'd for all non-Python libraries (language
defaulted to python; now resolved from the library registry)
- JSON-LD declared the og branding card as the image; now the real render
as ImageObject, plus license/codeRepository/dateModified/keywords
- spec-hub bot pages showed no real render; now best-quality render + assets
- HEAD answered 405 everywhere; ASGI middleware answers it like GET
- second-tier meta: robots max-image-preview:large, og:locale/image dims,
twitter:site/creator
- new GET /llms-full.txt whole-catalogue index (llmstxt.org convention)
- version identity unified: openapi/health report the packaged version,
registry-derived description, servers block in openapi.json
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 improves the “machine interface” of the anyplot API by making the read surface reachable and consistent for bots/agents, and by enriching bot-facing HTML/structured data so assistants can reliably discover code, renders, and metadata.
Changes:
- Opened
robots.txtfor the read API while keeping/debugand/proxyexcluded, and added a bot-friendly/llms-full.txtwhole-catalog index endpoint. - Fixed
/specs/{id}/{library}/codeto resolvelanguagefrom the library registry when the query parameter is omitted (while preserving explicit?language=behavior). - Unified API version identity across
/healthandopenapi.json, added explicitserversto the OpenAPI surface, and added middleware soHEADbehaves likeGET(without addingHEADops to OpenAPI). Also expanded SEO/bot meta + JSON-LD to prefer real renders.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/api/test_seo_helpers.py | Adds/updates unit coverage for robots meta, hub render selection/fallback, JSON-LD enrichment, and llms-full builder. |
| tests/unit/api/test_routers.py | Updates router tests for robots policy, llms-full endpoint behavior, language resolution for code endpoint, and HEAD semantics. |
| tests/unit/api/test_main.py | Aligns version/title/description/openapi server assertions with the new version source + registry-derived description. |
| docs/reference/seo.md | Updates SEO reference to reflect new og-image defaults, branded OG endpoint shape, and robots policy text. |
| docs/reference/api.md | Documents language resolution behavior for code endpoint and adds /llms-full.txt documentation. |
| core/constants.py | Adds LIBRARY_LANGUAGES registry map to resolve language from a library id. |
| CHANGELOG.md | Adds unreleased entries covering llms-full, robots opening, language resolution fix, HEAD middleware, structured data upgrades, and version unification. |
| api/version.py | Introduces a single source of truth (APP_VERSION) derived from installed package metadata. |
| api/routers/specs.py | Resolves language from LIBRARY_LANGUAGES when absent for /specs/{id}/{library}/code. |
| api/routers/seo.py | Enhances bot HTML meta tags, hub page render selection, implementation JSON-LD (ImageObject/license/etc.), and adds llms-full builder + endpoint. |
| api/routers/health.py | Switches / and /health to report APP_VERSION instead of a hardcoded version. |
| api/main.py | Unifies FastAPI identity (version/description/servers) and adds HeadAsGetMiddleware while preserving MCP trailing-slash behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (1)
api/routers/seo.py:596
- _spec_keywords() currently iterates (spec.tags or {}).values(), but Spec.tags is stored as JSONB (key order is not guaranteed). That makes the JSON-LD
keywordslist order dependent on DB serialization rather than a deliberate, stable ordering. Consider iterating a defined tag-key order (and optionally appending unknown keys) and deduplicating to keep the structured-data output deterministic.
def _spec_keywords(spec) -> list[str]:
"""Flatten the spec's tag bag ({plot_type, data_type, domain, features}) into a keyword list."""
keywords: list[str] = []
for values in (spec.tags or {}).values():
if isinstance(values, str):
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Copilot review on #10487: keyword order followed the stored JSON's insertion order, making the JSON-LD non-deterministic across sources/roundtrips. Categories now walk in canonical order (plot_type, data_type, domain, features, then stragglers alphabetically) with order-preserving dedup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#10488) ## Summary - Broadens the nginx crawler UA map: `~*grok` now catches the bare `Grok` token xAI's fetcher was seen sending (it fell through to the empty SPA shell — the exact "insufficient relevant content" a real Grok session reported), and adds the documented `Amazonbot` and `meta-externalagent` tokens that the existing `amzn-*` / `meta-externalfetcher` entries never matched, plus best-effort `Diffbot` / `Firecrawl`. - Rewrites `llms.txt` for machine consumers: a REST API section (base URL, retrieval endpoints, OpenAPI link), the GCS render URL pattern (themes, responsive widths, WebP), CORS-open GitHub raw source URLs, a worked three-step "fetch one plot" recipe, and an honest note that prerendered page HTML is UA-gated while these URLs work for every client. - Serves `anyplot.ai/llms-full.txt` by proxying the API's generated whole-catalogue index (endpoint shipped in #10487) — previously the URL was a soft-404 answering 200 with the homepage shell. - Adds a `<noscript>` fallback to the SPA shell so JS-less clients not on the UA allowlist see pointers to `llms.txt`, `llms-full.txt`, the JSON API and GitHub instead of a completely empty page. - Extends the daily bot-serving monitor with the new UA tokens and an `llms-full.txt` assertion (timeout recomputed for 23 checks). ## Plan App-side half of the fix order from the 2026-08-19 AI-access audit (steps 4 and 5); the api-side half is #10487. **Merge order: #10487 first, then this PR.** The `llms-full.txt` nginx location proxies to the API endpoint that #10487 adds — merging this first would 404 that one path until the API deploys (everything else in this PR is independent). ## Test plan - [x] `yarn lint`, `yarn fm:check`, `yarn type-check`, `yarn test`, `yarn build` all green; `noscript` block verified present in `dist/index.html` - [x] nginx change follows the existing `location = /llms.txt` + `@seo_proxy` patterns verbatim (proxy headers, TLS verify depth 4); no local nginx syntax check possible in this environment (no docker) — flagging per the known Cloud-Build-deploy verification gap - [ ] After deploy: `curl -A "Grok/1.0" https://<origin>/scatter-basic` returns the prerendered title; `curl https://anyplot.ai/llms-full.txt` returns the catalogue index; `curl -A "Amazonbot/0.1" …` prerendered — the extended bot-serving-check covers all three daily --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
- Fixes MCP `get_implementation` for the non-Python 28% of the
catalogue: the repository call carried no language, and its `python`
default made all 1,004 R/Julia/JavaScript implementations answer a false
"not found". The language now comes from the library's own DB row
(verified live for ggplot2/makie/d3/echarts/chartjs/muix/highcharts in
the audit).
- Fixes every discovery tool's `website_url`: `list_specs`,
`search_specs_by_tags` and `get_spec_detail` pointed at
`anyplot.ai/python/{spec}`, which 301s into a 404; all now point at the
hub. `get_spec_detail`'s per-implementation URLs — silently discarded by
`SpecDetailResponse` coercion — are attached after `model_dump` and
survive.
- Makes stateless MCP HTTP real: `stateless_http=True` is passed
explicitly to `http_app()`. The former
`os.environ.setdefault("FASTMCP_STATELESS_HTTP", "true")` ran after
fastmcp's `Settings` were instantiated and never engaged (reproduced
against fastmcp 3.4.5), leaving sessions pinned to one Cloud Run
instance with max-instances=3 and no affinity.
- Adds a `libraries` filter to `get_spec_detail` — the full 15-library
response carries ~0.5 MB of source, a context-window hazard for the
agents the tool serves.
- Gives the bot-served `/mcp` page its actual content (endpoint URL,
`claude mcp add` snippet, the six tools, JSON-API/llms-full.txt
fallbacks) — it rendered as title + one line for the exact audience it
exists for.
- Documents the `anyplot-images` bucket CORS policy (applied 2026-08-19)
in the project guide with the re-apply procedure — bucket metadata does
not deploy with the repo, so a rebuild would silently drop it.
## Plan
The targeted MCP pass from the 2026-08-19 AI-access audit — the final
step of the fix order after #10487 (api) and #10488 (app), as agreed.
Branch is rebased on the merged main.
## Test plan
- [x] `ruff check` + `ruff format --check` clean; `mypy api core` clean
- [x] 1,681 unit + 67 integration tests pass, including new tests:
language resolution from the library row (python and r), `website_url`
correctness on all three tools, per-implementation URLs surviving the
Pydantic roundtrip, the `libraries` filter, and the `/mcp` bot-page
content
- [x] fastmcp behavior verified directly: `settings.stateless_http`
stays `False` under the old env-setdefault route and `http_app` accepts
`stateless_http` explicitly
- [ ] After deploy: MCP `initialize` + `tools/call get_implementation
{spec_id: "audiogram-clinical", library: "ggplot2"}` returns code (was
"not found"); a request without `mcp-session-id` is accepted
(stateless); `curl -A GPTBot https://anyplot.ai/mcp` shows the tool list
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
api.anyplot.ai/robots.txtno longer serves a blanketDisallow: /— only/debugand/proxystay excluded. The old policy forbade the REST endpoints,openapi.jsonand the MCP transport on the very hostllms.txtadvertises as the machine interface (AI-access audit 2026-08-19)./specs/{id}/{library}/coderesolvedlanguage=pythonby default, 404ing all 7 R/Julia/JS libraries (28% of the catalogue, 1,004 implementations) at the obvious URL. The language now derives from the library registry (core/constants.py, newLIBRARY_LANGUAGES); an explicit?language=still wins. The equivalent MCP-side fix lands in the upcoming dedicated MCP PR.ImageObject(not the 1200×630 og card) pluslicense/codeRepository/dateModified/keywords/author/isBasedOn; spec-hub bot pages show the best-quality real render with the full asset list instead of the og collage; second-tier meta added (robots max-image-preview:large,og:locale,og:imagedims/alt,twitter:site/creator).GET /llms-full.txt: one line per spec (id, title, hub URL, libraries) under a header documenting the UA-independent retrieval recipes (code endpoint, GCS render URL pattern, OpenAPI, MCP). The nginx mapping foranyplot.ai/llms-full.txtfollows in the app-side PR — merge this one first./healthand the seo-proxy pages) without doubling the openapi surface, and unifies version identity:openapi.json(was 1.0.0, "9 libraries") and/health(was 0.2.0) now report the packaged version with a registry-derived description and aserversblock.Plan
Fix order approved from the 2026-08-19 AI-access audit (5-agent live audit of anyplot.ai / api.anyplot.ai / GCS): this PR is steps 2, 3 (REST half), and 6, plus the api-side half of step 4. Bucket CORS (step 1) is already applied and verified live. App-side steps (llms.txt content, nginx UA map, noscript) follow in a separate PR; the MCP overhaul is a dedicated follow-up.
Test plan
ruff check+ruff format --checkclean;mypy api coreclean/healthreports 3.1.0,HEAD /health200, new robots.txt served,/llms-full.txtlists all 324 specs,/specs/acf-pacf/{ggplot2,makie}/code200 without?language=, hub page shows the seaborn render, impl JSON-LD carries ImageObject + MIT license