0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Model Context Protocol完全解説 30日間シリーズ - Day 16【MCP応用 #16】Web API統合MCP:外部APIを安全にラップする方法

Posted at

この記事は、QiitaのModel Context Protocol(以下、MCP)解説シリーズの第16回です。

今回は、MCPのTools機能をさらに応用し、外部のWeb APIを安全にラップ(統合)する方法を解説します。これにより、LLMが外部のサービスと連携できるようになり、MCPアプリケーションの可能性が大きく広がります。

💡 なぜWeb APIのラッパーが必要なのか?

LLMに直接外部APIのURLや認証情報を教えるのは、セキュリティ上非常に危険です。MCPサーバーがWeb APIのラッパーとして機能することで、以下のメリットが得られます。

セキュリティの確保

  • 認証情報の隠蔽: APIキーやトークンをサーバー内部に隠蔽し、LLMに直接公開しません
  • アクセス制御: 特定のAPIエンドポイントのみに制限し、危険な操作を防ぎます
  • 入力検証: 不正なパラメータや悪意のある入力をフィルタリングします

データの最適化

  • レスポンスの整形: APIの生データをLLMが理解しやすい形に加工
  • 不要データの除去: プライバシー情報や大きなバイナリデータを除外
  • エラーメッセージの翻訳: 技術的なエラーを分かりやすいメッセージに変換

パフォーマンスの最適化

  • レート制限の管理: APIへのアクセス頻度を制御
  • キャッシュ機能: 同じリクエストの結果を一時保存して効率化
  • タイムアウト制御: 長時間のAPI呼び出しを適切に処理

🛠️ ステップ1:プロジェクトのセットアップ

今回は、複数のWeb APIをラップする包括的な例を実装します。郵便番号検索、天気情報、為替レートの3つのAPIを統合します。

mkdir mcp-api-wrapper
cd mcp-api-wrapper
npm init -y
npm install @modelcontextprotocol/sdk axios zod dotenv node-cache
npm install -D typescript ts-node @types/node @types/axios
npx tsc --init

環境変数の設定

.envファイルを作成:

# 天気API用(OpenWeatherMap)
WEATHER_API_KEY=your_openweathermap_api_key

# 為替レート API用(ExchangeRate-API)
EXCHANGE_RATE_API_KEY=your_exchangerate_api_key

# APIレート制限設定
API_RATE_LIMIT_PER_MINUTE=60
API_CACHE_TTL_SECONDS=300

📝 ステップ2:包括的なAPIラッパーサーバーの実装

server.tsファイルを作成:

#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  TextContent,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import axios, { AxiosResponse } from "axios";
import * as dotenv from "dotenv";
import NodeCache from "node-cache";

// 環境変数の読み込み
dotenv.config();

// レート制限とキャッシュの設定
class APIManager {
  private cache: NodeCache;
  private requestCounts: Map<string, { count: number; resetTime: number }>;
  private readonly rateLimit: number;
  private readonly cacheTTL: number;

  constructor() {
    this.cache = new NodeCache({ 
      stdTTL: parseInt(process.env.API_CACHE_TTL_SECONDS || '300'),
      checkperiod: 60 
    });
    this.requestCounts = new Map();
    this.rateLimit = parseInt(process.env.API_RATE_LIMIT_PER_MINUTE || '60');
    this.cacheTTL = parseInt(process.env.API_CACHE_TTL_SECONDS || '300');
  }

  // レート制限チェック
  checkRateLimit(apiName: string): boolean {
    const now = Date.now();
    const oneMinute = 60 * 1000;
    const key = apiName;
    
    const current = this.requestCounts.get(key);
    if (!current || now > current.resetTime) {
      this.requestCounts.set(key, { count: 1, resetTime: now + oneMinute });
      return true;
    }
    
    if (current.count >= this.rateLimit) {
      return false;
    }
    
    current.count++;
    return true;
  }

  // キャッシュから取得
  getFromCache(key: string): any {
    return this.cache.get(key);
  }

  // キャッシュに保存
  setToCache(key: string, value: any, ttl?: number): void {
    this.cache.set(key, value, ttl || this.cacheTTL);
  }

