📑

MCP Clientの作り方と注意点(Windows10)

2024/12/18に公開

はじめに

事前に説明しておきますが、2024年12月12日時点でのガイドのコードだとほぼ必ずエラーが起こります。エラーの内容と修正案を確認したい方は問題発生項目まで飛んでください。

AnthropicがModel Context Protocolを発表したことを知ったので、早速ガイドに沿って実装してみました。
参考ガイド

注意点

たぶん元のガイドはAnthropicがAIに書かせたものかと思うので、幾ばくかWindowsで実行できないコマンドが混ざっています。そういったコマンドは修正しています。

環境:

  • OS: Windows10
  • IDE: VSCode
  • Programming Language: Python
  • Version Control: uv
  • 前提: QuickstartにてweatherのMCP Serverを構築済み

環境構築

Windowsでのコマンドです。Unix/Macバージョンは元のガイドを参考してください。
Quickstartで初めてのMCP Serverを構築済みであればuvはinstall済のはずです。
uvはRustで書かれた実行速度が速いcondaみたいなPackage管理ツールです。

1. uvでプロジェクトの生成

Clientを作成したいフォルダに移動してuvでプロジェクトを作成します。

Command Prompt
uv init mcp-client
cd mcp-client

uvでVirtual Environmentを作成し、activateします。

Command Prompt
uv venv
.venv\Scripts\activate

Clientに必要なPackagesをinstallします。

Command Prompt
uv add mcp anthropic python-dotenv

デフォルトで作成されたhello.pyを削除し、client.pyを作成します。

Command Prompt
del hello.py
call>client.py

PowerShellでコマンドを打っている場合は、New-Itemを使います。

PowerShell
New-Item client.py

愚痴:どうしてcmdとPowerShellで使えるコマンドが違うんでしょうね。

2. 使用しているAIのAPI Keyを保存する

Anthropicのガイドに沿っているので、ここではAnthropicのAPIを使います。
作成されていない方はこちらから。
.envファイルを作成してAPI Keyを管理します。

Command Prompt
call>.env 

API Keyを書き込んで、

.env
ANTHROPIC_API_KEY=<your key here>

.env.gitignoreに付け加えます。API Keyは決して公開してはなりません。

Command Prompt
echo .env>> .gitignore

Client実装

この項目では解説も含めてAnthropicの元のガイドに沿って実装していきます。

1. Basic Client Structure

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

Clientが必要なpackagesを読み込み、load_dotenv().envファイルのAPI Keyを読み込んでいます。

2. Server Connection Management

この関数は、Clientを立ち上げた後に最初に実行する役割を持ちます。Quickstartで作成したMCP Serverを起動し、ClientとServer間のセッションを確立します。

動作の手順:

  1. サーバースクリプトの指定
    MCP Serverの実行ファイル(.pyまたは.js)を引数として受け取ります。

  2. ファイルの種類を判別
    実行ファイルがPythonスクリプト(.py)かJavaScriptスクリプト(.js)かを確認します。
    ファイルがどちらでもない場合、エラーを発生させます。

  3. 実行コマンドを構築
    スクリプトの種類に応じて、pythonまたはnodeのコマンドを選択し、それをmcp.StdioServerParameters()に格納します。

  4. セッションの確立
    構築したコマンドを使ってMCP Serverを起動し、ClientとServerのセッションを確立します。

  5. 使用可能なツールを取得
    MCP Serverから使用可能なツールリストを取得します(例:weather用のget-alertsget-forecast)。

client.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])

3. Query Processing Logic

この関数は、ユーザーからのクエリを処理する役割を持ちます。

動作の手順:

  1. 使用可能なツールを取得
    MCP Serverから利用可能なツール(tools)のリストを取得します。

  2. Claudeにクエリとツール情報を送信
    ユーザーからのクエリと、取得したツールの情報をClaudeに送信します。

  3. Claudeの返信を処理
    Claudeの返信内容を解析し、以下のように処理します:

    • テキストの読み込み
      Claudeの返信にテキストが含まれている場合、それをfinal_textに追加します。
    • ツールの呼び出し
      Claudeの返信が特定のツールの使用を要求している場合、MCP Serverを介してそのツールを実行し、結果をClaudeに返します。
      その後、ツールの結果を元に再びClaudeに応答をリクエストします。
  4. 最終的なテキストを返す
    final_textに収集した内容を結合し、ユーザーに返します。

client.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
        tool_results = []
        final_text = []
    
        for content in response.content:
            if content.type == 'text':
                final_text.append(content.text)
            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)
                tool_results.append({"call": tool_name, "result": result})
                final_text.append(f"[Calling tool {tool_name} with args {tool_args}]")
    
                # Continue conversation with tool results
                if hasattr(content, 'text') and content.text:
                    messages.append({
                      "role": "assistant",
                      "content": content.text
                    })
                messages.append({
                    "role": "user", 
                    "content": result.content
                })

                # Get next response from Claude
                response = self.anthropic.messages.create(
                    model="claude-3-5-sonnet-20241022",
                    max_tokens=1000,
                    messages=messages,
                )
    
                final_text.append(response.content[0].text)
    
        return "\n".join(final_text)

このfunctionを理解すれば、Claudeがどのように外部APIを利用しているのがわかるはずです。

