🐕

MCPサーバー統合で実現する拡張可能なコーディングエージェント

に公開

GitHub

🔗 GitHubリポジトリ: https://github.com/nogataka/coding-agent-sample-mcp

はじめに

本記事は「マルチプロバイダー対応コーディングエージェントの構築」の続編として、Model Context Protocol (MCP) を使用してコーディングエージェントに外部ツールを統合する方法を解説します。

前回の記事では、Anthropic ClaudeとOpenAI GPTの両方に対応したコーディングエージェントを構築しました。今回は、MCPという標準化されたプロトコルを使用して、エージェントの機能を動的に拡張する方法を学びます。

サンプル画面

この記事で学べること

  • MCP (Model Context Protocol) の基本概念と設計思想
  • JSON-RPC 2.0を使った双方向通信の実装
  • 独自のMCPサーバーの作成方法(Echo サーバー実装)
  • MCPクライアントとマネージャーの実装
  • 既存のエージェントへのMCP統合
  • 環境変数を使った安全な設定管理

前提知識

  • TypeScript/JavaScriptの基礎
  • Node.jsでの開発経験
  • 非同期プログラミング (async/await)
  • 前回の記事で構築したコーディングエージェント

完成するもの

本記事を読み終えると、以下の機能を持つMCP統合エージェントが完成します:

  • ✅ Echo MCPサーバー(テスト用の3つのツール)
  • ✅ MCPクライアント(JSON-RPC 2.0通信)
  • ✅ 複数サーバーを管理するMCPマネージャー
  • ✅ ローカルツールとMCPツールのシームレスな統合
  • ✅ 環境変数による安全な設定管理

第1章: MCPとは何か

1.1 Model Context Protocolの概要

MCP (Model Context Protocol) は、Anthropicが開発した標準化されたプロトコルで、LLMアプリケーションが外部ツールやデータソースと対話するための統一的な方法を提供します。

MCPの主な特徴

  1. 標準化された通信

    • JSON-RPC 2.0ベースのプロトコル
    • 明確な仕様とエラーハンドリング
  2. サーバー/クライアントモデル

    • MCPサーバー: ツールやリソースを提供
    • MCPクライアント: サーバーと通信してツールを利用
  3. 複数のトランスポート

    • stdio (標準入出力)
    • HTTP
    • WebSocket
  4. 動的なツール発見

    • 実行時にサーバーが提供するツールを自動検出
    • ツールのスキーマ定義

1.2 なぜMCPが必要なのか

従来の課題

// 従来: ツールをハードコーディング
class Agent {
  async executeTool(name: string, args: any) {
    switch(name) {
      case 'read_file':
        return await this.readFile(args);
      case 'execute_command':
        return await this.executeCommand(args);
      // 新しいツールを追加するたびにコード変更が必要...
    }
  }
}

問題点:

  • ツールを追加するたびにエージェントのコードを変更
  • ツールのロジックがエージェントに密結合
  • 他のプロジェクトでツールを再利用できない
  • ツールのバージョン管理が困難

MCPによる解決

// MCP: ツールを動的に読み込み
class Agent {
  async initialize() {
    // MCPサーバーから利用可能なツールを取得
    const tools = await this.mcpManager.getAllTools();
    // ツールは自動的に統合される
  }
  
  async executeTool(name: string, args: any) {
    // MCPサーバーにツール実行を委譲
    return await this.mcpManager.executeTool(name, args);
  }
}

メリット:

  • ✅ コード変更なしで新しいツールを追加
  • ✅ ツールのロジックを分離・再利用可能
  • ✅ 標準化されたインターフェース
  • ✅ 複数のツールプロバイダーを統合

1.3 MCPのアーキテクチャ


第2章: Echo MCPサーバーの実装

実際にMCPサーバーを作成して、プロトコルの動作を理解しましょう。

2.1 プロジェクト構造

mcp-servers/
└── echo/
    ├── index.js        # サーバー実装
    ├── package.json    # 設定ファイル
    └── README.md       # ドキュメント

2.2 package.jsonの作成

まず、サーバーの基本設定を定義します。

{
  "name": "mcp-server-echo",
  "version": "1.0.0",
  "description": "Simple Echo MCP Server for testing",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node index.js"
  },
  "keywords": ["mcp", "echo", "test"],
  "author": "",
  "license": "MIT"
}

ポイント:

  • "type": "module": ES Modulesを使用
  • "main": "index.js": エントリーポイント

2.3 サーバーの実装 - 基本構造

index.jsを作成し、基本的な構造を定義します。

#!/usr/bin/env node

import { createInterface } from 'readline';

// サーバー情報
const SERVER_INFO = {
  name: 'echo-server',
  version: '1.0.0'
};

// プロトコルバージョン
const PROTOCOL_VERSION = '2024-11-05';

// サーバーの状態
let isInitialized = false;

/**
 * メイン処理 - 標準入力からJSON-RPCリクエストを読み取る
 */