  // 統計情報の取得
  getStats(): any {
    return {
      cacheKeys: this.cache.keys().length,
      rateLimitStatus: Array.from(this.requestCounts.entries()).map(([api, data]) => ({
        api,
        requestCount: data.count,
        resetsAt: new Date(data.resetTime).toISOString()
      }))
    };
  }
}

// 入力スキーマの定義
const ZipcodeSchema = z.object({
  zipcode: z.string()
    .regex(/^\d{7}$/, "郵便番号は7桁の数字で入力してください")
    .describe("7桁の郵便番号(ハイフンなし)")
});

const WeatherSchema = z.object({
  city: z.string()
    .min(1, "都市名を入力してください")
    .describe("天気を取得したい都市名"),
  units: z.enum(["metric", "imperial"])
    .optional()
    .default("metric")
    .describe("温度の単位(metric: 摂氏、imperial: 華氏)")
});

const ExchangeRateSchema = z.object({
  from: z.string()
    .length(3, "通貨コードは3文字で入力してください")
    .toUpperCase()
    .describe("変換元の通貨コード(例: USD, JPY)"),
  to: z.string()
    .length(3, "通貨コードは3文字で入力してください") 
    .toUpperCase()
    .describe("変換先の通貨コード(例: USD, JPY)"),
  amount: z.number()
    .positive("金額は正の数で入力してください")
    .optional()
    .default(1)
    .describe("変換する金額(デフォルト: 1)")
});

class WebAPIWrapperServer {
  private server: Server;
  private apiManager: APIManager;

  constructor() {
    this.apiManager = new APIManager();
    
    this.server = new Server(
      {
        name: "web-api-wrapper-server",
        version: "1.0.0",
      },
      {
        capabilities: {
          tools: {},
        },
      }
    );

    this.setupHandlers();
  }

