T-CREATOR

MCP サーバー で外部 API を安全に呼ぶ:Tool 定義 → スキーマ検証 → エラー処理まで実装手順

MCP サーバー で外部 API を安全に呼ぶ:Tool 定義 → スキーマ検証 → エラー処理まで実装手順

MCP(Model Context Protocol)サーバーを活用すれば、AI エージェントから外部 API を安全に呼び出すことが可能です。しかし、実際に運用レベルで動かすには Tool 定義、スキーマ検証、エラーハンドリングなど、考慮すべきポイントがいくつもあります。

本記事では、MCP サーバーにおいて外部 API を呼び出す際の設計・実装の流れを、初心者の方でも理解できるように段階的に解説していきます。TypeScript と Node.js を使った具体例を交え、実践的な知識を身につけられるでしょう。

背景

MCP サーバーとは

MCP(Model Context Protocol)は、AI モデルと外部ツールを連携させるためのプロトコル仕様です。Claude などの LLM(大規模言語モデル)が、外部のデータソースや API にアクセスする際、標準化されたインターフェースを介してやり取りを行います。

MCP サーバーは、このプロトコルを実装したサーバーアプリケーションであり、以下の役割を担います。

  • AI エージェントからのリクエストを受け取る
  • 外部 API やデータベースへアクセスする
  • 取得したデータを AI エージェントへ返す

なぜ外部 API を MCP サーバー経由で呼ぶのか

AI エージェントが直接外部 API を呼ぶのではなく、MCP サーバーを経由することで、以下のメリットが得られます。

#メリット説明
1セキュリティ強化API キーやトークンを AI エージェント側に渡さず、サーバー側で管理できる
2入力検証の集約スキーマ検証やバリデーションをサーバー側で一元化できる
3エラーハンドリングの統一外部 API のエラーを適切に変換し、AI エージェントへ分かりやすく返せる
4レート制限・リトライ制御API 呼び出しの制御ロジックをサーバー側で実装できる

このように、MCP サーバーを中継層として配置することで、安全性・保守性・拡張性が向上します。

課題

MCP サーバーで外部 API を呼び出す際には、以下のような課題が発生します。

1. Tool 定義の複雑さ

MCP サーバーでは、AI エージェントが利用できる「ツール(Tool)」を定義する必要があります。この Tool 定義には以下の情報が必要です。

  • ツール名
  • 説明文(AI エージェントが理解できる形式)
  • 入力パラメータのスキーマ(JSON Schema 形式)
  • 出力形式

Tool 定義が不適切だと、AI エージェントが正しくツールを呼び出せません。

2. スキーマ検証の不足

AI エージェントから送られてくるパラメータが、期待する形式と異なる場合があります。たとえば以下のようなケースです。

  • 必須パラメータが欠けている
  • 数値型のはずが文字列で送られてくる
  • 許可されていない追加プロパティが含まれている

これらを実行前に検証しないと、外部 API 呼び出し時にエラーが発生し、原因の特定が難しくなります。

3. エラー処理の不統一

外部 API は様々なエラーを返します。

  • HTTP ステータスコード: 400 Bad Request401 Unauthorized500 Internal Server Error など
  • API 固有エラー: レート制限、タイムアウト、データ不正など

これらを適切にハンドリングし、AI エージェントへ分かりやすいエラーメッセージを返す仕組みが必要です。

以下の図は、これらの課題がどのフェーズで発生するかを示しています。

mermaidflowchart TD
  agent["AI エージェント"] -->|"Tool 呼び出し"| mcp["MCP サーバー"]
  mcp -->|"1. Tool 定義の複雑さ"| tool_def["Tool 定義<br/>不適切な定義"]
  mcp -->|"2. スキーマ検証の不足"| validation["入力検証<br/>不正パラメータ"]
  mcp -->|"3. エラー処理の不統一"| api_call["外部 API 呼び出し<br/>多様なエラー"]
  api_call -->|"エラー"| error_handling["エラーハンドリング<br/>不統一な返却"]
  error_handling -->|"分かりにくいエラー"| agent

