Skip to content

Commit a97b71a

Browse files
ankur-archclaude
andcommitted
docs/site: make prisma.io agent-ready (llms.txt split, skill.md, MCP discovery)
Fixes failing checks from the Mintlify agent-score audit of www.prisma.io/docs: - Split the 116k-char llms.txt into a small root index plus per-area section indexes (each under 50k chars), with a coverage guarantee for unmatched pages - Add llms.txt directives to both HTML (visually-hidden element) and markdown (blockquote) versions of every docs page - Improve markdown/HTML parity: include page descriptions in markdown, mark HTML-only page chrome with data-markdown-ignore - Slim llms-full.txt from ~7 MB to ~4.5 MB by excluding legacy /orm/v6 and deprecated product pages; absolutize in-body links so they resolve - Serve an agentskills.io-format skill at /skill.md, /docs/skill.md and /.well-known/agent-skills/* - Serve MCP discovery documents at /.well-known/mcp, /.well-known/mcp.json and server cards, pointing at the existing https://mcp.prisma.io/mcp Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 7cb6000 commit a97b71a

18 files changed

Lines changed: 730 additions & 24 deletions

File tree

apps/docs/src/app/(docs)/(default)/[[...slug]]/page.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,29 @@ export default async function Page({ params }: { params: Promise<PageParams> })
3131
const aiPromptSlug = (page.data as { aiPrompt?: string }).aiPrompt;
3232
const promptContent = aiPromptSlug ? await getPromptContent(aiPromptSlug) : null;
3333

34+
const pageMarkdownUrl = `https://www.prisma.io${withDocsBasePath(page.url)}.md`;
35+
3436
return (
3537
<>
38+
<div
39+
data-markdown-ignore
40+
style={{
41+
position: "absolute",
42+
width: 1,
43+
height: 1,
44+
padding: 0,
45+
margin: -1,
46+
overflow: "hidden",
47+
clip: "rect(0, 0, 0, 0)",
48+
whiteSpace: "nowrap",
49+
border: 0,
50+
}}
51+
>
52+
For the complete Prisma documentation index optimized for AI agents, see{" "}
53+
<a href="https://www.prisma.io/docs/llms.txt">https://www.prisma.io/docs/llms.txt</a>. A
54+
markdown version of this page is available at{" "}
55+
<a href={pageMarkdownUrl}>{pageMarkdownUrl}</a> (append <code>.md</code> to any docs URL).
56+
</div>
3657
<TechArticleSchema page={page} />
3758
<BreadcrumbSchema page={page} />
3859
<DocsPage
@@ -44,7 +65,7 @@ export default async function Page({ params }: { params: Promise<PageParams> })
4465
>
4566
<div className="flex flex-col md:flex-row items-start gap-4 pt-2 pb-1 md:justify-between">
4667
<DocsTitle>{page.data.title}</DocsTitle>
47-
<div className="flex flex-row gap-2 items-center">
68+
<div className="flex flex-row gap-2 items-center" data-markdown-ignore>
4869
{promptContent && <CopyPromptButton fullPrompt={promptContent.fullPrompt} />}
4970
{!page.url.startsWith("/management-api/endpoints") && (
5071
<LLMCopyButton markdownUrl={`${withDocsBasePath(page.url)}.mdx`} />
@@ -64,7 +85,10 @@ export default async function Page({ params }: { params: Promise<PageParams> })
6485
})}
6586
/>
6687
</DocsBody>
67-
<div className="flex flex-row flex-wrap items-center justify-between gap-4 border-t pt-6 text-sm">
88+
<div
89+
className="flex flex-row flex-wrap items-center justify-between gap-4 border-t pt-6 text-sm"
90+
data-markdown-ignore
91+
>
6892
<EditOnGitHub
6993
href={`https://github.com/prisma/docs/edit/main/apps/docs/content/docs/${page.path}`}
7094
/>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { mcpDiscoveryResponse } from "@/lib/mcp-discovery";
2+
3+
export const revalidate = false;
4+
5+
export function GET() {
6+
return mcpDiscoveryResponse();
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { mcpDiscoveryResponse } from "@/lib/mcp-discovery";
2+
3+
export const revalidate = false;
4+
5+
export function GET() {
6+
return mcpDiscoveryResponse();
7+
}
Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
import { source } from "@/lib/source";
22
import { getLLMText } from "@/lib/get-llm-text";
3-
import { createLLMsFullResponse } from "@/lib/llms";
3+
import { createLLMsFullResponse, filterPagesForLLMsIndex } from "@/lib/llms";
4+
import { getBaseUrl, withDocsBasePath } from "@/lib/urls";
45

56
export const revalidate = false;
67

78
export async function GET() {
9+
const baseUrl = getBaseUrl();
10+
const llmsTxtUrl = `${baseUrl}${withDocsBasePath("/llms.txt")}`;
11+
812
const description = `# Prisma Documentation - Full Content Feed
913
10-
This file contains the complete Prisma documentation in machine-readable format.
11-
Includes the current docs plus legacy v6 ORM pages.
14+
This file contains the current Prisma documentation in machine-readable format.
15+
Legacy Prisma ORM v6 content is not included here; fetch any v6 page directly by
16+
appending \`.md\` to its URL (for example, ${baseUrl}${withDocsBasePath("/orm/v6/...")}.md).
17+
For the documentation index, see ${llmsTxtUrl}.
1218
1319
---
1420
1521
`;
1622

17-
return createLLMsFullResponse(description, source.getPages(), getLLMText);
23+
const pages = filterPagesForLLMsIndex(source.getPages()).filter(
24+
(page) => page.url !== "/orm/v6" && !page.url.startsWith("/orm/v6/"),
25+
);
26+
27+
return createLLMsFullResponse(description, pages, getLLMText);
1828
}

apps/docs/src/app/llms.txt/route.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
formatLLMsLink,
1010
formatLLMsPageLink,
1111
formatLLMsSectionLink,
12+
getUnmatchedLLMsPages,
1213
llmsSections,
1314
} from "@/lib/llms";
1415

@@ -20,13 +21,23 @@ export async function GET() {
2021
getPageTitleText(a.data.title, a.url).localeCompare(getPageTitleText(b.data.title, b.url)),
2122
);
2223

24+
const availableSections = filterAvailableLLMsSections(llmsSections, latestPages);
25+
2326
const commonQueriesList = filterAvailableLLMsLinks(commonQueries, latestPages)
2427
.map((link) => formatLLMsLink(link, baseUrl))
2528
.join("\n");
26-
const subIndexList = filterAvailableLLMsSections(llmsSections, latestPages)
29+
const subIndexList = availableSections
2730
.map((section) => formatLLMsSectionLink(section, baseUrl))
2831
.join("\n");
29-
const latestDocsList = latestPages.map((page) => formatLLMsPageLink(page, baseUrl)).join("\n");
32+
const otherPages = getUnmatchedLLMsPages(latestPages, availableSections);
33+
const otherPagesList = otherPages.map((page) => formatLLMsPageLink(page, baseUrl)).join("\n");
34+
const otherPagesSection = otherPagesList
35+
? `
36+
37+
## Other pages
38+
39+
${otherPagesList}`
40+
: "";
3041

3142
const content = `# Prisma Documentation
3243
@@ -36,21 +47,17 @@ export async function GET() {
3647
> First, fetch https://www.prisma.io/changelog.md to check for recent or relevant breaking changes,
3748
> then look up the relevant topic in the documentation below.
3849
39-
> This documentation covers the current docs plus legacy v6 pages.
40-
> Prefer the Latest ORM section for current recommendations.
41-
> v6 pages are maintained for backwards compatibility only.
50+
> This index links to per-area indexes below. Each area index lists its pages with descriptions.
51+
> Append \`.md\` to any docs page URL to fetch its Markdown. Legacy Prisma ORM v6 pages are listed under
52+
> the "Prisma ORM v6 (legacy)" area and are maintained for backwards compatibility only.
4253
4354
## Common Queries
4455
4556
${commonQueriesList}
4657
4758
## Product Area Indexes
4859
49-
${subIndexList}
50-
51-
## Latest
52-
53-
${latestDocsList}
60+
${subIndexList}${otherPagesSection}
5461
5562
## Options
5663
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
export const revalidate = false;
2+
3+
const skill = `---
4+
name: prisma
5+
description: Set up and use Prisma ORM with Prisma Postgres — define a schema, run migrations, generate Prisma Client, and query a PostgreSQL database in TypeScript. Includes the remote Prisma MCP server for managing Prisma Postgres databases.
6+
license: Apache-2.0
7+
compatibility: Node.js and TypeScript projects using Prisma ORM (Prisma Client) and Prisma Postgres.
8+
metadata:
9+
product: Prisma ORM + Prisma Postgres
10+
version: "7"
11+
documentation: https://www.prisma.io/docs
12+
llms_txt: https://www.prisma.io/docs/llms.txt
13+
allowed-tools:
14+
- Bash
15+
- Read
16+
- Edit
17+
- Write
18+
---
19+
20+
# Prisma
21+
22+
Prisma is a next-generation TypeScript ORM and a managed PostgreSQL database:
23+
24+
- **Prisma ORM** — a type-safe database toolkit. You define your data model in a Prisma Schema, Prisma Migrate turns it into SQL migrations, and Prisma Client gives you a fully typed query API.
25+
- **Prisma Postgres** — a fully managed PostgreSQL database that scales to zero and integrates with Prisma ORM and Prisma Studio.
26+
27+
Prisma changes between versions. Before implementing a feature, verify against the current docs at https://www.prisma.io/docs and the changelog at https://www.prisma.io/changelog.md. Do not rely on training data for Prisma APIs, configuration, or conventions.
28+
29+
## Golden workflow
30+
31+
These are the core commands for a new TypeScript project (copied from the Prisma Postgres quickstart). Run the CLI with \`npx\`:
32+
33+
1. **Initialize the project and install dependencies**
34+
35+
\`\`\`bash
36+
npm init -y
37+
npm install typescript tsx @types/node --save-dev
38+
npm install prisma @types/pg --save-dev
39+
npm install @prisma/client @prisma/adapter-pg pg dotenv
40+
\`\`\`
41+
42+
2. **Scaffold Prisma ORM.** This creates \`prisma/schema.prisma\`, a \`.env\` with \`DATABASE_URL\`, and \`prisma.config.ts\`:
43+
44+
\`\`\`bash
45+
npx prisma init --output ../generated/prisma
46+
\`\`\`
47+
48+
3. **Provision a Prisma Postgres database** and replace \`DATABASE_URL\` in \`.env\` with the \`postgres://...\` connection string from the CLI output:
49+
50+
\`\`\`bash
51+
npx create-db
52+
\`\`\`
53+
54+
4. **Define your data model** in \`prisma/schema.prisma\`, for example:
55+
56+
\`\`\`prisma
57+
generator client {
58+
provider = "prisma-client"
59+
output = "../generated/prisma"
60+
}
61+
62+
datasource db {
63+
provider = "postgresql"
64+
}
65+
66+
model User {
67+
id Int @id @default(autoincrement())
68+
email String @unique
69+
name String?
70+
posts Post[]
71+
}
72+
73+
model Post {
74+
id Int @id @default(autoincrement())
75+
title String
76+
content String?
77+
published Boolean @default(false)
78+
author User @relation(fields: [authorId], references: [id])
79+
authorId Int
80+
}
81+
\`\`\`
82+
83+
5. **Create and apply a migration**, then **generate Prisma Client**:
84+
85+
\`\`\`bash
86+
npx prisma migrate dev --name init
87+
npx prisma generate
88+
\`\`\`
89+
90+
6. **Instantiate Prisma Client** with the driver adapter and query your database:
91+
92+
\`\`\`typescript
93+
import "dotenv/config";
94+
import { PrismaPg } from "@prisma/adapter-pg";
95+
import { PrismaClient } from "../generated/prisma/client";
96+
97+
const connectionString = \`\${process.env.DATABASE_URL}\`;
98+
const adapter = new PrismaPg({ connectionString });
99+
const prisma = new PrismaClient({ adapter });
100+
101+
const user = await prisma.user.create({
102+
data: { name: "Alice", email: "alice@prisma.io" },
103+
});
104+
const users = await prisma.user.findMany({ include: { posts: true } });
105+
\`\`\`
106+
107+
7. **Explore your data** visually with Prisma Studio:
108+
109+
\`\`\`bash
110+
npx prisma studio
111+
\`\`\`
112+
113+
When you change your schema later, re-run \`npx prisma migrate dev\` to create a new migration and \`npx prisma generate\` to update Prisma Client.
114+
115+
### Safety with destructive commands
116+
117+
Prisma ORM detects when it is invoked by AI coding agents and blocks destructive commands such as \`prisma migrate reset --force\`. Do not bypass this guardrail: stop, tell the user exactly which command you want to run and that it irreversibly destroys all data, and proceed only after explicit user consent.
118+
119+
## Prisma MCP server
120+
121+
Prisma provides a remote MCP server that lets AI tools manage Prisma Postgres databases (create databases, connection strings, and backups; run and introspect SQL) and search the Prisma documentation. It authenticates with Prisma Console via OAuth on first use.
122+
123+
Add it with the standard MCP configuration:
124+
125+
\`\`\`json
126+
{
127+
"mcpServers": {
128+
"Prisma": {
129+
"url": "https://mcp.prisma.io/mcp"
130+
}
131+
}
132+
}
133+
\`\`\`
134+
135+
The server exposes a \`search_prisma_documentation\` tool that returns cited answers grounded in the official Prisma docs — prefer it over training data for Prisma questions.
136+
137+
## Documentation for agents
138+
139+
- **Index:** https://www.prisma.io/docs/llms.txt — links to per-area indexes (Prisma ORM, Prisma Postgres, Guides, CLI, Studio, Platform, and more).
140+
- **Full content feed:** https://www.prisma.io/docs/llms-full.txt — the current docs as a single machine-readable file.
141+
- **Any page as Markdown:** append \`.md\` to any docs URL, e.g. https://www.prisma.io/docs/orm/prisma-client.md.
142+
- **Changelog:** https://www.prisma.io/changelog.md — check before implementing to catch breaking changes.
143+
`;
144+
145+
export async function GET() {
146+
return new Response(skill, {
147+
headers: {
148+
"Content-Type": "text/markdown; charset=utf-8",
149+
},
150+
});
151+
}

apps/docs/src/lib/get-llm-text.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { source } from "@/lib/source";
2-
import { normalizeProcessedMarkdown } from "@/lib/llm-markdown";
2+
import { normalizeProcessedMarkdown, protectFencedCodeBlocks } from "@/lib/llm-markdown";
33
import { getPageTitleText } from "@/lib/page-title";
44
import { getBaseUrl, withDocsBasePath } from "@/lib/urls";
55
import type { InferPageType } from "fumadocs-core/source";
@@ -173,10 +173,53 @@ function formatRelatedPages(relatedPages: RelatedPageLink[]) {
173173
return `\n\n## Related pages\n\n${links}`;
174174
}
175175

176+
/**
177+
* Rewrites in-body markdown links so the full feed and per-page markdown resolve
178+
* correctly when read outside the app. Root-relative links (`/orm/...`) do not
179+
* carry the `/docs` base path in the processed markdown, so an agent resolving
180+
* them against the feed URL would drop `/docs`. We resolve each link against the
181+
* docs source: known docs pages become absolute `<baseUrl>/docs/...` URLs, while
182+
* links that are not docs pages (e.g. `/pricing`) become site-root URLs so they
183+
* are not wrongly prefixed with `/docs`. Absolute, protocol-relative (`//`),
184+
* anchor-only, and relative (`./`, `../`) links are left untouched.
185+
*/
186+
function resolveInBodyHref(target: string, page: DocsPage, baseUrl: string) {
187+
const hashIndex = target.indexOf("#");
188+
const hash = hashIndex === -1 ? "" : target.slice(hashIndex);
189+
const path = hashIndex === -1 ? target : target.slice(0, hashIndex);
190+
191+
const resolved = getPageSource().getPageByHref(path, { dir: dirname(page.path) });
192+
if (resolved) {
193+
const resolvedHash = resolved.hash ? `#${resolved.hash}` : hash;
194+
return `${baseUrl}${withDocsBasePath(resolved.page.url)}${resolvedHash}`;
195+
}
196+
197+
// Not a docs page: treat as a site-root link (do not add the /docs base path).
198+
return `${baseUrl}${target}`;
199+
}
200+
201+
function absolutizeInBodyLinks(markdown: string, page: DocsPage, baseUrl: string) {
202+
// Protect fenced code blocks so example code containing markdown link syntax is
203+
// left untouched. Inline code (single backticks) cannot contain the `](...)` link
204+
// syntax we rewrite, so it needs no protection.
205+
const protectedCode = protectFencedCodeBlocks(markdown);
206+
207+
const rewritten = protectedCode.markdown.replace(/\]\((\/[^)\s]*)\)/g, (full, target: string) => {
208+
if (target.startsWith("//")) return full;
209+
return `](${resolveInBodyHref(target, page, baseUrl)})`;
210+
});
211+
212+
return protectedCode.restore(rewritten);
213+
}
214+
176215
export async function getLLMText(page: DocsPage) {
177-
const processed = normalizeProcessedMarkdown(await page.data.getText("processed"));
178-
const breadcrumbLine = getBreadcrumbLine(page);
179216
const baseUrl = getBaseUrl();
217+
const processed = absolutizeInBodyLinks(
218+
normalizeProcessedMarkdown(await page.data.getText("processed")),
219+
page,
220+
baseUrl,
221+
);
222+
const breadcrumbLine = getBreadcrumbLine(page);
180223
const explicitRelatedPages = getExplicitRelatedPages(page, baseUrl);
181224
const relatedPages =
182225
explicitRelatedPages.length > 0
@@ -185,7 +228,17 @@ export async function getLLMText(page: DocsPage) {
185228
const context = breadcrumbLine ? `${breadcrumbLine}\n\n` : "";
186229
const related = formatRelatedPages(relatedPages);
187230

231+
const llmsTxtUrl = `${baseUrl}${withDocsBasePath("/llms.txt")}`;
232+
const directive = `> For the complete Prisma documentation index, see [llms.txt](${llmsTxtUrl}). A markdown version of any docs page is available by appending \`.md\` to its URL.`;
233+
234+
const description =
235+
typeof page.data.description === "string" && page.data.description.trim().length > 0
236+
? `\n\n${page.data.description.trim()}`
237+
: "";
238+
188239
return `# ${getPageTitleText(page.data.title, page.url)} (${withDocsBasePath(page.url)})
189240
241+
${directive}${description}
242+
190243
${context}${processed}${related}`;
191244
}

0 commit comments

Comments
 (0)