function main() {
  console.error('[Echo Server] Starting...');
  console.error('[Echo Server] Waiting for JSON-RPC requests on stdin');
  
  const rl = createInterface({
    input: process.stdin,
    output: process.stdout,
    terminal: false
  });

  rl.on('line', (line) => {
    if (line.trim() === '') return;
    
    try {
      const request = JSON.parse(line);
      handleRequest(request);
    } catch (error) {
      console.error(`[Echo Server] Failed to parse JSON: ${error.message}`);
      sendError(null, -32700, 'Parse error');
    }
  });

  rl.on('close', () => {
    console.error('[Echo Server] Connection closed');
    process.exit(0);
  });
}

main();

重要な設計判断:

  1. 標準エラー出力でログ

    console.error('[Echo Server] Starting...');
    
    • console.error(): デバッグログ用(stderr)
    • console.log(): JSON-RPCレスポンス用(stdout)
    • この分離により、ログとデータを混在させない
  2. 行単位での処理

    rl.on('line', (line) => {
      const request = JSON.parse(line);
      handleRequest(request);
    });
    
    • JSON-RPCメッセージは改行で区切られる
    • 1行 = 1メッセージ
  3. shebang行

    #!/usr/bin/env node
    
    • Unixシステムで直接実行可能にする

2.4 JSON-RPCレスポンスの実装

MCPはJSON-RPC 2.0プロトコルを使用します。

/**
 * JSON-RPCレスポンスを送信
 */
function sendResponse(id, result) {
  const response = {
    jsonrpc: '2.0',
    id,
    result
  };
  console.log(JSON.stringify(response));
}

/**
 * JSON-RPCエラーレスポンスを送信
 */
function sendError(id, code, message, data = undefined) {
  const response = {
    jsonrpc: '2.0',
    id,
    error: {
      code,
      message,
      ...(data && { data })
    }
  };
  console.log(JSON.stringify(response));
}

/**
 * 通知を送信(idなし)
 */
function sendNotification(method, params) {
  const notification = {
    jsonrpc: '2.0',
    method,
    params
  };
  console.log(JSON.stringify(notification));
}

JSON-RPC 2.0の標準エラーコード:

コード 意味 説明
-32700 Parse error 無効なJSON
-32600 Invalid Request JSON-RPC仕様違反
-32601 Method not found メソッドが存在しない
-32602 Invalid params パラメータが不正
-32603 Internal error サーバー内部エラー
-32002 Server not initialized カスタムエラー(MCP)

2.5 initializeメソッドの実装

MCPサーバーの初期化処理を実装します。

/**
 * initializeリクエストの処理
 */
function handleInitialize(id, params) {
  // クライアント情報をログに記録
  console.error(`[Echo Server] Initialized with client: ${params?.clientInfo?.name || 'unknown'}`);
  
  // サーバーのケイパビリティを返す
  sendResponse(id, {
    protocolVersion: PROTOCOL_VERSION,
    capabilities: {
      tools: {},      // ツール機能をサポート
      resources: {},  // リソース機能(今回は未実装)
      prompts: {}     // プロンプト機能(今回は未実装)
    },
    serverInfo: SERVER_INFO
  });
  
  isInitialized = true;
}

初期化シーケンスの重要性:

初期化が完了するまで、他のリクエストは拒否されます:

function handleToolsList(id) {
  if (!isInitialized) {
    sendError(id, -32002, 'Server not initialized');
    return;
  }
  // ツールリストを返す...
}

2.6 tools/listメソッドの実装

利用可能なツールのリストを返します。

/**
 * tools/listリクエストの処理
 */
function handleToolsList(id) {
  if (!isInitialized) {
    sendError(id, -32002, 'Server not initialized');
    return;
  }

  // 提供するツールのリスト
  const tools = [
    {
      name: 'echo',
      description: 'Echoes back the provided message',
      inputSchema: {
        type: 'object',
        properties: {
          message: {
            type: 'string',
            description: 'The message to echo back'
          }
        },
        required: ['message']
      }
    },
    {
      name: 'reverse',
      description: 'Reverses the provided text',
      inputSchema: {
        type: 'object',
        properties: {
          text: {
            type: 'string',
            description: 'The text to reverse'
          }
        },
        required: ['text']
      }
    },
    {
      name: 'uppercase',
      description: 'Converts text to uppercase',
      inputSchema: {
        type: 'object',
        properties: {
          text: {
            type: 'string',
            description: 'The text to convert to uppercase'
          }
        },
        required: ['text']
      }
    }
  ];

  sendResponse(id, { tools });
  console.error(`[Echo Server] Sent ${tools.length} tools`);
}

ツール定義のベストプラクティス:

  1. 明確な命名

    name: 'echo'  // ✓ シンプルで分かりやすい
    // ではなく
    name: 'echoMessageBack'  // ✗ 冗長
    
  2. 詳細な説明

    description: 'Echoes back the provided message'
    // LLMがツールを選択する際の重要な情報
    
  3. 厳密なスキーマ定義

    inputSchema: {
      type: 'object',
      properties: {
        message: {
          type: 'string',
          description: 'The message to echo back'  // パラメータの説明
        }
      },
      required: ['message']  // 必須パラメータを明示
    }
    

2.7 tools/callメソッドの実装

実際のツール実行処理を実装します。

/**
 * tools/callリクエストの処理
 */
function handleToolsCall(id, params) {
  if (!isInitialized) {
    sendError(id, -32002, 'Server not initialized');
    return;
  }

  const { name, arguments: args } = params;
  
  console.error(`[Echo Server] Tool call: ${name} with args:`, JSON.stringify(args));

  try {
    let result;

    switch (name) {
      case 'echo':
        // パラメータのバリデーション
        if (!args.message) {
          sendError(id, -32602, 'Invalid params: message is required');
          return;
        }
        
        result = {
          content: [
            {
              type: 'text',
              text: `Echo: ${args.message}`
            }
          ]
        };
        break;

      case 'reverse':
        if (!args.text) {
          sendError(id, -32602, 'Invalid params: text is required');
          return;
        }
        
        const reversed = args.text.split('').reverse().join('');
        result = {
          content: [
            {
              type: 'text',
              text: `Reversed: ${reversed}`
            }
          ]
        };
        break;

      case 'uppercase':
        if (!args.text) {
          sendError(id, -32602, 'Invalid params: text is required');
          return;
        }
        
        result = {
          content: [
            {
              type: 'text',
              text: `Uppercase: ${args.text.toUpperCase()}`
            }
          ]
        };
        break;

      default:
        sendError(id, -32601, `Method not found: ${name}`);
        return;
    }

    sendResponse(id, result);
    console.error(`[Echo Server] Tool executed successfully: ${name}`);
    
  } catch (error) {
    console.error(`[Echo Server] Error executing tool: ${error.message}`);
    sendError(id, -32603, 'Internal error', error.message);
  }
}

ツール実行のベストプラクティス:

  1. 入力のバリデーション

    if (!args.message) {
      sendError(id, -32602, 'Invalid params: message is required');
      return;
    }
    
  2. 統一されたレスポンス形式

    result = {
      content: [
        {
          type: 'text',  // コンテンツタイプ
          text: `Echo: ${args.message}`  // 実際の内容
        }
      ]
    };
    
  3. 包括的なエラーハンドリング

    try {
      // ツール実行
    } catch (error) {
      sendError(id, -32603, 'Internal error', error.message);
    }
    

2.8 リクエストルーティング

各メソッドに適切にリクエストを振り分けます。

/**
 * リクエストを処理
 */
function handleRequest(request) {
  try {
    const { jsonrpc, method, id, params } = request;

    // JSON-RPC 2.0のバリデーション
    if (jsonrpc !== '2.0') {
      sendError(id, -32600, 'Invalid Request: jsonrpc must be "2.0"');
      return;
    }

    console.error(`[Echo Server] Received method: ${method}`);

    // メソッドに応じて処理を分岐
    switch (method) {
      case 'initialize':
        handleInitialize(id, params);
        break;

      case 'notifications/initialized':
        // initialized通知を受け取った
        console.error('[Echo Server] Client confirmed initialization');
        break;

      case 'tools/list':
        handleToolsList(id);
        break;

      case 'tools/call':
        handleToolsCall(id, params);
        break;

      case 'ping':
        // オプション: ヘルスチェック用
        sendResponse(id, { status: 'ok' });
        break;

      default:
        sendError(id, -32601, `Method not found: ${method}`);
    }
  } catch (error) {
    console.error(`[Echo Server] Error handling request: ${error.message}`);
    sendError(null, -32700, 'Parse error');
  }
}

2.9 エラーハンドリングとクリーンアップ

プロセスレベルのエラーハンドリングを追加します。

// エラーハンドリング
process.on('uncaughtException', (error) => {
  console.error(`[Echo Server] Uncaught exception: ${error.message}`);
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  console.error(`[Echo Server] Unhandled rejection at: ${promise}, reason: ${reason}`);
});

// SIGINT/SIGTERMハンドリング
process.on('SIGINT', () => {
  console.error('[Echo Server] Received SIGINT, shutting down...');
  process.exit(0);
});

process.on('SIGTERM', () => {
  console.error('[Echo Server] Received SIGTERM, shutting down...');
  process.exit(0);
});

Echo MCPサーバーの完成!

これで、JSON-RPC 2.0プロトコルに準拠した完全なMCPサーバーができました。次は、このサーバーと通信するクライアントを実装します。


第3章: MCPクライアントの実装

サーバーを作成したので、次はそれと通信するクライアントを実装します。

3.1 型定義

TypeScriptで型安全なクライアントを実装するため、まず型定義を作成します。

// src/mcp/types.ts

/**
 * JSON-RPC 2.0 リクエスト
 */
export interface JSONRPCRequest {
  jsonrpc: '2.0';
  id?: string | number;
  method: string;
  params?: any;
}

/**
 * JSON-RPC 2.0 レスポンス
 */