  private setupHandlers() {
    // ツール一覧の取得
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          name: "get_address_by_zipcode",
          description: "日本の7桁郵便番号から住所(都道府県、市区町村、町域)を取得する",
          inputSchema: {
            type: "object",
            properties: {
              zipcode: {
                type: "string",
                pattern: "^\\d{7}$",
                description: "7桁の郵便番号(ハイフンなし)"
              }
            },
            required: ["zipcode"]
          }
        },
        {
          name: "get_weather",
          description: "指定した都市の現在の天気情報を取得する",
          inputSchema: {
            type: "object",
            properties: {
              city: {
                type: "string",
                description: "天気を取得したい都市名"
              },
              units: {
                type: "string",
                enum: ["metric", "imperial"],
                default: "metric",
                description: "温度の単位(metric: 摂氏、imperial: 華氏)"
              }
            },
            required: ["city"]
          }
        },
        {
          name: "get_exchange_rate",
          description: "2つの通貨間の為替レートを取得し、金額を変換する",
          inputSchema: {
            type: "object",
            properties: {
              from: {
                type: "string",
                pattern: "^[A-Z]{3}$",
                description: "変換元の通貨コード(例: USD, JPY)"
              },
              to: {
                type: "string", 
                pattern: "^[A-Z]{3}$",
                description: "変換先の通貨コード(例: USD, JPY)"
              },
              amount: {
                type: "number",
                minimum: 0.01,
                default: 1,
                description: "変換する金額(デフォルト: 1)"
              }
            },
            required: ["from", "to"]
          }
        },
        {
          name: "get_api_stats",
          description: "APIラッパーの統計情報(キャッシュ状況、レート制限状況)を取得する",
          inputSchema: {
            type: "object",
            properties: {},
            required: []
          }
        }
      ]
    }));

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

      try {
        switch (name) {
          case "get_address_by_zipcode":
            return await this.handleAddressSearch(args);
          case "get_weather":
            return await this.handleWeatherSearch(args);
          case "get_exchange_rate":
            return await this.handleExchangeRate(args);
          case "get_api_stats":
            return await this.handleApiStats();
          default:
            throw new Error(`未知のツール: ${name}`);
        }
      } catch (error) {
        return this.handleError(error);
      }
    });
  }

  private async handleAddressSearch(args: any) {
    const parsed = ZipcodeSchema.parse(args);
    const apiName = "zipcloud";
    
    // レート制限チェック
    if (!this.apiManager.checkRateLimit(apiName)) {
      throw new Error("郵便番号検索APIのレート制限に達しました。しばらく後に再試行してください。");
    }

    // キャッシュチェック
    const cacheKey = `address:${parsed.zipcode}`;
    const cached = this.apiManager.getFromCache(cacheKey);
    if (cached) {
      return {
        content: [
          {
            type: "text",
            text: `📬 郵便番号 ${parsed.zipcode} の住所情報(キャッシュから取得):\n\n${JSON.stringify(cached, null, 2)}`
          } as TextContent
        ]
      };
    }

    try {
      const response = await axios.get('https://zipcloud.ibsnet.co.jp/api/search', {
        params: { zipcode: parsed.zipcode },
        timeout: 10000
      });

      const results = response.data.results;
      if (!results || results.length === 0) {
        throw new Error('指定された郵便番号の住所が見つかりませんでした。');
      }

      // データの整形
      const addressData = results.map((result: any) => ({
        prefecture: result.address1,
        city: result.address2, 
        town: result.address3,
        kana: {
          prefecture: result.kana1,
          city: result.kana2,
          town: result.kana3
        },
        zipcode: result.zipcode
      }));

      // キャッシュに保存
      this.apiManager.setToCache(cacheKey, addressData);

      return {
        content: [
          {
            type: "text",
            text: `📬 郵便番号 ${parsed.zipcode} の住所情報:\n\n${JSON.stringify(addressData, null, 2)}`
          } as TextContent
        ]
      };

    } catch (error) {
      if (axios.isAxiosError(error)) {
        throw new Error(`郵便番号検索APIエラー: ${error.response?.data?.message || error.message}`);
      }
      throw new Error(`住所検索中にエラーが発生しました: ${error.message}`);
    }
  }

  private async handleWeatherSearch(args: any) {
    const parsed = WeatherSchema.parse(args);
    const apiName = "openweather";
    
    const apiKey = process.env.WEATHER_API_KEY;
    if (!apiKey) {
      throw new Error("WEATHER_API_KEY環境変数が設定されていません");
    }

    // レート制限チェック
    if (!this.apiManager.checkRateLimit(apiName)) {
      throw new Error("天気APIのレート制限に達しました。しばらく後に再試行してください。");
    }

    // キャッシュチェック
    const cacheKey = `weather:${parsed.city}:${parsed.units}`;
    const cached = this.apiManager.getFromCache(cacheKey);
    if (cached) {
      return {
        content: [
          {
            type: "text",
            text: `🌤️ ${parsed.city}の天気情報(キャッシュから取得):\n\n${cached}`
          } as TextContent
        ]
      };
    }

    try {
      const response = await axios.get('https://api.openweathermap.org/data/2.5/weather', {
        params: {
          q: parsed.city,
          appid: apiKey,
          units: parsed.units,
          lang: 'ja'
        },
        timeout: 10000
      });

      const weather = response.data;
      const temp = Math.round(weather.main.temp);
      const feelsLike = Math.round(weather.main.feels_like);
      const description = weather.weather[0].description;
      const humidity = weather.main.humidity;
      const pressure = weather.main.pressure;
      const windSpeed = weather.wind?.speed || 0;
      
      const unit = parsed.units === 'imperial' ? 'F' : 'C';
      const windUnit = parsed.units === 'imperial' ? 'mph' : 'm/s';

      const weatherInfo = `🌡️ 気温: ${temp}°${unit} (体感: ${feelsLike}°${unit})
🌤️ 天候: ${description}
💧 湿度: ${humidity}%
🌬️ 風速: ${windSpeed}${windUnit}
🔽 気圧: ${pressure}hPa`;

      // キャッシュに保存(天気情報は5分間キャッシュ)
      this.apiManager.setToCache(cacheKey, weatherInfo, 300);

      return {
        content: [
          {
            type: "text",
            text: `🌤️ ${parsed.city}の現在の天気:\n\n${weatherInfo}`
          } as TextContent
        ]
      };

    } catch (error) {
      if (axios.isAxiosError(error)) {
        const status = error.response?.status;
        if (status === 401) {
          throw new Error("天気APIの認証に失敗しました。APIキーを確認してください。");
        } else if (status === 404) {
          throw new Error(`都市 "${parsed.city}" が見つかりません。都市名を確認してください。`);
        }
        throw new Error(`天気APIエラー: ${error.response?.data?.message || error.message}`);
      }
      throw new Error(`天気情報取得中にエラーが発生しました: ${error.message}`);
    }
  }

  private async handleExchangeRate(args: any) {
    const parsed = ExchangeRateSchema.parse(args);
    const apiName = "exchangerate";
    
    // レート制限チェック
    if (!this.apiManager.checkRateLimit(apiName)) {
      throw new Error("為替レートAPIのレート制限に達しました。しばらく後に再試行してください。");
    }

    // キャッシュチェック
    const cacheKey = `exchange:${parsed.from}:${parsed.to}`;
    const cached = this.apiManager.getFromCache(cacheKey);
    if (cached) {
      const convertedAmount = (cached.rate * parsed.amount).toFixed(2);
      const result = `💱 為替レート情報(キャッシュから取得):
📊 1 ${parsed.from} = ${cached.rate} ${parsed.to}
💰 ${parsed.amount} ${parsed.from} = ${convertedAmount} ${parsed.to}
📅 更新日時: ${cached.lastUpdate}`;

      return {
        content: [
          {
            type: "text",
            text: result
          } as TextContent
        ]
      };
    }

    try {
      // 無料のAPIを使用(fixer.ioの代替)
      const response = await axios.get(`https://api.exchangerate-api.com/v4/latest/${parsed.from}`, {
        timeout: 10000
      });

      const rates = response.data.rates;
      if (!rates[parsed.to]) {
        throw new Error(`通貨コード "${parsed.to}" がサポートされていません。`);
      }

      const rate = rates[parsed.to];
      const convertedAmount = (rate * parsed.amount).toFixed(2);
      
      const exchangeData = {
        rate: rate,
        lastUpdate: new Date(response.data.date).toLocaleDateString('ja-JP')
      };

      // キャッシュに保存(為替レートは15分間キャッシュ)
      this.apiManager.setToCache(cacheKey, exchangeData, 900);

      const result = `💱 為替レート情報:
📊 1 ${parsed.from} = ${rate} ${parsed.to}
💰 ${parsed.amount} ${parsed.from} = ${convertedAmount} ${parsed.to}
📅 更新日時: ${exchangeData.lastUpdate}`;

      return {
        content: [
          {
            type: "text",
            text: result
          } as TextContent
        ]
      };

    } catch (error) {
      if (axios.isAxiosError(error)) {
        throw new Error(`為替レートAPIエラー: ${error.response?.data?.message || error.message}`);
      }
      throw new Error(`為替レート取得中にエラーが発生しました: ${error.message}`);
    }
  }

  private async handleApiStats() {
    const stats = this.apiManager.getStats();
    
    const statsText = `📊 API統計情報:

🗃️ キャッシュ状況:
• キャッシュされたエントリ数: ${stats.cacheKeys}

⏰ レート制限状況:
${stats.rateLimitStatus.length === 0 ? '• 現在アクティブなAPIなし' : 
  stats.rateLimitStatus.map((api: any) => 
    `• ${api.api}: ${api.requestCount}回使用 (リセット: ${api.resetsAt})`
  ).join('\n')}

🔧 システム情報:
• Node.js バージョン: ${process.version}
• プロセス稼働時間: ${Math.floor(process.uptime())}秒`;

    return {
      content: [
        {
          type: "text",
          text: statsText
        } as TextContent
      ]
    };
  }

  private handleError(error: any) {
    console.error('Tool execution error:', error);

    let errorMessage: string;
    
    if (error instanceof z.ZodError) {
      errorMessage = `入力データが不正です: ${error.errors.map(e => e.message).join(', ')}`;
    } else if (error instanceof Error) {
      errorMessage = error.message;
    } else {
      errorMessage = '不明なエラーが発生しました';
    }

    return {
      content: [
        {
          type: "text",
          text: `❌ エラー: ${errorMessage}`
        } as TextContent
      ],
      isError: true
    };
  }

  async start() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error("Web API Wrapper Server が開始されました");
  }
}

