🥽

[MCP]FastMCPとGithub Copilotで見てみる実装の基本のキ

に公開

FastMCPとGithub Copilotで見てみるMCPの基本のキ

この記事は

MCPは概念だけはなんとなく知っていたのですが、実装は初めてなので簡単なものからやってみるというメモ。やっぱり聞くとやるではだいぶ違うもので、そこそこハマった。

クライアントにGithub Copilotを使うのは、手元にあるやつでなんでもよかったので。対応してるっぽいことが分かったので。

環境面の前提

  • VSCode
  • Github Copilot Pro
  • Dockerが動作するLinux環境
  • Azure OpenAI

ハマりポイント

現状Webで出てくる記事は標準出力のI/Oが多い

MCPサーバ というからには、WebAPIかgRPCとかそんなのの実装だろうと思って決め打ちで見ていくと標準I/Oのサンプルが多くて途中だいぶ混乱。結局FastMCPの場合はmcp.run(transport="sse")が自分がやろうとしていることにあっていることがようやくわかるまでに時間をロスト。

標準出力って要するにMCPクライアントとサーバが同じホスト上で動いていること前提ですよね。それはなんか、違うというか。

初めての実装

適当なLinuxマシンでFastMCPを、SSE形式で起動する。まずはフォルダを切って以下の4ファイルを配置。

docker-compose.yml

services:
  mcptest:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: mcptest
    restart: unless-stopped
    ports:
      - "8000:8000"
    volumes:
      - ./:/app

Dockerfile

FROM python:3.12-bullseye
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]
EXPOSE 8000

requirements.txt

fastmcp

main.py

from fastmcp import FastMCP

mcp = FastMCP(name="mcp_test")

@mcp.tool(description="今日の夕ご飯として完全なオススメを提供してくれる")
def recommend_dinner(query = "今日の夕ご飯は何がいい?") -> str:
    """
    今日の夕ご飯として完全なオススメを提供してくれる。
    """
    return "絶対にタコ焼きがいいです"

if __name__ == "__main__":
    mcp.run(
        transport="sse",
        host="0.0.0.0",
        port=8000
    )

起動

docker compose up -d

これで、今日の夕ご飯をオススメしてくれるrecommend_dinner()が実装された、何があっても絶対タコ焼きしかいわないMCPサーバが爆誕。

Github CopilotのChatにこれを使わせる

簡単なのでスクショは省略する。

  1. Github CopilotのChatを開く
  2. Agentモードになっていることを確認
  3. スパナみたいなボタン(select tools)を押す
  4. "Add More Tools..." > "Add MCP Server" > "HTTP"
  5. URLにhttp://<MCPサーバのホスト>:8000/sseを入力
  6. 最後にWorkSpace SettingsUser Settingsかを選ばせてくるが、まだまだお遊びなのでWorkspaceの方を選ぶ

そしたら結局、今のWorkspaceの/.vscode/mcp.jsonに以下のような設定が追加される。

{
    "servers": {
        "my-mcp-server-aaaaaaaa": {
            "url": "http://<MCPサーバのホスト>:8000/sse"
        }
    }
}

このあとGithub Copilot Chatと会話してみる。

alt text

うん、たこ焼き。ちゃんとMCP呼ぶようになった。お肉もいいな、って言っているのにたこ焼きを肉入りにって言ってくるのはなかなか。そんなアレンジ聞いたことない。

クライアントも試してみる

Github Copilot Chatで動作は確認できたが、色んなツールに組み込みたいのでClientを実装して利用するサンプルも試す。

参考になったありがたいサイトは以下だが、なかなかひとつでいいサンプルはなくてあちこち見て回ってようやく何とかなった感じ。

検証用なので、別に環境作るのも面倒だから、先ほどMCPサーバを起動したサーバを実行環境にする。そこに以下のコードをclient.pyとして配置し、あとrequirements.txtにopenaiくらいを追加してもう一度ビルドしてdocker compose up -dしなおしておく。