export interface JSONRPCResponse {
  jsonrpc: '2.0';
  id: string | number | null;
  result?: any;
  error?: JSONRPCError;
}

/**
 * JSON-RPC 2.0 エラー
 */
export interface JSONRPCError {
  code: number;
  message: string;
  data?: any;
}

/**
 * MCPサーバーの設定
 */
export interface MCPServerConfig {
  command: string;           // 実行コマンド
  args: string[];            // 引数
  transport: 'stdio' | 'http' | 'websocket';
  env?: Record<string, string>;  // 環境変数
  description?: string;
}

/**
 * MCPツールの定義
 */
export interface MCPToolDefinition {
  name: string;
  description: string;
  inputSchema: {
    type: 'object';
    properties: Record<string, any>;
    required?: string[];
  };
}

/**
 * MCPツール呼び出しのパラメータ
 */
export interface MCPToolCallParams {
  name: string;
  arguments: Record<string, any>;
}

/**
 * MCPツール実行結果
 */
export interface MCPToolResult {
  content: Array<{
    type: 'text' | 'image' | 'resource';
    text?: string;
    data?: string;
    mimeType?: string;
  }>;
}

/**
 * MCPクライアントの状態
 */
export enum MCPClientState {
  DISCONNECTED = 'disconnected',
  CONNECTING = 'connecting',
  CONNECTED = 'connected',
  INITIALIZED = 'initialized',
  ERROR = 'error'
}

3.2 MCPClientクラスの基本構造

個別のMCPサーバーとの通信を管理するクライアントを実装します。

// src/mcp/client.ts