// サーバー開始
async function main() {
  const server = new WebAPIWrapperServer();
  
  // グレースフル・シャットダウン
  process.on('SIGINT', () => {
    console.error('シャットダウン中...');
    process.exit(0);
  });

  await server.start();
}

// エラーハンドリング
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  process.exit(1);
});

process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  process.exit(1);
});

if (require.main === module) {
  main().catch((error) => {
    console.error("サーバー開始エラー:", error);
    process.exit(1);
  });
}

🔧 ステップ3:Claude Desktop設定

claude_desktop_config.jsonに以下の設定を追加:

{
  "mcpServers": {
    "web-api-wrapper": {
      "command": "npx",
      "args": ["ts-node", "/path/to/your/project/server.ts"],
      "env": {
        "WEATHER_API_KEY": "your_actual_weather_api_key",
        "EXCHANGE_RATE_API_KEY": "your_actual_exchange_rate_api_key"
      }
    }
  }
}

🚀 ステップ4:動作確認とテスト

サーバーの起動

npx ts-node server.ts

Claude Desktopでのテスト例

以下のような質問でテストしてみましょう:

  1. 郵便番号検索

    "郵便番号 1000001 の住所を教えて"
    
  2. 天気情報取得

    "東京の現在の天気を教えて"
    
  3. 為替レート

    "1万円をアメリカドルに換算すると?"
    
  4. システム統計

    "APIの利用状況を教えて"
    