図で理解できる要点

  • MCP サーバーは AI エージェントと外部 API の中継役を担う
  • Tool 定義、入力検証、エラー処理の各段階で課題が発生しやすい
  • 各課題を解決することで、安全で信頼性の高い API 呼び出しを実現できる

解決策

上記の課題を解決するため、以下の3 つのステップで実装を進めます。

ステップ 1: Tool 定義の設計

ステップ 2: スキーマ検証の実装

ステップ 3: エラー処理の統一

それぞれを詳しく見ていきましょう。

ステップ 1: Tool 定義の設計

Tool 定義とは

MCP プロトコルでは、サーバーが提供する機能を「Tool」として定義します。Tool は以下の要素で構成されます。

#要素説明
1nameツールの一意な識別子(例: fetch_weather
2descriptionAI エージェントが理解できる説明文
3inputSchema入力パラメータの JSON Schema

Tool 定義の例

以下は、天気情報を取得する Tool の定義例です。

typescript// Tool 定義の型
interface ToolDefinition {
  name: string;
  description: string;
  inputSchema: {
    type: 'object';
    properties: Record<string, unknown>;
    required?: string[];
  };
}

Tool 定義の型を定義します。inputSchema は JSON Schema 形式でパラメータを記述します。

typescript// 天気情報取得 Tool の定義
const weatherTool: ToolDefinition = {
  name: 'fetch_weather',
  description:
    '指定された都市の現在の天気情報を取得します。都市名は英語で指定してください。',
  inputSchema: {
    type: 'object',
    properties: {
      city: {
        type: 'string',
        description: '都市名(例: Tokyo, London)',
      },
      units: {
        type: 'string',
        enum: ['metric', 'imperial'],
        description:
          '温度の単位(metric: 摂氏、imperial: 華氏)',
      },
    },
    required: ['city'],
  },
};

実際の Tool 定義では、city を必須パラメータとし、units はオプションで指定できるようにしています。description は AI エージェントがツールの使い方を理解するための重要な情報です。

Tool 定義のポイント

Tool 定義で押さえておくべきポイントは以下の通りです。

  • 明確な description: AI エージェントがツールの目的と使い方を正しく理解できるように記述する
  • JSON Schema による型定義: パラメータの型、必須/任意、制約(enum、pattern など)を明示する
  • required フィールド: 必須パラメータを明示し、AI エージェントが適切な引数を渡せるようにする

ステップ 2: スキーマ検証の実装

なぜスキーマ検証が必要か

AI エージェントから送られてくるパラメータは、必ずしも Tool 定義に従っているとは限りません。以下のようなケースが考えられます。

  • 必須パラメータが欠けている
  • 型が一致しない(文字列のはずが数値など)
  • 許可されていない値が渡される(enum で定義した値以外など)

これらを実行前に検証することで、外部 API 呼び出し前にエラーを検出し、適切なエラーメッセージを返せます。

スキーマ検証ライブラリの選定

TypeScript では、JSON Schema によるバリデーションを行うライブラリがいくつかあります。

#ライブラリ特徴
1Ajv高速で JSON Schema 標準に完全準拠
2ZodTypeScript との親和性が高く、型推論が強力
3YupReact フォームバリデーションで人気

本記事では、Ajv を使用します。Ajv は JSON Schema に完全対応しており、MCP の Tool 定義との相性が良いためです。

Ajv のインストール

Yarn を使って Ajv をインストールします。

bashyarn add ajv ajv-formats

ajv-formats は、日付やメールアドレスなどの形式バリデーションを追加するパッケージです。

スキーマ検証の実装

以下のコードで、Tool 定義に基づいた入力パラメータの検証を行います。

typescriptimport Ajv from 'ajv';
import addFormats from 'ajv-formats';

// Ajv インスタンスの作成
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);

allErrors: true を設定することで、すべてのバリデーションエラーを取得できます。これにより、AI エージェントへ詳細なエラー情報を返せます。

typescript// スキーマ検証関数
function validateInput(
  schema: ToolDefinition['inputSchema'],
  input: unknown
): { valid: boolean; errors?: string[] } {
  const validate = ajv.compile(schema);
  const valid = validate(input);

  if (!valid && validate.errors) {
    const errors = validate.errors.map(
      (err) => `${err.instancePath} ${err.message}`
    );
    return { valid: false, errors };
  }

  return { valid: true };
}

