T-CREATOR

Node.js で作る MCP サーバー:実践的な実装手順とサンプルコード

Node.js で作る MCP サーバー:実践的な実装手順とサンプルコード

現代の AI アプリケーション開発において、外部ツールやデータソースとの統合は不可欠な要素となっています。Model Context Protocol(MCP)は、この課題を解決するために設計された革新的なプロトコルです。Node.js での MCP サーバー実装は、その柔軟性と豊富なエコシステムにより、開発者にとって最も実用的な選択肢の一つです。この記事では、Node.js を使用した MCP サーバーの構築方法を、基本的な概念から実践的な実装まで、サンプルコードを交えながら詳しく解説いたします。

背景

Model Context Protocol の基本概念

Model Context Protocol(MCP)は、AI アプリケーションが外部のツールやデータソースと安全かつ効率的にやり取りするための標準化されたプロトコルです。従来の AI アプリケーションでは、各サービスとの個別統合が必要でしたが、MCP により統一されたインターフェースでの連携が可能になります。

MCP の核となる概念は以下の通りです:

ツール(Tools): AI が実行できる具体的な機能を定義したもの。ファイル操作、API コール、計算処理など様々な処理を抽象化します。

リソース(Resources): AI がアクセス可能なデータやコンテンツ。ファイル、データベースレコード、Web ページなどを指します。

プロンプト(Prompts): 再利用可能なテンプレート化されたプロンプト。特定のタスクに最適化された指示文を提供します。

サンプリング(Sampling): AI モデルとの対話を管理する機能。会話の流れを制御し、適切なレスポンスを生成します。

Node.js でのサーバー開発の利点

Node.js での MCP サーバー開発には、以下のような顕著な利点があります:

非同期処理の優位性

Node.js のイベント駆動型アーキテクチャは、MCP サーバーが要求する高い並行性と応答性に最適です。複数の AI クライアントからの同時リクエストを効率的に処理できます。

javascript// 非同期処理の例
async function handleMultipleRequests(requests) {
  const results = await Promise.all(
    requests.map((request) => processToolCall(request))
  );
  return results;
}

豊富なパッケージエコシステム

npm エコシステムには、MCP サーバー開発に必要な様々なライブラリが揃っています:

  • WebSocket 通信: ws, socket.io
  • HTTP 処理: express, fastify
  • データベース連携: mongodb, mysql2, pg
  • ファイル操作: fs-extra, chokidar
  • API 連携: axios, node-fetch

TypeScript との親和性

TypeScript を使用することで、MCP プロトコルの複雑な型定義を安全に扱えます。コンパイル時の型チェックにより、ランタイムエラーを大幅に削減できます。

他の実装方法との比較

MCP サーバーの実装方法として、Node.js 以外にも Python、Go、Rust などの選択肢があります。それぞれの特徴を比較してみましょう。

実装言語学習コスト開発速度パフォーマンスエコシステム適用場面
Node.js中〜高非常に豊富Web 統合、API 連携
Python豊富データ分析、ML 統合
Go中〜高中程度高パフォーマンス要求
Rust低〜中非常に高成長中システムレベル処理

Node.js は、開発効率とパフォーマンスのバランスが優れており、特に Web アプリケーションとの統合や REST API の構築において強力です。

準備

開発環境のセットアップ

MCP サーバー開発を始めるために、以下の環境をセットアップします。

Node.js のインストール

最新の LTS バージョン(v20 以上推奨)をインストールします:

bash# Node.js公式サイトからダウンロード、またはnvmを使用
nvm install --lts
nvm use --lts

# バージョン確認
node --version
npm --version

Yarn の導入

パッケージ管理には Yarn を使用します:

bashnpm install -g yarn
yarn --version

開発ツールの準備

効率的な開発のために以下のツールを準備します:

bash# TypeScript環境
yarn global add typescript ts-node

# 開発用ツール
yarn global add nodemon eslint prettier

