MCPサーバー統合で実現する拡張可能なコーディングエージェント
🔗 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の主な特徴
-
標準化された通信
- JSON-RPC 2.0ベースのプロトコル
- 明確な仕様とエラーハンドリング
-
サーバー/クライアントモデル
- MCPサーバー: ツールやリソースを提供
- MCPクライアント: サーバーと通信してツールを利用
-
複数のトランスポート
- stdio (標準入出力)
- HTTP
- WebSocket
-
動的なツール発見
- 実行時にサーバーが提供するツールを自動検出
- ツールのスキーマ定義
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();
重要な設計判断:
-
標準エラー出力でログ
console.error('[Echo Server] Starting...');-
console.error(): デバッグログ用(stderr) -
console.log(): JSON-RPCレスポンス用(stdout) - この分離により、ログとデータを混在させない
-
-
行単位での処理
rl.on('line', (line) => { const request = JSON.parse(line); handleRequest(request); });- JSON-RPCメッセージは改行で区切られる
- 1行 = 1メッセージ
-
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`);
}
ツール定義のベストプラクティス:
-
明確な命名
name: 'echo' // ✓ シンプルで分かりやすい // ではなく name: 'echoMessageBack' // ✗ 冗長 -
詳細な説明
description: 'Echoes back the provided message' // LLMがツールを選択する際の重要な情報 -
厳密なスキーマ定義
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);
}
}
ツール実行のベストプラクティス:
-
入力のバリデーション
if (!args.message) { sendError(id, -32602, 'Invalid params: message is required'); return; } -
統一されたレスポンス形式
result = { content: [ { type: 'text', // コンテンツタイプ text: `Echo: ${args.message}` // 実際の内容 } ] }; -
包括的なエラーハンドリング
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;
}
// メソッドは以下で実装...
}
設計のポイント:
-
EventEmitterを継承
export class MCPClient extends EventEmitter- イベント駆動アーキテクチャ
-
disconnect、errorなどのイベントを発行
-
状態管理
private state: MCPClientState = MCPClientState.DISCONNECTED;- 明確な状態遷移
- 不正な操作を防ぐ
-
保留中のリクエスト管理
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;
}
}
重要な実装詳細:
-
stdio設定
stdio: ['pipe', 'pipe', 'pipe'][stdin, stdout, stderr]- すべてパイプで接続してプログラムから制御
-
環境変数のマージ
env: { ...process.env, ...this.config.env }- 親プロセスの環境変数を継承
- サーバー固有の環境変数を追加
-
イベントハンドリング
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サーバーを追加するのは簡単です:
-
サーバーを実装
// mcp-servers/custom/index.js // Echo サーバーを参考に実装 -
設定ファイルに追加
{ "mcpServers": { "echo": { ... }, "custom": { "command": "node", "args": ["mcp-servers/custom/index.js"], "transport": "stdio" } } } -
エージェントを再起動
node dist/index.js chat
自動的に新しいツールが利用可能になります!
まとめ
学んだこと
本記事では、MCP (Model Context Protocol) を使用してコーディングエージェントに外部ツールを統合する方法を学びました:
-
MCPプロトコルの理解
- JSON-RPC 2.0ベースの通信
- 標準化されたツール定義
- 動的なツール発見
-
MCPサーバーの実装
- stdin/stdoutでの双方向通信
- リクエスト/レスポンス処理
- エラーハンドリング
-
MCPクライアントの実装
- プロセス管理
- ストリームデータ処理
- 非同期通信
-
システム統合
- 複数サーバーの管理
- プレフィックス管理
- ツールのルーティング
MCPの利点
- ✅ 拡張性: 新しいツールをコード変更なしで追加
- ✅ 標準化: 統一されたインターフェース
- ✅ 再利用性: ツールを他のプロジェクトでも使用可能
- ✅ 保守性: ツールロジックとエージェントロジックの分離
次のステップ
-
実際のMCPサーバーを追加
- Filesystem server(ファイル操作)
- GitHub server(リポジトリ操作)
- Database server(データベースクエリ)
-
高度な機能の実装
- HTTP/WebSocketトランスポート
- リソース機能
- プロンプト機能
-
パフォーマンス最適化
- 接続プーリング
- キャッシング
- 並列実行
参考リソース
- 🔗 GitHub Repository - 本プロジェクトのソースコード
- Model Context Protocol Specification
- JSON-RPC 2.0 Specification
- Anthropic Claude API
- 前回の記事: マルチプロバイダー対応コーディングエージェント
📦 プロジェクトを試す
完全なソースコードは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