Claudeに国立国会図書館の知識を与えてみた

# はじめに

XやQiitaなどでMCPサーバーが話題となっており、自分でも触ってみたくなりました。

つい最近、国立国会図書館デジタルコレクションなるものがあることを知ったのですが、ちょっと検索するだけで終わってしまったので、
これをLLMで扱うことができたら面白いかなと思い、MCPサーバーを作ることにしました。

言語はPythonを使っています。

# やったこと

国立国会図書館デジタルコレクションで扱われている資料を検索できるAPIとして、次世代デジタルライブラリーAPIというものがあります。

今回はこのAPIをラップするだけのMCPサーバーを作成してみました。

https://lab.ndl.go.jp/service/tsugidigi/

https://lab.ndl.go.jp/dl/swagger-ui/

# 作成するツールについて

作成したツールは全部で3つです

1. search_books
    - 資料の横断検索が可能
    - 入力値
        - `keyword`(string): 検索キーワード
2. search_in_book
    - 指定した資料内の全文検索が可能
    - 入力値
        - `book_id`(string): 資料のPID
        - `keyword`(string): 検索キーワード
3. get_page
    - 指定したページのデータを取得可能
    - 入力値
        - `book_id`(string): 資料のPID
        - `page_number`(int): ページ番号

# 作成手順

## 環境設定