期待される動作

  1. 初回リクエスト: APIから直接データを取得し、キャッシュに保存
  2. 2回目以降: キャッシュからデータを高速取得
  3. レート制限: 制限に達すると適切なエラーメッセージを表示
  4. エラーハンドリング: 不正な入力や外部APIエラーを適切に処理

🛡️ セキュリティとベストプラクティス

1. 認証情報の管理

// ❌ 悪い例:ハードコーディング
const API_KEY = "sk-1234567890abcdef";

// ✅ 良い例:環境変数の使用
const API_KEY = process.env.WEATHER_API_KEY;
if (!API_KEY) {
  throw new Error("API_KEY environment variable is required");
}

2. 入力検証の徹底

// 複雑な検証ルール
const EmailSchema = z.string()
  .email("有効なメールアドレスを入力してください")
  .refine((email) => !email.includes("example.com"), {
    message: "テスト用ドメインは使用できません"
  });

const CurrencySchema = z.string()
  .length(3)
  .toUpperCase()
  .refine((code) => SUPPORTED_CURRENCIES.includes(code), {
    message: "サポートされていない通貨コードです"
  });

3. レート制限の実装

class AdvancedRateLimiter {
  private windows: Map<string, { requests: number[]; limit: number; window: number }>;

  constructor() {
    this.windows = new Map();
  }

  isAllowed(key: string, limit: number = 100, windowMs: number = 60000): boolean {
    const now = Date.now();
    const windowData = this.windows.get(key) || { 
      requests: [], 
      limit, 
      window: windowMs 
    };

    // 古いリクエストを除去
    windowData.requests = windowData.requests.filter(
      timestamp => now - timestamp < windowData.window
    );

    if (windowData.requests.length >= windowData.limit) {
      return false;
    }

    windowData.requests.push(now);
    this.windows.set(key, windowData);
    return true;
  }

  getRemainingRequests(key: string): number {
    const windowData = this.windows.get(key);
    if (!windowData) return 0;
    
    const now = Date.now();
    const validRequests = windowData.requests.filter(
      timestamp => now - timestamp < windowData.window
    );
    
    return Math.max(0, windowData.limit - validRequests.length);
  }
}

📊 監視とデバッグ

ログ機能の実装

class APILogger {
  static logRequest(api: string, params: any, responseTime: number, fromCache: boolean) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      api,
      params: this.sanitizeParams(params),
      responseTime: `${responseTime}ms`,
      fromCache,
      source: fromCache ? 'cache' : 'api'
    };
    
    console.log('API Request:', JSON.stringify(logEntry));
  }

  private static sanitizeParams(params: any): any {
    const sanitized = { ...params };
    // 機密情報の除去
    delete sanitized.apiKey;
    delete sanitized.token;
    delete sanitized.password;
    return sanitized;
  }
}

