· 24 分で読める · 11,823 文字
MCP Serverでカスタムツールを構築し、AI連携を自動化する実装手順
本記事では、Model Context Protocol(MCP)を使ってカスタムツールサーバーを構築し、Claude等のAIモデルと連携させる方法を実装レベルで解説します。実務で即座に使える設定例とトラブルシューティングも含めています。
MCP Serverとは何か、なぜ必要なのか
Model Context Protocol(MCP)は、Anthropicが開発したプロトコルで、AIアシスタント(主にClaude)と外部ツール・データソースを安全に連携させるための標準化されたインターフェースです。
従来の方法では、AIモデルが外部ツールを呼び出すためにカスタムコード(Function Calling等)を毎回実装する必要がありました。MCPを使うことで、ツール側とAI側の実装を分離でき、保守性と再利用性が劇的に向上します。
MCPが活躍する場面
- CRMやERPシステム連携: Salesforce、SAP等の企業システムをClaudeから直接操作
- カスタム業務ツール: 社内の独自APIやデータベースへの安全なアクセス
- ファイル処理パイプライン: PDFパース、画像解析等の複雑な処理を自動化
- リアルタイムデータ取得: 検索エンジン、天気API、株価等の動的データ統合
使うべきでない場面
- シンプルなテキスト変換やデータ整形(Promptingで十分)
- 低レイテンシーが必須な高頻度リクエスト(オーバーヘッドが大きい)
- AIモデルのみで完結できる処理
以下はMCPの全体的なアーキテクチャを示した図です。
graph TD
A[Claude Application] -->|MCP Protocol| B[MCP Server]
B -->|Tool Definition| C[Tool Registry]
B -->|実行| D[Resource Layer]
D -->|API Call| E[External System]
D -->|Database Query| F[Data Source]
D -->|File Operation| G[Local Storage]
E -->|Response| D
F -->|Response| D
G -->|Response| D
MCPサーバーの構築に必要な前提知識と環境
動作確認環境
本記事のコード例は以下の環境で検証しています:
- macOS 14 / Ubuntu 22.04
- Node.js 18以上 / Python 3.11以上
- MCP SDK: @modelcontextprotocol/sdk@0.9.0以上
- Claude API: claude-3-5-sonnet-20241022以上
必要な概念
- Tool Definition: ツールの入出力仕様をJSON Schemaで定義
- Resource Protocol: MCPサーバーが公開するリソース(API、ファイル等)
- Transport Layer: stdio、SSE等の通信方式の選択
実装1: Node.jsを使ったシンプルなMCPサーバー構築
プロジェクト初期化
まずはNode.js環境でMCPサーバーの基本形を作成します。
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk
npm install --save-dev typescript @types/node ts-node
package.jsonに以下を追加してください:
{
"name": "my-mcp-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "ts-node src/server.ts",
"build": "tsc"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^0.9.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"ts-node": "^10.0.0"
}
}
基本的なサーバー実装
src/server.tsを作成します。このサーバーは2つのツール(「天気取得」と「計算機」)を提供します:
import {
Server,
StdioServerTransport,
Tool,
TextContent,
} from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js";
// MCPサーバーのインスタンス作成
const server = new Server({
name: "my-tools-server",
version: "1.0.0",
});
// ツール定義:天気情報を取得
const weatherTool: Tool = {
name: "get_weather",
description: "指定された都市の現在の天気情報を取得します",
inputSchema: {
type: "object",
properties: {
city: {
type: "string",
description: "都市名(例: Tokyo, New York)",
},
unit: {
type: "string",
enum: ["celsius", "fahrenheit"],
description: "温度単位(デフォルト: celsius)",
default: "celsius",
},
},
required: ["city"],
},
};
// ツール定義:計算機
const calculatorTool: Tool = {
name: "calculate",
description: "四則演算を実行します",
inputSchema: {
type: "object",
properties: {
expression: {
type: "string",
description: "計算式(例: 2 + 2 * 3)",
},
},
required: ["expression"],
},
};
// ツール一覧をサーバーに登録
server.setRequestHandler(
{ method: "tools/list" },
async () => ({
tools: [weatherTool, calculatorTool],
})
);
// ツール実行ハンドラー
server.setRequestHandler(
{ method: "tools/call" },
async (request: CallToolRequest) => {
const { name, arguments: args } = request;
if (name === "get_weather") {
// 実際のAPIを呼び出す場合はここで呼び出し
// 例: OpenWeatherMap API、Weather API等
const city = args.city as string;
const unit = (args.unit || "celsius") as string;
// デモンストレーション用のモック応答
const mockWeather = {
city,
temperature: unit === "celsius" ? 22 : 72,
condition: "晴れ",
humidity: 65,
wind_speed: 8,
};
return {
content: [
{
type: "text",
text: JSON.stringify(mockWeather, null, 2),
},
],
};
}
if (name === "calculate") {
const expression = args.expression as string;
try {
// セキュリティ上、evalの使用は推奨されません
// 本番環境ではmath.js等の安全なパーサーを使用してください
const result = Function('"use strict"; return (' + expression + ")")();
return {
content: [
{
type: "text",
text: `計算結果: ${expression} = ${result}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `エラー: 無効な計算式です。詳細: ${error}`,
},
],
isError: true,
};
}
}
return {
content: [
{
type: "text",
text: `不明なツール: ${name}`,
},
],
isError: true,
};
}
);
// stdioトランスポート(標準入出力)でサーバーを起動
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server started on stdio");
}
main().catch(console.error);
このコードのポイント:
Toolインターフェースで入出力を定義(JSON Schemaベース)inputSchemaでパラメータの型とバリデーション規則を指定tools/callハンドラーで実際の処理を実装StdioServerTransportで標準入出力経由の通信
サーバーの起動と動作確認
npm start
コンソールに「MCP Server started on stdio」と表示されればサーバー起動成功です。Ctrl+Cで停止できます。
実装2: Python環境でのカスタムツール構築
Pythonでの環境セットアップ
Python環境を好む開発者向けの実装例です:
python3 -m venv venv
source venv/bin/activate # Mac/Linux
# または
# venv\Scripts\activate # Windows
pip install mcp
pip install fastapi uvicorn
Pythonサーバー実装
server.pyを作成:
import json
import asyncio
from mcp.server.models import InitializationOptions
from mcp.types import Tool, TextContent
from mcp.server import Server
import mcp.types as types
# MCPサーバーのセットアップ
server = Server("python-tools-server")
# ツール定義:テキスト解析ツール
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="analyze_sentiment",
description="テキストの感情分析を実行(ポジティブ/ネガティブ/ニュートラル)",
inputSchema={
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "分析対象のテキスト",
}
},
"required": ["text"],
},
),
Tool(
name="count_words",
description="テキスト内の単語数をカウント",
inputSchema={
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "単語数をカウント対象のテキスト",
}
},
"required": ["text"],
},
),
]
# ツール実行ハンドラー
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.ContentBlock]:
if name == "analyze_sentiment":
text = arguments.get("text", "")
# 簡易的な感情分析(本番環境ではtransformers等を使用)
positive_words = ["素晴らしい", "良い", "最高", "love", "great"]
negative_words = ["悪い", "最悪", "嫌", "hate", "terrible"]
pos_count = sum(1 for word in positive_words if word in text.lower())
neg_count = sum(1 for word in negative_words if word in text.lower())
if pos_count > neg_count:
sentiment = "ポジティブ"
elif neg_count > pos_count:
sentiment = "ネガティブ"
else:
sentiment = "ニュートラル"
result = {
"text": text,
"sentiment": sentiment,
"positive_score": pos_count,
"negative_score": neg_count,
}
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]
elif name == "count_words":
text = arguments.get("text", "")
words = text.split()
result = {
"text": text[:100] + ("..." if len(text) > 100 else ""),
"word_count": len(words),
"character_count": len(text),
}
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]
return [
TextContent(
type="text", text=f"Error: Unknown tool '{name}'", isError=True
)
]
# サーバー起動
async def main():
async with server:
print("Python MCP Server started (stdio transport)")
await asyncio.Event().wait()
if __name__ == "__main__":
asyncio.run(main())
Python実行
python server.py
実装3: MCPサーバーをClaudeと連携させる
Claude APIクライアントの実装
MCPサーバーをClaudeのFunction Callingで実行するクライアント:
import Anthropic from "@anthropic-ai/sdk";
import { spawn } from "child_process";
interface ToolCall {
name: string;
arguments: Record;
}
// MCPサーバープロセスの管理
class MCPClient {
private process: any;
private tools: Record[] = [];
async initialize(serverPath: string): Promise {
// MCPサーバープロセスを起動
this.process = spawn("node", [serverPath], {
stdio: ["pipe", "pipe", "pipe"],
});
this.process.on("error", (err: Error) => {
console.error("MCPサーバーエラー:", err);
});
// ツール一覧を取得
await this.loadTools();
}
private async loadTools(): Promise {
// 実装例では簡略化
this.tools = [
{
name: "get_weather",
description: "天気情報を取得",
input_schema: {
type: "object",
properties: {
city: { type: "string" },
},
required: ["city"],
},
},
{
name: "calculate",
description: "計算式を実行",
input_schema: {
type: "object",
properties: {
expression: { type: "string" },
},
required: ["expression"],
},
},
];
}
getTools(): Record[] {
return this.tools;
}
async callTool(name: string, args: Record): Promise {
// MCPサーバーにツール実行リクエストを送信
// 実装例では簡略化した応答を返す
return JSON.stringify({ result: "success", tool: name, args });
}
terminate(): void {
if (this.process) {
this.process.kill();
}
}
}
// Claude統合メイン処理
async function runWithMCPTools(userMessage: string): Promise {
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
const mcpClient = new MCPClient();
await mcpClient.initialize("./src/server.ts");
const tools: Anthropic.Tool[] = mcpClient.getTools().map((tool: any) => ({
name: tool.name,
description: tool.description,
input_schema: tool.input_schema,
}));
const messages: Anthropic.MessageParam[] = [
{
role: "user",
content: userMessage,
},
];
let response = await client.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1024,
tools,
messages,
});
// ツール呼び出しのループ
while (response.stop_reason === "tool_use") {
const toolUseBlocks = response.content.filter(
(block) => block.type === "tool_use"
) as Anthropic.ToolUseBlock[];
const toolResults: Anthropic.MessageParam[] = [];
for (const toolUse of toolUseBlocks) {
console.log(`\n🔧 ツール呼び出し: ${toolUse.name}`);
console.log(`📥 入力:`, JSON.stringify(toolUse.input, null, 2));
// MCPサーバーでツールを実行
const result = await mcpClient.callTool(
toolUse.name,
toolUse.input as Record
);
console.log(`📤 出力:`, result);
toolResults.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: toolUse.id,
content: result,
},
],
});
}
// ツール実行結果をClaudeに返す
messages.push({
role: "assistant",
content: response.content,
});
messages.push(...toolResults);
response = await client.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1024,
tools,
messages,
});
}
// 最終応答を出力
const finalText = response.content
.filter((block) => block.type === "text")
.map((block: any) => block.text)
.join("\n");
console.log("\n✅ Claude応答:");
console.log(finalText);
mcpClient.terminate();
}
// 実行
runWithMCPTools(
"東京の天気を調べてください。そして2+3*4の計算結果も教えてください。"
).catch(console.error);
このコードの実行フロー:
sequenceDiagram
participant User as ユーザー
participant Claude as Claude API
participant MCP as MCPサーバー
User->>Claude: ツール使用を伴うリクエスト
Claude->>Claude: ツール呼び出しを判定
Claude->>MCP: ツール実行リクエスト
MCP->>MCP: ツール処理実行
MCP->>Claude: 実行結果を返送
Claude->>Claude: 結果を解釈
Claude->>User: 最終応答を返す
実務的なハマりポイントと解決策
エラー1: MCPサーバーが起動しない
症状: Error: spawn ENOENTが出る
原因と対策:
- MCPサーバーファイルのパスが間違っている → 絶対パスで指定
- Node.jsがインストールされていない →
node -vで確認 - 依存パッケージが不足 →
npm installを再実行
// ❌ 相対パスは失敗しやすい
spawn("node", ["./src/server.ts"]);
// ✅ 絶対パスを使う
import { fileURLToPath } from "url";
import { dirname, join } from "path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const serverPath = join(__dirname, "src", "server.ts");
spawn("node", [serverPath]);
エラー2: ツール呼び出しがタイムアウトする
症状: MCPサーバーは起動しているが、Claude呼び出しで時間がかかる
原因と対策:
- サーバープロセスのstdioが正しく接続されていない
- ツール実装内で同期的なI/Oブロッキングが発生
- すべてのツール処理を非同期にする
# ❌ ブロッキングI/O
def call_tool(name: str, arguments: dict):
import time
time.sleep(5) # ハング
return "result"
# ✅ 非同期実装
async def call_tool(name: str, arguments: dict):
import asyncio
await asyncio.sleep(5)
return "result"
エラー3: 入力パラメータのバリデーション失敗
症状: inputSchema validation failed
原因と対策:
- JSON Schemaの定義が矛盾している
- Claude側から送信されたパラメータ型がスキーマと一致していない
- スキーマを厳密にテストする
// ❌ スキーマエラーの例
inputSchema: {
type: "object",
properties: {
count: {
type: "string", // 文字列と定義
},
},
required: ["count"],
}
// ✅ 正しい定義
inputSchema: {
type: "object",
properties: {
count: {
type: "integer", // 数値に統一
minimum: 1,
maximum: 100,
},
},
required: ["count"],
}
スキーマ検証用のテストスイート例:
import Ajv from "ajv";
const ajv = new Ajv();
const schema = {
type: "object",
properties: {
city: { type: "string" },
unit: { enum: ["celsius", "fahrenheit"] },
},
required: ["city"],
};
const validate = ajv.compile(schema);
// テスト
const valid = validate({ city: "Tokyo", unit: "celsius" });
console.log(valid); // true
const invalid = validate({ city: 123 }); // 型エラー
console.log(invalid); // false
console.log(validate.errors); // エラー詳細
エラー4: セキュリティ脆弱性 - evalの使用
前述の計算機実装でevalやFunctionを使うのは危険です。本番環境では必ず安全なパーサーを使用してください。
// ❌ 危険:任意のコード実行
const result = Function('"use strict"; return (' + expression + ")")();
// ✅ 安全:math.jsを使用
import { evaluate } from "mathjs";
const result = evaluate(expression);
npm install mathjs
MCPサーバーのデプロイと運用
コンテナ化(Docker)
MCPサーバーをDocker環境で運用する場合のDockerfile例:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY src ./src
COPY tsconfig.json ./
EXPOSE 3000
CMD ["npm", "start"]
本番環境での構成
複数のMCPサーバーを運用する場合、以下のように構成することをお勧めします:
graph TD
A[Claude Application] -->|Router| B[Load Balancer]
B -->|Server 1| C[MCP Server Instance A]
B -->|Server 2| D[MCP Server Instance B]
B -->|Server 3| E[MCP Server Instance C]
C -->|Cache| F[Redis]
D -->|Cache| F
E -->|Cache| F
C -->|Logging| G[CloudWatch/DataDog]
D -->|Logging| G
E -->|Logging| G
パフォーマンス最適化
- キャッシング: 同じクエリ結果をRedis等で再利用
- 接続プーリング: データベース接続を再利用
- 非同期処理: 長時間処理はバックグラウンドジョブに
- レート制限: API呼び出し数を制限し、過負荷を防止
import NodeCache from "node-cache";
const cache = new NodeCache({ stdTTL: 600 }); // 10分キャッシュ
server.setRequestHandler(
{ method: "tools/call" },
async (request: CallToolRequest) => {
const cacheKey = `${request.name}:${JSON.stringify(request.arguments)}`;
// キャッシュをチェック
const cached = cache.get(cacheKey);
if (cached) {
return cached;
}
// ツール実行
const result = await executeToolInternal(request);
// 結果をキャッシュ
cache.set(cacheKey, result);
return result;
}
);
MCPと類似ツールとの比較
| ツール | 特徴 | 使い分け |
|---|---|---|
| MCP | 標準化されたプロトコル、多くのAIモデルに対応予定 | エンタープライズ向け、複数のAI連携 |
| Function Calling | Claude/OpenAI等が独自実装、シンプル | シンプルなツール連携、単一プロバイダー |
| LangChain Tools | Pythonフレームワーク、エージェント統合 |
𝕏 ポスト
Facebook
LINE
おすすめAIリソース
|