問題の概要:MCPサーバー構築とローカルLLM連携の課題
Model Context Protocol (MCP) は、大規模言語モデル(LLM)が外部ツールやデータソースに安全にアクセスするための標準規格です。特にローカル環境で動作するLLM(Llama 3.1, Gemma 2, Qwen 2.5など)とMCPサーバーを連携させることで、APIキーを必要とせず、プライベートなデータを活用した高度なAIアシスタントを構築できます。
しかし、開発者が実際にMCPサーバーを構築し、ローカルLLMと連携させようとすると、以下のような典型的なエラーや課題に直面します:
よくあるエラーメッセージと課題
エラー 1: ConnectionError: Failed to connect to MCP server at 'http://localhost:8080'
エラー 2: ModuleNotFoundError: No module named 'mcp'
エラー 3: RuntimeError: Tool execution failed: Invalid response format from server
エラー 4: ローカルLLMがMCPサーバーからのツール呼び出しを理解できない
エラー 5: セキュリティ設定(CORS、認証)による接続ブロック
これらの問題は、MCPの仕様理解不足、環境構築の不備、またはローカルLLMのプロンプト設計が適切でないことが原因で発生します。本記事では、ステップバイステップでこれらの課題を解決し、実用的なMCPサーバーを構築する方法を解説します。
原因の解説:なぜ接続と連携が失敗するのか
MCPサーバーとローカルLLMの連携が失敗する主な原因は3つあります。
1. 環境構築と依存関係の不整合
MCPは比較的新しいプロトコルであり、必要なPythonパッケージ(mcp, uvicorn, fastapiなど)のバージョンが競合したり、正しくインストールされていない場合があります。また、仮想環境を使用していないと、グローバル環境のパッケージと衝突する可能性があります。
2. MCPサーバーの実装ミス
MCPサーバーは標準化されたHTTPエンドポイントとSSE (Server-Sent Events) ストリームを提供する必要があります。ツール(関数)の定義方法、レスポンスのJSONフォーマット、またはエラーハンドリングが仕様に沿っていないと、クライアント(LLM)から正しく認識されません。
3. ローカルLLMへの適切な「ツール使用」指示の欠如
ChatGPTやClaudeなどのクラウドLLMとは異なり、多くのローカルLLMはデフォルトで「ツールや関数を呼び出す」能力が有効化されていません。プロンプト内で、利用可能なツールの説明と、それらをいつ・どのように使うべきかの明確な指示(システムプロンプト)を与える必要があります。指示が不十分だと、LLMはツールの存在を無視したり、誤った形式で呼び出そうとします。
解決方法:ステップバイステップ実装ガイド
ステップ1: 開発環境のセットアップ
まず、クリーンなPython環境を構築し、必要なパッケージをインストールします。
# 1. プロジェクトディレクトリ作成と仮想環境の有効化
mkdir my-mcp-project && cd my-mcp-project
python -m venv venv
# Windows: venvScriptsactivate
# Mac/Linux: source venv/bin/activate
# 2. 必須パッケージのインストール
pip install mcp fastapi uvicorn httpx pydantic
# 3. インストール確認
python -c "import mcp; print(f'MCP version: {mcp.__version__}')"
ステップ2: 基本的なMCPサーバーの実装
最も単純な、天気情報を返すツールを持つMCPサーバーを作成します。ファイル名はserver.pyとします。
# server.py
import asyncio
from mcp.server import Server
from mcp.server.models import InitializationOptions
import mcp.server.stdio
from mcp.types import Tool, TextContent
# MCPサーバーのインスタンスを作成
server = Server("my-local-tools")
# ツールの定義: 天気を取得する
@server.list_tools()
async def handle_list_tools():
return [
Tool(
name="get_weather",
description="指定された都市の現在の天気を取得します。",
inputSchema={
"type": "object",
"properties": {
"city": {"type": "string", "description": "都市名 (例: '東京', '大阪')"}
},
"required": ["city"]
}
)
]
# ツールの実行ロジック
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict):
if name == "get_weather":
city = arguments.get("city", "未知の都市")
# ここではダミーデータを返す。実際はAPIを呼び出す。
weather_data = {
"東京": "晴れ、気温25℃",
"大阪": "曇り、気温23℃",
"福岡": "雨、気温20℃"
}
forecast = weather_data.get(city, "天気情報がありません")
return [
TextContent(
type="text",
text=f"{city}の天気: {forecast}"
)
]
raise ValueError(f"未知のツール: {name}")
# サーバーの起動
async def main():
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="my-local-tools",
server_version="0.1.0"
)
)
if __name__ == "__main__":
asyncio.run(main())
ステップ3: サーバーの起動とテスト
別のターミナルを開き、サーバーが正しく動作するかをテスト用クライアントで確認します。
# サーバーを起動 (ターミナル1)
python server.py
# 別ターミナルでテストクライアントを実行 (ターミナル2)
# test_client.pyを作成
import asyncio
from mcp import ClientSession
from mcp.client.stdio import stdio_client
async def test_client():
async with stdio_client(["python", "server.py"]) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print("利用可能なツール:", [t.name for t in tools])
# ツールを実行
result = await session.call_tool("get_weather", {"city": "東京"})
print("実行結果:", result)
asyncio.run(test_client())
# 期待される出力:
# 利用可能なツール: ['get_weather']
# 実行結果: [TextContent(type='text', text='東京の天気: 晴れ、気温25℃')]
このテストが成功すれば、MCPサーバーは正常に動作しています。
ステップ4: ローカルLLM (LM Studio / Ollama) との連携
ここが最も重要なステップです。ローカルLLMに、MCPサーバーで利用可能なツールを認識させ、使用させるためのプロンプトを設計します。Ollamaを使用する例を示します。
まず、MCPサーバーをHTTP/SSEインターフェースで公開するように変更します(server_http.py)。
# server_http.py - HTTPで公開するバージョン
from fastapi import FastAPI
from mcp.server.fastapi import create_mcp_app
# ... (ツール定義はserver.pyと同じ) ...
app = create_mcp_app(server, path="/sse")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080) # ポート8080で公開
次に、Ollamaでモデルを起動する際に、強力なシステムプロンプトを与えます。
# ollama_run.sh またはプロンプト
# 以下のシステムプロンプトをOllamaのモデル設定に追加、または会話開始時に送信
"""
あなたは高度なAIアシスタントです。ユーザーを助けるために、以下のツールを利用できます。
ツールを使用する必要がある場合は、以下の厳格なJSON形式のみで応答してください:
{
"tool": "ツール名",
"input": {"引数名": "引数の値"}
}
利用可能なツール:
1. get_weather
- 説明: 指定された都市の現在の天気を取得します。
- 引数: `city` (string, 必須): 都市名 (例: '東京', '大阪')
例:
ユーザー: 「東京の天気は?」
あなた: {"tool": "get_weather", "input": {"city": "東京"}}
ツールの結果を受け取ったら、その結果を踏まえて自然な言葉でユーザーに答えてください。
ツールが必要ない質問には、直接答えてください。
"""
実際の連携では、litellmやinstructorなどのライブラリを使って、LLMの出力をパースし、MCPクライアント経由でツールを実行するバックエンドを構築することになります。
コード例・コマンド例:実践的な拡張ツール
データベースに接続するより実践的なツールの例を示します。
# 拡張例: メモを管理するツールを追加
import sqlite3
from datetime import datetime
# ... (既存のserver.pyに追加) ...
@server.list_tools()
async def handle_list_tools():
return [
Tool(name="get_weather", ...), # 既存のツール
Tool(
name="add_note",
description="データベースに新しいメモを追加します。",
inputSchema={
"type": "object",
"properties": {
"title": {"type": "string"},
"content": {"type": "string"}
},
"required": ["title", "content"]
}
),
Tool(
name="get_notes",
description="保存されたメモの一覧を取得します。",
inputSchema={
"type": "object",
"properties": {
"limit": {"type": "integer", "description": "取得件数 (デフォルト: 10)"}
},
"required": []
}
)
]
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict):
if name == "get_weather":
# ... 既存の処理 ...
elif name == "add_note":
conn = sqlite3.connect('notes.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS notes
(id INTEGER PRIMARY KEY, title TEXT, content TEXT, created_at TIMESTAMP)''')
c.execute("INSERT INTO notes (title, content, created_at) VALUES (?, ?, ?)",
(arguments['title'], arguments['content'], datetime.now()))
conn.commit()
conn.close()
return [TextContent(type="text", text=f"メモ '{arguments['title']}' を追加しました。")]
elif name == "get_notes":
limit = arguments.get('limit', 10)
conn = sqlite3.connect('notes.db')
c = conn.cursor()
c.execute("SELECT title, content FROM notes ORDER BY created_at DESC LIMIT ?", (limit,))
notes = c.fetchall()
conn.close()
note_list = "n".join([f"・{title}: {content}" for title, content in notes])
return [TextContent(type="text", text=f"直近のメモ:n{note_list}" if note_list else "メモはありません。")]
raise ValueError(f"未知のツール: {name}")
まとめ・補足情報
MCPサーバーを構築しローカルLLMと連携させることで、APIコストをかけず、完全にプライベートな環境で拡張性の高いAIアシスタントシステムを構築できます。成功のポイントは以下の3点です。
- 環境の分離: 必ず仮想環境を使用し、パッケージのバージョン競合を避ける。
- 段階的なテスト: サーバー単体 → テストクライアント → 簡単なLLM連携の順で、各段階で動作を確認する。
- プロンプト設計の重要性: ローカルLLMはツール使用について明示的で構造化された指示を必要とする。JSON形式の出力を強制するシステムプロンプトが必須。
トラブルシューティングの最終手段: 複雑なエラーが発生した場合は、公式のMCPサンプルリポジトリ(github.com/modelcontextprotocol/servers)を参考にし、最もシンプルな例から再構築してみてください。MCPは発展中のプロトコルであるため、コミュニティフォーラムやDiscordチャンネルで最新情報をキャッチアップすることも有効です。
このガイドを参考に、自分だけのツールを搭載したローカルAIアシスタントの構築に挑戦してみてください。