必要なパッケージとツール

MCP サーバー開発に必要な主要パッケージを整理します。

核心ライブラリ

json{
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.5.0",
    "express": "^4.19.2",
    "ws": "^8.16.0",
    "uuid": "^9.0.1",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/node": "^20.11.24",
    "@types/express": "^4.17.21",
    "@types/ws": "^8.5.10",
    "@types/uuid": "^9.0.8",
    "typescript": "^5.3.3",
    "ts-node": "^10.9.2",
    "nodemon": "^3.1.0"
  }
}

パッケージの役割説明

  • @modelcontextprotocol/sdk: MCP プロトコルの公式実装
  • express: HTTP サーバー機能
  • ws: WebSocket 通信
  • uuid: 一意識別子生成
  • zod: ランタイム型検証

プロジェクト構造の設計

拡張性とメンテナンス性を考慮したプロジェクト構造を設計します。

csharpmcp-server/
├── src/
│   ├── types/
│   │   ├── mcp.ts              # MCP型定義
│   │   └── tools.ts            # ツール型定義
│   ├── tools/
│   │   ├── base.ts             # ツール基底クラス
│   │   ├── file-operations.ts  # ファイル操作ツール
│   │   ├── web-api.ts          # Web API連携ツール
│   │   └── database.ts         # データベースツール
│   ├── resources/
│   │   ├── manager.ts          # リソース管理
│   │   └── providers/          # リソースプロバイダー
│   ├── handlers/
│   │   ├── tool-handler.ts     # ツールハンドラー
│   │   ├── resource-handler.ts # リソースハンドラー
│   │   └── error-handler.ts    # エラーハンドラー
│   ├── utils/
│   │   ├── logger.ts           # ログ機能
│   │   ├── config.ts           # 設定管理
│   │   └── validation.ts       # バリデーション
│   ├── server.ts               # サーバーメイン
│   └── app.ts                  # アプリケーション初期化
├── tests/
│   ├── unit/                   # 単体テスト
│   ├── integration/            # 統合テスト
│   └── fixtures/               # テストデータ
├── config/
│   ├── default.json            # デフォルト設定
│   ├── development.json        # 開発環境設定
│   └── production.json         # 本番環境設定
├── docker/
│   ├── Dockerfile              # コンテナ定義
│   └── docker-compose.yml      # 開発環境
├── docs/                       # ドキュメント
├── package.json
├── tsconfig.json
├── .eslintrc.js
├── .prettierrc
└── README.md

TypeScript 設定

tsconfig.json の設定例:

json{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}

基本実装

最小構成の MCP サーバー作成

まず、最もシンプルな MCP サーバーを作成して基本的な動作を確認します。

プロジェクト初期化

bashmkdir mcp-server
cd mcp-server
yarn init -y
yarn add @modelcontextprotocol/sdk express ws uuid zod
yarn add -D @types/node @types/express @types/ws @types/uuid typescript ts-node nodemon

基本的なサーバー実装

src​/​server.ts:

typescriptimport { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
  ToolSchema,
} from '@modelcontextprotocol/sdk/types.js';

class BasicMCPServer {
  private server: Server;

  constructor() {
    this.server = new Server(
      {
        name: 'basic-mcp-server',
        version: '1.0.0',
      },
      {
        capabilities: {
          tools: {},
        },
      }
    );

    this.setupHandlers();
  }

