fix(mcp): make every tool's output true for the whole catalogue - #10489
Conversation
Targeted MCP pass from the 2026-08-19 AI-access audit:
- get_implementation resolves the language from the library's own DB row;
the repository's language_id='python' default made all 1,004 R/Julia/JS
implementations (28% of the catalogue) answer a false 'not found'
- website_url in list_specs / search_specs_by_tags / get_spec_detail
pointed at anyplot.ai/python/{spec}, which 301s into a 404; now the hub
- get_spec_detail attaches per-implementation website_urls AFTER
model_dump - SpecDetailResponse coercion silently dropped them before -
and gains an optional 'libraries' filter so a 15-library spec's ~0.5 MB
response can be trimmed to the libraries actually asked for
- stateless MCP HTTP is now real: passed explicitly to http_app();
the former os.environ.setdefault ran after fastmcp's Settings were
instantiated and never engaged, leaving sessions instance-pinned
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CORS policy applied 2026-08-19 (AI-access audit) is bucket metadata, not code — without this runbook entry a bucket rebuild would silently drop it and cross-origin image embedding would break again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The page whose entire audience is AI agents rendered as title plus a one-line description; it now carries the endpoint URL, a setup snippet, the six tools and fallback pointers (JSON API, llms-full.txt). Includes the changelog entries for the whole MCP pass. 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 fixes MCP correctness and discoverability across the full anyplot catalogue by (1) resolving non-Python implementations correctly, (2) repairing broken website_url outputs, and (3) making FastMCP stateless HTTP actually take effect for horizontal scaling. It also adds a libraries filter to reduce get_spec_detail payload size, improves the bot-rendered /mcp page content, and documents the anyplot-images bucket CORS policy.
Changes:
- Fix MCP tool outputs: correct language resolution for
get_implementation, correct hubwebsite_urlfor discovery tools, and preserve per-implementation URLs inget_spec_detail. - Enable stateless MCP HTTP mode by explicitly passing
stateless_http=Truetohttp_app(). - Improve agent-facing documentation and safety: add
librariesfiltering, enrich/seo-proxy/mcpbot HTML, and document GCS CORS policy + reapply steps.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
api/mcp/server.py |
Fixes MCP tool correctness (language resolution, URLs), adds libraries filter, preserves per-implementation URLs post-Pydantic coercion. |
api/main.py |
Enables stateless MCP HTTP mode via explicit http_app(..., stateless_http=True). |
api/routers/seo.py |
Adds a real bot-visible /mcp body with endpoint, setup snippet, and tool list. |
tests/unit/api/mcp/test_tools.py |
Adds/updates unit coverage for hub URLs, per-implementation URLs, libraries filter, and non-Python language resolution. |
tests/unit/api/test_routers.py |
Adds bot-page test ensuring /seo-proxy/mcp includes connection instructions and tool names. |
agentic/docs/project-guide.md |
Documents the anyplot-images bucket CORS config and re-apply procedure. |
CHANGELOG.md |
Records the MCP correctness fixes, stateless HTTP fix, /mcp bot-page improvements, and CORS documentation 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! |
… semantics Copilot review on #10489: the /mcp bot-page test now asserts every documented tool name, and the get_spec_detail docstring states that an empty libraries list means 'all' — agents commonly send [] for 'no filter', and answering it with zero implementations would read as a missing spec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (2)
tests/unit/api/mcp/test_tools.py:283
- The docstring says the response is “multi-megabyte”, but the surrounding code/comments and CHANGELOG describe this payload as ~0.5 MB. Keeping the test description accurate will avoid overstating the size and confusing future readers.
"""The optional libraries filter trims a multi-megabyte response to what's asked for."""
api/routers/seo.py:928
- The
claude mcp addsnippet here uses a different argument order than the human/mcppage (app/src/pages/McpPage.tsx:147). For bots/agents copying this, it’s safer to match the documented CLI form (claude mcp add anyplot --transport http …).
"<pre><code>claude mcp add --transport http anyplot https://api.anyplot.ai/mcp/</code></pre>"
|
Added one commit from the post-deploy live-verification sweep (49 checks across crawler UAs, machine files, REST, images/CORS, JSON-LD, MCP — zero failures on everything #10487/#10488 deployed): og-card CORS for foreign origins, llms-full.txt lines as |
|
Correction: the verification-sweep commit mentioned above raced this PR's squash-merge and never landed here — it ships as #10490 (recovered by cherry-picking onto the merged main; the stale recreated branch was deleted). |
…10490) ## Summary - og cards now send `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). `setdefault` keeps CORSMiddleware's own header for allowlisted origins. - `llms-full.txt` lines 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. - Error responses on prerendered pages echo the public path — crawlers hitting a dead spec URL were shown the internal `/seo-proxy/{slug}` routing prefix in the JSON body. - MCP `serverInfo` reports the app version via `FastMCP(version=APP_VERSION)` — it showed fastmcp's package version 3.4.5 while `/health` said 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 - [x] `ruff check` + `ruff format --check` clean; `mypy api core` clean; 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) - [ ] After merge + deploy: `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 | head` shows `{language}/{library}` entries; MCP `initialize` serverInfo.version == 3.1.0 --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
get_implementationfor the non-Python 28% of the catalogue: the repository call carried no language, and itspythondefault 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).website_url:list_specs,search_specs_by_tagsandget_spec_detailpointed atanyplot.ai/python/{spec}, which 301s into a 404; all now point at the hub.get_spec_detail's per-implementation URLs — silently discarded bySpecDetailResponsecoercion — are attached aftermodel_dumpand survive.stateless_http=Trueis passed explicitly tohttp_app(). The formeros.environ.setdefault("FASTMCP_STATELESS_HTTP", "true")ran after fastmcp'sSettingswere 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.librariesfilter toget_spec_detail— the full 15-library response carries ~0.5 MB of source, a context-window hazard for the agents the tool serves./mcppage its actual content (endpoint URL,claude mcp addsnippet, the six tools, JSON-API/llms-full.txt fallbacks) — it rendered as title + one line for the exact audience it exists for.anyplot-imagesbucket 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
ruff check+ruff format --checkclean;mypy api corecleanwebsite_urlcorrectness on all three tools, per-implementation URLs surviving the Pydantic roundtrip, thelibrariesfilter, and the/mcpbot-page contentsettings.stateless_httpstaysFalseunder the old env-setdefault route andhttp_appacceptsstateless_httpexplicitlyinitialize+tools/call get_implementation {spec_id: "audiogram-clinical", library: "ggplot2"}returns code (was "not found"); a request withoutmcp-session-idis accepted (stateless);curl -A GPTBot https://anyplot.ai/mcpshows the tool list