【Function Calling】PythonスクリプトからMCPサーバーを呼び出してみる

# 0. はじめに

前回はRoo CodeをMCPクライアントとしてMCPサーバーを作成して使用してみましたが、今回は自分のPythonスクリプト＋OpenAIのAPIで、自作のMCPサーバーを呼び出すことをやったのでメモ代わりに残します。

**要するに、OpenAIのAPIに質問する際、そのままLLMとして回答を生成してもらうのではなく、MCPサーバーの中に作成した関数を実行してもらうことで、OpenAIの知識にないことも加味させることができるようになります。**

https://qiita.com/kuk_a_i_ai/items/d1f2b15bd82420efec04

- 動作環境
・ OS : Windows11 pro
・ python : 3.12.8(uv環境)
・ mcp : 1.9.1
・ python-dotenv :1.1.0
・ openai :1.82.0


# 1. uvでPython環境（fastmcp_check）を作成

uvで環境を作成します。

https://qiita.com/kuk_a_i_ai/items/be8854553486ee1a640a

今回はfastmcp_checkプロジェクトをPython3.12.8で作成して必要なライブラリを入れます

```cmd:コマンド プロンプト
; uvでfastmcp_checkプロジェクトを作成(python3.12.8)
uv init fastmcp_check -p 3.12.8 && cd fastmcp_check

; venvを作成
uv venv

; openai python-dotenv mcpライブラリをadd
uv add openai python-dotenv mcp
```

# 2. MCPサーバーを作成する(math_server.py)

ここではOpenAIに提供したい機能を盛り込む。
盛り込み方は、`@mcp.tool()`を関数の前に付与するだけで簡単である。
今回はサンプルとしてシンプルな「足し算」と「引き算」機能を定義してみる（こんなレベルは素のOpenAIで十分実行可能だが・・）

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

# MCPサーバーのインスタンスを作成
mcp = FastMCP("MathServer")

# 足し算ツールを定義
@mcp.tool()
def add(a: int, b: int) -> int:
    """2つの整数を加算します。"""
    return a + b

# 引き算ツールを定義
@mcp.tool()
def subtract(a: int, b: int) -> int:
    """2つの整数を減算します。aからbを引きます。"""
    return a - b

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

# 3. MCPクライアントを作成する(main.py)

今回は自作のスクリプトをMCPクライアント（チャット等のクエリの入り口）とするのでそのコードを以下に示す。

まずはOpenAIのAPI Keyを.envファイル(python-dotenvで読み込める)に書き込んでおく
```cmd:.envファイル
# OpenAI API Key
OPENAI_API_KEY=sk-*****
```

続いてはmainプログラムであるが、中身の詳細はコードコメントを書いたのでそれを参照してほしい。
質問はコマンドライン引数の形式とし、必要に応じて先ほど定義した「math_server.py」を参照するようにしている。