ajv.compile でスキーマをコンパイルし、validate(input) で検証を実行します。エラーがある場合は、人間が読みやすい形式に変換して返します。

スキーマ検証の使用例

実際に Tool 呼び出し時にスキーマ検証を行う例です。

typescript// Tool 呼び出しハンドラ
function handleToolCall(toolName: string, input: unknown) {
  // Tool 定義の取得(ここでは weatherTool を使用)
  const tool = weatherTool;

  // スキーマ検証
  const validation = validateInput(tool.inputSchema, input);

  if (!validation.valid) {
    return {
      success: false,
      error: `入力パラメータが不正です: ${validation.errors?.join(
        ', '
      )}`,
    };
  }

  // 検証が通った場合、外部 API を呼び出す
  return callExternalAPI(input);
}

スキーマ検証を通過しない場合は、外部 API を呼び出さずにエラーを返します。これにより、不正なリクエストによる API コールを防げます。

スキーマ検証のフロー図

以下の図は、スキーマ検証のフローを示しています。

mermaidflowchart TD
  start["AI エージェントからのリクエスト"] --> validate["スキーマ検証"]
  validate -->|"検証成功"| api["外部 API 呼び出し"]
  validate -->|"検証失敗"| error_response["エラーレスポンス返却"]
  api --> response["成功レスポンス返却"]
  error_response --> done["完了"]
  response --> done

図で理解できる要点

  • スキーマ検証は外部 API 呼び出し前に行う
  • 検証失敗時は即座にエラーを返し、API コストを削減できる
  • 検証成功時のみ外部 API を呼び出す

ステップ 3: エラー処理の統一

外部 API のエラーパターン

外部 API を呼び出す際、様々なエラーが発生する可能性があります。

#エラータイプ
1ネットワークエラータイムアウト、接続失敗
2HTTP エラー400 Bad Request401 Unauthorized500 Internal Server Error
3API 固有エラーレート制限超過、データ不正
4パースエラーレスポンスが JSON でない、形式が不正

これらを適切にハンドリングし、AI エージェントへ分かりやすいエラーメッセージを返す必要があります。

エラーハンドリングの基本方針

以下の方針でエラー処理を実装します。

  • エラー種別の分類: ネットワーク、HTTP、API、パースの 4 種類に分類
  • 統一エラー形式: AI エージェントへ返すエラー形式を統一
  • ログ記録: デバッグ用に詳細なエラー情報をログに記録
  • リトライ制御: 一時的なエラーの場合はリトライを試みる

統一エラー型の定義

まず、エラー情報を表す型を定義します。

typescript// エラーレスポンスの型定義
interface ErrorResponse {
  success: false;
  errorType:
    | 'network'
    | 'http'
    | 'api'
    | 'parse'
    | 'validation';
  errorCode?: string;
  message: string;
  details?: unknown;
}

errorType でエラーの種類を分類し、errorCode で具体的なエラーコードを記録します。details には元のエラー情報を含めることができます。

外部 API 呼び出し関数の実装

以下は、天気 API を呼び出す関数の例です。

typescriptimport axios, { AxiosError } from 'axios';

// axios のインストール(事前に yarn add axios を実行)

API 呼び出しには axios ライブラリを使用します。エラーハンドリングが容易で、TypeScript との相性も良好です。

typescript// 天気 API の呼び出し
async function fetchWeather(
  city: string,
  units: string = 'metric'
) {
  try {
    const response = await axios.get(
      'https://api.openweathermap.org/data/2.5/weather',
      {
        params: {
          q: city,
          units: units,
          appid: process.env.OPENWEATHER_API_KEY,
        },
        timeout: 5000, // 5秒でタイムアウト
      }
    );

    return {
      success: true,
      data: response.data,
    };
  } catch (error) {
    return handleAPIError(error);
  }
}

axios.get で API を呼び出し、エラーが発生した場合は専用のエラーハンドラで処理します。timeout を設定することで、長時間の待機を防ぎます。

エラーハンドラの実装

次に、エラーを分類して適切なエラーレスポンスを返す関数を実装します。

