AI
Give AI tools your docs as Markdown, add a chat, or connect clients over MCP.
Use any of these three features:
- llms.txt gives AI tools your docs as Markdown. It is on by default.
- AI Chat answers your readers on your site.
- MCP lets tools like code editors search your docs.
llms.txt
A new site already has these files:
/llms.txt: an index of your pages./llms-full.txt: the text of all pages in one file.<page>.md: a Markdown version of each page, like/docs/install.md. The root page is/index.md.
Pass its options to recommendedPreset():
import { createPress, recommendedPreset } from "@fumapress/astro/config";
createPress(content).config({
preset: recommendedPreset({ llms: { autoRedirect: false } }),
});Set includeProcessedMarkdown in press() to use processed
Markdown. Without it, pages use their source files. Custom pages have no Markdown version.
Page Text
Return the text of a page from getLLMText, or undefined to leave it out of /llms-full.txt:
recommendedPreset({
llms: {
async getLLMText(page) {
if (page.path.startsWith("internal/")) return;
return `# ${page.data.title} (${this.absoluteUrl(page.url)})
${await this.getPageText(page)}`;
},
},
});Content Negotiation
An AI agent can ask for Markdown at the URL of a page, without knowing about <page>.md:
curl -H "Accept: text/markdown" https://docs.example.com/docs/installTurn it off with autoRedirect: false.
This needs a server, and a static build doesn't add one for it. On Vercel, Netlify and Cloudflare, it only applies to pages rendered on demand, because these platforms serve prerendered pages without your server.
llms.txt Options
Prop
Type
AI Chat
The chat searches your pages to answer questions, and cites them as links. Its endpoint renders on demand, so the site needs a server.
Install
npm i @fumapress/aiAdd the Plugin
Pass a model: an AI Gateway model ID, or a model from a provider package.
import { aiPlugin } from "@fumapress/ai";
export default createPress(content).plugins(aiPlugin({ model: "openai/gpt-5" }));Add the Styles
Add this after the imports in your stylesheet:
@import "@fumapress/ai/css/preset.css";Now the Docs Layout docks the chat, and a floating "Ask AI" button opens it. Ctrl + / opens it too.
Rate Limiting
The endpoint calls your model provider, and has no limit on body size or message count. Anyone who can reach it can spend your budget, so add rate limiting before you go live.
Return a Response from beforeRequest to reject a request:
aiPlugin({
model: "openai/gpt-5",
async beforeRequest(req) {
if (await isRateLimited(req)) {
return new Response("Too Many Requests", { status: 429 });
}
},
});System Prompt
aiPlugin({
model: "openai/gpt-5",
systemPrompt: "You are the assistant for the Acme docs. Keep answers short, with links.",
});Custom Layout
The plugin passes the chat to layouts as the aiChat prop, through useLayoutProps(). The default layout reads it for you.
In a custom layout, your island reads it:
import { createIsland, useLayoutProps } from "@fumapress/astro/client";
export const DocsIsland = createIsland(
({ layoutProps, pageData, toc, children }: IslandProps<typeof app, "docs">) => (
<DocsLayout {...layoutProps} {...useLayoutProps()}>
<DocsPage toc={toc}>
<DocsTitle>{pageData.title}</DocsTitle>
<DocsBody>{children}</DocsBody>
</DocsPage>
</DocsLayout>
),
);The Docs and Notebook layouts open the chat with a floating "Ask AI" button. The Glass and Spacious layouts show their own button.
Chat Options
pageToIndex controls which pages the model can search, as in MCP. To keep the endpoint without the chat UI, set configureUI: false.
Prop
Type
MCP
An MCP client, like a code editor, can search your docs through an endpoint on your site. The endpoint renders on demand, so the site needs a server.
import { mcpPlugin } from "@fumapress/ai";
export default createPress(content).plugins(mcpPlugin());Then point your client at /mcp on your site, like https://docs.example.com/mcp, over Streamable HTTP. The client gets three tools:
search, which also takes alocalewith internationalization.get_page, which returns the text of a page.list_pages, which lists your pages.
Filtering Pages
Return null from pageToIndex to leave a page out:
mcpPlugin({
async pageToIndex(page) {
if (page.path.startsWith("internal/")) return null;
return {
title: page.data.title ?? "",
description: page.data.description ?? "",
url: page.url,
content: (await this.getPageText(page)) ?? "",
locale: page.locale ?? "",
};
},
});Custom Tools
tools receives the McpServer of @modelcontextprotocol/server:
import { mcpPlugin } from "@fumapress/ai";
import { z } from "zod";
mcpPlugin({
tools(server) {
server.registerTool(
"get_site_name",
{
title: "Get Site Name",
description: "Return the site name from the config",
inputSchema: z.object({}),
},
async () => ({
content: [{ type: "text", text: this.siteConfig.name }],
}),
);
},
});MCP Options
Prop
Type
Last updated on