Skip to main content
Glama
zero3kw

mcp-docs-template

by zero3kw
README.md
# mcp-docs-template

手元の md を Claude から**関連度順に検索**できるようにする MCP サーバのテンプレート。

`docs/` に md を置くだけで、`##` 見出しごとに BM25 で索引化される。追加依存は `mcp` パッケージのみ、
索引は起動時にメモリ上に作るので DB もビルド手順も要らない。

devcontainer 込みなので、clone して開けば Claude Code と Codex がそのまま使える
([claude-devcontainer-template](https://github.com/zero3kw/claude-devcontainer-template) と同じ構成)。

## 先に読むこと: 本当に MCP が要るか

**完全一致で足りるなら Grep のほうが速い。** このサーバが効くのは次の2つが要るときだけ:

- **語を分けても拾える** — 「節 検索」で検索すると、同梱の [docs/example.md](docs/example.md) の
  「検索のしかた」節が返る。`rg "節 検索"` のほうは 0 件(連続していないと当たらない)
- **関連度順の上位N件** — grep のヒット順はファイル順なので、100件当たったら100件読むことになる

さらに、Claude Code だけで使うなら **Skill でも同じことができる**。BM25 の実装([scripts/](scripts/))は
どちらの形でもそのまま使えるので、MCP を選ぶ理由は次のどちらかに絞られる:

| | MCP にする | Skill で足りる |
| --- | --- | --- |
| 使う側 | Claude Desktop や他エディタからも使う | Claude Code だけ |
| コンテキスト | ツール定義が常時載る | `description` 1行だけ |

## 使い方

### 1. テンプレートから作る

GitHub の "Use this template"、またはローカルで:

```sh
git clone <this-repo> my-docs && cd my-docs && rm -rf .git && git init
```

### 2. md を置く

`docs/` に置く。ファイル名が文書 id、本文の `# ` 行がタイトルになる。
[docs/example.md](docs/example.md) が形式の見本なので、確認したら消す。

元資料が Word / PowerPoint / Excel / PDF / Web ページなら、同梱の
[import-docs](.claude/skills/import-docs/SKILL.md) スキルに変換させる:

```
/import-docs  この資料を取り込んで
```

**検索の単位は `##` 節**なので、変換の主目的は意味のまとまりごとに見出しを付けることになる。
単にテキストを抜いただけでは索引が効かない。スキルはそこまで面倒を見る。

出典を残すなら先頭にフロントマターを書く。索引には入らない:

```markdown
---
source: https://example.com/some-document
captured: 2026-01-01
---
```

**節が検索の単位**なので、見出しを細かく切るほど結果が絞り込まれる。見出しの無い長文は
1節として丸ごと返るため、長い資料は `##` を入れてから置く。

### 3. コンテナを開く

VS Code で `Dev Containers: Reopen in Container`。`pip install -r requirements.txt` は
`postCreateCommand` が済ませる。

`.mcp.json` はプロジェクトスコープの設定なので、Claude Code が自動で読む。初回は
ワークスペースの信頼を尋ねられるので承認する。接続できているかは `claude mcp list` で分かる。

パスは**リポジトリルートからの相対**にしてある。絶対パスに書き換えるとディレクトリ名を変えたときに
壊れる。`${workspaceFolder}`(VS Code の変数)や `${CLAUDE_PROJECT_DIR}` は `.mcp.json` では
展開されず、未定義の環境変数として警告になるので使わない。

### 4. 使う

```
list_documents()                     索引している文書を出典つきで一覧
get_document(document, section)      指定文書の全文、または節だけ
search_docs(query, limit)            節単位で横断検索(関連度順)
reload_index()                       docs/ を読み直して索引を作り直す
```

## 資料を取り込む流れ

例として Word 文書を取り込む場合:

```
1. source/報告書.docx に置く
2. Claude に「source/報告書.docx を取り込んで」と頼む
     → import-docs スキルが起動し、pandoc で変換して ## 見出しを整え、
       出典つきで docs/report.md を書く
3. Claude が reload_index() を呼ぶ
4. search_docs("...") で引けるようになる
```

**3 を飛ばすと引けない。** 索引は起動時にメモリ上に作るので、md を足しただけでは反映されない。
stdio の MCP サーバは Claude Code から再起動されないため、セッションを続けたまま反映するには
`reload_index()` を呼ぶ(スキルの手順にも入っている)。

## リポジトリ外の md も索引する

個人メモなど、コミットしたくない md を足したいとき:

```jsonc
// .mcp.json
"env": { "MCP_EXTRA_DOCS_DIRS": "/path/to/notes:/path/to/more" }
```

`os.pathsep`(Linux では `:`)区切りで複数指定できる。`docs/` と同じ形式で索引される。
**中身は検索時に LLM へ渡る**点に注意。

## ツールを足す

[scripts/mcp_server.py](scripts/mcp_server.py) に `@mcp.tool()` を付けた関数を書くだけ。
型注釈と docstring がそのまま Claude 側のツール定義になるので、引数の説明は docstring の
`Args:` に書く。

```python
@mcp.tool()
def count_sections(document: str) -> str:
    """指定した文書の節数を返す。

    Args:
        document: 文書id か タイトル(部分可)
    """
    ...
```

## 構成

```
docs/      変換後の md。コミットする(索引対象)
source/    変換元の .docx / .pptx / .xlsx / .pdf。gitignore 済みでコミットしない
scripts/   MCP サーバ本体
```

変換元をコミットしないのは、機密や著作物を公開リポジトリに入れる事故を防ぐため。
出所は変換後の md のフロントマターに残るので、URL のあるものは取り直せる。

| ファイル | 中身 |
| --- | --- |
| [scripts/mcp_server.py](scripts/mcp_server.py) | MCP サーバ本体、BM25、md のパース |
| [scripts/common.py](scripts/common.py) | パス解決、表記の正規化、bigram トークナイザ |
| [.mcp.json](.mcp.json) | プロジェクトスコープの MCP サーバ登録 |
| [.claude/skills/import-docs/](.claude/skills/import-docs/) | 元資料を md に変換するスキル |

変換に使うもの(**MCP の実行には不要**。使うときだけ入れる):

| 形式 | 手段 |
| --- | --- |
| Word (.docx) | `pandoc -t gfm` |
| PowerPoint (.pptx) | 同梱の `pptx_to_md.py`(pandoc は pptx を読めない) |
| Excel (.xlsx) | 同梱の `xlsx_to_md.py`(同上) |
| PDF | `Read` ツールでページを読んで書き起こす(抽出ツールより表の再現が効く) |
| Web | `WebFetch` |

## 検索の限界

形態素解析を使わない。英数字は単語、かな漢字は文字bigramに分割して BM25 にかけるだけなので、
辞書も追加依存も要らないかわりに次が効かない:

- **同義語や言い換え** — 「価格」で「値段」は出ない
- **英数字・かな・漢字**以外の文字。ハングルやキリル文字はトークンが空になり、
  エラーも出さずに検索結果から消える。使うなら `common.py` の `_CJK` / `_WORD` を広げる

長い文書は `get_document` が全文を返さず節の一覧を返す(既定 8,000 文字超)。
資料1本でコンテキストを使い切らないための制限で、閾値は `mcp_server.py` の `_MAX_FULL`。