Skip to Content

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():

press.config.tsx
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:

press.config.tsx
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/install

Turn 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/ai

Add the Plugin

Pass a model: an AI Gateway model ID, or a model from a provider package.

press.config.tsx
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:

src/app.css
@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:

press.config.tsx
aiPlugin({
  model: "openai/gpt-5",
  async beforeRequest(req) {
    if (await isRateLimited(req)) {
      return new Response("Too Many Requests", { status: 429 });
    }
  },
});

System Prompt

press.config.tsx
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:

src/components/docs-island.tsx
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.

press.config.tsx
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 a locale with 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:

press.config.tsx
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:

press.config.tsx
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

On this page