feat(agents): publish the OpenAPI spec and make agent surfaces recoverable - #4067
feat(agents): publish the OpenAPI spec and make agent surfaces recoverable#4067revmag wants to merge 6 commits into
Conversation
…rable Gaps found by the is-agentic.com scan of signoz.io. All four are website-side fixes; OAuth scopes and unauthenticated MCP resources remain product-side. - Publish the spec we already fetch for /api-reference at the conventional /openapi.json and /openapi.yaml, stamping the empty upstream info.version with the release tag it was fetched from. - Answer markdown 404s with a short markdown body linking llms.txt, the docs index, both sitemaps, and the spec, instead of nine bytes of text/plain. - Add /api-reference.md, built from the spec (auth, base URL, and every operation grouped by tag) since the interactive viewer converts poorly. Markdown requests for /api-reference now serve it. - Give the MCP discovery document display metadata and an icon, and serve a server card at /.well-known/mcp/server-card.json. - llms.txt gains the `>` summary blockquote its own test already asserted, plus an API section linking the new endpoints. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @revmag's task in 2m 58s —— View job Claude PR ReviewReviewed with the Code findingsLow
Nits (non-blocking, no inline)
Verified good
Not a blocker — the two Low items are worth a quick look before merge.
|
Resolves conflicts with #4094, which landed on main and touched the same llms.txt surfaces as this branch. Both branches independently fixed the llmstxt.org H1 blockquote and both added an API section, so the resolution merges them rather than picking a side: - app/(site)/llms.txt/route.ts — one `## API and access control` section. #4094 contributed the heading, the SigNoz-Api-Key intro line and the four IAM/access-control docs (which come from LLM_STARTER_ROUTE_MATCHERS, so their labels and descriptions stay in sync with the docs index). This branch contributes /api-reference.md and /openapi.json + /openapi.yaml. The interim /api/api-reference-openapi/latest/ link becomes /openapi.json now that this branch publishes it — the follow-up #4094 explicitly flagged. - tests/llms-txt.test.js — section list keeps "API and access control"; the spec assertion moves from the interim URL to /openapi.json, plus a new assertion for /api-reference.md. The trailing-slash allowlist already accepted .json/.yaml on this branch and auto-merged. Verification: 116 tests pass across llms-txt, agent-discovery, proxy, openapi-markdown, mcp-discovery, agent-markdown-routing, docs-markdown-routing, agent-response-headers; check:stale-urls passes; lint 0 errors; yarn build succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @revmag's task in 2m 39s —— View job Claude PR ReviewReviewed with the Code findingsLow
Nits (non-blocking)
Reviewer-decision items (raised in the PR body)
Verified good
Not a blocker — the two Low items are quick consistency cleanups worth a look before merge. |
| export type LatestOpenAPISpec = { | ||
| version: string | ||
| /** Raw YAML exactly as published in the SigNoz release. */ | ||
| yaml: string |
There was a problem hiding this comment.
Unused yaml field — Low
LatestOpenAPISpec.yaml is populated at line 51 but never read: /openapi.yaml serializes spec.document via stringify(...) (correctly, so the stamped info.version is included), and no other consumer reads spec.yaml (confirmed repo-wide). Serving the raw YAML would re-introduce the empty info.version the stamping exists to fix, so this field is intentionally dead — drop it from the type and the return object, or add a one-liner explaining why it's retained, since the "raw YAML exactly as published" doc comment implies it's the served representation. (Flagged in the prior automated review and still present.)
| return new NextResponse(JSON.stringify(spec.document), { | ||
| headers: { | ||
| 'Content-Type': 'application/json; charset=utf-8', | ||
| 'Cache-Control': `public, s-maxage=${API_SPEC_REVALIDATE_SECONDS}, stale-while-revalidate=86400`, | ||
| 'X-SigNoz-API-Version': spec.version, | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Raw NextResponse diverges from the shared agent-response pattern — Low
Both /openapi.json and /openapi.yaml build a raw NextResponse instead of going through agentResponse, so they diverge from every other agent endpoint in two ways:
- No
X-Robots-Tag: noindex. As you flagged in the SEO section, a 534 KB JSON / 768 KB YAML is indexable as written; agents reach them viallms.txt, not search. Recommend addingX-Robots-Tag: noindexto both for consistency withagentResponse. Cache-Controlcarriess-maxage/stale-while-revalidate. Per the comment inutils/agentResponseHeaders.ts, Vercel strips those directives fromCache-Controlbefore it reaches the client, leaving a barepublic— which is exactly why the shared helper splits edge policy intoVercel-CDN-Cache-Control. These two routes reintroduce that split-brain. Consider mirroring the helper (or emittingVercel-CDN-Cache-Controlalongside a client-facingCache-Control).
Neither is a correctness bug — just consistency with the pattern the rest of the agent surfaces already use.
Pull Request
📄 Summary
Agents reach
/api-referencemore than any other non-docs surface on signoz.io — 5,190 requests in the last 30 days, includingclaude-code/2.1.x(~360),ChatGPT-User(372),PerplexityBot(42),GPTBot(37) andOAI-SearchBot(29). It is also the one page with no markdown representation:/api-referenceis onPAGE_MARKDOWN_EXCLUDED_PREFIXES, so.mdandAccept: text/markdownboth fall through to the interactive viewer's HTML — 840 words of visible text once tags are stripped. The page where an agent most needs structured data is the page that gives it the least.This PR closes that, and the related discovery gaps:
Publish the OpenAPI spec. We already fetch a full OpenAPI 3.0.3 document (211 operations, 142 paths, security schemes) to render
/api-reference, but only at/api/api-reference-openapi/<version>as YAML — referenced 0 times inllms.txt,llms-full.txt,docs/sitemap.mdorrobots.txt. It is now also served at the conventional/openapi.jsonand/openapi.yaml. The upstream spec ships an emptyinfo.version, which reads as malformed to validators, so it is stamped with the release tag it was fetched from./api-reference.md. A dedicated route built from the spec — auth schemes, base URL, and every operation grouped by the spec's own tags. Built from the spec rather than run through the generic HTML→markdown pipeline, because the viewer's DOM carries almost no text.Accept: text/markdownon/api-referencerewrites to it.Markdown 404s. The four markdown endpoints answered a miss with nine bytes of
text/plainNot Found. A sharedagentNotFoundResponse()now returns 404 with a short markdown body naming the missing path and linkingllms.txt, the docs index, both sitemaps and the spec. Status stays 404 — this changes the body, not the code, so it does not create soft-404s.MCP registry branding. Adds
displayName,icon,icons,websiteUrl, and a server card at/.well-known/mcp/server-card.json.llms.txt. Emits the
>summary blockquote its own test already asserted (pre-existing red test), plus an## APIsection linking the three new endpoints.Issues closed by this PR
Part of the Agent Score epic (SigNoz/growth-pod#1170).
✅ Change Type
Select all that apply
🧪 Testing Strategy
Tests added/updated: 16 new tests across
openapi-markdown,mcp-discovery,agent-response-headers,llms-txt,proxy. The two proxy tests that encoded the old/api-referencepassthrough are updated. Full suite: 285/285 node, 83/83 vitest,yarn lintclean,yarn check:stale-urls+yarn test:stale-urlspass,yarn buildgreen with the new routes prerendered at 1d revalidate.Manual verification — run against the preview deployment
https://signoz-web-git-feat-agent-openapi-discovery-signoz.vercel.app:curl -sI $P/openapi.jsonapplication/json, 534,591 B,x-signoz-api-version: v0.139.0curl -sI $P/openapi.yamlapplication/yaml, 768,492 B, same version headercurl -sI $P/api-reference.mdtext/markdown, 19,685 Bcurl -sI $P/.well-known/mcp/server-card.jsonapplication/json, 2,011 Bcurl -sIL -H 'Accept: text/html' $P/api-referencetext/html✅curl -sIL -H 'Accept: text/markdown' $P/api-referencetext/markdown✅curl -sIL -A '…Googlebot/2.1…' $P/api-referencetext/html✅public, s-maxage=86400, stale-while-revalidate=86400curl -s -H 'Accept: text/markdown' $P/nonexistenttext/markdownwith the recovery link listReproduce with
P=https://signoz-web-git-feat-agent-openapi-discovery-signoz.vercel.app.Edge cases covered: trailing-slash variants; per-release specs at
/api-reference/<version>unchanged;POSTnever rewritten; self-fetch recursion guard; nonexistent slugs;/llms.txt.md.Blast radius
/openapi.json,/openapi.yaml,/api-reference.md,/.well-known/mcp/server-card.jsonare new paths that previously 404'd. Nothing that worked before changes./api-referencewithAccept: text/markdownnow returns markdown instead of HTML. No browser and no search crawler sends that header (verified in test 7 — Googlebot gets HTML). Everything else about/api-referenceis byte-identical, canonical included.agentNotFoundResponse()changes the 404 body on/api/docs-markdown,/api/content-markdown,/api/page-markdownand/api-reference-openapi. Status code and cache semantics unchanged.data/docs/**.Potential regressions
Vary: Accept./openapi.yamlis 768 KB. Served at 1d revalidate + SWR, so it is a cache fill, not a per-request cost.getLatestOpenAPISpec()returning null yieldsagentNotFoundResponse(404) rather than a 500.export const revalidatemust be a literal — Next rejectsAPI_SPEC_REVALIDATE_SECONDS, so the routes use86400with a comment, matchingapp/(site)/api-reference/page.tsx.Rollback plan: revert. Every new path returns to 404 and
/api-referencestops negotiating; nothing else is affected.🔍 SEO Considerations
Verified on the preview, with production compared as the control:
sitemap.xmlblogs808,docs2,929,products22,alternatives12,corporate10 = 3,781 URLs)/api-referencehttps://signoz.io/api-reference/— identical to production. The.mdtwin does not alter itnoindex/api-reference.mdusesagentResponse, which setsX-Robots-Tag: noindex(confirmed on prod/docs/introduction.md) — no duplicate-content risk against/api-referenceTwo items for reviewer decision:
1.
/openapi.jsonand/openapi.yamlare notnoindex. They build a rawNextResponseinstead of going throughagentResponse, so unlike every other agent endpoint they carry noX-Robots-Tag. (Thenoindexvisible on the preview is Vercel's blanket preview header — confirmed, preview/and/docs/introduction/carry it too while production does not. It is an environment artifact, not route behaviour.) A 534 KB JSON and 768 KB YAML are indexable as written. Agents find them viallms.txt, not search, so I'd addX-Robots-Tag: noindexto both for consistency — happy to do so if reviewers agree.2.
Vary: Acceptdoes not reach the client on HTML responses. Preview/api-reference/returns onlyvary: rsc, next-router-state-tree, …. The proxy appendsVary: Accept, but it does not survive on statically prerendered HTML. This is pre-existing, not introduced here — production/docs/introduction/behaves identically, and docs have negotiated since before #3948. Markdown responses do carryvary: Accept(verified on/api-reference.mdand prod/docs/introduction.md), and Vercel keys rewritten requests to different cache entries, so no poisoning has been observed. But this PR extends negotiation to a page that ranks, so it inherits the exposure. Worth a follow-up to setVary: Acceptat thenext.config.jsheaders level, where it survives prerendering — out of scope here.📝 Changelog
signoz.io/openapi.jsonandsignoz.io/openapi.yaml, and the API reference is available as markdown atsignoz.io/api-reference.mdor by sendingAccept: text/markdown.📋 Checklist
/api-reference)👀 Notes for Reviewers
Not addressed here. OAuth 2.0 / scoped permissions and unauthenticated MCP
resources/listare product-side: the API authenticates withSigNoz-Api-Key, and the OAuth 2.1 flow lives onmcp.<region>.signoz.cloud, which already serves RFC 9728 metadata.mcp.us.signoz.cloud/mcpcorrectly returns401with awww-authenticate: Bearer resource_metadata=…challenge — so the is-agentic checks "MCP resources exposed" and "MCP resource quality" are structurally unpassable without exposing customer telemetry anonymously. They should be ignored, not chased. Organization JSON-LDcontactPoint/addressis separate, pending a decision on which entity address to publish.Related PR. #4094 adds an
API and access controlsection tollms.txtpointing at the spec's current URL (/api/api-reference-openapi/latest/). No file overlap with this PR. Once this merges, that link should be switched to/openapi.json.🤖 Generated with Claude Code