import { spawn, ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import { MCPServerConfig, MCPToolDefinition, MCPClientState } from './types';
import chalk from 'chalk';

export class MCPClient extends EventEmitter {
  private config: MCPServerConfig;
  private serverName: string;
  private process: ChildProcess | null = null;
  private state: MCPClientState = MCPClientState.DISCONNECTED;
  private requestId = 0;
  private pendingRequests = new Map<number, {
    resolve: (value: any) => void;
    reject: (error: Error) => void;
    timeout: NodeJS.Timeout;
  }>();
  private buffer = '';
  private serverInfo: { name: string; version: string } | null = null;
  private tools: MCPToolDefinition[] = [];

  constructor(serverName: string, config: MCPServerConfig) {
    super();
    this.serverName = serverName;
    this.config = config;
  }
  
  // メソッドは以下で実装...
}

設計のポイント:

  1. EventEmitterを継承

    export class MCPClient extends EventEmitter
    
    • イベント駆動アーキテクチャ
    • disconnecterrorなどのイベントを発行
  2. 状態管理

    private state: MCPClientState = MCPClientState.DISCONNECTED;
    
    • 明確な状態遷移
    • 不正な操作を防ぐ
  3. 保留中のリクエスト管理

    private pendingRequests = new Map<number, {...}>();
    
    • リクエストIDとPromiseを紐付け
    • タイムアウト管理

3.3 サーバープロセスの起動

MCPサーバープロセスを起動し、通信チャネルを確立します。

/**
 * サーバーに接続
 */
async connect(): Promise<void> {
  if (this.state !== MCPClientState.DISCONNECTED) {
    throw new Error(`Cannot connect: client is in ${this.state} state`);
  }

  this.state = MCPClientState.CONNECTING;
  console.error(chalk.blue(`[MCP Client: ${this.serverName}] Connecting...`));

  try {
    // サーバープロセスを起動
    this.process = spawn(this.config.command, this.config.args, {
      stdio: ['pipe', 'pipe', 'pipe'],  // stdin, stdout, stderr
      env: { ...process.env, ...this.config.env }
    });

    // 標準出力からのデータを処理
    this.process.stdout?.on('data', (data) => {
      this.handleData(data);
    });

    // 標準エラー出力を表示(デバッグ用)
    this.process.stderr?.on('data', (data) => {
      console.error(chalk.gray(`[MCP Server: ${this.serverName}] ${data.toString().trim()}`));
    });

    // プロセス終了時の処理
    this.process.on('close', (code) => {
      console.error(chalk.yellow(`[MCP Client: ${this.serverName}] Server process exited with code ${code}`));
      this.state = MCPClientState.DISCONNECTED;
      this.emit('disconnect');
    });

    // プロセスエラー処理
    this.process.on('error', (error) => {
      console.error(chalk.red(`[MCP Client: ${this.serverName}] Process error: ${error.message}`));
      this.state = MCPClientState.ERROR;
      this.emit('error', error);
    });

    this.state = MCPClientState.CONNECTED;
    console.error(chalk.green(`[MCP Client: ${this.serverName}] Connected`));
    
  } catch (error: any) {
    this.state = MCPClientState.ERROR;
    console.error(chalk.red(`[MCP Client: ${this.serverName}] Connection failed: ${error.message}`));
    throw error;
  }
}

重要な実装詳細:

  1. stdio設定

    stdio: ['pipe', 'pipe', 'pipe']
    
    • [stdin, stdout, stderr]
    • すべてパイプで接続してプログラムから制御
  2. 環境変数のマージ

    env: { ...process.env, ...this.config.env }
    
    • 親プロセスの環境変数を継承
    • サーバー固有の環境変数を追加
  3. イベントハンドリング

    this.process.on('close', (code) => {
      this.emit('disconnect');
    });
    
    • プロセス終了を親に通知

3.4 ストリームデータの処理

stdin/stdoutはストリームなので、不完全なデータを適切に処理する必要があります。

/**
 * サーバーからのデータを処理
 */
private handleData(data: Buffer): void {
  this.buffer += data.toString();

  // 改行で区切られた複数のメッセージを処理
  const lines = this.buffer.split('\n');
  this.buffer = lines.pop() || ''; // 最後の不完全な行をバッファに保持

  for (const line of lines) {
    if (line.trim() === '') continue;

    try {
      const response: JSONRPCResponse = JSON.parse(line);
      this.handleResponse(response);
    } catch (error: any) {
      console.error(chalk.red(`[MCP Client: ${this.serverName}] Failed to parse response`));
    }
  }
}

なぜバッファリングが必要か:

ストリームデータは一度に完全なJSONが来るとは限りません:

// データの到着パターン例
// 1回目: '{"jsonrpc":"2.0","id":1,"result":{"too'
// 2回目: 'ls":[]}}\n{"jsonrpc":"2.0","id":2,'
// 3回目: '"result":{"status":"ok"}}\n'

// バッファリング処理で完全なメッセージを組み立てる

3.5 非同期リクエスト/レスポンス管理

リクエストIDとPromiseを紐付けて、非同期通信を実現します。

/**
 * JSON-RPCリクエストを送信
 */
private async sendRequest<T = any>(method: string, params?: any): Promise<T> {
  if (!this.process || !this.process.stdin) {
    throw new Error('Process not available');
  }

  const id = ++this.requestId;
  const request: JSONRPCRequest = {
    jsonrpc: '2.0',
    id,
    method,
    ...(params && { params })
  };

  return new Promise((resolve, reject) => {
    // タイムアウトを設定(30秒)
    const timeout = setTimeout(() => {
      this.pendingRequests.delete(id);
      reject(new Error(`Request timeout: ${method}`));
    }, 30000);

    // リクエストを登録
    this.pendingRequests.set(id, { resolve, reject, timeout });

    // リクエストを送信
    const requestStr = JSON.stringify(request) + '\n';
    this.process!.stdin!.write(requestStr);
  });
}

/**
 * JSON-RPCレスポンスを処理
 */
private handleResponse(response: JSONRPCResponse): void {
  const { id, result, error } = response;

  if (id === null || id === undefined) {
    // 通知(サーバーからのイベント)
    return;
  }

  const pending = this.pendingRequests.get(Number(id));
  if (!pending) {
    return;
  }

  // タイムアウトをクリア
  clearTimeout(pending.timeout);
  this.pendingRequests.delete(Number(id));

  // レスポンスまたはエラーを処理
  if (error) {
    pending.reject(new Error(`${error.message} (code: ${error.code})`));
  } else {
    pending.resolve(result);
  }
}

非同期通信の流れ:

3.6 初期化とツール取得

サーバーを初期化し、利用可能なツールを取得します。

/**
 * サーバーを初期化
 */
async initialize(): Promise<MCPInitializeResult> {
  if (this.state !== MCPClientState.CONNECTED) {
    throw new Error(`Cannot initialize: client is in ${this.state} state`);
  }

  const result = await this.sendRequest<MCPInitializeResult>('initialize', {
    protocolVersion: '2024-11-05',
    capabilities: { tools: {} },
    clientInfo: {
      name: 'coding-agent',
      version: '1.0.0'
    }
  });

  this.serverInfo = result.serverInfo;
  
  // initialized 通知を送信
  await this.sendNotification('notifications/initialized');
  
  this.state = MCPClientState.INITIALIZED;
  
  return result;
}

/**
 * 利用可能なツールのリストを取得
 */
async listTools(): Promise<MCPToolDefinition[]> {
  if (this.state !== MCPClientState.INITIALIZED) {
    throw new Error(`Cannot list tools: client is in ${this.state} state`);
  }

  const result = await this.sendRequest<MCPToolsListResult>('tools/list');
  this.tools = result.tools;
  
  return this.tools;
}

/**
 * ツールを呼び出し
 */
async callTool(params: MCPToolCallParams): Promise<MCPToolResult> {
  if (this.state !== MCPClientState.INITIALIZED) {
    throw new Error(`Cannot call tool: client is in ${this.state} state`);
  }

  const result = await this.sendRequest<MCPToolResult>('tools/call', params);
  
  return result;
}

3.7 クリーンアップと切断

リソースを適切に解放します。

/**
 * 接続を切断
 */
async disconnect(): Promise<void> {
  if (this.process) {
    // すべての保留中のリクエストをキャンセル
    for (const [id, pending] of this.pendingRequests) {
      clearTimeout(pending.timeout);
      pending.reject(new Error('Client disconnected'));
    }
    this.pendingRequests.clear();

    // プロセスを終了
    this.process.kill();
    this.process = null;
    this.state = MCPClientState.DISCONNECTED;
  }
}

第4章: MCPClientManagerの実装

複数のMCPサーバーを統合管理するマネージャーを実装します。

4.1 設定ファイルの定義

// mcp-config.json
{
  "mcpServers": {
    "echo": {
      "command": "node",
      "args": ["mcp-servers/echo/index.js"],
      "transport": "stdio",
      "description": "Simple echo server for testing"
    }
  }
}

4.2 MCPClientManagerの実装

// src/mcp/manager.ts

import * as fs from 'fs';
import * as path from 'path';
import { MCPClient } from './client';
import chalk from 'chalk';

export class MCPClientManager {
  private clients = new Map<string, MCPClient>();
  private allTools: MCPToolDefinition[] = [];
  private configPath: string;

  constructor(configPath?: string) {
    this.configPath = configPath || process.env.MCP_CONFIG_PATH || './mcp-config.json';
  }

  /**
   * 設定ファイルを読み込んでサーバーに接続
   */
  async initialize(): Promise<void> {
    const config = this.loadConfig();
    
    if (Object.keys(config.mcpServers).length === 0) {
      console.error(chalk.yellow('[MCP Manager] No MCP servers configured'));
      return;
    }

    // 各サーバーに接続
    for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
      try {
        await this.connectServer(serverName, serverConfig);
      } catch (error: any) {
        console.error(chalk.red(`Failed to connect to ${serverName}: ${error.message}`));
        // エラーが発生しても他のサーバーへの接続を続ける
      }
    }
  }

  /**
   * 個別のサーバーに接続
   */
  private async connectServer(serverName: string, config: MCPServerConfig): Promise<void> {
    const client = new MCPClient(serverName, config);

    // 接続 → 初期化 → ツール取得
    await client.connect();
    await client.initialize();
    const tools = await client.listTools();
    
    // ツールにプレフィックスを付けて保存
    const prefixedTools = tools.map(tool => ({
      ...tool,
      name: `mcp_${serverName}_${tool.name}`,
      description: `[${serverName}] ${tool.description}`
    }));

    this.allTools.push(...prefixedTools);
    this.clients.set(serverName, client);
  }
  
  // 続く...
}

