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の使用

前述の計算機実装でevalFunctionを使うのは危険です。本番環境では必ず安全なパーサーを使用してください。


// ❌ 危険:任意のコード実行
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フレームワーク、エージェント統合
K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →