この記事は、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でのテスト例
以下のような質問でテストしてみましょう:
-
郵便番号検索
"郵便番号 1000001 の住所を教えて" -
天気情報取得
"東京の現在の天気を教えて" -
為替レート
"1万円をアメリカドルに換算すると?" -
システム統計
"APIの利用状況を教えて"
期待される動作
- 初回リクエスト: APIから直接データを取得し、キャッシュに保存
- 2回目以降: キャッシュからデータを高速取得
- レート制限: 制限に達すると適切なエラーメッセージを表示
- エラーハンドリング: 不正な入力や外部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)
)
);
}
}
🔍 トラブルシューティング
よくある問題と解決法
-
APIキーの問題
# 環境変数の確認 echo $WEATHER_API_KEY # .envファイルの読み込み確認 node -e "require('dotenv').config(); console.log(process.env.WEATHER_API_KEY)" -
レート制限エラー
// デバッグ用の制限状況確認 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 }); } -
キャッシュの問題
// キャッシュのクリア 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サーバーの実装方法を学びました。
重要なポイント
-
セキュリティファースト
- 認証情報の環境変数管理
- 入力検証の徹底
- アクセス制御の実装
-
パフォーマンス最適化
- インテリジェントなキャッシュシステム
- レート制限の管理
- 並列処理の活用
-
エラーハンドリング
- 包括的なエラー処理
- ユーザーフレンドリーなメッセージ
- デバッグ機能の充実
-
拡張性
- モジュラー設計
- 新しい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をさらに強化し、フォルダ構造を効率的に扱う高度なテクニックを解説します。お楽しみに!