プレフィックス管理の重要性:

// 元のツール名: echo
// ↓
// 統合後のツール名: mcp_echo_echo
//                    ^^^^ ^^^^^ ^^^^
//                    |    |     └─ ツール名
//                    |    └─ サーバー名
//                    └─ MCPプレフィックス

// メリット:
// 1. 名前の衝突を防ぐ
// 2. ツールの出所が明確
// 3. ルーティングが容易

4.3 ツール実行とルーティング

/**
 * すべてのMCPツールを取得
 */
getAllTools(): MCPToolDefinition[] {
  return this.allTools;
}

/**
 * MCPツールを実行
 */
async executeTool(toolName: string, args: Record<string, any>): Promise<string> {
  // ツール名からサーバー名と実際のツール名を抽出
  const match = toolName.match(/^mcp_([^_]+)_(.+)$/);
  
  if (!match) {
    throw new Error(`Invalid MCP tool name format: ${toolName}`);
  }

  const [, serverName, actualToolName] = match;
  const client = this.clients.get(serverName);

  if (!client) {
    throw new Error(`MCP server not found: ${serverName}`);
  }

  try {
    const result = await client.callTool({
      name: actualToolName,
      arguments: args
    });

    // 結果を文字列に変換
    const output = result.content
      .map(item => item.type === 'text' && item.text ? item.text : JSON.stringify(item))
      .join('\n');
    
    return output;
  } catch (error: any) {
    console.error(chalk.red(`Tool execution failed: ${error.message}`));
    throw error;
  }
}

/**
 * すべてのサーバーから切断
 */
async disconnectAll(): Promise<void> {
  for (const [serverName, client] of this.clients) {
    try {
      await client.disconnect();
    } catch (error: any) {
      console.error(chalk.red(`Failed to disconnect ${serverName}`));
    }
  }

  this.clients.clear();
  this.allTools = [];
}

第5章: エージェントへの統合

既存のCodingAgentにMCP機能を統合します。

5.1 エージェントクラスの更新

// src/agent.ts

import { ToolManager } from './tools/toolManager';
import { AIProvider } from './providers/types';
import { MCPClientManager } from './mcp';

export class CodingAgent {
  private provider: AIProvider;
  private toolManager: ToolManager;
  private mcpManager: MCPClientManager;
  private mcpEnabled: boolean;

  constructor(provider: AIProvider, enableMCP: boolean = true) {
    this.provider = provider;
    this.toolManager = new ToolManager();
    this.mcpManager = new MCPClientManager();
    this.mcpEnabled = enableMCP;
  }

