- Authors

- Name
- Youngju Kim
- @fjvbn20031
- MCPとは何か?
- MCP以前の世界(問題点)
- MCPアーキテクチャ
- PythonでMCPサーバーを作る
- Claude DesktopでMCPサーバーを使う
- 既存のアプローチとの比較
- 2025〜2026年のMCPエコシステム
- 良いMCPサーバーを作るための実践的なヒント
- まとめ
AIビルダーなら誰もが直面する瞬間があります:「このエージェントがデータベースにアクセスできたらいいのに。」そこでLangChainツールを書き、OpenAI function calling向けにラップし、Claudeにはまた別の方法で実装する... モデルごとに統合コードを書き直す悪循環に陥ります。
MCP(Model Context Protocol)は、2024年末にAnthropicが発表したこの問題への解答です。
MCPとは何か?
MCPはAIモデルと外部ツール・データソースを接続するための標準化されたプロトコルです。USB-Cのようなもの:どのデバイスを繋いでも、どのポートでも、ただ動く。MCPはAIに対して同じことをします。
主な特性:
- オープン標準:Anthropicが作成したが特定の企業に縛られない
- 双方向:クライアント(AI)もサーバー(ツール)もメッセージを送れる
- JSON-RPCベース:シンプルで実証済みのプロトコル
MCP以前の世界(問題点)
M×N統合問題は現実のものでした:
モデル数 × ツール数 = 統合コード数
Claude + (Slack + GitHub + DB + Notion) = 4つのClaude統合
GPT-4 + (Slack + GitHub + DB + Notion) = 4つのGPT-4統合
Gemini + (Slack + GitHub + DB + Notion) = 4つのGemini統合
合計:12個の別々の統合コードベース
MCPの後:
ツール数 = MCPサーバー数(モデル横断で再利用可能)
1つのSlack MCPサーバー → Claude、GPT-4、Gemini、将来のモデル全てで使用可能
1つのGitHub MCPサーバー → 同様
一度書いたサーバーは、MCPに対応するどのAIクライアントでも自動的に動作します。
MCPアーキテクチャ
┌─────────────────────┐ ┌──────────────────────┐
│ MCP Client │◄───────►│ MCP Server │
│ (Claude Desktop、 │ JSON │ (あなたのツール、 │
│ Cursor、自作アプリ)│ RPC │ DB、API、ファイル)│
└─────────────────────┘ └──────────────────────┘
MCP Serverが公開する3つのプリミティブ:
├── Resources(読み取り専用データ)
│ └── ファイル、DBクエリ結果、APIレスポンス
│
├── Tools(実行可能なアクション)
│ └── ファイル書き込み、API呼び出し、DB変更
│
└── Prompts(再利用可能なプロンプトテンプレート)
└── 「このコードをレビューして」などの構造化テンプレート
コンポーネント:
- MCPクライアント:Claude Desktop、Cursor、自作のAIアプリ
- MCPサーバー:ツールサーバー(データベース、API、ファイルシステムなど)
- トランスポート:ローカルはstdio、リモートはHTTP/SSE
PythonでMCPサーバーを作る
実際に動作するサーバーを構築しましょう。顧客データベースを公開するサーバーです。
from mcp.server import Server
from mcp.server.models import InitializationOptions
import mcp.types as types
import json
import asyncio
app = Server("my-database-server")
# Resourcesの登録:読み取り専用データを公開
@app.list_resources()
async def list_resources() -> list[types.Resource]:
return [
types.Resource(
uri="db://customers",
name="Customer Database",
description="顧客レコードにアクセス。最大100件を返します。",
mimeType="application/json"
),
types.Resource(
uri="db://orders",
name="Order History",
description="全顧客の注文履歴にアクセス",
mimeType="application/json"
)
]
@app.read_resource()
async def read_resource(uri: str) -> str:
if uri == "db://customers":
customers = await db.fetch_all(
"SELECT id, name, email, created_at FROM customers LIMIT 100"
)
return json.dumps([dict(c) for c in customers])
elif uri == "db://orders":
orders = await db.fetch_all(
"SELECT id, customer_id, total, status, created_at FROM orders LIMIT 100"
)
return json.dumps([dict(o) for o in orders])
raise ValueError(f"Unknown resource: {uri}")
# Toolsの登録:実行可能なアクションを公開
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="create_customer",
description=(
"データベースに新しい顧客レコードを作成します。"
"ユーザーが新しい顧客を追加したい場合に使用。"
"成功時に新しい顧客のIDを返します。"
),
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "顧客のフルネーム"
},
"email": {
"type": "string",
"description": "顧客のメールアドレス"
},
"phone": {
"type": "string",
"description": "顧客の電話番号(オプション)"
}
},
"required": ["name", "email"]
}
),
types.Tool(
name="search_customers",
description=(
"名前またはメールで顧客を検索します。"
"新規作成前に既存顧客を確認するために使用してください。"
),
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "検索クエリ — 名前とメールフィールドに対してマッチング"
}
},
"required": ["query"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
if name == "create_customer":
try:
result = await db.execute(
"INSERT INTO customers (name, email, phone) VALUES (:name, :email, :phone)",
{
"name": arguments["name"],
"email": arguments["email"],
"phone": arguments.get("phone", None)
}
)
return [types.TextContent(
type="text",
text=f"顧客を正常に作成しました。ID: {result.lastrowid}"
)]
except Exception as e:
return [types.TextContent(
type="text",
text=f"顧客作成エラー: {str(e)}"
)]
elif name == "search_customers":
query = f"%{arguments['query']}%"
customers = await db.fetch_all(
"SELECT id, name, email FROM customers WHERE name LIKE :q OR email LIKE :q",
{"q": query}
)
if not customers:
return [types.TextContent(type="text", text="該当する顧客が見つかりませんでした")]
result = "\n".join([
f"ID: {c.id}, Name: {c.name}, Email: {c.email}"
for c in customers
])
return [types.TextContent(type="text", text=result)]
raise ValueError(f"Unknown tool: {name}")
# サーバーの実行
async def main():
from mcp.server.stdio import stdio_server
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
InitializationOptions(
server_name="my-database-server",
server_version="0.1.0"
)
)
if __name__ == "__main__":
asyncio.run(main())
Claude DesktopでMCPサーバーを使う
サーバーを作ったら、Claude Desktopの設定ファイルに登録します:
// macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
// Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"my-database": {
"command": "python",
"args": ["/path/to/your/mcp_server.py"],
"env": {
"DATABASE_URL": "postgresql://user:pass@localhost/mydb"
}
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/username/projects"]
}
}
}
Claude Desktopを再起動すると、Claudeがデータベースに直接アクセスできるようになります。「顧客一覧を見せて」「山田太郎という顧客を追加して」といった自然言語コマンドが機能します。
既存のアプローチとの比較
OpenAI Function Calling
# OpenAI APIのフォーマットに縛られる
tools = [{"type": "function", "function": {"name": "search", ...}}]
response = openai.chat.completions.create(tools=tools, ...)
OpenAIエコシステムでは完璧に動作します。Claudeを使いたい?最初から再実装が必要です。
LangChain Tools
# LangChainフレームワークに縛られる
from langchain.tools import tool
@tool
def search(query: str) -> str:
"""Search the web"""
return web_search(query)
LangChainをフル活用しているなら良いでしょう。LangChainを使わない環境には移植できません。
MCP
# MCPに対応するどのクライアントでも動作
@app.list_tools()
async def list_tools():
return [types.Tool(name="search", ...)]
一度ビルドすれば、Claude Desktop、Cursor、自作アプリ — MCPを話すクライアントはどこでも動作します。
| 特性 | OpenAI Function Calling | LangChain Tools | MCP |
|---|---|---|---|
| モデル独立性 | なし(OpenAI専用) | 部分的 | あり |
| オープン標準 | なし | なし | あり |
| 再利用性 | 低 | 中 | 高 |
| セットアップ複雑度 | 低 | 中 | 中 |
| エコシステム | OpenAIエコシステム | LangChainエコシステム | 急速成長中 |
2025〜2026年のMCPエコシステム
Anthropicが提供する公式サーバー:
@modelcontextprotocol/server-filesystem— ローカルファイルアクセス@modelcontextprotocol/server-github— GitHubリポジトリ@modelcontextprotocol/server-postgres— PostgreSQLデータベース@modelcontextprotocol/server-brave-search— Brave Search API@modelcontextprotocol/server-slack— Slackメッセージング
コミュニティサーバーはNotion、Jira、Linear、Docker、Kubernetes、AWS、GCPなどをカバー。
インストール:
# Node.jsサーバー
npx -y @modelcontextprotocol/server-github
# Pythonサーバー
pip install mcp
良いMCPサーバーを作るための実践的なヒント
1. ツールの説明を具体的に書く
AIはdescriptionを使っていつどうやってツールを呼ぶかを決めます。曖昧な説明は誤った呼び出しにつながります。
# 悪い例
types.Tool(name="query_db", description="データベースをクエリ")
# 良い例
types.Tool(
name="search_customers",
description=(
"名前またはメールで顧客レコードを検索します。"
"既存の顧客を見つける必要があるときに使用してください。"
"顧客ID、名前、メール、アカウント作成日を返します。"
"注文関連のクエリにはget_ordersを使用してください。"
)
)
2. エラーをツール結果として返す(例外ではなく)
@app.call_tool()
async def call_tool(name: str, arguments: dict):
try:
result = await execute_operation(arguments)
return [types.TextContent(type="text", text=json.dumps(result))]
except Exception as e:
# 通常のレスポンスとしてエラーを返す — AIが適応できる
return [types.TextContent(
type="text",
text=f"エラー: {str(e)}。入力を確認して再試行してください。"
)]
3. 読み取りと書き込みをアーキテクチャレベルで分離する
Resources = 読み取り専用(SELECT、ファイル読み取り、GETリクエスト) Tools = 書き込み操作(INSERT/UPDATE/DELETE、ファイル書き込み、POSTリクエスト)
この境界があることで、AIが安全性を推論しやすくなります。「この操作はデータを変更するか?」という判断がアーキテクチャから明確になります。
まとめ
MCPはまだ成熟途上の標準です。しかし方向は明確です。Cursor、Zed、VS CodeがMCPサポートを追加しています。サードパーティサーバーのエコシステムが急速に成長しています。
今日AIアプリを構築しているなら、ツール統合をMCPフォーマットで実装しておくことをお勧めします。後で別のモデルをサポートしたいときに感謝することになるでしょう。
次の記事ではマルチエージェントシステム — AutoGen、CrewAI、LangGraphを比較して、ユースケースに合ったフレームワークを選べるようにします。