```python:main.py
import asyncio
import os
import json
import argparse
from dotenv import load_dotenv
from mcp.client.stdio import stdio_client
from mcp import StdioServerParameters, ClientSession
from openai import OpenAI


# ==============================
# 1. 環境変数の読み込み
# ==============================
load_dotenv()

# OpenAIクライアントの初期化
openai_client = OpenAI(api_key=os.environ['OPENAI_API_KEY'])
model_name = "gpt-4.1"

# ==============================
# 2. コマンドライン引数の処理
# ==============================
parser = argparse.ArgumentParser(description="OpenAIがMCPツールの使用を判断して質問を処理します。")
parser.add_argument("--question", type=str, required=True, help="ユーザーの質問を指定")
args = parser.parse_args()

# ==============================
# 3. MCPツールの定義(OpenAIのFunction Calling機能)
# ==============================
"""
★GPTはこの情報をもとに、「計算が必要だ」と判断した時だけ、自動でツール呼び出し（Function Calling）を実行する★

type: 「function」（関数型ツール）であることを示す
function:
    ・name: 関数の名前（GPTから見ての関数名。例: add_numbers）
    ・description: この関数が何をするのかの説明。GPTに「いつ使うべきか」も説明している
    ・parameters:
        ・type: オブジェクト型（複数引数をまとめるため）
        ・properties: 引数の一覧。ここでは「a」と「b」（どちらもinteger型、説明付き）
        ・required: 必須引数のリスト（"a"と"b"）
"""
tools = [
    {
        "type": "function",
        "function": {
            "name": "add_numbers",
            "description": "2つの整数を足し算します。数値計算が必要な場合に使用してください。",
            "parameters": {
                "type": "object",
                "properties": {
                    "a": {"type": "integer", "description": "1つ目の整数"},
                    "b": {"type": "integer", "description": "2つ目の整数"}
                },
                "required": ["a", "b"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "subtract_numbers",
            "description": "2つの整数を引き算します。数値計算が必要な場合に使用してください。",
            "parameters": {
                "type": "object",
                "properties": {
                    "a": {"type": "integer", "description": "引かれる数（1つ目の整数）"},
                    "b": {"type": "integer", "description": "引く数（2つ目の整数）"}
                },
                "required": ["a", "b"]
            }
        }
    }
]

# ==============================
# 4. MCPツール呼び出し用 非同期関数（足し算）
# ==============================
async def call_mcp_add(a: int, b: int) -> str:
    """MCPサーバーを使って足し算を実行"""
    print(f"🔧 MCPツールで計算中: {a} + {b}")
    
    # サーバーパラメータの設定（uv経由でmath_server.pyを起動）
    server_params = StdioServerParameters(
        command="uv",
        args=["run", "python", "math_server.py"],
        env=None
    )
    
    try:
        # stdio_client経由でMCPサーバーへ接続
        async with stdio_client(server_params) as (read, write):
            # セッションの開始
            async with ClientSession(read, write) as session:
                await session.initialize()  # セッション初期化
                # ツール（足し算）を呼び出し
                result = await session.call_tool("add", {"a": a, "b": b})
                
                # 結果を抽出して返却
                if result.content and len(result.content) > 0:
                    answer = result.content[0].text
                    print(f"✅ MCP計算結果: {answer}")
                    return f"{a} + {b} = {answer}"
                return "計算エラーが発生しました"
    except Exception as e:
        print(f"❌ MCP エラー: {e}")
        return f"MCPツールでエラーが発生しました: {e}"

# ==============================
# 5. MCPツール呼び出し用 非同期関数（引き算）
# ==============================
async def call_mcp_subtract(a: int, b: int) -> str:
    """MCPサーバーを使って引き算を実行"""
    print(f"🔧 MCPツールで計算中: {a} - {b}")
    
    # サーバーパラメータの設定（uv経由でmath_server.pyを起動）
    server_params = StdioServerParameters(
        command="uv",
        args=["run", "python", "math_server.py"],
        env=None
    )
    
    try:
        # stdio_client経由でMCPサーバーへ接続
        async with stdio_client(server_params) as (read, write):
            # セッションの開始
            async with ClientSession(read, write) as session:
                await session.initialize()  # セッション初期化
                # ツール（引き算）を呼び出し
                result = await session.call_tool("subtract", {"a": a, "b": b})
                
                # 結果を抽出して返却
                if result.content and len(result.content) > 0:
                    answer = result.content[0].text
                    print(f"✅ MCP計算結果: {answer}")
                    return f"{a} - {b} = {answer}"
                return "計算エラーが発生しました"
    except Exception as e:
        print(f"❌ MCP エラー: {e}")
        return f"MCPツールでエラーが発生しました: {e}"

# ==============================
# 6. メイン関数
# ==============================
async def main():
    # コマンドラインから渡された質問を取得
    question = args.question.strip()
    print(f"🤔 質問: {question}")
    print("🧠 OpenAIが適切なツールを選択します...")
    
    # チャット履歴の初期化
    messages = [
        {"role": "system", "content": "あなたは有能なアシスタントです。数値計算が必要な場合は、利用可能なツールを使用してください。"},
        {"role": "user", "content": question}
    ]
    
    # OpenAI Chat APIでツール選択付きプロンプトを実行
    response = openai_client.chat.completions.create(
        model=model_name,
        messages=messages,
        tools=tools,        # 定義したMCPツール
        tool_choice="auto"  # ツールを自動選択させる
    )
    
    # 返答メッセージを取得
    response_message = response.choices[0].message
    
    # もしツールコールがあればMCPで実行
    if response_message.tool_calls:
        # 決定されたツール名を表示
        tool_names = [tool_call.function.name for tool_call in response_message.tool_calls]
        print(f"🔧 OpenAIがツールの使用を決定しました: {', '.join(tool_names)}")
        
        messages.append(response_message)
        
        # ツールコールごとにMCPへリクエスト
        for tool_call in response_message.tool_calls:
            function_name = tool_call.function.name
            function_args = json.loads(tool_call.function.arguments)  # 引数をパース
            
            function_result = ""
            # MCP関数呼び出し
            if function_name == "add_numbers":
                function_result = await call_mcp_add(function_args["a"], function_args["b"])
            elif function_name == "subtract_numbers":
                function_result = await call_mcp_subtract(function_args["a"], function_args["b"])
            else:
                function_result = f"未知のツール: {function_name}"
            
            # ツールの実行結果を履歴に追加
            messages.append({
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": function_name,
                "content": function_result
            })
        
        # 計算実行後、再度OpenAIに最終回答をリクエスト
        final_response = openai_client.chat.completions.create(
            model=model_name,
            messages=messages
        )
        print("🎯 最終回答:", final_response.choices[0].message.content)
    else:
        print("💬 OpenAI直接回答:", response_message.content)

# ==============================
# 7. エントリポイント
# ==============================
if __name__ == "__main__":
    try:
        asyncio.run(main())  # メイン関数を非同期で実行
    except KeyboardInterrupt:
        print("\n🛑 プログラムが中断されました")
```


# 4. 実際に動かしてみる

main.pyの階層にcdで移動後、以下コマンドライン引数込みでmain.pyを動作させる

```cmd:コマンド プロンプト
uv run main.py --question "質問文"
```

実際にvscodeのターミナルからやってみたスクリーンショットを張っておく
**質問文から足し算ツールや引き算ツールの使用を自動で判断し、適切なツールを選べていることがわかる。**

![スクリーンショット 2025-05-27 223551.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373060/3c8ca578-230e-4baf-aa66-2ef4c4540982.png)


# 5. おわりに

MCPの記事でMCPクライアントとしてClineやClaude Desktop等はいくつかあったが、スクリプトから呼び出しているサンプルをあまり見かけなかったので、自分用も兼ねてメモとして残しておいた。

最近ますますAI界隈の流れが激しくて、どれにベットすればいいか迷うが、なんとかついていくしかないですね（汗）