  private setupHandlers(): void {
    // ツール一覧の取得
    this.server.setRequestHandler(
      ListToolsRequestSchema,
      async () => {
        return {
          tools: [
            {
              name: 'echo',
              description: 'Echo back the input text',
              inputSchema: {
                type: 'object',
                properties: {
                  text: {
                    type: 'string',
                    description: 'Text to echo back',
                  },
                },
                required: ['text'],
              },
            },
            {
              name: 'add',
              description: 'Add two numbers',
              inputSchema: {
                type: 'object',
                properties: {
                  a: {
                    type: 'number',
                    description: 'First number',
                  },
                  b: {
                    type: 'number',
                    description: 'Second number',
                  },
                },
                required: ['a', 'b'],
              },
            },
          ],
        };
      }
    );

    // ツール実行
    this.server.setRequestHandler(
      CallToolRequestSchema,
      async (request) => {
        const { name, arguments: args } = request.params;

        switch (name) {
          case 'echo':
            return {
              content: [
                {
                  type: 'text',
                  text: `Echo: ${args.text}`,
                },
              ],
            };

          case 'add':
            const result =
              (args.a as number) + (args.b as number);
            return {
              content: [
                {
                  type: 'text',
                  text: `Result: ${args.a} + ${args.b} = ${result}`,
                },
              ],
            };

          default:
            throw new Error(`Unknown tool: ${name}`);
        }
      }
    );
  }

  async start(): Promise<void> {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error('Basic MCP Server started');
  }
}

// サーバー起動
const server = new BasicMCPServer();
server.start().catch(console.error);

起動スクリプトの設定

package.json に起動スクリプトを追加:

json{
  "scripts": {
    "dev": "nodemon --exec ts-node src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js",
    "test": "jest"
  }
}

TypeScript による型安全な実装

型安全性を向上させるために、詳細な型定義を作成します。

MCP 型定義

src​/​types​/​mcp.ts:

typescriptimport { z } from 'zod';

// ツール定義のスキーマ
export const ToolDefinitionSchema = z.object({
  name: z.string(),
  description: z.string(),
  inputSchema: z.object({
    type: z.literal('object'),
    properties: z.record(z.any()),
    required: z.array(z.string()).optional(),
  }),
});

export type ToolDefinition = z.infer<
  typeof ToolDefinitionSchema
>;

// ツール実行結果のスキーマ
export const ToolResultSchema = z.object({
  content: z.array(
    z.object({
      type: z.enum(['text', 'image', 'resource']),
      text: z.string().optional(),
      data: z.string().optional(),
      mimeType: z.string().optional(),
    })
  ),
  isError: z.boolean().optional(),
});

export type ToolResult = z.infer<typeof ToolResultSchema>;

// ツール実行パラメータ
export interface ToolExecutionContext {
  name: string;
  arguments: Record<string, any>;
  requestId?: string;
  userId?: string;
}

// エラー情報
export interface MCPError {
  code: number;
  message: string;
  data?: any;
}

ツール型定義

src​/​types​/​tools.ts:

typescriptimport {
  ToolDefinition,
  ToolResult,
  ToolExecutionContext,
} from './mcp.js';

// 抽象ツールクラス
export abstract class BaseTool {
  abstract readonly name: string;
  abstract readonly description: string;
  abstract readonly inputSchema: ToolDefinition['inputSchema'];

  // ツール定義の取得
  getDefinition(): ToolDefinition {
    return {
      name: this.name,
      description: this.description,
      inputSchema: this.inputSchema,
    };
  }

  // ツール実行(抽象メソッド)
  abstract execute(
    context: ToolExecutionContext
  ): Promise<ToolResult>;

  // 入力検証
  protected validateInput(args: Record<string, any>): void {
    const required = this.inputSchema.required || [];

    for (const field of required) {
      if (!(field in args)) {
        throw new Error(
          `Required field '${field}' is missing`
        );
      }
    }
  }
}

// ツールレジストリ
export class ToolRegistry {
  private tools = new Map<string, BaseTool>();

  register(tool: BaseTool): void {
    this.tools.set(tool.name, tool);
  }

  unregister(name: string): void {
    this.tools.delete(name);
  }

  get(name: string): BaseTool | undefined {
    return this.tools.get(name);
  }

  list(): ToolDefinition[] {
    return Array.from(this.tools.values()).map((tool) =>
      tool.getDefinition()
    );
  }

  async execute(
    context: ToolExecutionContext
  ): Promise<ToolResult> {
    const tool = this.tools.get(context.name);
    if (!tool) {
      throw new Error(`Tool '${context.name}' not found`);
    }

    return await tool.execute(context);
  }
}

基本的なツール定義とハンドラー

実用的なツールを実装して、MCP サーバーの機能を拡張します。

Echo ツール

src​/​tools​/​echo.ts:

typescriptimport {
  BaseTool,
  ToolResult,
  ToolExecutionContext,
} from '../types/tools.js';

export class EchoTool extends BaseTool {
  readonly name = 'echo';
  readonly description =
    'Echo back the input text with optional formatting';
  readonly inputSchema = {
    type: 'object' as const,
    properties: {
      text: {
        type: 'string',
        description: 'Text to echo back',
      },
      format: {
        type: 'string',
        enum: [
          'plain',
          'uppercase',
          'lowercase',
          'reverse',
        ],
        description: 'Text formatting option',
      },
      prefix: {
        type: 'string',
        description: 'Optional prefix to add',
      },
    },
    required: ['text'],
  };

  async execute(
    context: ToolExecutionContext
  ): Promise<ToolResult> {
    this.validateInput(context.arguments);

    const {
      text,
      format = 'plain',
      prefix = '',
    } = context.arguments;
    let result = text as string;

    // フォーマット適用
    switch (format) {
      case 'uppercase':
        result = result.toUpperCase();
        break;
      case 'lowercase':
        result = result.toLowerCase();
        break;
      case 'reverse':
        result = result.split('').reverse().join('');
        break;
    }

    // プレフィックス追加
    if (prefix) {
      result = `${prefix}: ${result}`;
    }

    return {
      content: [
        {
          type: 'text',
          text: result,
        },
      ],
    };
  }
}

計算ツール

src​/​tools​/​calculator.ts:

typescriptimport {
  BaseTool,
  ToolResult,
  ToolExecutionContext,
} from '../types/tools.js';

export class CalculatorTool extends BaseTool {
  readonly name = 'calculator';
  readonly description =
    'Perform mathematical calculations';
  readonly inputSchema = {
    type: 'object' as const,
    properties: {
      operation: {
        type: 'string',
        enum: [
          'add',
          'subtract',
          'multiply',
          'divide',
          'power',
          'sqrt',
        ],
        description: 'Mathematical operation to perform',
      },
      a: {
        type: 'number',
        description: 'First operand',
      },
      b: {
        type: 'number',
        description:
          'Second operand (not required for sqrt)',
      },
    },
    required: ['operation', 'a'],
  };

