💡 はじめに
Model Context Protocol (MCP) は、AIエージェントと各種データソース間の標準化された通信プロトコルです。MCPサーバーは、データベース、API、ファイルシステムなど、様々なデータソースへのアクセスを提供します。
しかし、MCPサーバーを本番環境で効率的に運用するには、適切なデータ保存戦略とインデックス管理が不可欠です。本記事では、MCPサーバーにおけるデータ管理のベストプラクティスを、実装例とともに解説します。
1. MCPサーバーのデータ管理アーキテクチャ
MCPサーバーは、以下の3層構造でデータを管理します:
┌─────────────────────────────────────┐
│ AIエージェント (Claude等) │
└────────────┬────────────────────────┘
│ MCP Protocol
▼
┌─────────────────────────────────────┐
│ MCPサーバー │
│ ┌───────────────────────────────┐ │
│ │ キャッシュ層 (Redis/Memory) │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ インデックス層 (SQLite/PG) │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ データソース接続層 │ │
│ └───────────────────────────────┘ │
└────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 実データソース │
│ (DB, API, FileSystem, etc.) │
└─────────────────────────────────────┘
2. データ保存戦略
2.1. リソースのキャッシング
MCPサーバーは、頻繁にアクセスされるリソースをキャッシュすることで、レスポンス速度を大幅に改善できます。
実装例(TypeScript):
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import LRU from "lru-cache";
// LRUキャッシュの設定
const resourceCache = new LRU<string, string>({
max: 500, // 最大500エントリ
ttl: 1000 * 60 * 15, // 15分のTTL
updateAgeOnGet: true,
});
const server = new Server(
{
name: "cached-resource-server",
version: "1.0.0",
},
{
capabilities: {
resources: {},
},
}
);
server.setRequestHandler("resources/read", async (request) => {
const uri = request.params.uri;
// キャッシュヒット確認
const cached = resourceCache.get(uri);
if (cached) {
return {
contents: [{
uri,
mimeType: "text/plain",
text: cached,
}],
};
}
// キャッシュミス: 実データソースから取得
const data = await fetchFromDataSource(uri);
resourceCache.set(uri, data);
return {
contents: [{
uri,
mimeType: "text/plain",
text: data,
}],
};
});
キャッシング戦略の選択:
データの特性 | 推奨キャッシュ戦略 | TTL |
---|---|---|
静的コンテンツ | メモリキャッシュ | 1時間〜1日 |
半静的データ | Redis | 15分〜1時間 |
リアルタイムデータ | キャッシュなし or 短TTL | 1分以下 |
大容量ファイル | ディスクキャッシュ | 用途による |
2.2. データの永続化
MCPサーバーが管理するメタデータ(リソースのインデックス、アクセスログ等)は、適切なデータベースに永続化する必要があります。
SQLiteによる実装例:
import Database from "better-sqlite3";
class MCPDataStore {
private db: Database.Database;
constructor(dbPath: string) {
this.db = new Database(dbPath);
this.initTables();
}
private initTables() {
// リソースメタデータテーブル
this.db.exec(`
CREATE TABLE IF NOT EXISTS resources (
uri TEXT PRIMARY KEY,
mime_type TEXT NOT NULL,
title TEXT,
description TEXT,
last_modified INTEGER NOT NULL,
size INTEGER,
tags TEXT,
indexed_at INTEGER NOT NULL
)
`);
// 全文検索用のFTSテーブル
this.db.exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS resources_fts
USING fts5(uri, title, description, tags, content)
`);
// アクセスログテーブル
this.db.exec(`
CREATE TABLE IF NOT EXISTS access_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
resource_uri TEXT NOT NULL,
accessed_at INTEGER NOT NULL,
session_id TEXT,
operation TEXT NOT NULL,
FOREIGN KEY (resource_uri) REFERENCES resources(uri)
)
`);
// インデックス作成
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_logs_uri
ON access_logs(resource_uri, accessed_at DESC)
`);
}
// リソースの登録
indexResource(resource: {
uri: string;
mimeType: string;
title?: string;
description?: string;
size?: number;
tags?: string[];
}) {
const stmt = this.db.prepare(`
INSERT OR REPLACE INTO resources
(uri, mime_type, title, description, size, tags,
last_modified, indexed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(
resource.uri,
resource.mimeType,
resource.title || null,
resource.description || null,
resource.size || null,
resource.tags ? JSON.stringify(resource.tags) : null,
Date.now(),
Date.now()
);
}
// アクセスログの記録
logAccess(uri: string, operation: string, sessionId?: string) {
const stmt = this.db.prepare(`
INSERT INTO access_logs (resource_uri, accessed_at, session_id, operation)
VALUES (?, ?, ?, ?)
`);
stmt.run(uri, Date.now(), sessionId || null, operation);
}
}
3. インデックス管理戦略
3.1. リソースの全文検索
大量のリソースを持つMCPサーバーでは、効率的な検索機能が必須です。
FTS5を使用した全文検索:
class ResourceSearchEngine {
private db: Database.Database;
constructor(db: Database.Database) {
this.db = db;
}
// リソースのインデックス化
indexResourceContent(uri: string, content: string) {
const resource = this.getResourceMetadata(uri);
const stmt = this.db.prepare(`
INSERT INTO resources_fts (uri, title, description, tags, content)
VALUES (?, ?, ?, ?, ?)
`);
stmt.run(
uri,
resource.title || "",
resource.description || "",
resource.tags || "",
content
);
}
// 全文検索
search(query: string, limit: number = 10): Array<{
uri: string;
rank: number;
snippet: string;
}> {
const stmt = this.db.prepare(`
SELECT
uri,
rank,
snippet(resources_fts, 4, '<mark>', '</mark>', '...', 64) as snippet
FROM resources_fts
WHERE resources_fts MATCH ?
ORDER BY rank
LIMIT ?
`);
return stmt.all(query, limit) as any;
}
// タグベースのフィルタリング
searchByTags(tags: string[]): string[] {
const placeholders = tags.map(() => '?').join(',');
const stmt = this.db.prepare(`
SELECT uri FROM resources
WHERE json_each(tags) IN (${placeholders})
GROUP BY uri
HAVING COUNT(DISTINCT json_each.value) = ?
`);
const rows = stmt.all(...tags, tags.length);
return rows.map((r: any) => r.uri);
}
}
3.2. プロンプトのインデックス化
MCPサーバーが提供するプロンプトも、効率的に検索できるようインデックス化します。
server.setRequestHandler("prompts/list", async () => {
const stmt = dataStore.db.prepare(`
SELECT name, description, arguments
FROM prompts
WHERE active = 1
ORDER BY usage_count DESC, name ASC
`);
const prompts = stmt.all().map((p: any) => ({
name: p.name,
description: p.description,
arguments: JSON.parse(p.arguments || "[]"),
}));
return { prompts };
});
server.setRequestHandler("prompts/get", async (request) => {
const { name, arguments: args } = request.params;
// 使用回数をインクリメント
dataStore.db.prepare(`
UPDATE prompts SET usage_count = usage_count + 1
WHERE name = ?
`).run(name);
// プロンプトテンプレートを取得・レンダリング
const prompt = renderPromptTemplate(name, args);
return {
messages: prompt.messages,
};
});
4. パフォーマンス最適化
4.1. バッチ処理とプリフェッチング
複数のリソースに順次アクセスすることが予想される場合、バッチ取得とプリフェッチングが有効です。
class BatchResourceLoader {
private pending: Map<string, Promise<any>> = new Map();
async load(uri: string): Promise<any> {
// 既存のリクエストがあればそれを返す(deduplication)
if (this.pending.has(uri)) {
return this.pending.get(uri)!;
}
const promise = this.fetchResource(uri);
this.pending.set(uri, promise);
// 完了後にマップから削除
promise.finally(() => this.pending.delete(uri));
return promise;
}
async loadMany(uris: string[]): Promise<Map<string, any>> {
const results = await Promise.all(
uris.map(uri => this.load(uri).catch(err => ({ error: err })))
);
return new Map(uris.map((uri, i) => [uri, results[i]]));
}
private async fetchResource(uri: string): Promise<any> {
// 実装...
}
}
4.2. 段階的なインデックス構築
大量のリソースを持つサーバーでは、起動時の全インデックス再構築は避け、段階的に更新します。
class IncrementalIndexer {
private indexQueue: string[] = [];
private isIndexing: boolean = false;
queueForIndexing(uri: string) {
if (!this.indexQueue.includes(uri)) {
this.indexQueue.push(uri);
}
this.startIndexing();
}
private async startIndexing() {
if (this.isIndexing) return;
this.isIndexing = true;
while (this.indexQueue.length > 0) {
const uri = this.indexQueue.shift()!;
try {
await this.indexResource(uri);
} catch (error) {
console.error(`Failed to index ${uri}:`, error);
}
// CPU負荷を抑えるため、少し待機
await new Promise(resolve => setTimeout(resolve, 100));
}
this.isIndexing = false;
}
private async indexResource(uri: string) {
// リソースの内容を取得してインデックス化
const content = await fetchResourceContent(uri);
searchEngine.indexResourceContent(uri, content);
}
}
5. 監視とメンテナンス
5.1. アクセスパターンの分析
class AnalyticsCollector {
getPopularResources(limit: number = 10) {
return dataStore.db.prepare(`
SELECT
resource_uri,
COUNT(*) as access_count,
MAX(accessed_at) as last_accessed
FROM access_logs
WHERE accessed_at > ?
GROUP BY resource_uri
ORDER BY access_count DESC
LIMIT ?
`).all(Date.now() - 7 * 24 * 60 * 60 * 1000, limit);
}
getCacheHitRate(): number {
const stats = resourceCache.info();
return stats.hits / (stats.hits + stats.misses);
}
}
5.2. 定期メンテナンスタスク
// 古いログの削除
setInterval(() => {
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30日前
dataStore.db.prepare(`
DELETE FROM access_logs WHERE accessed_at < ?
`).run(cutoff);
}, 24 * 60 * 60 * 1000); // 毎日実行
// データベースの最適化
setInterval(() => {
dataStore.db.prepare(`VACUUM`).run();
dataStore.db.prepare(`ANALYZE`).run();
}, 7 * 24 * 60 * 60 * 1000); // 週次実行
6. ベストプラクティスまとめ
データ保存
- ✅ 適切なキャッシング戦略でレスポンス速度を改善
- ✅ メタデータは構造化データベースで管理
- ✅ 大容量ファイルは参照のみ保持し、実体は元のストレージに
インデックス
- ✅ 全文検索にはFTS5などの専用エンジンを活用
- ✅ 頻繁に検索される属性には適切なインデックスを設定
- ✅ 段階的なインデックス構築で初期起動時間を短縮
パフォーマンス
- ✅ バッチ処理で複数リソースの取得を効率化
- ✅ プリフェッチングで予測可能なアクセスに対応
- ✅ 定期的なメンテナンスでデータベースを最適化
監視
- ✅ アクセスログを記録して利用パターンを分析
- ✅ キャッシュヒット率を監視して設定を調整
- ✅ インデックスの効果を定期的に評価
まとめ
MCPサーバーの効率的な運用には、適切なデータ管理戦略が不可欠です。キャッシング、インデックス化、バッチ処理などの技術を適切に組み合わせることで、大規模なデータソースに対しても高速で信頼性の高いアクセスを実現できます。
本記事で紹介した実装パターンを参考に、あなたのユースケースに最適なMCPサーバーを構築してください。
注意: MCPはAnthropicが開発した比較的新しいプロトコルです。最新の情報については、公式ドキュメントを参照してください。