# Fumadocs (Framework Mode, TanStack Start): AI と LLM
URL: /ja/docs/tanstack-start/integrations/llms
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/[framework]/integrations/llms.ja.mdx

Fumadocs に AI 機能を組み込みます。

## LLM向けドキュメント [#docs-for-llm]

LLMやAIエージェント向けに、ドキュメントをMarkdown形式で提供します。

```package-install
npx @fumadocs/cli feature llms
```

これにより、docs コレクションで処理済みの Markdown が有効になり、`lib/source.ts` に `docsLlms` が追加されて、以下の[ファイル](#files)が配信されます。手動でセットアップする場合に備えて、生成されるファイルを以下に記載しています。

### `docsLlms` [#docsllms]

`llms()` は、ドキュメントを LLM 向けにレンダリングします。`renderPage` は、生のファイル内容ではなく処理済みのドキュメントをもとに、1 つのページを Markdown に変換します。

```ts title="lib/source.ts"
import { llms } from 'fumadocs-core/source';

export const docsLlms = llms(source, {
  renderPage: async (page) => `# ${page.data.title} (${page.url})

${await page.data.getText('processed')}`,
});

```

| メソッド           | 出力                                |
| -------------- | --------------------------------- |
| `index(lang?)` | ページツリーから構築される `llms.txt` インデックス   |
| `page(page)`   | `renderPage` でレンダリングした 1 つのページ    |
| `full(lang?)`  | `renderPage` でレンダリングした全ページを結合したもの |
| `files()`      | [エージェント向けファイル](#files)のパス         |
| `serve(path)`  | 指定したパスにあるファイルのレスポンス               |

<Callout title="renderPage は必須です">
コンテンツソースが Markdown をどのように公開しているかを Fumadocs 側では判断できないため、`page()`、`full()`、`files()`、`serve()` は
`renderPage` を渡した場合にのみ利用できます。
</Callout>

`renderPage` はページの処理済み Markdown を読み取るため、`includeProcessedMarkdown` を有効にしてください。

```ts title="source.config.ts"
import { defineDocs } from 'fumadocs-mdx/config';

export const docs = defineDocs({
  dir: 'content/docs',
  docs: { // [!code ++:5]
    postprocess: {
      includeProcessedMarkdown: true,
    },
  },
});

```

MDX コンポーネントは JSX 構文のまま出力されます。これらを [Markdown 形式](/docs/headless/utils/markdown#asmarkdown)にレンダリングするには、[MDX コンポーネント](/docs/headless/mdx/remark-llms#jsx-elements)をページの props として渡してください。

[`@fumadocs/local-md`](/docs/tanstack-start/integrations/content/local-md) のようなランタイムのコンテンツソースは、リクエスト時に解決されます。その `getSource` を渡すと、CLI が同じファイルを生成します。

```ts
export const docsLlms = llms(getSource, {
  renderPage: (page) => `# ${page.data.title} (${page.url})

${page.data.content}`,
});
```

`getSource` を使用する場合、`index()` と `indexNode()` は文字列ではなく Promise を返します。

### ファイル [#files]

CLIは、AIエージェント向けに次のファイルを配信します。

* `/llms.txt`: ドキュメントのインデックスです。各ページのMarkdownへのリンクが含まれます。
* `/llms-full.txt`: 全ページのコンテンツです。
* `<page>.md`: 各ページのMarkdownです (例: `/docs/ui.md`) 。`/` のページは `/index.md` になります。
* `<section>/llms-full.txt`: [セクション](#sections)のコンテンツです (例: `/docs/ui/llms-full.txt`) 。

下書きなどのページをこれらのファイルから除外するには、`filter` を指定します。除外したページも `llms.txt` には引き続き掲載され、HTMLページへのリンクになります。

```ts title="lib/source.ts"
export const docsLlms = llms(source, {
  filter: (page) => page.slugs[0] !== 'drafts',
  renderPage: async (page) => `# ${page.data.title}\n\n${await page.data.getText('processed')}`,
});
```

これらのファイルでは、`renderUrl` はリンク先のファイルのパス (ページの `.md` ファイルなど) を受け取ります。

ページの HTML を読み取るエージェントは、`<head>` 内のリンクからそのページの Markdown を見つけます：

```html
<link rel="alternate" type="text/markdown" href="/docs/ui.md" />
```

### セクション [#sections]

セクションとは、独自の `llms-full.txt` を持つページツリー内のフォルダーです。これにより、エージェントは大規模なドキュメントを部分ごとに読み込めます。デフォルトでは、すべてのルートフォルダーがセクションになります。`llms.txt` では、各フォルダーの行がそのセクションの `llms-full.txt` へのリンクになります。

セクションとするフォルダーを指定するには、`section` を渡します。この関数はフォルダーを受け取り、そのフォルダーをセクションとするかどうかを返します。

```ts title="lib/source.ts"
export const docsLlms = llms(source, {
  // index ページを持つすべてのフォルダー
  section: (folder) => folder.index !== undefined,
  renderPage: async (page) => `# ${page.data.title}\n\n${await page.data.getText('processed')}`,
});
```

大規模なサイトでは、セクションを折りたたむと `llms.txt` を短く保てます。折りたたんだセクションは、`llms.txt` ではそのセクション専用の `<section>/llms.txt` への1つのリンクとして記載され、セクション内のページはそのファイルに一覧表示されます。

```ts title="lib/source.ts"
export const docsLlms = llms(source, {
  section: (folder) => (folder.root ? { collapse: true } : false),
  renderPage: async (page) => `# ${page.data.title}\n\n${await page.data.getText('processed')}`,
});
```

セクションのファイルは、そのフォルダーのインデックスページの URL 配下で配信されます。インデックスページがないフォルダーでは、代わりに配下のページの URL ディレクトリが使われます。フォルダーグループのように 2 つのフォルダーが同じ URL を共有する場合は、`asPath` を渡すと、一方を別のパスで配信できます (例：`{ asPath: '/docs/guides' }`) 。

### ルート [#routes]

`files()` はファイルのパスを列挙し、`serve(path)` は指定したパスのファイルをレスポンスとして返します。そのため、各ルートはリクエストパスを `serve()` に渡すだけで済みます。

ドキュメントページ `docs/$` は `/docs` 配下のすべてのパスにマッチするため、ドキュメント配下のファイル用のルートには `docs/{$}.md` のように、より具体的なパスを使用します：

```ts tab="src/routes/llms[.]txt.ts"
import { createFileRoute } from '@tanstack/react-router';
import { docsLlms } from '@/lib/source';

export const Route = createFileRoute('/llms.txt')({
  server: {
    handlers: {
      GET: async ({ request }) => (await docsLlms.serve(new URL(request.url).pathname)) ?? new Response(null, { status: 404 }),
    },
  },
});

```

```ts tab="src/routes/llms-full[.]txt.ts"
import { createFileRoute } from '@tanstack/react-router';
import { docsLlms } from '@/lib/source';

export const Route = createFileRoute('/llms-full.txt')({
  server: {
    handlers: {
      GET: async ({ request }) => (await docsLlms.serve(new URL(request.url).pathname)) ?? new Response(null, { status: 404 }),
    },
  },
});

```

```ts tab="src/routes/\{$\}[.]md.ts"
import { createFileRoute } from '@tanstack/react-router';
import { docsLlms } from '@/lib/source';

export const Route = createFileRoute('/{$}.md')({
  server: {
    handlers: {
      GET: async ({ request }) => (await docsLlms.serve(new URL(request.url).pathname)) ?? new Response(null, { status: 404 }),
    },
  },
});

```

```ts tab="src/routes/docs/\{$\}[.]md.ts"
import { createFileRoute } from '@tanstack/react-router';
import { docsLlms } from '@/lib/source';

export const Route = createFileRoute('/docs/{$}.md')({
  server: {
    handlers: {
      GET: async ({ request }) => (await docsLlms.serve(new URL(request.url).pathname)) ?? new Response(null, { status: 404 }),
    },
  },
});

```

```ts tab="src/routes/docs/\{$\}[.]txt.ts"
import { createFileRoute } from '@tanstack/react-router';
import { docsLlms } from '@/lib/source';

export const Route = createFileRoute('/docs/{$}.txt')({
  server: {
    handlers: {
      GET: async ({ request }) => (await docsLlms.serve(new URL(request.url).pathname)) ?? new Response(null, { status: 404 }),
    },
  },
});

```

`serve()` はファイルが存在しないパスに対して `undefined` を返すため、ミドルウェアはリクエストを後続の処理に渡せます。`hideLocale: 'always'` を指定した場合は URL にロケールが含まれないため、言語を `serve()` の第2引数、および `files()` の第1引数として渡してください。

#### 静的出力 [#static-output]

SPA モードでは、Nitro でファイルをプリレンダリングします。TanStack Start はプリレンダリング対象のパスに末尾スラッシュを付けてリクエストしますが、ファイル用のルートはこれに一致しません。Vite はプラグインを適用せずに設定を読み込むため、`runnerImport` を使ってソースを読み込んでください。

```ts title="vite.config.ts"
import { defineConfig, runnerImport } from 'vite';
import { fumadocsMdx } from 'fumadocs-mdx/vite';
import { nitro } from 'nitro/vite';

const { docsLlms } = (
  await runnerImport<typeof import('./src/lib/source')>('./src/lib/source.ts', {
    plugins: [fumadocsMdx()],
    resolve: { tsconfigPaths: true },
  })
).module;

export default defineConfig({
  plugins: [
    // その他のプラグイン
    nitro({
      prerender: { routes: await docsLlms.files() },
    }),
  ],
});
```

#### `Accept` [#accept]

AIエージェントは、`Accept` ヘッダーを使って Markdown を要求できます。

リクエストが Markdown を優先する場合は、ページの Markdown へリダイレクトするようにします。

```ts title="src/start.ts"
import { createMiddleware, createStart } from '@tanstack/react-start';
import { redirect } from '@tanstack/react-router';
import { isMarkdownPreferred } from 'fumadocs-core/negotiation';

const llmMiddleware = createMiddleware().server(({ next, request }) => {
  const url = new URL(request.url);

  // `/docs/ui.md` のようなパスはファイルのルートで配信されます
  if (
    url.pathname.startsWith('/docs') &&
    !/\.(md|txt)$/.test(url.pathname) &&
    isMarkdownPreferred(request)
  ) {
    url.pathname += '.md';
    throw redirect({ href: url.href, headers: { Vary: 'Accept' } }); // [!code highlight]
  }

  return next();
});

export const startInstance = createStart(() => ({
  requestMiddleware: [llmMiddleware],
}));
```

同じ URL に2つの表現が存在することになるため、キャッシュの衝突を防ぐには、レスポンスに `Vary: Accept` を含める必要があります。

### ページアクション [#page-actions]

ページアクションを使うと、読者はページの Markdown をコピーしたり、AI アプリでページを開いたりできます。ページアクションは、ページの [`.md` ファイル](#files)を読み込みます。

![AI ページアクション](/docs/ai-page-actions.png)

docs ページのローダーから Markdown の URL を返し、ページ内でレンダリングしてください。

```tsx
import { MarkdownCopyButton, ViewOptionsPopover } from 'fumadocs-ui/layouts/docs/page';

// ローダー内
return {
  path: page.path,
  markdownUrl: `${page.url}.md`,
};

// ページ内
<div className="flex flex-row gap-2 items-center border-b pt-2 pb-6">
  <MarkdownCopyButton markdownUrl={markdownUrl} />
  <ViewOptionsPopover
    markdownUrl={markdownUrl}
    githubUrl={`https://github.com/${owner}/${repo}/blob/main/content/docs/${encodeURI(path)}`}
  />
</div>;
```

## MCP サーバー [#mcp-server]

[MCP](https://modelcontextprotocol.io) サーバーを通じてドキュメントをAIエージェントに公開できます。ページの一覧表示・検索・読み取りを行うツールも提供されます。

```package-install
npx @fumadocs/cli feature mcp
```

上記の [LLM ルート](#docs-for-llm) に加えて、Streamable HTTP トランスポートを使用する `/api/mcp` ルートを作成します。

ツールは [`fumadocs-core/mcp`](/docs/headless/utils/mcp) によって提供されます。

```ts title="src/routes/api/mcp.ts"
import { createFileRoute } from '@tanstack/react-router';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import { registerSearchTool, registerSourceTools } from 'fumadocs-core/mcp';
import { createFromSource } from 'fumadocs-core/search/server';
import { docsLlms, source } from '@/lib/source';

const handler = createMcpHandler(() => {
  const mcp = new McpServer(
    {
      name: 'docs',
      version: '1.0.0',
    },
    // the tools never change, a stream to watch them would hold a serverless function open
    { capabilities: { tools: { listChanged: false } } },
  );

  registerSourceTools(mcp, source, docsLlms);
  registerSearchTool(mcp, createFromSource(source));

  return mcp;
});

export const Route = createFileRoute('/api/mcp')({
  server: {
    handlers: {
      GET: async ({ request }) => handler.fetch(request),
      POST: async ({ request }) => handler.fetch(request),
      DELETE: async ({ request }) => handler.fetch(request),
    },
  },
});

```

エージェントを接続するには、次のようにします。

```json
{
  "mcpServers": {
    "docs": { "url": "https://your-site.com/api/mcp" }
  }
}
```

## WebMCP [#webmcp]

<Callout type="warn" title="実験的機能">
[WebMCP](https://webmachinelearning.github.io/webmcp/) は策定初期段階の Web 標準で、Chrome 149 以降で
`#enable-webmcp-testing` フラグを有効にするか、オリジントライアルを通じて利用できます。API は今後
変更される可能性があります。
</Callout>

前述の MCP サーバーがサイトに接続してくるエージェント向けであるのに対し、WebMCP は読者が閲覧しているページ上で、ブラウザに組み込まれた AIエージェント
にツールを公開します。

```package-install
npx @fumadocs/cli feature webmcp
```

[LLM ルート](#docs-for-llm)をベースにした `WebMCP` コンポーネントが作成されます。これを `<RootProvider />` の内側でレンダリングしてください。

* `search_docs` は検索 API ルートにクエリを送信します。`fetchClient` は[他のクライアント](/docs/tanstack-start/search)に置き換えることもできます。
* `read_page` は、`/docs/page.md` からページの Markdown を取得します。

## AIに質問 [#ask-ai]

![AI Search](/docs/ai-search.png)

Fumadocs CLI を使用して、AI チャットダイアログをインストールできます。

<Tabs items={["AI SDK","LLMGateway","Inkeep AI"]}>
<Tab>
```npm
npx @fumadocs/cli add ai/openrouter
```

Vercel AI SDK を使用して [OpenRouter](https://openrouter.ai) 向けに自動で設定され、AI 用の `/search` ツールも含まれます。

`/api/chat` ルートを更新すれば、他のモデルも使用できます。

> Fumadocs は AI モデルを提供していません。モデルはご自身で用意してください。
>
> AI モデルには、上記で生成した `llms-full.txt` ファイルを利用できます。また、サードパーティのソリューションと組み合わせれば、より多様な情報ソースを活用することもできます。
</Tab>

<Tab>
```npm
npx @fumadocs/cli add ai/llmgateway
```

Vercel AI SDK を使用して [LLMGateway](https://llmgateway.io) 向けに自動で設定され、AI 用の `/search` ツールも含まれます。

LLMGateway は、300 以上の言語モデルへのリクエストをプロキシするオープンソースの API ゲートウェイです。そのため、モデル ID を変更するだけで、同じルートハンドラーのままプロバイダーを切り替えられます。

LLMGateway の API キーを環境変数に追加します。

```dotenv
LLM_GATEWAY_API_KEY="..."
```

デフォルトでは、ルートは `anthropic/claude-3.5-sonnet` を使用します。LLMGateway がサポートする他のモデルに切り替えるには、`LLM_GATEWAY_MODEL` 環境変数を設定してください。コードを変更する必要はありません。

```dotenv
# https://llmgateway.io/models に掲載されている任意のモデル ID
LLM_GATEWAY_MODEL="openai/gpt-4o"
```
</Tab>

<Tab>
```npm
npx @fumadocs/cli add ai/inkeep
```

Vercel AI SDK を使用して [Inkeep AI](https://inkeep.com) 向けに自動で設定されます。

Inkeep の API キーを環境変数に追加します。

```dotenv
INKEEP_API_KEY="..."
```
</Tab>
</Tabs>

Docs レイアウトは、`aiChat` オプションをもとにチャットをレンダリングします。このオプションはクライアントの状態を受け取るため、レイアウト用のクライアントコンポーネントを作成し、元のレイアウトの代わりに使用してください。

```tsx tab="components/ai/layout.tsx"
'use client';
// Notebook、Glass、Spacious レイアウトも使用できます
import { DocsLayout as Layout, type DocsLayoutProps } from 'fumadocs-ui/layouts/docs';
// インストールしたコンポーネント
import { AIChat, AIChatPanel, useAIChat } from './search';

export function DocsLayout(props: DocsLayoutProps) {
  return (
    <AIChat>
      <ChatLayout {...props} />
    </AIChat>
  );
}

function ChatLayout(props: DocsLayoutProps) {
  const { open, setOpen } = useAIChat();

  return (
    <Layout
      {...props}
      aiChat={{ open, onOpenChange: setOpen, panel: <AIChatPanel />, trigger: true }}
    />
  );
}
```

```tsx tab="src/routes/docs/$.tsx"
import { DocsLayout } from 'fumadocs-ui/layouts/docs'; // [!code --]
import { DocsLayout } from '@/components/ai/layout'; // [!code ++]
```