Ollamaで動くMCPクライアントを作成する

## はじめに

近年、AIアシスタントの開発では複数の外部ツールやAPIを統合するニーズが高まっています。  
中でもMCPサーバーに関する話題は毎日のように新しいニュースがあり、あれもこれもMCPを実装して使えるようになったと耳にします。
MCPクライアントは、Cline, Claude desktop, Cursorなど既存のアプリケーションを前提にしているものが多いです。
でも、OllamaなどローカルPCでLLMを動かして、その恩恵を確認したいですよね。Ollamaで動くMCPクライアントを自作してみたいですよね。

本記事では、[langchain-mcp-adapters](https://github.com/langchain-ai/langchain-mcp-adapters) の `MultiServerMCPClient` を活用し、  
複数のMCPサーバーに対応したMCPクライアントを [Ollama](https://ollama.com/) のローカルLLMと組み合わせて構築する方法を解説します。


## 1. 試したこと
- 複数のMCPサーバーを同時に利用する方法を考える
- ローカルLLM（Ollama）と組み合わせたMCPクライアントを構築する
- LangChainを使うとできるらしいぞ

---

## 2. 必要なライブラリ・環境

- Python 3.12
- [Ollama](https://ollama.com/)（ローカルでLLMを動かす）
- langchain, langchain-mcp-adapters, langchain-ollama, langgraph など

```bash
pip install langchain langchain-mcp-adapters langchain-ollama langgraph
```

---

## 3. MCPとは？MultiServerMCPClientとは？

- **MCP (Modular Command Protocol)**  
  外部ツールやAPIを統一的に呼び出すためのプロトコル。
- **MultiServerMCPClient**  
  複数のMCPサーバーを同時に扱えるクライアント。  
  サーバーごとに異なるツール群を統合し、LangChainエージェントから一元的に利用可能。
  langchain-mcp-adaptersは以下のように概念図が示されています。
  ![image.png](https://github.com/langchain-ai/langchain-mcp-adapters/raw/main/static/img/mcp.png)


---

## 4. 構成

```mermaid
flowchart BT
    user[ユーザー]
    ollama[ローカルLLM（Ollama）]
    agent[LangChain Agent]
    tools[Tools（MCP経由）]
    mcpweather["MCP Server (weather)"]
    mcpmath["MCP Server (Math)"]

    user --> agent
    agent --> ollama
    agent --> tools
    tools --> mcpweather
    tools --> mcpmath
```

---

## 5. サンプルコード解説

### 設定ファイル例

`claude_desktop_config.json` からMCPサーバーの情報を取得します。
これはClaudeデスクトップを使用しているとMCPサーバーの設定が以下のように書き込まれていきます。

```json
{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": [
        "--directory",
        "D:\\mpc\\weather",
        "run",
        "weather.py"
      ]
    },
    "Math": {
      "command": "uv",
      "args": [
        "run",
        "--with",
        "mcp[cli]",
        "mcp",
        "run",
        "D:\\mcp\\math_server.py"
      ]
    }
  }
}
```

### サーバーの中身
- **weather**
これはopenweathermapにアクセスして天気を取得するもので、FastMCPで実装されたMCPサーバーです。よくあるサンプル的なものですね。
- **math**
こちらもFastMCPで実装されたMCPサーバーです。AIがToolをちゃんと使っているかなど確認するために足し算をわざと間違った解となるようにしています。(正解+1にしています)
```python
@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b + 1
```

### メインコード
まずOllama自体はローカルPCにセットアップされてport:11434で接続できることと、llama3.2を使用していることを前提にしています。

```python
import asyncio
import os
import sys
import json
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
from langchain_core.messages import SystemMessage
from langchain_ollama import ChatOllama

# LLMのセットアップ
llm = ChatOllama(
    model="llama3.2",
    temperature=0,
    base_url="http://localhost:11434",
)

# MCPサーバー設定の読み込み
CONFIG_PATH = os.path.join(os.path.expanduser("~"), "AppData", "Roaming", "Claude", "claude_desktop_config.json")

def load_json_config(path=CONFIG_PATH):
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)
config = load_json_config().get("mcpServers", {})

def print_messages_by_type(result):
    messages = result.get("messages", result)  # result["messages"] or result itself if already a list
    if not isinstance(messages, list):
        print("messagesがリストではありません")
        return
    for msg in messages:
        # クラス名またはtype属性で判定
        msg_type = msg.get("type") if isinstance(msg, dict) else type(msg).__name__
        if not msg_type and hasattr(msg, "__class__"):
            msg_type = msg.__class__.__name__
        print(f"\n--- {msg_type} ---")
        if isinstance(msg, dict):
            for k, v in msg.items():
                print(f"{k}: {v}")
        else:
            print(msg)
            
async def main():
    # マルチサーバークライアントの設定
    async with MultiServerMCPClient(config) as client:
        sys_message = SystemMessage(
            content="あなたはMCPサーバーを使用するAIアシスタントです。"\
                "Toolの結果を優先して回答として採用してください"\
                "回答は日本語でお願いします。"\
            )
        # ReactエージェントでLLMとクライアントを接続
        agent = create_react_agent(llm, client.get_tools(), prompt=sys_message)
        
        # エージェントにクエリを送信
        # result = await agent.ainvoke({"messages": "What is the weather in San Francisco?"})
        result = await agent.ainvoke({"messages": "東京(Tokyo)の天気は？"})
        print_messages_by_type(result)

        print("\n", "*" * 50)

        result = await agent.ainvoke({"messages": "What is 2 + 2?"})
        print_messages_by_type(result)
        
if __name__ == "__main__":
    asyncio.run(main())
```

---

## 6. 実行例
一部省略しています。足し算が5になっているので、toolをちゃんと使っていますね。
```
--- HumanMessage ---
content='東京(Tokyo)の天気は？' 

--- ToolMessage ---
content='Tokyoの天気はmoderate rain、気温は16度です。' 

--- AIMessage ---
content='東京の天気は、雨が降っています。気温は16度でした。 (Toolの結果を使用して回答を生成しました)

 **************************************************

--- HumanMessage ---
content='What is 2 + 2?' 

--- ToolMessage ---
content='5' name='add' 

--- AIMessage ---
content='答えは 5 です。' 
```

---

## 7. まとめ

- `MultiServerMCPClient` を使うことで、複数のMCPサーバーのツールを統合し、LLMエージェントから一元的に利用できます。
- OllamaなどローカルLLMと組み合わせることも可能びっく
- MCPサーバーの追加・切り替えも設定ファイルで柔軟に対応できそうです。

---

## 参考リンク

- [langchain-mcp-adapters GitHub](https://github.com/langchain-ai/langchain-mcp-adapters)
- [Ollama公式](https://ollama.com/)
- [LangChain公式](https://python.langchain.com/)

---

ご参考になれば幸いです！