ヘルスチェック機能

private async handleHealthCheck() {
  const health = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    apis: {
      zipcloud: await this.checkApiHealth('https://zipcloud.ibsnet.co.jp'),
      openweather: process.env.WEATHER_API_KEY ? 'configured' : 'not configured',
      exchangerate: 'available'
    }
  };

  return {
    content: [
      {
        type: "text",
        text: `🏥 システム健康状態:\n\n${JSON.stringify(health, null, 2)}`
      } as TextContent
    ]
  };
}

🚀 高度な機能の実装

Webhook対応

class WebhookHandler {
  private subscribers: Map<string, Function[]> = new Map();

  subscribe(event: string, callback: Function) {
    const callbacks = this.subscribers.get(event) || [];
    callbacks.push(callback);
    this.subscribers.set(event, callbacks);
  }

  async trigger(event: string, data: any) {
    const callbacks = this.subscribers.get(event) || [];
    for (const callback of callbacks) {
      try {
        await callback(data);
      } catch (error) {
        console.error(`Webhook callback error for ${event}:`, error);
      }
    }
  }
}

バッチ処理対応

private async handleBatchRequest(args: any) {
  const BatchRequestSchema = z.object({
    requests: z.array(z.object({
      type: z.enum(['address', 'weather', 'exchange']),
      params: z.any()
    })).max(10, "一度に処理できるリクエストは10件までです")
  });

  const parsed = BatchRequestSchema.parse(args);
  const results = [];

  for (const request of parsed.requests) {
    try {
      let result;
      switch (request.type) {
        case 'address':
          result = await this.handleAddressSearch(request.params);
          break;
        case 'weather':
          result = await this.handleWeatherSearch(request.params);
          break;
        case 'exchange':
          result = await this.handleExchangeRate(request.params);
          break;
      }
      results.push({ success: true, data: result });
    } catch (error) {
      results.push({ success: false, error: error.message });
    }
    
    // レート制限を考慮した遅延
    await new Promise(resolve => setTimeout(resolve, 100));
  }

  return {
    content: [
      {
        type: "text",
        text: `📦 バッチ処理結果:\n\n${JSON.stringify(results, null, 2)}`
      } as TextContent
    ]
  };
}

🎯 実用的な応用例

E-commerce統合

// 商品価格の多通貨対応
async function getProductWithMultiCurrencyPrice(productId: string, currencies: string[]) {
  const product = await getProduct(productId);
  const baseCurrency = 'USD';
  
  const prices = await Promise.all(
    currencies.map(async (currency) => {
      const rate = await this.getExchangeRate(baseCurrency, currency);
      return {
        currency,
        price: (product.price * rate).toFixed(2),
        formattedPrice: new Intl.NumberFormat('ja-JP', {
          style: 'currency',
          currency
        }).format(product.price * rate)
      };
    })
  );
  
  return { ...product, prices };
}

位置情報サービス統合

