MCPクライアントの動作を確認してみた

# 背景・目的
以前、下記を実施しMCPサーバの挙動を確かめました。今回は、クライアントを作成し理解を進めます。

https://qiita.com/zumax/items/ec81f9fcca0929e4856d


# 実践

下記を元に試します。

https://modelcontextprotocol.io/quickstart/client


構成は下記のとおりです。([こちら](https://qiita.com/zumax/items/ec81f9fcca0929e4856d#claude-for-desktop%E3%81%A7%E3%82%B5%E3%83%BC%E3%83%90%E3%82%92%E3%83%86%E3%82%B9%E3%83%88%E3%81%99%E3%82%8B)と同じです。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/b67c987e-14a2-4f06-8dc1-c1215f4508c2.png)



## 環境準備

1. MCPクライアント用のディレクトリを作成します
    ```bash
    % uv init mcp-client
    Initialized project `mcp-client` at `/Users/XXXX/git/mcp-client`
    % cd mcp-client 
    % 
    ```

1. venvでPythonの仮想環境を作成します
    ```bash
    % uv venv
    Using CPython 3.12.5 interpreter at: 
    /Users/XXXX/.pyenv/versions/3.12.5/bin/python3.12
    Creating virtual environment at: .venv
    Activate with: source .venv/bin/activate
    % 
    ```

1. 仮想環境をアクティベートします
    ```bash
    % source .venv/bin/activate
    (mcp-client) % 
    ```

1. パッケージをインストールします
    ```bash
    (mcp-client)  % uv add mcp anthropic python-dotenv
    Resolved 26 packages in 257ms
    Prepared 4 packages in 221ms
    Installed 24 packages in 65ms
     + annotated-types==0.7.0
     + anthropic==0.52.1
     + anyio==4.9.0
     + certifi==2025.4.26
     + click==8.2.1
     + distro==1.9.0
     + h11==0.16.0
     + httpcore==1.0.9
     + httpx==0.28.1
     + httpx-sse==0.4.0
     + idna==3.10
     + jiter==0.10.0
     + mcp==1.9.2
     + pydantic==2.11.5
     + pydantic-core==2.33.2
     + pydantic-settings==2.9.1
     + python-dotenv==1.1.0
     + python-multipart==0.0.20
     + sniffio==1.3.1
     + sse-starlette==2.3.6
     + starlette==0.47.0
     + typing-extensions==4.13.2
     + typing-inspection==0.4.1
     + uvicorn==0.34.3
    (mcp-client)  % 
    ```

1. `main.py`を削除します
    ```bash
    (mcp-client) % ls -l main.py 
    -rw-r--r--  1 XXXXX  XXXXX  88  6  1 21:18 main.py
    (mcp-client) % cat main.py 
    def main():
        print("Hello from mcp-client!")
    
    
    if __name__ == "__main__":
        main()
    (mcp-client) % rm main.py 
    (mcp-client) % ls -l main.py
    ls: main.py: No such file or directory
    (mcp-client) % 
    ```

1. `client.py`を作成します
    ```bash
    (mcp-client) % touch client.py
    (mcp-client) % ls -l client.py 
    -rw-r--r--  1 XXXXX  XXXXX  0  6  1 21:25 client.py
    (mcp-client) % 
    ```

## APIキーの設定
### ClaudeのAPIキー発行
1. Anthropic Consoleにサインインします
1. ナビゲーションペインの歯車マークをクリックします
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/6190915e-9d63-4815-8b64-4cee3c9465b1.png" width=50 />

1. 「Billing」をクリックします
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/a999de11-22c3-4020-a3d7-181a1f0886aa.png" width=200 />

1. 「Buy credits」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/af2078d3-92b3-4873-bc73-edb9bb731152.png)

1. 必要事項を入力し、5 USDをチャージします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/1bf55194-bfe9-486a-92f5-33f16a13daa2.png)

1. 「API Keys」をクリックします
    <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/0fb709e7-618a-40da-b3d3-296e254ff8f0.png" width=140 />

1. 名前をつけて、「Add」をクリックします。API キーをメモしておきます

### APIキーの保存

1. `.env` ファイルを作成します
    ```bash
    (mcp-client) % touch .env
    ```

1. APIキーの保存します
    ```bash
    ANTHROPIC_API_KEY=<your key here>
    ```

1. `.gitignore`ファイルに`.env`を追加します
    ```bash
    % echo ".env" >> .gitignore
    ```

## クライアントの作成
### 基本的なクライアント構造
1. `client.py`に、基本的なクライアントクラスを作成します
    ```py
    import asyncio
    from typing import Optional
    from contextlib import AsyncExitStack
    
    from mcp import ClientSession, StdioServerParameters
    from mcp.client.stdio import stdio_client
    
    from anthropic import Anthropic
    from dotenv import load_dotenv
    
    load_dotenv()  # load environment variables from .env
    
    class MCPClient:
        def __init__(self):
            # Initialize session and client objects
            self.session: Optional[ClientSession] = None
            self.exit_stack = AsyncExitStack()
            self.anthropic = Anthropic()
        # methods will go here
    ```

