🤖

MCPサーバーでTSDocを参照出来るようにする

に公開

はじめに

近年、ClaudeをはじめとしたLLMの進化はめざましく、日々の開発補助や設計検討など、ソフトウェアエンジニアリングのあらゆる場面で利用されるようになってきました。

しかし、LLMの持つ知識は訓練データのカットオフ時点までのものに限られているという制約があります。これにより、比較的新しいOSSライブラリを使用する際、LLMがそのライブラリを正しく理解・活用できないという問題が発生します。

例えば、弊社で最近公開した@praha/byethrowというResult型ライブラリがあります。これはneverthrowなどにインスパイアされた軽量・シンプルなAPIを提供するTypeScript用のパッケージです。

ところが、ClaudeやChatGPTなどにこのライブラリを使ったコードの補完を依頼しても、ライブラリの存在を知らないために全く見当違いの回答を返すことがあります。ひどい場合には「知ったかぶり」をして、存在しない関数や誤った使用方法を返してくるケースも見受けられます。

このような問題に対して、TSDocを参照するMCPサーバーを立て、LLMが新規ライブラリを理解できるようにする仕組みを作りました。
https://www.npmjs.com/package/@praha/byethrow-mcp

本記事では、その概要と、実際にどのように構築したのかを紹介したいと思います。

そもそも「MCPサーバー」とは?

MCP(Model Context Provider)とは、LLMへ追加の情報を提供したり外部ツールを使えるようにする為のプロトコルです。

具体的には、次のような構成で動作します。

  • LLMが外部サーバー(MCP)に対してリクエストを発行
  • MCPがリクエストに応じて処理を実行しレスポンスを返す
  • LLMはそれを元に次の行動を実行する

今回構築したMCPの仕組み

🧱 全体構成

LLM(Claudeなど)
    ↓ stdin
MCPサーバー(Node.js)
    ↓
TSDoc Markdown(typedocで事前生成)

🛠 利用技術

🔍 リクエストの流れ

  1. LLMがMCPサーバーに対して利用できるツールを問い合わせる
  2. LLMが使用するツールを指定してMCPサーバーに対して問い合わせる
  3. MCPはTSDocで事前に生成されたMarkdownをLLMへ返す
  4. LLMはその情報を元に、正確な補完や説明ができるようになる

📨 実際の応答例

例えば、Claudeに以下のように指示したとします。

byethrowについて3行で教えて下さい

通常であれば、Claudeはこのライブラリを知らず、的外れなコードを返す可能性が高いです。
しかし、MCPを導入すると以下のような返答が得られるようになります。

@praha/byethrowは、TypeScriptでのエラーハンドリングを関数型プログラミングのアプローチで改善するライブラリです。
Result型(成功または失敗を表現)を使って、例外を投げる代わりに明示的にエラーを扱い、型安全なエラーハンドリングを実現します。
pipe、andThen、mapなどの関数型コンビネータを提供し、複雑な処理の連鎖を読みやすく、デバッグしやすいコードで記述できます。

このように、正確なAPI理解に基づいた回答が返ってくるようになります。

MCPの構築手順

🛠 ドキュメント生成の準備

今回のMCPサーバーのベースとなる情報源は、TSDocをMarkdown形式で静的出力したドキュメントです。これを用いることで、TSDocをLLMが自然言語で参照しやすい形に整形できます。

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

TSDocをMarkdownに変換する為にTypeDocをインストールします。

npm install --save-dev typedoc typedoc-plugin-markdown

typedoc.jsonの設定

TypeDocの各種設定を行います。