// 郵便番号から天気を一括取得
async function getLocationWeatherByZipcode(zipcode: string) {
  const addressInfo = await this.handleAddressSearch({ zipcode });
  const prefecture = addressInfo.content[0].text.match(/prefecture": "([^"]+)"/)?.[1];
  
  if (prefecture) {
    const weatherInfo = await this.handleWeatherSearch({ city: prefecture });
    return {
      address: addressInfo,
      weather: weatherInfo
    };
  }
  
  throw new Error('住所から天気情報を取得できませんでした');
}

通知システム統合

class NotificationIntegrator {
  private webhooks: string[] = [];
  
  async sendAlert(message: string, severity: 'info' | 'warning' | 'error') {
    const notification = {
      timestamp: new Date().toISOString(),
      message,
      severity,
      source: 'mcp-api-wrapper'
    };
    
    // Slack, Discord, Teams等への通知
    await Promise.all(
      this.webhooks.map(webhook => 
        axios.post(webhook, notification).catch(console.error)
      )
    );
  }
}

🔍 トラブルシューティング

よくある問題と解決法

  1. APIキーの問題

    # 環境変数の確認
    echo $WEATHER_API_KEY
    
    # .envファイルの読み込み確認
    node -e "require('dotenv').config(); console.log(process.env.WEATHER_API_KEY)"
    
  2. レート制限エラー

    // デバッグ用の制限状況確認
    private debugRateLimit(apiName: string) {
      const current = this.requestCounts.get(apiName);
      console.log(`Rate limit for ${apiName}:`, {
        current: current?.count || 0,
        limit: this.rateLimit,
        resetTime: current?.resetTime ? new Date(current.resetTime) : null
      });
    }
    
  3. キャッシュの問題

    // キャッシュのクリア
    clearApiCache(pattern?: string) {
      if (pattern) {
        const keys = this.cache.keys().filter(key => key.includes(pattern));
        keys.forEach(key => this.cache.del(key));
      } else {
        this.cache.flushAll();
      }
    }
    

デバッグモードの実装

class DebugMode {
  static enabled = process.env.DEBUG_MODE === 'true';
  
  static log(context: string, data: any) {
    if (this.enabled) {
      console.debug(`[DEBUG:${context}]`, JSON.stringify(data, null, 2));
    }
  }
  
  static time(label: string) {
    if (this.enabled) {
      console.time(label);
    }
  }
  
  static timeEnd(label: string) {
    if (this.enabled) {
      console.timeEnd(label);
    }
  }
}

📈 パフォーマンス最適化

同期処理の最適化

// 複数API呼び出しの並列化
async function getLocationSummary(zipcode: string) {
  const [addressResult, weatherPromise] = await Promise.allSettled([
    this.handleAddressSearch({ zipcode }),
    // 住所が取得できたら天気情報も並行取得
    this.handleAddressSearch({ zipcode }).then(addr => {
      const city = this.extractCityFromAddress(addr);
      return this.handleWeatherSearch({ city });
    })
  ]);
  
  return {
    address: addressResult.status === 'fulfilled' ? addressResult.value : null,
    weather: weatherPromise.status === 'fulfilled' ? weatherPromise.value : null
  };
}

メモリ使用量の最適化

class MemoryOptimizer {
  private static maxCacheSize = 1000;
  
  static optimizeCache(cache: NodeCache) {
    const keys = cache.keys();
    if (keys.length > this.maxCacheSize) {
      // LRU (Least Recently Used) 方式でキャッシュを削減
      const sortedKeys = keys
        .map(key => ({ key, stats: cache.getStats() }))
        .sort((a, b) => a.stats.hits - b.stats.hits);
      
      const keysToRemove = sortedKeys.slice(0, keys.length - this.maxCacheSize);
      keysToRemove.forEach(({ key }) => cache.del(key));
    }
  }
}

🎯 まとめ

今回は、外部Web APIを安全かつ効率的にラップするMCPサーバーの実装方法を学びました。

重要なポイント

  1. セキュリティファースト

    • 認証情報の環境変数管理
    • 入力検証の徹底
    • アクセス制御の実装
  2. パフォーマンス最適化

    • インテリジェントなキャッシュシステム
    • レート制限の管理
    • 並列処理の活用
  3. エラーハンドリング

    • 包括的なエラー処理
    • ユーザーフレンドリーなメッセージ
    • デバッグ機能の充実
  4. 拡張性

    • モジュラー設計
    • 新しいAPIの容易な追加
    • バッチ処理対応

応用可能なAPI例

  • ソーシャルメディア: Twitter, Facebook Graph API
  • クラウドサービス: AWS, Google Cloud, Azure
  • 決済サービス: Stripe, PayPal
  • コミュニケーション: Slack, Microsoft Teams
  • ファイルストレージ: Google Drive, Dropbox
  • AI/ML: OpenAI, Google AI Platform

次のステップ

このパターンをベースに、以下のような高度な機能を実装できます:

  • マイクロサービス統合: 複数のサービスを組み合わせた複雑なワークフロー
  • リアルタイム通信: WebSocketを使った双方向通信
  • 機械学習統合: AI/MLサービスとの連携
  • ブロックチェーン統合: Web3.jsを使った分散アプリケーション連携

次回は、ファイル操作MCPをさらに強化し、フォルダ構造を効率的に扱う高度なテクニックを解説します。お楽しみに!

🔗 参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?