ClaudeなどのAIはLLM(大規模言語モデル)であり、文章を生成することはできますが、自分で外部APIを直接呼び出すことはできません。しかし、使用可能なツールを教えると、そのツールを利用するための指示を書き出すことができます。

MCP Clientは、AIが「ツールを使いたい」という要望を受け取り、代わりにそのツールを実行します。これにより、あたかもAI自身がツールを操作しているような形で機能します。

例えるなら、AIが人間の脳だとすると、MCP Clientはその体にあたります。脳が考えたことを体が実際に行動に移す、そんな仕組みです。

4. Interactive Chat Interface

この関数は、ユーザーからのクエリを受け取るインターフェースとして動作します。ユーザーが「quit」と入力するまで、対話を継続する仕組みです。

client.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()

  1. chat_loop 関数
    • インターフェースとして、ユーザーのクエリを待ち受けます。
    • ユーザーが「quit」を入力するまで、クエリを処理し続けます。
    • self.process_query() を使ってクエリを処理し、結果を表示します。
  2. cleanup 関数
    • リソースの解放や後処理を担当します。

5. Main Entry Point

最後に、MCPクライアントを起動し、指定されたサーバースクリプトに接続して対話を開始します。

動作の手順:

  1. コマンドライン引数の確認
    サーバースクリプトのパスが指定されていない場合、使用方法を表示して終了します。

  2. クライアントの起動と接続
    サーバースクリプトのパスを使ってMCPサーバーに接続します。

  3. チャットループの開始
    ユーザーが「quit」と入力するまで対話を続けます。

  4. リソースのクリーンアップ
    プログラム終了時に接続を閉じ、後処理を行います。

client.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())

Clientを実行してみよう

path/to/server.pyを、Quickstartで実装したMCP Serverのserver.pyのパスに書き換えてください。

Command Prompt
uv run client.py path/to/server.py

問題発生

Command Prompt
...\mcp-client>uv run client.py ../weather/src/weather/server.py
Traceback (most recent call last):
  File "...\weather\src\weather\server.py", line 3, in <module>
    import httpx
ModuleNotFoundError: No module named 'httpx'

このようなエラーが発生すると思います。
QuickstartをPythonで立ち上げて、コードに目を通しながらこのClientを実装した方なら多少違和感を感じていたかもしれません。

原因は仮想環境

Pythonプログラムは基本的に、実行ディレクトリの仮想環境を使用してコードを実行します。

上記のコードのように、client.pyの中でそのままpython path/to/server.pyを実行しても、mcp-clientというプロジェクトの仮想環境を使うことになり、mcp-clientにないpackageを使おうとするとエラーが起こってしまいます。
エラーメッセージで記されている通り、weatherではhttpxが使われますが、mcp-clienthttpxは含まれていません。

つまり、正常にserver.pyを呼び起こすには、weatherの仮想環境下で実行しないといけないのです。

mcp-clientの仮想環境で実行しているclient.pyの中で、weatherの仮想環境でしか実行できないserver.pyを実行するには、どうすればいいのか?

答えは、uv run--directoryオプションです。

uv runDocumentではこう書かれています:

--directory directory
Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

See --project to only change the project root directory.

このオプションの利用は、Quickstartでclaude_desktop_config.jsonに書かれているコンフィグにもあります。--directoryで実行ディレクトリを指定することで、server.pyは元々の仮想環境下で実行されます。

claude_desktop_config.json
{
    "mcpServers": {
        "weather": {
            "command": "uv",
            "args": [
                "--directory",
                "ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather",
                "run",
                "weather"
            ]
        }
    }
}

コード修正

--directoryを使うとコマンドが複雑になるので、Claude Desktopを習ってconfig.jsonを使います。

Command Prompt
call>config.json

config.jsonにはQuickstartで使っていたclaude_desktop_config.jsonの内容をそのままコピーします。

config.json
{
    "mcpServers": {
        "weather": {
            "command": "uv",
            "args": [
                "--directory",
                "ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\weather",
                "run",
                "weather"
            ]
        }
    }
}

関数connect_to_serverを改変します。

client.py
+ import json

-    async def connect_to_server(self, server_script_path: str):
+    async def connect_to_server(self, server_name: str):
        """Connect to an MCP server
        
        Args:
-            server_script_path: Path to the server script (.py or .js)
+            server_name: The name of the MCP Server
        """
-        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
-        )
+        with open("config.json", mode="r", encoding="utf-8") as json_file:
+            server_config = json.load(json_file).get("mcpServers").get(server_name)
+        server_params = StdioServerParameters(**server_config)
        
        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])

server.pyの位置はconfig.jsonに記載されているので、コードに与える必要はなくなりました。その代わりに、MCP Serverの名前を与えることで、どのMCP Serverを起用するかを教えます。

このような改変を行ったら、Clientの実行コマンドも変わります。

Command Prompt
uv run client.py weather

MCP Serverの名前を因数としてclient.pyに渡します。実行結果は以下の通りです。

...\mcp-client>uv run client.py weather

Connected to server with tools: ['get-alerts', 'get-forecast']

MCP Client Started!
Type your queries or 'quit' to exit.

Query: What is the weather in Pittsburgh?

Error: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.'}}

Query: quit

残念ながらAnthropicのcreditがないためAnthropic APIには接続不可能でした。しかしClientもServerも正常に起動していることがわかります。

あとはお金を入れるだけですね。今回は割愛させていただきます。

めでたしめでたし。

Discussion