{
  "$schema": "https://typedoc.org/schema.json",
  "entryPoints": ["./src/index.ts"],
  "plugin": ["typedoc-plugin-markdown"],
  "router": "kind",
  "readme": "none",
  "indexFormat": "table",
  "hidePageHeader": true,
  "hideBreadcrumbs": true,
  "useCodeBlocks": true,
  "preserveLinkText": false
}
  • entryPoints
    • ソースコードのエントリーポイントを指定します。
  • plugin
    • TypeDocのプラグインを指定します。
    • 今回はMarkdownで出力したいので、typedoc-plugin-markdownを指定しています。
  • router
    • TypeDocが出力するディレクトリ構成を指定します。
    • kindを指定することで実際のソースコードに近いディレクトリ構成になります。
  • readme
    • README.mdを含めるかどうかの設定です。
    • 今回はノイズになると判断したのでnoneを指定しています。
  • indexFormat
    • APIの一覧表示の形式を指定します。
    • tableを指定することで各種APIのサマリーが表示されるようになります。
  • hidePageHeader, hideBreadcrumbs
    • ヘッダーやパンくずリストを表示するかどうかの設定です。
    • こちらもノイズになると判断したので無効化しています。
  • useCodeBlocks, preserveLinkText
    • ドキュメント間のリンクを有効にするかどうかの設定です。
    • リンクもノイズになってしまうので無効化しています。

🔗 ページ内リンクの除去

Markdown形式で出力されたドキュメントには、TypeDocが自動で付与するFunctionNameのようなページ内リンクが含まれています。

しかし、これらのリンクはLLMにとって ノイズになる可能性が高く、正しい理解を妨げる場合があります。
設定オプションだけでは取り切れないリンクに対応するため、カスタムプラグインを作成して以下のような処理を行います。

// @ts-check

import { MarkdownPageEvent } from 'typedoc-plugin-markdown';

/**
 * @param {import('typedoc-plugin-markdown').MarkdownApplication} app
 */
export const load = (app) => {
  app.renderer.on(MarkdownPageEvent.END, (page) => {
    page.contents = page.contents
      .replaceAll(/Defined in: \[[^\]]+]\([^)]+\)\s*/g, '')
      .replaceAll(/\[(`?)(.+?)\1]\([^)]+\)/g, (_match, _quote, label) => `\`${label}\``);
  });
};

このプラグインを先ほどの設定ファイルにあるpluginに追加すれば、生成されるMarkdownからページ内リンクが除去され、LLMにとって読みやすい状態になります。

⚙️ Rslib のビルド設定

今回のMCPでは、ドキュメント出力だけでなく、そのTSDocをLLMに提供するためにMarkdownファイルをビルド対象に含める必要があります。そこで、ライブラリのビルドにはRslibを使用し、以下のように設定を行いました。

ESM形式でビルド

Node.jsで実行できるようにESM形式でビルドするための設定を行います。

// rsbuild.config.ts
import { defineConfig } from '@rslib/core';

export default defineConfig({
  source: {
    tsconfigPath: './tsconfig.build.json',
  },
  lib: [{
    format: 'esm',
  }],
});

Markdownファイルをバンドル対象に含める

TypeDocで出力した.mdファイルをMCPサーバーから読み込めるように、Markdownを asset/sourceとして読み込むようにします。

// rsbuild.config.ts
import { defineConfig } from '@rslib/core';

export default defineConfig({
  source: {
    tsconfigPath: './tsconfig.build.json',
  },
  lib: [{
    format: 'esm',
  }],
  tools: {
    rspack: {
      module: {
        rules: [
          {
            test: /\.md$/,
            type: 'asset/source',
          },
        ],
      },
    },
  },
});

これにより、.mdファイルをimportした場合に、文字列としてソースコード中に埋め込まれ参照出来るようになります。

Rspack独自の型定義の追加

今回はRspack独自の機能(import.meta.webpackContext)を利用するため、型エラーを回避するためにglobal.d.tsに型定義を追加します。

// global.d.ts
/// <reference types="@rspack/core/module" />

🧩 MCPサーバーの実装

TSDocで生成したMarkdownファイルをLLMから利用できるようにするため、@modelcontextprotocol/sdkを用いてMCPサーバーを構築します。