- MCPクライアントの初期化
    - self.session:HTTPセッションを管理
    - self.exit_stack:複数の非同期コンテキストを安全に管理するためのスタック
    - self.anthropic:Anthropicクライアントの初期化

### サーバー接続管理
1. MCPサーバに接続するメソッドを追加します
    ```py
    async def connect_to_server(self, server_script_path: str):
        """Connect to an MCP server
    
        Args:
            server_script_path: Path to the server script (.py or .js)
        """
        is_python = server_script_path.endswith('.py')
        is_js = server_script_path.endswith('.js')
        if not (is_python or is_js):
            raise ValueError("Server script must be a .py or .js file")
    
        command = "python" if is_python else "node"
        server_params = StdioServerParameters(
            command=command,
            args=[server_script_path],
            env=None
        )
    
        stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
        self.stdio, self.write = stdio_transport
        self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
    
        await self.session.initialize()
    
        # List available tools
        response = await self.session.list_tools()
        tools = response.tools
        print("\nConnected to server with tools:", [tool.name for tool in tools])
    ```

- MCPサーバーへの接続を管理する
    - is_python、is_js：サーバースクリプトがPython（.py）かJavaScript（.js）かを判定
    - self.session:クライアントセッションを作成
    - tools = response.tools:サーバーで利用可能なツールの一覧を取得

### クエリ処理ロジック
1. クエリを処理し、ツール呼び出しを処理するためのコア機能を追加します
    ```py
    async def process_query(self, query: str) -> str:
        """Process a query using Claude and available tools"""
        messages = [
            {
                "role": "user",
                "content": query
            }
        ]
    
        response = await self.session.list_tools()
        available_tools = [{
            "name": tool.name,
            "description": tool.description,
            "input_schema": tool.inputSchema
        } for tool in response.tools]
    
        # Initial Claude API call
        response = self.anthropic.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=1000,
            messages=messages,
            tools=available_tools
        )
    
        # Process response and handle tool calls
        final_text = []
    
        assistant_message_content = []
        for content in response.content:
            if content.type == 'text':
                final_text.append(content.text)
                assistant_message_content.append(content)
            elif content.type == 'tool_use':
                tool_name = content.name
                tool_args = content.input
    
                # Execute tool call
                result = await self.session.call_tool(tool_name, tool_args)
                final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
    
                assistant_message_content.append(content)
                messages.append({
                    "role": "assistant",
                    "content": assistant_message_content
                })
                messages.append({
                    "role": "user",
                    "content": [
                        {
                            "type": "tool_result",
                            "tool_use_id": content.id,
                            "content": result.content
                        }
                    ]
                })
    
                # Get next response from Claude
                response = self.anthropic.messages.create(
                    model="claude-3-5-sonnet-20241022",
                    max_tokens=1000,
                    messages=messages,
                    tools=available_tools
                )
    
                final_text.append(response.content[0].text)
    
        return "\n".join(final_text)
    ```

- ユーザーのクエリを処理し、Claudeとツールを連携させる
    - messages:ユーザーのクエリを初期メッセージとして設定
    - response = await self.session.list_tools():サーバーから利用可能なツールの一覧を取得し、各ツールの名前、説明、入力スキーマを収集
    - self.anthropic.messages.create：Claudeにクエリを送信
    - content in response.content：Claudeの応答を解析
    - messages.append：ツールの実行結果を会話履歴に追加
    - self.anthropic.messages.create：Claudeの応答を取得するための処理です


### インタラクティブチャットインターフェース
1. チャットループとクリーンアップ機能を追加します
    ```py
    async def chat_loop(self):
        """Run an interactive chat loop"""
        print("\nMCP Client Started!")
        print("Type your queries or 'quit' to exit.")
    
        while True:
            try:
                query = input("\nQuery: ").strip()
    
                if query.lower() == 'quit':
                    break
    
                response = await self.process_query(query)
                print("\n" + response)
    
            except Exception as e:
                print(f"\nError: {str(e)}")
    
    async def cleanup(self):
        """Clean up resources"""
        await self.exit_stack.aclose()
    ```


- チャットループを実装
    - print("\nMCP Client Started!"):初期メッセージ
    - uery = input("\nQuery: ").strip()：input関数でユーザ入力を待つ

### メインエントリーポイント
1. メインの実行ロジックを追加します
    ```py
    async def main():
        if len(sys.argv) < 2:
            print("Usage: python client.py <path_to_server_script>")
            sys.exit(1)
    
        client = MCPClient()
        try:
            await client.connect_to_server(sys.argv[1])
            await client.chat_loop()
        finally:
            await client.cleanup()
    
    if __name__ == "__main__":
        import sys
        asyncio.run(main())
    ```

