0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

背景・目的

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

実践

下記を元に試します。

構成は下記のとおりです。(こちらと同じです。)
image.png

環境準備

  1. MCPクライアント用のディレクトリを作成します

    % uv init mcp-client
    Initialized project `mcp-client` at `/Users/XXXX/git/mcp-client`
    % cd mcp-client 
    % 
    
  2. venvでPythonの仮想環境を作成します

    % 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
    % 
    
  3. 仮想環境をアクティベートします

    % source .venv/bin/activate
    (mcp-client) % 
    
  4. パッケージをインストールします

    (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)  % 
    
  5. main.pyを削除します

    (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) % 
    
  6. client.pyを作成します

    (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にサインインします

  2. ナビゲーションペインの歯車マークをクリックします

  3. 「Billing」をクリックします

  4. 「Buy credits」をクリックします
    image.png

  5. 必要事項を入力し、5 USDをチャージします
    image.png

  6. 「API Keys」をクリックします

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

APIキーの保存

  1. .env ファイルを作成します

    (mcp-client) % touch .env
    
  2. APIキーの保存します

    ANTHROPIC_API_KEY=<your key here>
    
  3. .gitignoreファイルに.envを追加します

    % echo ".env" >> .gitignore
    

クライアントの作成

基本的なクライアント構造

  1. client.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サーバに接続するメソッドを追加します
    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. クエリを処理し、ツール呼び出しを処理するためのコア機能を追加します
    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. チャットループとクリーンアップ機能を追加します
    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. メインの実行ロジックを追加します
    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を実行します

    (weather) % uv run weather.py        
    
  2. client.pyを実行します

    (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: 
    
  3. 東京の天気を探してみます

    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: 
    
  4. ロサンゼルスの天気を調べてみます。表示されました。

    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をクリックします
  2. ClaudeのAPIキーで使用したコストを確認します。何回か確認したので、0.08USDになっています
    image.png

Cost

  1. Costタブをクリックします
  2. より詳細な使用状況が確認できました
    image.png

考察

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

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?