typescript// エラーハンドリング関数
function handleAPIError(error: unknown): ErrorResponse {
  // Axios エラーの場合
  if (axios.isAxiosError(error)) {
    const axiosError = error as AxiosError;

    // ネットワークエラー(タイムアウト、接続失敗など)
    if (!axiosError.response) {
      return {
        success: false,
        errorType: 'network',
        errorCode: axiosError.code,
        message: `ネットワークエラーが発生しました: ${axiosError.message}`,
      };
    }

    // HTTP エラー
    const status = axiosError.response.status;
    return {
      success: false,
      errorType: 'http',
      errorCode: `HTTP_${status}`,
      message: `HTTP エラー ${status}: ${getHTTPErrorMessage(
        status
      )}`,
      details: axiosError.response.data,
    };
  }

  // その他のエラー
  return {
    success: false,
    errorType: 'api',
    message: '予期しないエラーが発生しました',
    details: error,
  };
}

axios.isAxiosError で Axios 特有のエラーかを判定します。response が存在しない場合はネットワークエラー、存在する場合は HTTP エラーとして処理します。

typescript// HTTP ステータスコードに応じたメッセージ
function getHTTPErrorMessage(status: number): string {
  switch (status) {
    case 400:
      return 'リクエストが不正です';
    case 401:
      return '認証が必要です';
    case 403:
      return 'アクセスが拒否されました';
    case 404:
      return 'リソースが見つかりません';
    case 429:
      return 'API リクエスト制限を超過しました';
    case 500:
      return 'サーバー内部エラーが発生しました';
    case 503:
      return 'サービスが一時的に利用できません';
    default:
      return 'エラーが発生しました';
  }
}

HTTP ステータスコードごとに、AI エージェントが理解しやすいメッセージを返します。

リトライ処理の追加

一時的なエラー(タイムアウト、503 など)の場合、リトライを試みることで成功率を向上させられます。

typescript// リトライ処理を含む API 呼び出し
async function fetchWeatherWithRetry(
  city: string,
  units: string = 'metric',
  maxRetries: number = 3
) {
  let lastError: ErrorResponse | null = null;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    const result = await fetchWeather(city, units);

    if (result.success) {
      return result;
    }

    lastError = result as ErrorResponse;

    // リトライ対象のエラーかチェック
    if (shouldRetry(lastError)) {
      console.log(
        `リトライ ${attempt}/${maxRetries}: ${lastError.message}`
      );
      await sleep(1000 * attempt); // 指数バックオフ
      continue;
    }

    // リトライ不要なエラーの場合は即座に返す
    return result;
  }

  return lastError!;
}

最大 3 回までリトライを試み、指数バックオフ(待機時間を徐々に増やす)でサーバー負荷を軽減します。

typescript// リトライすべきエラーか判定
function shouldRetry(error: ErrorResponse): boolean {
  // ネットワークエラーはリトライ
  if (error.errorType === 'network') {
    return true;
  }

  // 503(Service Unavailable)はリトライ
  if (error.errorCode === 'HTTP_503') {
    return true;
  }

  // 429(Rate Limit)は少し待ってリトライ
  if (error.errorCode === 'HTTP_429') {
    return true;
  }

  return false;
}

// スリープ関数
function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

ネットワークエラーや一時的なサーバーエラーの場合のみリトライを行い、認証エラーなど恒久的なエラーは即座に返します。

エラー処理フロー図

以下の図は、エラーハンドリングとリトライのフローを示しています。

mermaidflowchart TD
  startNode["API 呼び出し"];
  callNode["HTTP リクエスト送信"];
  successQ{"成功?"};
  successDone["成功レスポンス"];
  classifyNode["エラー分類"];
  networkQ{"ネットワークエラー?"};
  retryCheck["リトライ可能か判定"];
  httpErrQ{"HTTPエラー?"};
  errorReturn["エラーレスポンス返却"];
  retryQ{"リトライ回数内?"};
  waitNode["待機(指数バックオフ)"];
  doneNode["完了"];

  startNode --> callNode;
  callNode --> successQ;

  successQ --|はい|--> successDone;
  successQ --|いいえ|--> classifyNode;

  classifyNode --> networkQ;
  networkQ --|はい|--> retryCheck;
  networkQ --|いいえ|--> httpErrQ;

  httpErrQ --|503/429|--> retryCheck;
  httpErrQ --|その他|--> errorReturn;

  retryCheck --> retryQ;
  retryQ --|はい|--> waitNode;
  retryQ --|いいえ|--> errorReturn;

  waitNode --> callNode;
  successDone --> doneNode;
  errorReturn --> doneNode;

