Skip to content

fix(api): open the machine interface AI agents were locked out of - #10487

Merged
MarkusNeusinger merged 3 commits into
mainfrom
fix/ai-access-api
Aug 19, 2026
Merged

fix(api): open the machine interface AI agents were locked out of#10487
MarkusNeusinger merged 3 commits into
mainfrom
fix/ai-access-api

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

Summary

  • Opens the read API to robots-compliant agents: api.anyplot.ai/robots.txt no longer serves a blanket Disallow: / — only /debug and /proxy stay excluded. The old policy forbade the REST endpoints, openapi.json and the MCP transport on the very host llms.txt advertises as the machine interface (AI-access audit 2026-08-19).
  • Fixes the language trap: /specs/{id}/{library}/code resolved language=python by 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, new LIBRARY_LANGUAGES); an explicit ?language= still wins. The equivalent MCP-side fix lands in the upcoming dedicated MCP PR.
  • Enriches the machine-facing pages: implementation JSON-LD now carries the real render as an ImageObject (not the 1200×630 og card) plus license/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:image dims/alt, twitter:site/creator).
  • Adds 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 for anyplot.ai/llms-full.txt follows in the app-side PR — merge this one first.
  • Answers HEAD like GET via an ASGI middleware (was: 405 on every route, including /health and 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 a servers block.

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 --check clean; mypy api core clean
  • 1,677 unit + 67 integration tests pass (new tests: robots policy, language resolution incl. override + unknown-library fallback, HEAD semantics, llms-full builder/endpoint, JSON-LD ImageObject/license, hub render + fallback, robots-meta content)
  • Live smoke against a local server on the shared DB: /health reports 3.1.0, HEAD /health 200, new robots.txt served, /llms-full.txt lists all 324 specs, /specs/acf-pacf/{ggplot2,makie}/code 200 without ?language=, hub page shows the seaborn render, impl JSON-LD carries ImageObject + MIT license
  • After merge: Cloud Build deploy green, then re-verify the same URLs against api.anyplot.ai

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>
Copilot AI lite review requested due to automatic review settings August 19, 2026 18:26
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.txt for the read API while keeping /debug and /proxy excluded, and added a bot-friendly /llms-full.txt whole-catalog index endpoint.
  • Fixed /specs/{id}/{library}/code to resolve language from the library registry when the query parameter is omitted (while preserving explicit ?language= behavior).
  • Unified API version identity across /health and openapi.json, added explicit servers to the OpenAPI surface, and added middleware so HEAD behaves like GET (without adding HEAD ops 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.

Comment thread api/routers/seo.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 keywords list 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

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.13483% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
api/routers/seo.py 91.80% 5 Missing ⚠️
api/version.py 60.00% 2 Missing ⚠️

📢 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>
Copilot AI review requested due to automatic review settings August 19, 2026 18:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

@MarkusNeusinger
MarkusNeusinger merged commit 764b6fc into main Aug 19, 2026
10 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the fix/ai-access-api branch August 19, 2026 18:53
MarkusNeusinger added a commit that referenced this pull request Aug 19, 2026
#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>
MarkusNeusinger added a commit that referenced this pull request Aug 19, 2026
## 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>
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.

2 participants