- MCPClientのインスタンスを作成
- サーバーに接続
- チャットループを開始
- 終了時にクリーンアップを実行

## クライアントの実行
1. 最初に[weather.api](https://qiita.com/zumax/items/ec81f9fcca0929e4856d#claude-for-desktop%E3%81%A7%E3%82%B5%E3%83%BC%E3%83%90%E3%82%92%E3%83%86%E3%82%B9%E3%83%88%E3%81%99%E3%82%8B)を実行します
    ```bash
    (weather) % uv run weather.py        
    ```

1. client.pyを実行します
    ```bash
    (mcp-client)  % uv run client.py ../weather/weather.py
    Processing request of type ListToolsRequest
    
    Connected to server with tools: ['get_alerts', 'get_forecast']
    
    MCP Client Started!
    Type your queries or 'quit' to exit.
    
    Query: 
    ```

1. 東京の天気を探してみます
    ```bash
    Query: 東京の天気は？
    Processing request of type ListToolsRequest
    Processing request of type CallToolRequest
    HTTP Request: GET https://api.weather.gov/points/35.6762,139.6503 "HTTP/1.1 404 Not Found"
    
    申し訳ありませんが、このシステムでは日本の天気予報を直接取得することはできません。利用可能な機能は:
    
    1. アメリカの州の気象警報を取得する機能
    2. 緯度と経度を指定して天気予報を取得する機能
    
    東京の天気を知りたい場合は、東京の緯度（約35.6762°N）と経度（約139.6503°E）を使って検索することができます。確認してみましょうか？
    [Calling tool get_forecast with args {'latitude': 35.6762, 'longitude': 139.6503}]
    申し訳ありませんが、このシステムでは東京の位置の天気予報データを取得することができませんでした。このツールはアメリカ国内の天気予報に特化している可能性があります。日本の天気予報については、気象庁のウェブサイトや他の天気予報サービスをご利用いただくことをお勧めします。
    
    Query: 
    ```

1. ロサンゼルスの天気を調べてみます。表示されました。
    ```bash
    Query: ロサンゼルスの天気は？
    Processing request of type ListToolsRequest
    Processing request of type CallToolRequest
    HTTP Request: GET https://api.weather.gov/points/34.0522,-118.2437 "HTTP/1.1 200 OK"
    HTTP Request: GET https://api.weather.gov/gridpoints/LOX/155,45/forecast "HTTP/1.1 200 OK"
    
    ロサンゼルスの天気予報を取得するために、get_forecastを使用しますが、緯度(latitude)と経度(longitude)の情報が必要です。
    
    ロサンゼルスの代表的な座標を使用して天気予報を確認しましょう。ロサンゼルスの中心部の座標はおよそ緯度34.0522°N、経度118.2437°Wです。
    [Calling tool get_forecast with args {'latitude': 34.0522, 'longitude': -118.2437}]
    ロサンゼルスの天気予報をお伝えします：
    
    現在から明日にかけて：
    - 夜間は気温59°F（約15°C）で、霧が断続的に発生し、曇り空。南西からの弱い風（0-5 mph）。
    
    土曜日：
    - 日中は気温76°F（約24°C）まで上昇。
    - 午前11時までは霧が断続的に発生。
    - その後は概ね晴れ。
    - 南西の風0-10 mph。
    
    土曜日夜：
    - 気温は59°F（約15°C）まで下がる。
    - 午前5時以降は霧が発生する可能性。
    - 部分的に曇り。
    
    日曜日：
    - 気温は80°F（約27°C）まで上昇。
    - 午前中は霧の可能性があるが、その後は概ね晴れ。
    - 南西の風0-10 mph。
    
    日曜日夜：
    - 気温61°F（約16°C）。
    - 夜11時以降は霧の可能性。
    - 部分的に曇り。
    
    全体的に見ると、週末は穏やかで比較的温暖な天気が続く見込みです。朝晩は霧の発生に注意が必要ですが、日中は晴れて過ごしやすい天気になりそうです。
    
    Query: 
    ```

## APIのCostを確認
1. Anthropic Consoleにサインインします

### API Keys
1. Settingsをクリックし、API Keysをクリックします
1. ClaudeのAPIキーで使用したコストを確認します。何回か確認したので、0.08USDになっています
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/8ea31592-e16f-4db9-b90c-3c605c73ec13.png)

### Cost
1. Costタブをクリックします
1. より詳細な使用状況が確認できました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/f3a418e0-05b8-4734-9cde-e73cd0c1af2d.png)


# 考察
今回、ClaudeのMCP構成をクライアントで構築した。MCPサーバとLLMの挙動がわかりより理解が深まりました。
今後は、複数ツールの連携を確認してみたいと思います。


# 参考
https://qiita.com/zumax/items/0d5efe0177fbb427c927

https://qiita.com/zumax/items/0d5efe0177fbb427c927

https://qiita.com/zumax/items/ec81f9fcca0929e4856d

https://modelcontextprotocol.io/quickstart/client