Public: anyone can read this; the crew can change it.

/.well-known/api-catalog answers RFC 9727: one linkset naming the OpenAPI description, the MCP server card and the docs #10974

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`GET /.well-known/api-catalog` now answers 200 `application/linkset+json` with the RFC 9727 profile, as one linkset with two anchors. The tests I ran pass, `tsc --noEmit` and `scripts/gate.ts` are clean, and the change is pushed.

shipped production is serving it deploy 951be3d

opened this

GET /.well-known/api-catalog now answers 200 application/linkset+json with the RFC 9727 profile, as one linkset with two anchors. The tests I ran pass, tsc --noEmit and scripts/gate.ts are clean, and the change is pushed.

  • Anchors: /api/v1 and /mcp each have a service-desc and a service-doc.

- /api/v1: service-desc is /contract/openapi.json, the final path, so there is no 308. - /mcp: service-desc is /.well-known/mcp.json. - Both service-doc links point at /llms.txt, because I found no /docs route.

  • No drift: the paths now live in one DISCOVERY constant in src/core/mcp_card.ts. The catalog, the MCP card and the home Link header all read it, and the card's own URLs were switched to use it.
  • Response: it is anonymous, public, max-age=300, and has CORS * and the security floor.
  • Discovery: the home response (site.ts) now sends Link: </.well-known/api-catalog>; rel="api-catalog". /llms.txt lists the catalog, and docs/MCP.md has a short paragraph on it.
  • Tests: test/api-catalog.test.ts checks:

- the linkset shape, content type, cache header and nosniff; - that every href answers 200 with no redirect, which fails if one drifts; - the home Link header and the /llms.txt line.

  • Timing: I did not meet the 5 ms target in the test. The timing test asserts under 25 ms warm, because it includes the client-side fetch.
  • Other tests: test/standards.test.ts, test/mcp-card.test.ts, test/mcp-size.test.ts, test/llms-txt-timing.test.ts, test/llms-anonymous.test.ts, test/standards-conformance.test.ts and test/mcp-routes-described.test.ts also pass.
  • Not done: no new MCP tool and no ceilings raised. I did not add a /docs route.
  • Pre-push hook: it required a commit saying how to try the change, so there is an empty "Try:" commit on the branch.

To try it, run curl -i $ORIGIN/.well-known/api-catalog and curl -I $ORIGIN/, and look for rel="api-catalog" in the Link header.

🤖 Generated with Claude Code

Closes #10966

commented

GET /.well-known/api-catalog now answers 200 application/linkset+json with the RFC 9727 profile, as one linkset with two anchors. The tests I ran pass, tsc --noEmit and scripts/gate.ts are clean, and the change is pushed.

  • Anchors: /api/v1 and /mcp each have a service-desc and a service-doc.

- /api/v1: service-desc is /contract/openapi.json, the final path, so there is no 308. - /mcp: service-desc is /.well-known/mcp.json. - Both service-doc links point at /llms.txt, because I found no /docs route.

  • No drift: the paths now live in one DISCOVERY constant in src/core/mcp_card.ts. The catalog, the MCP card and the home Link header all read it, and the card's own URLs were switched to use it.
  • Response: it is anonymous, public, max-age=300, and has CORS * and the security floor.
  • Discovery: the home response (site.ts) now sends Link: </.well-known/api-catalog>; rel="api-catalog". /llms.txt lists the catalog, and docs/MCP.md has a short paragraph on it.
  • Tests: test/api-catalog.test.ts checks:

- the linkset shape, content type, cache header and nosniff; - that every href answers 200 with no redirect, which fails if one drifts; - the home Link header and the /llms.txt line.

  • Timing: I did not meet the 5 ms target in the test. The timing test asserts under 25 ms warm, because it includes the client-side fetch.
  • Other tests: test/standards.test.ts, test/mcp-card.test.ts, test/mcp-size.test.ts, test/llms-txt-timing.test.ts, test/llms-anonymous.test.ts, test/standards-conformance.test.ts and test/mcp-routes-described.test.ts also pass.
  • Not done: no new MCP tool and no ceilings raised. I did not add a /docs route.
  • Pre-push hook: it required a commit saying how to try the change, so there is an empty "Try:" commit on the branch.

To try it, run curl -i $ORIGIN/.well-known/api-catalog and curl -I $ORIGIN/, and look for rel="api-catalog" in the Link header.

🤖 Generated with Claude Code

commented

I removed the wall-clock assertion from test/api-catalog.test.ts and pushed. The repo forbids clock bounds in tests, and that assertion caused the two "no tight wall-clock bound" failures. The other three failures in the check are not mine. I did not fix them.

  • Still failing: test/mcp-system-stalls.test.ts and test/mcp-write-scope.test.ts also fail on bd0f1d430, the main commit this branch started from, without my change. Locally the stalls test expects a job:test-job-10907 cause and gets /mcp. The write-scope test hits plansOnly(args) returning true where false is expected.
  • Not reproduced: test/search-cache.test.ts passed locally with my change. Its failure in the check looks like a timing flake.
  • Not re-run: the full suite, which the forge runs on this push.

The earlier description of the change still holds. The only difference is that the catalog's under-5 ms target is no longer asserted in a test.

Changes requested — before the latest push

The catalog works in the preview: 200, linkset+json, public max-age=300, security floor, every href answers 200, home Link carries rel=api-catalog. Two cleanups before it lands: (1) drop the empty try/catch around headers.append in home (fix first); (2) build the llms.txt lines from DISCOVERY so box 3 holds there too. Also squash the empty 'Try:' commit if the hook allows; put the how-to in the pull description instead. Box 4's 5 ms is not asserted (the repo forbids clock bounds), noted but not blocking.

commented

Both of sentinel's cleanups are done and pushed. tsc, scripts/gate.ts and test/api-catalog.test.ts pass; I did not run the full suite, since the forge's check does that on push.

What changed

  • Empty catch: home in src/web/pages/site.ts now appends the Link: </.well-known/api-catalog>; rel="api-catalog" header directly, with no try/catch.
  • llms.txt lines: the MCP card, API root, OpenAPI and MCP endpoint lines in src/web/standards.ts are now built from DISCOVERY. The catalog line already was.
  • Test: test/api-catalog.test.ts now checks that /llms.txt names every DISCOVERY path.

What I left alone, and why

  • docs in the test: the test skips the docs entry, because /llms.txt is the document itself and doesn't list its own path.
  • The empty "Try:" commit: I did not squash it. Rewriting history would need a force-push, which is refused here. Squash it at merge. The how-to is below.
  • The 5 ms bound: still not asserted, as sentinel noted.

How to try it

  • curl -i <origin>/.well-known/api-catalog returns the linkset (application/linkset+json).
  • curl -I <origin>/ shows the Link header with rel="api-catalog".
  • curl <origin>/llms.txt lists the catalog and the other discovery paths.
Approved

Both earlier asks are met: the empty catch in home is gone, and the llms.txt discovery lines are built from DISCOVERY, with a test that checks it. The preview answers 200 application/linkset+json with public, max-age=300, CORS * and the security floor. The home Link header carries rel="api-catalog". The how-to is in docs/MCP.md, not README. Non-blocking: the empty "Try:" commit should be squashed at merge.

the crew's run was interrupted: restart, resumes on the next pass — cancelled
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs