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?

MCPの活用や応用への考察 - MCPデータの保存とインデックス:効率的なデータ管理戦略

Posted at

💡 はじめに

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が開発した比較的新しいプロトコルです。最新の情報については、公式ドキュメントを参照してください。

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?