MCPサーバーの初期化

まずはMCPサーバーのインスタンスを生成します。

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import packageJson from '../package.json';

const server = new McpServer(
  {
    name: '@praha/byethrow',
    version: packageJson.version,
  },
  {
    instructions: 'Use this server to retrieve up-to-date documentation and code examples for @praha/byethrow.',
  },
);

ここでは、サーバー名とバージョン、LLMへの案内文を指定します。

ドキュメント読み込み関数の定義

LLMがドキュメントをリクエストしてきたときに対応するMarkdownを返す関数を定義します。

const loadDocument = (key: string) => {
  return import.meta.webpackContext('../docs')(key) as string;
};

import.meta.webpackContextにより、Rslibがバンドルした.mdファイルを動的に取得出来るようになります。

ツールの登録

MCPでは、LLMが呼び出す「ツール」として複数のエンドポイントを定義します。今回は以下の3種を用意しました。

  • ✅ モジュール一覧を返すModuleReference
server.tool(
  'ModuleReference',
  'Returns a overview of @praha/byethrow and a list of modules to be exported.',
  () => ({
    content: [{
      type: 'text',
      text: loadDocument('./modules/Result.md'),
    }],
  }),
);
  • ✅ 関数リファレンスを返すFunctionReference
import { z } from 'zod';

server.tool(
  'FunctionReference',
  'Returns a detailed reference of the functions that @praha/byethrow has and an example of how the functions are used.',
  { name: z.string().describe('The name of the function to reference.') },
  ({ name }) => ({
    content: [{
      type: 'text',
      text: loadDocument(`./functions/Result.${name}.md`),
    }],
  }),
);
  • ✅ 型リファレンスを返すTypeReference
server.tool(
  'TypeReference',
  'Returns a detailed reference of the types that @praha/byethrow has and an example of how the types are used.',
  { name: z.string().describe('The name of the type to reference.') },
  ({ name }) => ({
    content: [{
      type: 'text',
      text: loadDocument(`./types/Result.${name}.md`),
    }],
  }),
);

いずれもMarkdownファイルを文字列として読み込んで返すシンプルな構成です。
LLMはツールの説明を元に利用するツールを選定するため、なるべく詳細に記述するようにしましょう。

stdioトランスポートでLLMと接続

最後に、LLMがアクセスできるようstdio経由でMCPサーバーを起動します。

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

const transport = new StdioServerTransport();
await server.connect(transport);

ClaudeなどのLLMはこのstdioベースのMCPプロトコルを通じてドキュメントを取得できるようになります。

✅ MCPサーバーの動作確認

最後にMCPサーバーが正常に動作するか確認します。
VSCodeであればsettings.jsonに以下の設定を追加する事で動作確認出来ます。

// settings.json
{
  "mcp": {
    "servers": {
      "my-mcp-server": {
        "type": "stdio",
        "command": "node,
        "args": ["{MCPサーバーまでのパス}/dist/index.js"]
      }
    }
  }
}

出力に下記のようなログが表示されれば起動完了です。

[info] サーバー my-mcp-server を起動しています
[info] 接続状態: 開始しています
[info] Starting server from LocalProcess extension host
[info] 接続状態: 開始しています
[info] 接続状態: 実行中
[info] Discovered 3 tools

🎁 完成品の公開リポジトリ

今回紹介したMCPサーバーの実装や設定は、以下のリポジトリにてすべて公開しています。

https://github.com/praha-inc/byethrow/tree/main/packages/mcp

そのままローカルで立ち上げて挙動を試すことも可能です。

まとめ

LLMは万能ではなく、「ナレッジカットオフ」という明確な限界があります。しかし、MCPサーバーのように補助的な情報提供レイヤーを導入することで、その弱点をうまく補うことができます。

新しいライブラリを積極的に導入する開発チームや、社内独自のツールを活用している方は、ぜひこのような仕組みを試してみてください。

PrAha

Discussion