  /**
   * エージェントを初期化(MCP接続を含む)
   */
  async initialize(): Promise<void> {
    if (this.mcpEnabled) {
      try {
        await this.mcpManager.initialize();
      } catch (error: any) {
        console.error(`Failed to initialize MCP: ${error.message}`);
        this.mcpEnabled = false;
      }
    }
  }
}

5.2 ツール定義の統合

/**
 * ツール定義を取得
 */
private getToolDefinitions(): any[] {
  // ローカルツールを取得
  let tools = this.toolManager.getAnthropicToolDefinitions();
  
  // MCPツールを追加
  if (this.mcpEnabled) {
    const mcpTools = this.mcpManager.getAllTools();
    tools = [...tools, ...mcpTools];
  }
  
  return tools;
}

5.3 ツール実行の振り分け

/**
 * ツールを実行
 */
private async executeTools(toolCalls: ToolCall[]): Promise<any[]> {
  const results: any[] = [];

  for (const toolCall of toolCalls) {
    try {
      let result;
      
      // MCPツールかローカルツールかを判定
      if (toolCall.name.startsWith('mcp_')) {
        // MCPツールを実行
        const output = await this.mcpManager.executeTool(
          toolCall.name,
          toolCall.arguments
        );
        result = { success: true, output };
      } else {
        // ローカルツールを実行
        result = await this.toolManager.executeTool(
          toolCall.name,
          toolCall.arguments
        );
      }

      const resultMessage = this.provider.createToolResultMessage(
        toolCall.id,
        result.success ? result.output : result.error
      );
      results.push(resultMessage);
      
    } catch (error: any) {
      const resultMessage = this.provider.createToolResultMessage(
        toolCall.id,
        `Error: ${error.message}`
      );
      results.push(resultMessage);
    }
  }

  return results;
}

5.4 クリーンアップ処理

/**
 * クリーンアップ
 */
async cleanup(): Promise<void> {
  if (this.mcpEnabled) {
    await this.mcpManager.disconnectAll();
  }
}

第6章: 環境変数による設定管理

APIキーやMCP設定を安全に管理します。

6.1 dotenvの設定

// src/index.ts
import dotenv from 'dotenv';

async function main() {
  // 環境変数を読み込む
  dotenv.config();
  
  // ...
}

6.2 .envファイルの作成

# .env
ANTHROPIC_API_KEY=sk-ant-api03-your-key-here
OPENAI_API_KEY=sk-proj-your-key-here
MCP_CONFIG_PATH=./mcp-config.json

6.3 環境変数チェックツール

// check-env.js
require('dotenv').config();
const chalk = require('chalk');

console.log(chalk.bold.cyan('\n=== Environment Variables Check ===\n'));

const envVars = [
  { name: 'ANTHROPIC_API_KEY', description: 'Anthropic Claude API Key' },
  { name: 'OPENAI_API_KEY', description: 'OpenAI GPT API Key' },
  { name: 'MCP_CONFIG_PATH', description: 'MCP Configuration Path' }
];

for (const envVar of envVars) {
  const value = process.env[envVar.name];
  const isSet = value && value.length > 0;
  
  if (isSet) {
    const maskedValue = value.substring(0, 10) + '...';
    console.log(chalk.green('✓'), envVar.name);
    console.log(chalk.gray(`  Value: ${maskedValue}`));
  } else {
    console.log(chalk.yellow('○'), envVar.name);
    console.log(chalk.gray('  Not set'));
  }
  console.log();
}

第7章: テストと動作確認

7.1 MCPテストスクリプト

// test-mcp.js
require('dotenv').config();

const { MCPClientManager } = require('./dist/mcp/manager');
const chalk = require('chalk');

async function testMCP() {
  console.log(chalk.bold.cyan('\n=== MCP Integration Test ===\n'));

  const manager = new MCPClientManager();

  try {
    // 1. 初期化
    console.log(chalk.blue('Step 1: Initializing...'));
    await manager.initialize();
    console.log(chalk.green('✓ Initialization complete\n'));

    // 2. ステータス表示
    console.log(chalk.blue('Step 2: Checking status...'));
    manager.printStatus();

    // 3. Echoツールのテスト
    console.log(chalk.blue('Step 3: Testing echo tool...'));
    const echoResult = await manager.executeTool('mcp_echo_echo', {
      message: 'Hello from MCP!'
    });
    console.log(chalk.green('✓ Echo result:'));
    console.log(chalk.gray(`  ${echoResult}\n`));

    // 4. Reverseツールのテスト
    console.log(chalk.blue('Step 4: Testing reverse tool...'));
    const reverseResult = await manager.executeTool('mcp_echo_reverse', {
      text: 'Hello World'
    });
    console.log(chalk.green('✓ Reverse result:'));
    console.log(chalk.gray(`  ${reverseResult}\n`));

    // 5. Uppercaseツールのテスト
    console.log(chalk.blue('Step 5: Testing uppercase tool...'));
    const uppercaseResult = await manager.executeTool('mcp_echo_uppercase', {
      text: 'hello world'
    });
    console.log(chalk.green('✓ Uppercase result:'));
    console.log(chalk.gray(`  ${uppercaseResult}\n`));

    // 6. クリーンアップ
    console.log(chalk.blue('Step 6: Cleaning up...'));
    await manager.disconnectAll();
    console.log(chalk.green('✓ Cleanup complete\n'));

    console.log(chalk.bold.green('=== All tests passed! ===\n'));
    process.exit(0);

  } catch (error) {
    console.error(chalk.red('\n✗ Test failed:'), error.message);
    process.exit(1);
  }
}