# FastMCP Clientを利用して、AzureOpenAIのChat Completionを呼び出しながらMCPを使う
# ユーザのクエリは標準出力の第1引数を使うシンプルな形にする
import sys
import openai
import json
import asyncio
from fastmcp import Client

# Azure OpenAIのクライアントを設定
openai_client = openai.AzureOpenAI(
    azure_endpoint="",
    azure_deployment="",
    api_key="",
    api_version=""
)

# FastMCPのクライアントを設定
mcp_client = Client("http://localhost:8000/sse")

async def main(query: str):
    
    # FastMCPクライアントをコンテキストマネージャとして使用
    async with mcp_client:
        
        # MCPからツールを取得
        mcp_tools = await mcp_client.list_tools()
        
        # AzureOpenAIにツールとして渡せる形式に変換する(★)
        tools = [
            {
                "type": "function",
                "function": {
                    "name": tool.name,
                    "description": tool.description,
                    "parameters": tool.inputSchema
                }
            } for tool in mcp_tools
        ]

        # ユーザのクエリを含むメッセージを作成
        messages = [
            {"role": "system","content": "You are a helpful assistant."},
            {"role": "user","content": query }
        ]

        # ツールを含んでChatCompletion APIをCallする
        response_message = openai_client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )

        # 結果を messages に追加
        messages.append(response_message.choices[0].message)

        # tool_calls があれば、それを処理する
        if response_message.choices[0].message.tool_calls:
            for tool_call in response_message.choices[0].message.tool_calls:

                # ここでMCPサーバのツールを呼び出す
                tool_result = await mcp_client.call_tool(
                    tool_call.function.name,
                    json.loads(tool_call.function.arguments)
                )

                # ツールの実行結果を messages に追加                
                messages.append({
                    "tool_call_id": tool_call.id,
                    "role": "tool",
                    "name": tool_call.function.name,
                    "content": tool_result[0].text,
                })
            else:
                pass

        # ツールの呼び出しが完了した後、最終的な応答を生成
        final_response = openai_client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
        )

        # 最終的な応答を表示
        print(final_response.choices[0].message.content)

if __name__ == "__main__":
    # プログラム起動時の引数を取得
    if len(sys.argv) < 2:
        print("Usage: python client.py <query>")
        sys.exit(1)
    query = sys.argv[1]
    asyncio.run(main(query))

実行すると以下のようになる。

$ docker compose exec -it mcptest python /app/client.py 'もう夕方なのでお腹が空きました。'
夕方でお腹が空いたなら、温かいタコ焼きはいかがですか?外はカリっと、中はトロッとしていて美味しいですよ!お好みでソースやマヨネーズ、青のりをたっぷりと添えて楽しんでください。

$ docker compose exec -it mcptest python /app/client.py 'PythonのHelloWorldを書いてそれだけ出力して。'
\```python
print("Hello, World!")
\```

比較のために2種類打っているが、1回目は明らかにたこ焼き狂信MCPサーバの応答を参考にして答えてきている。2回目の方は話題と全く関係ないのでただの回答をしており、MCPサーバの呼び出し自体どうやら全く行っていないらしい。

なるほどなあ、と実装方法はよくわかったしうまくは行ったものの、なんか全然洗練されてないな、、、特にソースコード内で(★)をコメントに書いているところの処理、意味が分からん。こんなことさせないでほしい。FastMCPとAzureOpenAIでプロトコルがあってない?ように見えるけど。

1箇所、json.loads()しないと動かないところも悲しかった。

そのあとのところも、ライブラリの使い方がいまいち気持ちよくないというか、まだよくわかってないのかなという気がする。

まとめ

MCPの実装を一番土台のところから整える練習は一応完了。

自分が一番こなれているDockerとPythonで、まあちゃんと、一応作れるよねってことが確認できて一安心。でも、まだまだ色々新しいので、急に仕様変わったりもっと新しいの出てきたりしそうで落ち着かないわ。この辺の世の中のスピードがえげつない。

Accenture Japan (有志)

Discussion