  async execute(
    context: ToolExecutionContext
  ): Promise<ToolResult> {
    this.validateInput(context.arguments);

    const { operation, a, b } = context.arguments;
    let result: number;
    let expression: string;

    try {
      switch (operation) {
        case 'add':
          if (b === undefined)
            throw new Error(
              'Second operand required for addition'
            );
          result = a + b;
          expression = `${a} + ${b} = ${result}`;
          break;

        case 'subtract':
          if (b === undefined)
            throw new Error(
              'Second operand required for subtraction'
            );
          result = a - b;
          expression = `${a} - ${b} = ${result}`;
          break;

        case 'multiply':
          if (b === undefined)
            throw new Error(
              'Second operand required for multiplication'
            );
          result = a * b;
          expression = `${a} × ${b} = ${result}`;
          break;

        case 'divide':
          if (b === undefined)
            throw new Error(
              'Second operand required for division'
            );
          if (b === 0)
            throw new Error(
              'Division by zero is not allowed'
            );
          result = a / b;
          expression = `${a} ÷ ${b} = ${result}`;
          break;

        case 'power':
          if (b === undefined)
            throw new Error(
              'Exponent required for power operation'
            );
          result = Math.pow(a, b);
          expression = `${a}^${b} = ${result}`;
          break;

        case 'sqrt':
          if (a < 0)
            throw new Error(
              'Square root of negative number is not allowed'
            );
          result = Math.sqrt(a);
          expression = `√${a} = ${result}`;
          break;

        default:
          throw new Error(
            `Unknown operation: ${operation}`
          );
      }

      return {
        content: [
          {
            type: 'text',
            text: `計算結果: ${expression}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: 'text',
            text: `計算エラー: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
}

改良されたサーバー実装

src​/​server.ts を更新して、ツールレジストリを使用します:

typescriptimport { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { ToolRegistry } from './types/tools.js';
import { EchoTool } from './tools/echo.js';
import { CalculatorTool } from './tools/calculator.js';

class EnhancedMCPServer {
  private server: Server;
  private toolRegistry: ToolRegistry;

  constructor() {
    this.server = new Server(
      {
        name: 'enhanced-mcp-server',
        version: '1.0.0',
      },
      {
        capabilities: {
          tools: {},
        },
      }
    );

    this.toolRegistry = new ToolRegistry();
    this.registerTools();
    this.setupHandlers();
  }

  private registerTools(): void {
    this.toolRegistry.register(new EchoTool());
    this.toolRegistry.register(new CalculatorTool());
  }

  private setupHandlers(): void {
    // ツール一覧の取得
    this.server.setRequestHandler(
      ListToolsRequestSchema,
      async () => {
        return {
          tools: this.toolRegistry.list(),
        };
      }
    );

    // ツール実行
    this.server.setRequestHandler(
      CallToolRequestSchema,
      async (request) => {
        try {
          const result = await this.toolRegistry.execute({
            name: request.params.name,
            arguments: request.params.arguments || {},
          });

          return result;
        } catch (error) {
          console.error('Tool execution error:', error);
          return {
            content: [
              {
                type: 'text',
                text: `Error: ${error.message}`,
              },
            ],
            isError: true,
          };
        }
      }
    );
  }

  async start(): Promise<void> {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error('Enhanced MCP Server started');
  }
}

// サーバー起動
const server = new EnhancedMCPServer();
server.start().catch(console.error);

この基本実装により、拡張可能な MCP サーバーの土台が完成しました。次のセクションでは、さらに高度な機能を実装していきます。

機能拡張

カスタムツールの実装

より実用的なツールを実装して、MCP サーバーの機能を拡張します。

日時ツール

typescriptimport {
  BaseTool,
  ToolResult,
  ToolExecutionContext,
} from '../types/tools.js';

export class DateTimeTool extends BaseTool {
  readonly name = 'datetime';
  readonly description =
    '現在の日時を取得したり、日時を変換したりします';
  readonly inputSchema = {
    type: 'object' as const,
    properties: {
      operation: {
        type: 'string',
        enum: ['now', 'format', 'parse', 'add', 'diff'],
        description: '実行する操作',
      },
      format: {
        type: 'string',
        description: 'フォーマット文字列',
      },
      timezone: {
        type: 'string',
        description: 'タイムゾーン(例: Asia/Tokyo)',
      },
    },
    required: ['operation'],
  };

  async execute(
    context: ToolExecutionContext
  ): Promise<ToolResult> {
    const {
      operation,
      format = 'YYYY-MM-DD HH:mm:ss',
      timezone = 'Asia/Tokyo',
    } = context.arguments;

    try {
      switch (operation) {
        case 'now':
          const now = new Date();
          return {
            content: [
              {
                type: 'text',
                text: `現在時刻: ${now.toLocaleString(
                  'ja-JP',
                  { timeZone: timezone }
                )}`,
              },
            ],
          };

        default:
          throw new Error(`未対応の操作: ${operation}`);
      }
    } catch (error) {
      return {
        content: [
          {
            type: 'text',
            text: `エラー: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
}

リソース管理機能の追加

MCP サーバーにリソース管理機能を追加します。

typescriptimport {
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

class ResourceManager {
  private resources = new Map<string, any>();

  addResource(
    uri: string,
    content: any,
    mimeType?: string
  ): void {
    this.resources.set(uri, { content, mimeType });
  }

  getResource(uri: string): any {
    return this.resources.get(uri);
  }

  listResources(): Array<{
    uri: string;
    name: string;
    mimeType?: string;
  }> {
    return Array.from(this.resources.entries()).map(
      ([uri, data]) => ({
        uri,
        name: uri.split('/').pop() || uri,
        mimeType: data.mimeType,
      })
    );
  }
}

エラーハンドリングの実装

堅牢なエラーハンドリングシステムを構築します。

typescriptexport class MCPErrorHandler {
  static handleToolError(
    error: any,
    toolName: string
  ): ToolResult {
    console.error(`Tool error in ${toolName}:`, error);

    return {
      content: [
        {
          type: 'text',
          text: `ツール '${toolName}' でエラーが発生しました: ${error.message}`,
        },
      ],
      isError: true,
    };
  }

  static isValidationError(error: any): boolean {
    return (
      error.name === 'ValidationError' ||
      error.code === 'VALIDATION_ERROR'
    );
  }
}

実践例

ファイル操作ツールの作成

実用的なファイル操作ツールを実装します。

typescriptimport * as fs from 'fs/promises';
import * as path from 'path';
import {
  BaseTool,
  ToolResult,
  ToolExecutionContext,
} from '../types/tools.js';

export class FileOperationTool extends BaseTool {
  readonly name = 'file_operations';
  readonly description =
    'ファイルの読み書きや操作を行います';
  readonly inputSchema = {
    type: 'object' as const,
    properties: {
      operation: {
        type: 'string',
        enum: ['read', 'write', 'list', 'exists', 'delete'],
        description: '実行する操作',
      },
      path: {
        type: 'string',
        description: 'ファイル・ディレクトリのパス',
      },
      content: {
        type: 'string',
        description: '書き込むコンテンツ(write操作時)',
      },
    },
    required: ['operation', 'path'],
  };

  async execute(
    context: ToolExecutionContext
  ): Promise<ToolResult> {
    const {
      operation,
      path: filePath,
      content,
    } = context.arguments;

    try {
      switch (operation) {
        case 'read':
          const data = await fs.readFile(filePath, 'utf-8');
          return {
            content: [
              {
                type: 'text',
                text: `ファイル内容:\n${data}`,
              },
            ],
          };

        case 'write':
          if (!content)
            throw new Error(
              '書き込み内容が指定されていません'
            );
          await fs.writeFile(filePath, content, 'utf-8');
          return {
            content: [
              {
                type: 'text',
                text: `ファイルに書き込みました: ${filePath}`,
              },
            ],
          };

        case 'list':
          const files = await fs.readdir(filePath);
          return {
            content: [
              {
                type: 'text',
                text: `ディレクトリ内容:\n${files.join(
                  '\n'
                )}`,
              },
            ],
          };

        default:
          throw new Error(`未対応の操作: ${operation}`);
      }
    } catch (error) {
      return {
        content: [
          {
            type: 'text',
            text: `ファイル操作エラー: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
}

Web API 連携機能の実装

外部 API との連携機能を実装します。

typescriptimport axios from 'axios';
import {
  BaseTool,
  ToolResult,
  ToolExecutionContext,
} from '../types/tools.js';

export class WebAPITool extends BaseTool {
  readonly name = 'web_api';
  readonly description =
    'Web API への HTTP リクエストを実行します';
  readonly inputSchema = {
    type: 'object' as const,
    properties: {
      method: {
        type: 'string',
        enum: ['GET', 'POST', 'PUT', 'DELETE'],
        description: 'HTTP メソッド',
      },
      url: {
        type: 'string',
        description: 'リクエスト URL',
      },
      headers: {
        type: 'object',
        description: 'リクエストヘッダー',
      },
      data: {
        type: 'object',
        description: 'リクエストボディ',
      },
    },
    required: ['method', 'url'],
  };

  async execute(
    context: ToolExecutionContext
  ): Promise<ToolResult> {
    const {
      method,
      url,
      headers = {},
      data,
    } = context.arguments;

    try {
      const response = await axios({
        method: method.toLowerCase(),
        url,
        headers,
        data,
        timeout: 10000,
      });

      return {
        content: [
          {
            type: 'text',
            text: `API レスポンス (${
              response.status
            }):\n${JSON.stringify(response.data, null, 2)}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: 'text',
            text: `API リクエストエラー: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
}

運用

テスト環境の構築

MCP サーバーのテスト環境を構築します。

typescriptimport {
  describe,
  it,
  expect,
  beforeEach,
} from '@jest/globals';
import { ToolRegistry } from '../src/types/tools.js';
import { EchoTool } from '../src/tools/echo.js';

describe('MCP Server Tools', () => {
  let registry: ToolRegistry;

  beforeEach(() => {
    registry = new ToolRegistry();
    registry.register(new EchoTool());
  });

  it('should execute echo tool correctly', async () => {
    const result = await registry.execute({
      name: 'echo',
      arguments: { text: 'Hello, World!' },
    });

    expect(result.content[0].text).toBe('Hello, World!');
    expect(result.isError).toBeFalsy();
  });
});

デバッグとトラブルシューティング

効果的なデバッグ手法を紹介します。

typescriptimport winston from 'winston';

export const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({
      filename: 'error.log',
      level: 'error',
    }),
    new winston.transports.File({
      filename: 'combined.log',
    }),
    new winston.transports.Console({
      format: winston.format.simple(),
    }),
  ],
});

本番環境での運用考慮事項

Docker 化

dockerfileFROM node:20-alpine

WORKDIR /app

COPY package*.json yarn.lock ./
RUN yarn install --production

COPY dist/ ./dist/

EXPOSE 3000

CMD ["node", "dist/server.js"]

環境変数管理

typescriptexport const config = {
  port: process.env.PORT || 3000,
  nodeEnv: process.env.NODE_ENV || 'development',
  logLevel: process.env.LOG_LEVEL || 'info',
  maxRequestSize: process.env.MAX_REQUEST_SIZE || '10mb',
};

まとめ

本記事では、Node.js を使用した MCP サーバーの実装について、基本的な概念から実践的な応用まで幅広く解説いたしました。

実装のポイント振り返り

型安全性の重要性: TypeScript と Zod を活用することで、ランタイムエラーを大幅に削減し、開発効率を向上させることができます。

拡張可能な設計: ツールレジストリパターンを採用することで、新しいツールの追加が容易になり、コードの保守性が向上します。

エラーハンドリング: 適切なエラーハンドリングにより、AI クライアントに有用なフィードバックを提供できます。

テスト駆動開発: 単体テストと統合テストを組み合わせることで、品質の高い MCP サーバーを構築できます。

次のステップとさらなる学習リソース

MCP サーバー開発をさらに深めるために、以下のリソースをご活用ください:

公式ドキュメント

コミュニティリソース

学習の次のステップ

  1. 複雑なツールの実装: データベース連携やファイルシステム操作
  2. パフォーマンス最適化: キャッシュ機能やバッチ処理の実装
  3. セキュリティ強化: 認証・認可機能の追加
  4. マイクロサービス化: 複数の MCP サーバーの連携

Node.js での MCP サーバー開発は、AI アプリケーションの可能性を大きく広げる技術です。本記事の内容を基に、ぜひ独自の MCP サーバーを構築し、AI との新しい連携を探求してください。実装過程で得られる学びは、必ず皆様の開発スキル向上に寄与することでしょう。

関連リンク