図で理解できる要点

  • エラーを種類ごとに分類し、適切な処理を行う
  • リトライ可能なエラーは自動的に再試行する
  • 恒久的なエラーは即座に AI エージェントへ返す

具体例

ここまでの内容を統合し、実際に動作する MCP サーバーの実装例を示します。

プロジェクト構成

以下のようなディレクトリ構成でプロジェクトを作成します。

bashmcp-weather-server/
├── package.json
├── tsconfig.json
├── .env
└── src/
    ├── index.ts          # エントリーポイント
    ├── tools.ts          # Tool 定義
    ├── validator.ts      # スキーマ検証
    ├── weatherAPI.ts     # 外部 API 呼び出し
    └── errorHandler.ts   # エラーハンドリング

package.json

プロジェクトの依存関係を定義します。

json{
  "name": "mcp-weather-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "tsx src/index.ts",
    "build": "tsc"
  },
  "dependencies": {
    "axios": "^1.6.2",
    "ajv": "^8.12.0",
    "ajv-formats": "^2.1.1",
    "dotenv": "^16.3.1"
  },
  "devDependencies": {
    "@types/node": "^20.10.0",
    "typescript": "^5.3.0",
    "tsx": "^4.7.0"
  }
}

tsx は TypeScript を直接実行できる開発用ツールです。

tsconfig.json

TypeScript のコンパイル設定を行います。

