Zenn
📚

Vitepress ドキュメントを MCP 化する

2025/03/25に公開
2

はじめに

今回の実装は mastra というフレームワークの初期化時に追加できる MCP のドキュメントサーバーがめちゃくちゃ便利だったので、それに影響を受けて手元の Vitepress 製のドキュメントを MCP (Model Context Protocol) 化した際の記録です。

今回は自分の手元の Vitepress 製のドキュメントを MCP 経由で取得できるようにしてみました。

Cursor Agentがドキュメントを参照している例

環境情報

今回使用した環境は以下の通りです。

  • Node.js: v20.17.1
  • pnpm: 9.11.5
  • VitePress: 1.6.3

準備

パッケージのインストール

Vitepress プロジェクトにパッケージを追加します。 fastmcp というパッケージを使うと MCP サーバーを簡単に作れて便利でした。

# MCP関連
pnpm add fastmcp node-html-markdown zod

# TypeScript関連
pnpm add @types/node typescript

package.jsonの設定

package.json のtypemoduleに設定します。package のパスを取得するために必要でした。

{
    "type": "module",
    // ...その他の設定
}

その他は、必要に応じて tsconfig.json を追加します。これで環境構築は完了です。

Vitepress のビルド

次に、Vitepress をビルドして配信用の HTML ファイルを生成します。

# `vitepress build` が実行されます。
pnpm run docs:build

MCP サーバーの実装

.vitepress/dist 以下の HTML ファイルを Markdown に変換して提供する MCP サーバーを実装します。
今回は、dist 直下のディレクトリを tool の単位することにしました。(例: basic/, advanced/ など)
MCP のクライアントが basic/ を要求すると basic/ 以下のドキュメントのパスを一覧として返却し、basic/getting-started.html などのパスを要求するとドキュメントを返します。

https://www.npmjs.com/package/@mastra/mcp-docs-server

以下が server.ts の全コードです。

import { FastMCP } from 'fastmcp';
import { NodeHtmlMarkdown } from 'node-html-markdown';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'url';
import { z } from 'zod';

const nhm = new NodeHtmlMarkdown();

// パッケージのルートディレクトリを取得する関数
function getPackageRoot(): string {
  // ESM環境では __dirname が使えないため、import.meta.url から解決
  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);
  return path.resolve(__dirname);
}

function getTopPaths(): string[]{
  const distPath = path.join(getPackageRoot(), '.vitepress', 'dist');
  const dirEntries = fs.readdirSync(distPath, { withFileTypes: true });
  return dirEntries.filter(entry => entry.isDirectory() && entry.name !== 'assets').map(entry => entry.name);
}

function _getDocuments(dir: string): string[] {
  const result: string[] = [];
  
  function traverseDirectory(currentDir: string) {
    const entries = fs.readdirSync(currentDir, { withFileTypes: true });
    
    for (const entry of entries) {
      const fullPath = path.join(currentDir, entry.name);
      
      if (entry.isDirectory()) {
        traverseDirectory(fullPath);
      } else {
        result.push(fullPath);
      }
    }
  }
  
  traverseDirectory(dir);
  return result;
}

function getDocuments(dir: string): string[] {
  const distPath = path.join(getPackageRoot(), '.vitepress', 'dist');
  const dirPath = path.join(distPath, dir);
  const documents = _getDocuments(dirPath);
  return documents.map(document => document.replace(distPath, ''));
}

function getDocument(url: string): string {
  const distPath = path.join(getPackageRoot(), '.vitepress', 'dist');
  const documentPath = path.join(distPath, url);
  const doc = fs.readFileSync(documentPath, 'utf-8');
  return nhm.translate(doc);
}

const server = new FastMCP({
  name: 'ドキュメントサーバー',
  version: '1.0.0',
});


getTopPaths().forEach(path => {
  server.addTool({
    name: path,
    description: `Get ${path} content. Without a URL, returns a list of all ${path} content.With a URL, returns the specific ${path} post content in markdown format.`,
    parameters: z.object({
    url: z
      .string()
      .describe(
        `URL of a specific ${path} post to fetch. If the string /${path} is passed as the url it returns a list of all ${path} documents.`,
      ),
  }),
    execute: async (params) => {
      if (params.url === `/${path}`) {
        return JSON.stringify({
          documents: getDocuments(path),
        });
      } else {
        return JSON.stringify({
          document: getDocument(params.url),
        });
      }
    },
  });
});

// サーバー起動
server.start({
  transportType: 'stdio', // CLIからの起動用
});

MCP サーバーの登録

ここまで実装できたら、あとは MCP サーバーを Cursor など MCP クライアントとして使えるアプリケーションに登録します。以下は Cursor に設定した場合な mcp.json の例です。

/${DOCS_ROOT}/server.ts には先ほど実装した server.ts の絶対パスを指定します。

{
  "mcpServers": {
    "cline-docs": {
      "command": "pnpx",
      "args": [
        "tsx",
        "/${DOCS_ROOT}/server.ts"
      ],
      "env": {},
      "disabled": false,
      "autoApprove": [
        "getCategories",
        "getDocument",
        "searchDocuments"
      ]
    }
  }
}

動作確認

これで設定は完了で、Cursor Agent から MCP 経由で Vitepress のドキュメントを取得できるようになりました。

Cursor Agentがドキュメントを参照している例

まとめ

Vitepress が静的 HTML を吐いてくれるので、お手軽に MCP 化に出来るのがかなりいいと思いました。社内ドキュメントなどで Vitepress を使っている組織も多いと思うのでオススメです。

ただ、もっとスマートなやり方があると思うので、試した方はご共有いただけるとありがたいです。

2

Discussion

ログインするとコメントできます