MCP導入でよくある問題と解決策 — 開発者・SaaS企業向け
MCP(Model Context Protocol)はAIエージェントと外部サービスを接続する標準プロトコルとして急速に普及している。しかし、導入時に多くの開発者が同じ問題に直面している。
本記事では、KanseiLinkが14ツールのMCP Server運用で蓄積した実践知をもとに、よくある問題TOP 10とその解決策をコード例付きで解説する。
1. MCP導入時のよくある問題 TOP 10
以下は、KanseiLinkのサポートデータと開発者コミュニティのフィードバックから集計した、MCP導入時に最も頻繁に報告される問題のランキングである。
最も報告が多い問題。MCP ServerがOAuth 2.0でSaaSに接続する際、アクセストークンの有効期限が切れているにもかかわらず、リフレッシュ処理が実装されていないケースが頻発。
access_tokenの有効期限(通常1時間)後にリクエストを送信。refresh_tokenによる自動更新ロジックが未実装。
リクエスト前にtoken有効期限を確認し、期限切れの場合は自動でrefresh_tokenを使って更新する。
class TokenManager {
private accessToken: string;
private refreshToken: string;
private expiresAt: number;
async getValidToken(): Promise<string> {
// 有効期限の5分前にリフレッシュ
if (Date.now() >= this.expiresAt - 300000) {
await this.refresh();
}
return this.accessToken;
}
private async refresh(): Promise<void> {
const res = await fetch('https://oauth.example.com/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
client_id: process.env.CLIENT_ID!,
}),
});
const data = await res.json();
this.accessToken = data.access_token;
this.expiresAt = Date.now() + data.expires_in * 1000;
}
}
ブラウザベースのMCPクライアントからHTTP TransportのMCP Serverに接続する際、CORSポリシーによりリクエストがブロックされる。
MCP ServerにCORSヘッダーが設定されていない。ブラウザのプリフライトリクエスト(OPTIONS)が拒否される。
MCP ServerにCORSミドルウェアを追加する。または、サーバーサイドプロキシ経由で接続する。
import cors from 'cors';
app.use(cors({
origin: [
'https://your-app.com',
'http://localhost:3000'
],
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-MCP-Session'
],
credentials: true,
}));
エージェントが高頻度でAPIを呼び出し、Rate Limitに達してリクエストがブロックされる。429エラーが連続し、タスク全体が失敗する。
リトライロジック未実装、Rate Limit値の未確認、バースト的なリクエスト送信。
Exponential Backoff + ヘッダー監視による自動スロットリング。
async function fetchWithRetry(
url: string,
options: RequestInit,
maxRetries = 3
): Promise<Response> {
for (let i = 0; i < maxRetries; i++) {
const res = await fetch(url, options);
if (res.status === 429) {
const retryAfter = res.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.pow(2, i) * 1000; // 1s, 2s, 4s
console.warn(`Rate limited. Retry in ${delay}ms`);
await new Promise(r => setTimeout(r, delay));
continue;
}
return res;
}
throw new Error('Max retries exceeded');
}
ローカル実行にHTTP Transportを使用したり、リモート接続にstdioを使用する設計ミス。パフォーマンスの低下や接続不能の原因になる。
MCP仕様の2つのTransport(stdio / Streamable HTTP)の使い分けを理解していない。
ローカル実行 = stdio、リモート/マルチユーザー = Streamable HTTP。SSEは非推奨(2025仕様)。
// Transport選択の判断基準
//
// Q: MCP Serverはどこで動作する?
// ├─ ローカルマシン上 → stdio Transport
// │ ✓ 低レイテンシ、認証不要、シンプル
// │
// └─ リモートサーバー → Streamable HTTP Transport
// ✓ マルチユーザー対応、ファイアウォール越え
// ✓ セッション管理、認証ヘッダー対応
//
// ⚠️ SSE Transport は 2025年MCP仕様で非推奨
// 新規実装ではStreamable HTTPを使用すること
日本語を含むAPIリクエスト・レスポンスで文字化けが発生。特にWindows環境でのBOM付きUTF-8ファイルが原因となるケースが多い。
UTF-8 BOM(EF BB BF)がJSONパーサーに渡される。Content-Typeにcharsetが未指定。
BOM除去処理の追加、Content-Typeヘッダーへのcharset=utf-8明記。
function removeBOM(text: string): string {
// UTF-8 BOM (EF BB BF) をバイト列で除去
if (text.charCodeAt(0) === 0xFEFF) {
return text.slice(1);
}
return text;
}
// MCP Server側のレスポンスヘッダー
res.setHeader(
'Content-Type',
'application/json; charset=utf-8'
);
MCP ServerのTool定義でZod schemaを使用する際、inputSchemaの型定義ミスにより、クライアントがToolを呼び出せない。
Zodスキーマの定義がJSON Schemaとして正しく変換されない。optionalとrequiredの混在。
zodToJsonSchemaでの変換結果を必ず検証。MCP Inspectorでツール定義をテスト。
import { z } from 'zod';
// 正しい: descriptionを全フィールドに付与
const SearchSchema = z.object({
query: z.string()
.describe('検索キーワード(日本語可)'),
category: z.enum(['crm', 'accounting', 'hr'])
.optional()
.describe('サービスカテゴリで絞り込み'),
limit: z.number()
.min(1).max(50)
.default(10)
.describe('取得件数(1-50, デフォルト10)'),
});
server.tool(
'search_services',
'SaaSサービスをキーワードで検索',
SearchSchema,
async ({ query, category, limit }) => {
// Tool実装
}
);
MCP Serverが大量のデータを返却する際、クライアント側のタイムアウトに達して接続が切断される。
ページネーション未実装で全データを一括返却。レスポンスサイズが数MBに達する。
ページネーション実装 + レスポンスサイズ制限。大量データはcursorベースページネーションが推奨。
// Tool定義にcursorパラメータを追加
const ListSchema = z.object({
cursor: z.string().optional()
.describe('次ページのカーソル'),
limit: z.number().default(20)
.describe('1ページあたりの件数'),
});
// レスポンスに次ページ情報を含める
return {
content: [{
type: 'text',
text: JSON.stringify({
items: results,
nextCursor: hasMore ? lastId : null,
totalCount: total,
}),
}],
};
MCP SDKをアップデートした際に、APIの破壊的変更により既存のMCP Serverが動作しなくなる。
package.jsonで^(キャレット)指定によりマイナーバージョンが自動更新。CHANGELOG未確認。
SDKバージョンを固定(~ではなく正確なバージョン指定)。更新前にCHANGELOGを確認。
{
"dependencies": {
// NG: メジャー互換の範囲で自動更新
"@modelcontextprotocol/sdk": "^1.12.0",
// OK: 正確なバージョンを固定
"@modelcontextprotocol/sdk": "1.12.0"
}
}
MCP ServerのソースコードやクライアントサイドコードにAPI Keyがハードコードされ、GitHubリポジトリ経由で漏洩するケース。
API Keyをソースコードに直接記述。.envファイルをコミット。フロントエンドにAPI Keyを埋め込み。
環境変数管理 + .gitignore設定 + サーバーサイド専用のシークレット管理。
# .gitignore に必ず追加
.env
.env.local
.env.production
# 環境変数で管理(コードに直接書かない)
# .env ファイル
SAAS_API_KEY=sk-xxxxxxxxxxxxx
OAUTH_CLIENT_SECRET=cs-xxxxxxxxxxxxx
# コード側
const apiKey = process.env.SAAS_API_KEY;
if (!apiKey) {
throw new Error('SAAS_API_KEY is not set');
}
MCP Serverのテスト方法がわからず、手動テストのみで品質が安定しない。本番環境で直接テストしてしまうリスク。
MCP専用のテストフレームワークの認知度が低い。SaaS側にsandbox環境がない。
MCP Inspector + ユニットテスト + モックサーバーの3層テスト戦略。
// 1. MCP Inspector でTool定義を検証
// npx @modelcontextprotocol/inspector
// 2. ユニットテスト(Vitest)
import { describe, it, expect } from 'vitest';
describe('search_services tool', () => {
it('returns results for valid query', async () => {
const result = await searchServices({
query: '会計',
limit: 5,
});
expect(result.items.length).toBeLessThanOrEqual(5);
expect(result.items[0]).toHaveProperty('name');
});
});
// 3. モックサーバーで外部API依存を排除
import { setupServer } from 'msw/node';
const mockServer = setupServer(
http.get('https://api.saas.com/v1/*', () => {
return HttpResponse.json({ data: mockData });
})
);
2. SaaS企業がMCP対応する際のチェックリスト
自社SaaSをMCP対応(エージェント対応)する際、以下のチェックリストを確認してください。全項目をクリアすることで、AEOスコアの大幅な改善が期待できます。
-
OpenAPI仕様は最新か
APIの全エンドポイントがOpenAPI 3.0+仕様で文書化され、実際の挙動と一致している。Swagger UIで公開されていることが望ましい。 -
認証フローのドキュメントはあるか
OAuth 2.0の認可エンドポイント、トークンエンドポイント、スコープ一覧、リフレッシュトークンの使い方が明確に文書化されている。 -
エラーレスポンスはJSON形式か
全てのエラーレスポンスが構造化されたJSON形式(error_code, message, details)で返される。HTMLエラーページではなく。 -
Rate Limitはドキュメントに記載されているか
エンドポイントごとのRate Limit値(リクエスト数/時間)が文書化されている。レスポンスヘッダーにX-RateLimit-Remaining等を含む。 -
Sandbox環境は提供されているか
開発者・エージェントがテスト用データで安全にAPIを検証できるsandbox環境が提供されている。 -
APIバージョニングは明確か
URLパスまたはヘッダーでAPIバージョンが指定される。非推奨化のポリシー(最低3ヶ月の猶予期間)が文書化されている。 -
MCP Serverの公式提供または推奨があるか
公式MCP Serverパッケージの提供、またはコミュニティ製MCP Serverの品質検証・推奨を行っている。 -
Webhook/イベント通知はあるか
ポーリング不要でリアルタイム通知を受け取れるWebhookまたはイベントストリームが提供されている。
3. KanseiLink MCPの実装例から学ぶベストプラクティス
KanseiLinkは14のMCP Toolを提供する実運用MCP Serverである。ここでは、その設計から得られたベストプラクティスを共有する。
14ツールの設計パターン
KanseiLinkのMCP Serverは以下の14ツールで構成されている。各ツールは単一責任の原則に基づき、1つのツールが1つの明確な目的を持つ。
// 検索・発見系
search_services // SaaSサービス検索(FTS5全文検索)
get_service_detail // サービス詳細取得
find_combinations // サービス組み合わせ提案
get_recipe // 自動化レシピ取得
// インテリジェンス系
get_insights // AEO分析・市場インサイト
get_service_tips // サービス活用Tips
check_updates // 更新情報チェック
// 品質管理系
get_inspection_queue // 検査キュー取得
submit_inspection // 検査結果提出
report_outcome // 実行結果レポート
// コンテンツ生成系
generate_aeo_report // AEOレポート生成
generate_aeo_article // AEO記事生成
// 設計原則:
// 1. 1ツール = 1目的(Single Responsibility)
// 2. 全ツールに日本語description
// 3. inputSchemaに詳細なdescribe()付与
// 4. エラーはisError: trueで明示的に返却
PIIマスキング
個人情報保護のためのマスキング実装
KanseiLinkのMCP Serverは、全てのレスポンスに対してPII(個人識別情報)マスキングを適用する。メールアドレス、電話番号、クレジットカード番号などは、出力前に自動的にマスクされる。
これにより、エージェントが意図せず個人情報をLLMに送信するリスクを最小化している。MCP Server側でのマスキングは、クライアント側の実装に依存しないため、より堅牢なセキュリティを実現する。
const PII_PATTERNS = [
{ regex: /[\w.-]+@[\w.-]+\.\w+/g, replace: '[EMAIL]' },
{ regex: /\d{3}-\d{4}-\d{4}/g, replace: '[PHONE]' },
{ regex: /\d{4}-\d{4}-\d{4}-\d{4}/g, replace: '[CARD]' },
];
function maskPII(text: string): string {
let masked = text;
for (const { regex, replace } of PII_PATTERNS) {
masked = masked.replaceAll(regex, replace);
}
return masked;
}
// MCP Serverのレスポンス生成時に適用
return {
content: [{
type: 'text',
text: maskPII(JSON.stringify(data)),
}],
};
FTS5全文検索
SQLite FTS5による高速日本語検索
KanseiLinkのsearch_servicesツールは、SQLite FTS5(Full-Text Search 5)エンジンを使用した全文検索を実装している。日本語のトークナイズにはtrigramを使用し、部分一致検索にも対応。
これにより、エージェントが「会計 クラウド」「経費精算 自動化」などの日本語キーワードで高速に検索でき、1,000件以上のサービスデータから10ms以下でヒットする。
-- FTS5仮想テーブルの作成
CREATE VIRTUAL TABLE services_fts USING fts5(
name,
description,
category,
tags,
content='services',
content_rowid='id',
tokenize='trigram'
);
-- 検索クエリ(ランキング付き)
SELECT
s.*,
rank AS relevance
FROM services_fts
JOIN services s ON services_fts.rowid = s.id
WHERE services_fts MATCH '会計 OR クラウド'
ORDER BY rank
LIMIT 10;
エラーハンドリングのパターン
MCP仕様に準拠したエラー返却
KanseiLinkでは、ツール実行時のエラーをMCP仕様のisErrorフラグで明示的に返却する。これにより、LLMがエラーを正しく認識し、ユーザーへの適切なフィードバックやリトライ判断が可能になる。
server.tool(
'get_service_detail',
'サービスの詳細情報を取得',
{ slug: z.string().describe('サービスのslug') },
async ({ slug }) => {
try {
const service = await db.getService(slug);
if (!service) {
return {
content: [{
type: 'text',
text: `サービス "${slug}" は見つかりませんでした。`,
}],
isError: true,
};
}
return {
content: [{
type: 'text',
text: maskPII(JSON.stringify(service)),
}],
};
} catch (error) {
return {
content: [{
type: 'text',
text: `内部エラー: ${error.message}`,
}],
isError: true,
};
}
}
);
MCP対応でお困りですか?
KanseiLinkはSaaS企業のMCP対応・AEOスコア改善を支援しています。
14ツールの実運用知見をベースにした実践的なコンサルティング。