[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にこれを使わせる
簡単なのでスクショは省略する。
- Github CopilotのChatを開く
- Agentモードになっていることを確認
- スパナみたいなボタン(select tools)を押す
- "Add More Tools..." > "Add MCP Server" > "HTTP"
- URLに
http://<MCPサーバのホスト>:8000/sse
を入力 - 最後に
WorkSpace Settings
かUser Settings
かを選ばせてくるが、まだまだお遊びなのでWorkspaceの方を選ぶ
そしたら結局、今のWorkspaceの/.vscode/mcp.json
に以下のような設定が追加される。
{
"servers": {
"my-mcp-server-aaaaaaaa": {
"url": "http://<MCPサーバのホスト>:8000/sse"
}
}
}
このあとGithub Copilot Chatと会話してみる。
うん、たこ焼き。ちゃんとMCP呼ぶようになった。お肉もいいな、って言っているのにたこ焼きを肉入りにって言ってくるのはなかなか。そんなアレンジ聞いたことない。
クライアントも試してみる
Github Copilot Chatで動作は確認できたが、色んなツールに組み込みたいのでClientを実装して利用するサンプルも試す。
参考になったありがたいサイトは以下だが、なかなかひとつでいいサンプルはなくてあちこち見て回ってようやく何とかなった感じ。
- FastMCP での MCPサーバ と MCPクライアント の構築を試す - npaka
- OpenAI API Reference
- Function Calling - Azure AI Services
検証用なので、別に環境作るのも面倒だから、先ほど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で、まあちゃんと、一応作れるよねってことが確認できて一安心。でも、まだまだ色々新しいので、急に仕様変わったりもっと新しいの出てきたりしそうで落ち着かないわ。この辺の世の中のスピードがえげつない。
Discussion