uvで環境を作成します。
ここはMCPの[QuickStart](https://modelcontextprotocol.io/quickstart/server)とまったく同じです。

```bash
$ uv init ndl
$ cd ndl

$ uv venv
$ source .venv/Scripts/activate

$ uv add mcp[cli] httpx
$ touch ndl.py
```

## コード

### FastMCP

はじめにFastMCPサーバーの定義をします。
FastMCPはMCPサーバーを作るためのライブラリです。
これを使うことでクライアントとサーバー間の通信などを意識せずMCPサーバーを簡単に作成することが可能です。

```python:ndl.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("ndl")

if __name__ == "__main__":
    mcp.run(transport='stdio')
```

今回は標準入出力で通信を行うため、`mcp.run`の引数を`stdio`にしています。

### 共通関数

次にAPIにリクエストを送るための関数を作成します。

```python:ndl.py
from typing import Any
import httpx
import json

API_BASE_URL = "https://lab.ndl.go.jp"

async def make_request(url: str) -> dict[str, Any] | None:
    """Make a GET request to the NDL API."""
    async with httpx.AsyncClient() as client:
        response = await client.get(url, timeout=10.0)
        if response.status_code == 200:
            return response.json()
        else:
            return None
```

この関数は、引数で受け取ったURLに対してリクエストを投げ、帰ってきたレスポンスをJSONにしてそのまま返すだけの関数です。

### ツール

次にツールを作成します。

`@mcp.tool`のアノテーションを付与することによって、MCPスキーマを自動で生成してくれ、クライアントからツールとして認識できるようになります。

ここでは受け取る引数と、出力する文字列の定義を行います。
返却されるレスポンスの中から、必要な情報を抜き出し、文字列として返却します。

```python:ndl.py
@mcp.tool()
async def search_books(keyword: str) -> dict[str, Any] | None:
    """Search for books using the NDL API."""
    url = f"{API_BASE_URL}/dl/api/book/search?keyword={keyword}"
    data = await make_request(url)

    if not data:
        return None
    
    books = []
    for book in data["list"]:
        books.append(
            f"""
Id: {book['id']}
Title: {book['title']}
Volume: {book['volume']}
Responsibility: {book['responsibility']}
Publishyear {book['publishyear']}
            """
        )

    return "\n---\n".join(books)

@mcp.tool()
async def search_in_book(book_id: str, keyword: str) -> dict[str, Any] | None:
    """Search for a keyword in a book using the NDL API."""
    url = f"{API_BASE_URL}/dl/api/page/search?f-book={book_id}&q-contents={keyword}"
    data = await make_request(url)

    if not data:
        return None

    pages = []
    for page in data["list"]:
        pages.append(
            f"""
Id: {page['id']}
PageNumber: {page['page']}
Contents: {page['contents']}
            """
        )
    
    return "\n---\n".join(pages)

@mcp.tool()
async def get_page(book_id: str, page_number: int) -> dict[str, Any] | None:
    """Get a specific page of a book using the NDL API."""
    url = f"{API_BASE_URL}/dl/api/page/{book_id}_{page_number}"
    data = await make_request(url)

    if not data and data["coordjson"] is None:
        return None
    
    logging.info(f"ContentText: {data['coordjson']}")

    coordjson = json.loads(data["coordjson"])
    content_text = "".join([item["contenttext"] for item in coordjson])

    return f"""
ContentText: {content_text}
    """
```

## MCPサーバーの設定

ここまで作成したらClaudeにMCPサーバーの設定をすれば、MCPサーバーを使用することが可能です。

Claudeデスクトップの場合は、ファイル > 設定 > 開発者 > 構成を編集 で参照できる`claude_desktop_config.json` を編集することでMCPサーバーを追加することができます。

```json:claude_desktop_config.json
{
  "mcpServers": {
    "ndl": {
      "command": "/path/to/uv",
      "args": [
          "--directory",
          "/path/to/ndl",
          "run",
          "ndl.py"
      ]
    }
  }
}
```

# 使用例

実際にMCPサーバーをClaudeに使ってもらいました。

```
すき焼きについて過去の書籍でどんな記述があるか簡単にまとめて
```

```
日本の伝統料理であるすき焼きについて、過去の書籍での記述を調べてみましょう。
国立国会図書館のデータベースを検索して情報をまとめます。
---
中略
---

検索結果からより関連性の高い書籍を詳しく調べてみましょう。
「最新割烹指導書」と「一年間のお惣菜」はすき焼きについての情報が含まれている可能性が高いです。  
---
中略
---

すき焼きに関する古い書籍から得られた情報をまとめてみましょう：
すき焼きに関する古書籍の記述まとめ

料理書での分類：

1927年発行の「最新割烹指導書」では、すき焼きは「鍋物」として分類されており、
他の鍋物料理（鶏の水炊き、ちり鍋、どて焼き、寄せ鍋、鯨鍋など）と並んで紹介されています。


具材と調理法：

1930年発行の「一年間のお惣菜」では、伝統的な牛肉のすき焼きだけでなく、
「いかのすき焼き」という海鮮バージョンが紹介されています。
いかのすき焼きは、いかを短冊に切り、焼豆腐や葱と一緒に醤油や味醂で味付けした煮汁で調理します。
いかは長く煮ると固くなるため、火が通る程度に短時間で調理することが推奨されています。


調理のコツ：

いかのすき焼きでは、初めに野菜を煮てから後でいかを入れるようにと指示されています。
また、「ロース肉の附焼き」という牛肉料理では、肉を「すき焼のように切って」という記述があり、
すき焼きの肉の切り方が一般的な知識として認識されていたことがわかります。


地域性や季節性：

書籍の目次から、すき焼きは日本の伝統的な鍋物として位置づけられていたことが窺えます。
春や夏の料理としてもすき焼きが登場しており、季節を問わず親しまれていたことがわかります。



これらの記述から、すき焼きは少なくとも1920年代には日本の家庭料理として広く認知され、
調理法も確立されていたことがわかります。
また、牛肉だけでなくいかなど多様な材料を用いたバリエーションも存在していたことが興味深い点です。
```

こんな感じで全体からキーワードで書籍を検索、見つけてきた書籍でさらにキーワード検索を行い、まとめてくれました。
今回は、`search_books` と `search_in_book` を使っていました。

内容が前後のページにまたがる場合は、`get_page` を使ってさらに詳しい内容を見てくれると思います。

# 参考文献

https://lab.ndl.go.jp/service/tsugidigi/

https://lab.ndl.go.jp/dl/swagger-ui/

https://modelcontextprotocol.io/introduction

https://github.com/jlowin/fastmcp