json{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

.env

環境変数として API キーを設定します(実際の値は各自取得してください)。

iniOPENWEATHER_API_KEY=your_api_key_here

OpenWeatherMap の API キーは公式サイトで無料取得できます。

src/tools.ts

Tool 定義を管理するファイルです。

typescript// Tool 定義の型
export interface ToolDefinition {
  name: string;
  description: string;
  inputSchema: {
    type: 'object';
    properties: Record<string, unknown>;
    required?: string[];
  };
}
typescript// 天気情報取得 Tool
export const weatherTool: ToolDefinition = {
  name: 'fetch_weather',
  description:
    '指定された都市の現在の天気情報を取得します。都市名は英語で指定してください。',
  inputSchema: {
    type: 'object',
    properties: {
      city: {
        type: 'string',
        description:
          '都市名(例: Tokyo, London, New York)',
      },
      units: {
        type: 'string',
        enum: ['metric', 'imperial'],
        description:
          '温度の単位(metric: 摂氏、imperial: 華氏)',
        default: 'metric',
      },
    },
    required: ['city'],
  },
};
typescript// すべての Tool を配列で管理
export const tools: ToolDefinition[] = [weatherTool];

src/validator.ts

スキーマ検証を行うモジュールです。

typescriptimport Ajv from 'ajv';
import addFormats from 'ajv-formats';
import type { ToolDefinition } from './tools.js';

// Ajv インスタンスの作成
const ajv = new Ajv({ allErrors: true, strict: false });
addFormats(ajv);
typescript// スキーマ検証結果の型
export interface ValidationResult {
  valid: boolean;
  errors?: string[];
}
typescript// スキーマ検証関数
export function validateInput(
  schema: ToolDefinition['inputSchema'],
  input: unknown
): ValidationResult {
  const validate = ajv.compile(schema);
  const valid = validate(input);

  if (!valid && validate.errors) {
    const errors = validate.errors.map(
      (err) =>
        `${err.instancePath || 'root'} ${err.message}`
    );
    return { valid: false, errors };
  }

  return { valid: true };
}

src/errorHandler.ts

エラーハンドリングを行うモジュールです。

typescriptimport axios, { AxiosError } from 'axios';

// エラーレスポンスの型
export interface ErrorResponse {
  success: false;
  errorType:
    | 'network'
    | 'http'
    | 'api'
    | 'parse'
    | 'validation';
  errorCode?: string;
  message: string;
  details?: unknown;
}
typescript// エラーハンドラ
export function handleAPIError(
  error: unknown
): ErrorResponse {
  if (axios.isAxiosError(error)) {
    const axiosError = error as AxiosError;

    // ネットワークエラー
    if (!axiosError.response) {
      return {
        success: false,
        errorType: 'network',
        errorCode: axiosError.code,
        message: `ネットワークエラー: ${axiosError.message}`,
      };
    }

    // HTTP エラー
    const status = axiosError.response.status;
    return {
      success: false,
      errorType: 'http',
      errorCode: `HTTP_${status}`,
      message: `HTTP ${status}: ${getHTTPErrorMessage(
        status
      )}`,
      details: axiosError.response.data,
    };
  }

  return {
    success: false,
    errorType: 'api',
    message: '予期しないエラーが発生しました',
    details: error,
  };
}
typescript// HTTP ステータスメッセージ
function getHTTPErrorMessage(status: number): string {
  const messages: Record<number, string> = {
    400: 'リクエストが不正です',
    401: '認証が必要です(API キーを確認してください)',
    403: 'アクセスが拒否されました',
    404: '指定された都市が見つかりません',
    429: 'API リクエスト制限を超過しました',
    500: 'サーバー内部エラー',
    503: 'サービスが一時的に利用できません',
  };

  return messages[status] || 'エラーが発生しました';
}
typescript// リトライ判定
export function shouldRetry(error: ErrorResponse): boolean {
  return (
    error.errorType === 'network' ||
    error.errorCode === 'HTTP_503' ||
    error.errorCode === 'HTTP_429'
  );
}
typescript// スリープ関数
export function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

src/weatherAPI.ts

外部 API 呼び出しを行うモジュールです。

typescriptimport axios from 'axios';
import {
  handleAPIError,
  shouldRetry,
  sleep,
} from './errorHandler.js';
import type { ErrorResponse } from './errorHandler.js';

// 成功レスポンスの型
interface SuccessResponse {
  success: true;
  data: unknown;
}

type APIResponse = SuccessResponse | ErrorResponse;
typescript// 天気 API 呼び出し(単発)
async function fetchWeatherOnce(
  city: string,
  units: string
): Promise<APIResponse> {
  try {
    const response = await axios.get(
      'https://api.openweathermap.org/data/2.5/weather',
      {
        params: {
          q: city,
          units: units,
          appid: process.env.OPENWEATHER_API_KEY,
        },
        timeout: 5000,
      }
    );

    return {
      success: true,
      data: response.data,
    };
  } catch (error) {
    return handleAPIError(error);
  }
}
typescript// 天気 API 呼び出し(リトライ付き)
export async function fetchWeather(
  city: string,
  units: string = 'metric',
  maxRetries: number = 3
): Promise<APIResponse> {
  let lastError: ErrorResponse | null = null;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    const result = await fetchWeatherOnce(city, units);

    if (result.success) {
      return result;
    }

    lastError = result;

    if (shouldRetry(lastError) && attempt < maxRetries) {
      console.log(
        `リトライ ${attempt}/${maxRetries}: ${lastError.message}`
      );
      await sleep(1000 * attempt);
      continue;
    }

    return result;
  }

  return lastError!;
}

src/index.ts

MCP サーバーのエントリーポイントです。

typescriptimport 'dotenv/config';
import { tools, weatherTool } from './tools.js';
import { validateInput } from './validator.js';
import { fetchWeather } from './weatherAPI.js';

