From d7658465cdf0fe588ab8225704618d52f7226461 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:31:41 +0200 Subject: [PATCH 1/3] fix(agent-score): directive placement, markdown parity, MCP endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the three regressing checks in the Mintlify/afdocs agent-readiness audit and adds guards so they stay fixed: Content Discoverability (directive buried past 50% of the HTML): - Move the hidden llms.txt directive to the first child of in the docs root layout; the sidebar markup previously pushed it to 30-88% of the body. Now sits at 0-1% on every page. Markdown Content Parity (pages failing at 52-90% missing): - Restore heading markers in getLLMText output from the page toc — fumadocs' processed markdown emits headings as bare "Text [#anchor]" lines, so "## 1. Set up your project" degraded to a stripped list item on the markdown side. - Convert
/ to a bold summary + dedented body; the serialized 2-space indent broke the code fences inside. - Wrap the OpenAPI explorer (APIPage) in data-markdown-ignore; the interactive reference has no markdown equivalent and the .md already carries the generated API summary. - Unescape remark escapes (\_, \{, \}) outside code so prose like snake_case matches the rendered HTML. MCP Server Discoverable (no server at expected endpoints): - New /docs/mcp route proxies MCP protocol traffic to mcp.prisma.io/mcp (browser GETs redirect to the /mcp marketing page). - Header-matched beforeFiles rewrites on the site route MCP traffic on www.prisma.io/mcp to the real server; browsers still get the page. Guards: lint-agent-ready now checks directive-first-in-body, heading markers across all pages,
leakage, the APIPage parity wrapper, and both MCP endpoints. Skill docs updated with the new invariants and an afdocs reproduction playbook. Verified with the afdocs CLI against a local production build: directive + parity + markdown-url + content-negotiation all pass on the 20 pages that previously failed or warned (avg 1% missing, was avg 8%). Co-Authored-By: Claude Fable 5 --- .claude/skills/docs-agent-ready/SKILL.md | 25 ++- apps/docs/scripts/lint-agent-ready.ts | 163 +++++++++++++++--- .../app/(docs)/(default)/[[...slug]]/page.tsx | 24 +-- apps/docs/src/app/layout.tsx | 24 +++ apps/docs/src/app/mcp/route.ts | 84 +++++++++ apps/docs/src/components/api-page.tsx | 16 +- apps/docs/src/lib/get-llm-text.ts | 11 +- apps/docs/src/lib/llm-markdown.ts | 73 +++++++- apps/site/next.config.mjs | 25 +++ 9 files changed, 391 insertions(+), 54 deletions(-) create mode 100644 apps/docs/src/app/mcp/route.ts diff --git a/.claude/skills/docs-agent-ready/SKILL.md b/.claude/skills/docs-agent-ready/SKILL.md index 3700c470a0..cd1933aec5 100644 --- a/.claude/skills/docs-agent-ready/SKILL.md +++ b/.claude/skills/docs-agent-ready/SKILL.md @@ -3,7 +3,7 @@ name: docs-agent-ready description: Use when adding a new docs section or product area, editing llms.ts / the llms.txt or llms/[...slug] / llms-full.txt routes / get-llm-text / skill.md / .well-known endpoints, or working on the "agent score", "llms.txt", or anything "agent-ready" in the docs and site apps. Explains the invariants the Mintlify agent-readiness audit measures and how to hold them. metadata: author: Prisma - version: "2026.7.21" + version: "2026.7.24" --- # Docs agent-readiness @@ -15,8 +15,10 @@ Keep Prisma's docs machine-readable so the Mintlify **agent-score** audit does n - **Root `llms.txt` < 50k bytes** (warn at 35k). It links to per-area section indexes, not every page. - **Each section index < 50k bytes** (warn at 40k). Over budget means split the section. - **Every page is reachable** — each `filterPagesForLLMsIndex` page appears in a section file or the root "Other pages" list. The guard asserts against the generated content, not just membership. -- **Directives in HTML + Markdown** — every page's Markdown (`getLLMText`) starts with the hidden `llms.txt` directive blockquote; the HTML page keeps a hidden directive as its first child. -- **HTML/Markdown parity** via `data-markdown-ignore` on human-only chrome so the Markdown mirrors the page. +- **Directives in HTML + Markdown** — every page's Markdown (`getLLMText`) starts with the hidden `llms.txt` directive blockquote; the HTML keeps a hidden directive as the **first child of `` in the root layout** (`apps/docs/src/app/layout.tsx`), NOT inside the page component. Audits measure the directive's byte position in the body and warn when it sits past 50%, which is where it lands if rendered after the sidebar markup. +- **HTML/Markdown parity** via `data-markdown-ignore` on human-only chrome so the Markdown mirrors the page. The OpenAPI explorer (`APIPage` wrapper in `src/components/api-page.tsx`) carries `data-markdown-ignore` because the interactive reference has no markdown equivalent — the `.md` serves the generated API summary instead. +- **Markdown keeps real headings** — fumadocs' processed output emits headings as bare `Text [#anchor]` lines; `getLLMText` restores `##` markers from the page toc (`restoreHeadingMarkers` in `llm-markdown.ts`). Without them, parity checkers strip list-like heading text ("## 1. Set up …") and agents see prose instead of structure. +- **`
` blocks are converted** to a bold summary line + dedented body (`formatDetails` in `llm-markdown.ts`); serialized `
` children are 2-space indented, which silently breaks the code fences inside for markdown consumers. - **`llms-full.txt` excludes** legacy `/orm/v6` and the Accelerate/Optimize products (`getLLMsFullPages`). - **Skill + MCP endpoints live at BOTH roots**: `www.prisma.io` (apps/site) and `/docs` (apps/docs). @@ -32,6 +34,8 @@ Keep Prisma's docs machine-readable so the Mintlify **agent-score** audit does n | `/docs/.well-known/mcp[.json]` | `apps/docs/src/lib/mcp-discovery.ts` | | `/skill.md`, `/.well-known/agent-skills/*` | `apps/site/src/lib/agent-skills.ts` (`buildSkillMarkdown`) | | `/.well-known/mcp*` (site) | `apps/site/src/lib/agent-skills.ts` (`buildMcpDiscovery`, server cards) | +| `/docs/mcp` (MCP proxy) | `apps/docs/src/app/mcp/route.ts` → proxies protocol traffic to `mcp.prisma.io/mcp` | +| `/mcp` (site, MCP traffic) | header-matched `beforeFiles` rewrites in `apps/site/next.config.mjs` (browser GETs still get the marketing page) | The route handlers are thin wrappers: shared builders in `llms.ts` are the single source of truth, so the guard measures exactly what the routes serve. @@ -41,7 +45,7 @@ The route handlers are thin wrappers: shared builders in `llms.ts` are the singl **(b) Section over budget.** When a section fails/warns on size, split it into two sections in `llmsSections` (narrower `prefixes`, or carve a sub-tree out with a new slug). Re-run the guard. -**(c) Changing page chrome** in `apps/docs/src/app/(docs)/(default)/[[...slug]]/page.tsx`: keep the hidden `llms.txt` directive as the first child, and put `data-markdown-ignore` on any human-only chrome (banners, nav, badges) so it stays out of the Markdown. +**(c) Changing page chrome.** The hidden `llms.txt` directive lives in `apps/docs/src/app/layout.tsx` as the first child of `` — keep it there (before `` within the first 10% of the (nav/script/style-stripped) `` on sampled pages and warns when every match sits past 50%; the parity check compares HTML text segments against the `.md`, strips `data-markdown-ignore` elements from the HTML side, and only treats a fenced code block as protected when the fence starts at column 0 — which is why `
` bodies must be dedented and headings must keep their `#` markers. The separate "MCP Server Discoverable" check probes `/mcp` with an MCP initialize request (discovery documents alone do not count), which is what the `/docs/mcp` proxy route and the site `/mcp` rewrites are for. diff --git a/apps/docs/scripts/lint-agent-ready.ts b/apps/docs/scripts/lint-agent-ready.ts index a77fc4e970..5b53414a58 100644 --- a/apps/docs/scripts/lint-agent-ready.ts +++ b/apps/docs/scripts/lint-agent-ready.ts @@ -146,6 +146,8 @@ if (unmatched.length > CATCHALL_WARN) { // fast enough to cover the full set. const directiveFailures: string[] = []; const missingDescription: string[] = []; +const headingMarkerFailures: string[] = []; +const detailsLeaks: string[] = []; for (const page of indexPages) { let text: string; try { @@ -168,6 +170,27 @@ for (const page of indexPages) { if (description && !text.includes(description)) { missingDescription.push(page.url); } + + // Heading markers: the processed markdown emits headings as bare + // "Text [#anchor]" lines; getLLMText restores the `#` markers from the toc. + // If a toc anchor appears in the output, the line carrying it must be a real + // markdown heading — otherwise agents see prose and the afdocs parity check + // strips list-like heading text ("## 1. Set up …") on the markdown side. + for (const item of page.data.toc ?? []) { + if (typeof item.url !== "string" || !item.url.startsWith("#")) continue; + const anchorRef = `[${item.url}]`; + const anchorLine = lines.find((line) => line.includes(anchorRef)); + if (anchorLine !== undefined && !/^#{1,6} /.test(anchorLine)) { + headingMarkerFailures.push(`${page.url} (${item.url})`); + } + } + + //
blocks must be converted to plain markdown (formatDetails in + // llm-markdown.ts); a leaked
means its body is still 2-space + // indented, which breaks code fences for markdown consumers. + if (text.includes(" 0) { @@ -192,39 +215,88 @@ if (missingDescription.length > 0) { pass("Description in markdown", "all frontmatter descriptions present in markdown"); } +if (headingMarkerFailures.length > 0) { + fail( + "Heading markers restored", + `${headingMarkerFailures.length} toc heading(s) rendered without markdown markers (restoreHeadingMarkers in llm-markdown.ts regressed):\n ${headingMarkerFailures + .slice(0, 10) + .join("\n ")}`, + ); +} else { + pass("Heading markers restored", "all toc anchors in markdown output sit on real headings"); +} + +if (detailsLeaks.length > 0) { + fail( + "No
leakage", + `${detailsLeaks.length} page(s) leak raw
into markdown (formatDetails in llm-markdown.ts regressed):\n ${detailsLeaks + .slice(0, 10) + .join("\n ")}`, + ); +} else { + pass("No
leakage", "all
blocks converted to plain markdown"); +} + // ── Check 5b: HTML surface source guard ────────────────────────────────────── -// The rendered HTML page carries the same directive via a hidden element. -// Rendering React in this script is not worth it; instead guard at the source -// level that the docs page component emits a hidden element referencing llms.txt -// BEFORE it renders