testMCP();

7.2 実行方法

# 1. ビルド
npm run build

# 2. 環境変数チェック
npm run check-env

# 3. MCPテスト
npm run test-mcp

# 4. エージェント起動
node dist/index.js chat

7.3 期待される出力

=== MCP Integration Test ===

Step 1: Initializing...
[MCP Manager] Connecting to server: echo
✓ Initialization complete

Step 2: Checking status...
=== MCP Manager Status ===
Connected servers: 1

  Server: echo
    Status: initialized
    Tools: 3
      - mcp_echo_echo
      - mcp_echo_reverse
      - mcp_echo_uppercase

Step 3: Testing echo tool...
✓ Echo result:
  Echo: Hello from MCP!

Step 4: Testing reverse tool...
✓ Reverse result:
  Reversed: dlroW olleH

Step 5: Testing uppercase tool...
✓ Uppercase result:
  Uppercase: HELLO WORLD

Step 6: Cleaning up...
✓ Cleanup complete

=== All tests passed! ===

第8章: 実践的な使い方

8.1 エージェントでMCPツールを使う

$ node dist/index.js chat

🔌 Initializing MCP connections...
[MCP Manager] Connecting to server: echo
✓ Connected

🤖 Coding Agent - Interactive Mode
Type your requests or "exit" to quit.

You: Use the echo tool to say "Hello MCP!"

[Tool Call]: mcp_echo_echo
{
  "message": "Hello MCP!"
}

[Tool Result]: Success
Echo: Hello MCP!

[Assistant]: I used the echo tool and it returned: "Hello MCP!"

You: Reverse the word "integration"

[Tool Call]: mcp_echo_reverse
{
  "text": "integration"
}

[Tool Result]: Success
Reversed: noitargetni

[Assistant]: The reversed text is: "noitargetni"

8.2 カスタムMCPサーバーの追加

新しいMCPサーバーを追加するのは簡単です:

  1. サーバーを実装

    // mcp-servers/custom/index.js
    // Echo サーバーを参考に実装
    
  2. 設定ファイルに追加

    {
      "mcpServers": {
        "echo": { ... },
        "custom": {
          "command": "node",
          "args": ["mcp-servers/custom/index.js"],
          "transport": "stdio"
        }
      }
    }
    
  3. エージェントを再起動

    node dist/index.js chat
    

自動的に新しいツールが利用可能になります!


まとめ

学んだこと

本記事では、MCP (Model Context Protocol) を使用してコーディングエージェントに外部ツールを統合する方法を学びました:

  1. MCPプロトコルの理解

    • JSON-RPC 2.0ベースの通信
    • 標準化されたツール定義
    • 動的なツール発見
  2. MCPサーバーの実装

    • stdin/stdoutでの双方向通信
    • リクエスト/レスポンス処理
    • エラーハンドリング
  3. MCPクライアントの実装

    • プロセス管理
    • ストリームデータ処理
    • 非同期通信
  4. システム統合

    • 複数サーバーの管理
    • プレフィックス管理
    • ツールのルーティング

MCPの利点

  • 拡張性: 新しいツールをコード変更なしで追加
  • 標準化: 統一されたインターフェース
  • 再利用性: ツールを他のプロジェクトでも使用可能
  • 保守性: ツールロジックとエージェントロジックの分離

次のステップ

  1. 実際のMCPサーバーを追加

    • Filesystem server(ファイル操作)
    • GitHub server(リポジトリ操作)
    • Database server(データベースクエリ)
  2. 高度な機能の実装

    • HTTP/WebSocketトランスポート
    • リソース機能
    • プロンプト機能
  3. パフォーマンス最適化

    • 接続プーリング
    • キャッシング
    • 並列実行

参考リソース


📦 プロジェクトを試す

完全なソースコードはGitHubで公開されています:

# クローン
git clone https://github.com/nogataka/coding-agent-sample-mcp.git
cd coding-agent-sample-mcp

# インストールとビルド
npm install
npm run build

# 環境変数設定
cp .env.example .env
# .env を編集してAPIキーを設定

# MCPテスト
npm run test-mcp

# エージェント起動
node dist/index.js chat

これで、拡張可能なMCP統合コーディングエージェントの完成です!

MCPを使用することで、エージェントの機能を動的に拡張し、様々な外部ツールと統合できるようになりました。この知識を活用して、自分だけのカスタムツールやサーバーを作成してみてください!

Discussion