// Tool 呼び出しハンドラ
async function handleToolCall(
  toolName: string,
  args: unknown
) {
  // Tool 定義の取得
  if (toolName !== 'fetch_weather') {
    return {
      success: false,
      errorType: 'validation',
      message: `未知の Tool: ${toolName}`,
    };
  }

  // スキーマ検証
  const validation = validateInput(
    weatherTool.inputSchema,
    args
  );
  if (!validation.valid) {
    return {
      success: false,
      errorType: 'validation',
      message: `入力パラメータエラー: ${validation.errors?.join(
        ', '
      )}`,
    };
  }

  // 型安全な引数の取得
  const { city, units = 'metric' } = args as {
    city: string;
    units?: string;
  };

  // 外部 API 呼び出し
  return await fetchWeather(city, units);
}
typescript// メイン処理(テスト用)
async function main() {
  console.log('MCP Weather Server - Tool 一覧:');
  tools.forEach((tool) => {
    console.log(`- ${tool.name}: ${tool.description}`);
  });

  console.log('\n--- テスト 1: 正常なリクエスト ---');
  const result1 = await handleToolCall('fetch_weather', {
    city: 'Tokyo',
  });
  console.log(JSON.stringify(result1, null, 2));

  console.log('\n--- テスト 2: パラメータエラー ---');
  const result2 = await handleToolCall('fetch_weather', {});
  console.log(JSON.stringify(result2, null, 2));

  console.log('\n--- テスト 3: 存在しない都市 ---');
  const result3 = await handleToolCall('fetch_weather', {
    city: 'InvalidCity12345',
  });
  console.log(JSON.stringify(result3, null, 2));
}

main();

実行方法

以下のコマンドでプロジェクトをセットアップし、実行します。

bash# 依存パッケージのインストール
yarn install
bash# 開発サーバーの起動
yarn dev

実行すると、以下のような出力が得られます。

luaMCP Weather Server - Tool 一覧:
- fetch_weather: 指定された都市の現在の天気情報を取得します。都市名は英語で指定してください。

--- テスト 1: 正常なリクエスト ---
{
  "success": true,
  "data": {
    "weather": [...],
    "main": { "temp": 15.2, ... }
  }
}

--- テスト 2: パラメータエラー ---
{
  "success": false,
  "errorType": "validation",
  "message": "入力パラメータエラー: root must have required property 'city'"
}

--- テスト 3: 存在しない都市 ---
{
  "success": false,
  "errorType": "http",
  "errorCode": "HTTP_404",
  "message": "HTTP 404: 指定された都市が見つかりません"
}

実装のポイント

この実装例では、以下のポイントを押さえています。

  • Tool 定義の分離: tools.ts で Tool を一元管理
  • スキーマ検証の徹底: Ajv による事前検証で不正なリクエストを防ぐ
  • エラー処理の統一: すべてのエラーを ErrorResponse 型で返す
  • リトライ機能: 一時的なエラーに対する自動リトライ
  • 型安全性: TypeScript の型システムを活用し、実行時エラーを削減

拡張案

この実装をベースに、以下のような拡張が可能です。

#拡張内容説明
1キャッシュ機能同じリクエストの結果をキャッシュして API コールを削減
2レート制限クライアントごとの呼び出し回数を制限
3複数 Tool 対応天気以外の API にも対応できるよう Tool を追加
4ロギング強化構造化ロギングでデバッグやモニタリングを容易に
5テスト追加Jest などでユニットテスト・統合テストを実装

まとめ

本記事では、MCP サーバーで外部 API を安全に呼び出すための実装手順を解説しました。

重要ポイントの再確認

  • Tool 定義: AI エージェントが理解できる形で、入力スキーマと説明を明確に定義する
  • スキーマ検証: Ajv などのライブラリで事前検証を行い、不正なリクエストを防ぐ
  • エラー処理: ネットワーク、HTTP、API エラーを分類し、統一形式で返す
  • リトライ制御: 一時的なエラーには自動リトライを実装し、成功率を向上させる

セキュリティ上の注意点

MCP サーバーを運用する際は、以下のセキュリティ対策も忘れずに実施してください。

#対策説明
1API キーの保護環境変数で管理し、コードにハードコードしない
2レート制限悪意あるリクエストによる過負荷を防ぐ
3入力サニタイゼーションSQL インジェクションなどの攻撃を防ぐ
4HTTPS 通信外部 API との通信は必ず暗号化する

次のステップ

MCP サーバーの基本が理解できたら、以下のステップへ進むことをおすすめします。

  • 実際のプロダクション環境での運用設計
  • モニタリング・アラート機能の追加
  • 複数の Tool を管理するアーキテクチャの構築
  • AI エージェントとの統合テスト

MCP サーバーを活用することで、AI エージェントの能力を大きく拡張できます。本記事の内容が、皆さんの開発の一助となれば幸いです。

関連リンク