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アプリケーションを効率的に構築するには、どのデータをMCPサーバー側で管理し、どのデータをクライアント(AIエージェント)側で処理するかを適切に設計することが重要です。

本記事では、MCPにおける実践的なデータ管理戦略と、パフォーマンスと保守性を両立させる設計パターンを解説します。

1. MCPのデータ管理における2つのレイヤー

MCPアーキテクチャでは、データ管理が2つの明確なレイヤーに分かれています:

┌────────────────────────────────────────┐
│  クライアント側 (AIエージェント)         │
│  ┌──────────────────────────────────┐  │
│  │ セッションコンテキスト           │  │
│  │ - 会話履歴                       │  │
│  │ - 一時的なステート               │  │
│  │ - クライアントキャッシュ         │  │
│  └──────────────────────────────────┘  │
└────────────┬───────────────────────────┘
             │ MCP Protocol (JSON-RPC)
             ▼
┌────────────────────────────────────────┐
│  サーバー側 (MCPサーバー)               │
│  ┌──────────────────────────────────┐  │
│  │ 永続データ管理                   │  │
│  │ - リソースのインデックス         │  │
│  │ - プロンプトテンプレート         │  │
│  │ - ツールの実装とステート         │  │
│  │ - データソース接続               │  │
│  └──────────────────────────────────┘  │
└────────────┬───────────────────────────┘
             │
             ▼
┌────────────────────────────────────────┐
│  実データソース                         │
│  (Database, API, FileSystem, etc.)     │
└────────────────────────────────────────┘

2. サーバー側データ管理:永続性と信頼性

2.1. サーバー側で管理すべきデータ

MCPサーバーは以下のようなデータを管理する責任があります:

データタイプ 役割と目的 実装例
リソース定義 利用可能なデータリソースのメタデータとアクセス方法 データベーステーブル定義、APIエンドポイント情報
プロンプトテンプレート 再利用可能なプロンプトの定義とパラメータ テンプレートエンジンによる動的生成
ツールの実装 AIエージェントが呼び出せる機能の実装 ビジネスロジック、外部API連携
認証情報 データソースへのアクセスに必要な認証情報 暗号化されたAPIキー、データベース接続情報
アクセス制御 リソースやツールへのアクセス権限 ユーザーロール、リソースレベルのパーミッション
監査ログ リソースアクセスやツール呼び出しの履歴 構造化ログ、タイムスタンプ付きイベント

2.2. サーバー側実装パターン

例: リソース管理の実装

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { z } from "zod";

class ResourceManager {
  private resources: Map<string, ResourceDefinition> = new Map();
  private accessLog: AccessLog[] = [];
  
  // リソース定義を登録
  registerResource(definition: ResourceDefinition) {
    this.resources.set(definition.uri, definition);
  }
  
  // リソースの一覧を取得
  listResources(filter?: ResourceFilter): ResourceInfo[] {
    return Array.from(this.resources.values())
      .filter(r => this.matchesFilter(r, filter))
      .map(r => ({
        uri: r.uri,
        name: r.name,
        mimeType: r.mimeType,
        description: r.description,
      }));
  }
  
  // リソースの内容を取得(アクセス制御付き)
  async readResource(
    uri: string,
    context: AccessContext
  ): Promise<ResourceContent> {
    const resource = this.resources.get(uri);
    if (!resource) {
      throw new Error(`Resource not found: ${uri}`);
    }
    
    // アクセス制御チェック
    if (!this.checkAccess(resource, context)) {
      throw new Error(`Access denied to resource: ${uri}`);
    }
    
    // 監査ログに記録
    this.logAccess({
      uri,
      timestamp: Date.now(),
      user: context.userId,
      operation: "read",
    });
    
    // リソースの内容を取得
    return await resource.fetch();
  }
  
  private checkAccess(
    resource: ResourceDefinition,
    context: AccessContext
  ): boolean {
    // アクセス制御ロジック
    if (resource.permissions.public) {
      return true;
    }
    
    return resource.permissions.allowedUsers?.includes(context.userId) 
      ?? false;
  }
  
  private logAccess(log: AccessLog) {
    this.accessLog.push(log);
    
    // 永続化(データベースへの書き込み等)
    this.persistLog(log);
  }
}

2.3. ステートフルなツールの管理

サーバー側でステートを管理する必要があるツールの例:

class StatefulToolManager {
  private sessions: Map<string, ToolSession> = new Map();
  
  async executeTool(
    toolName: string,
    args: any,
    sessionId: string
  ): Promise<any> {
    // セッションの取得または作成
    let session = this.sessions.get(sessionId);
    if (!session) {
      session = this.createSession(sessionId);
      this.sessions.set(sessionId, session);
    }
    
    // ツールの実行(セッションステートを利用)
    const result = await this.executeWithState(
      toolName,
      args,
      session
    );
    
    // セッションステートを更新
    session.lastAccess = Date.now();
    
    return result;
  }
  
  private createSession(sessionId: string): ToolSession {
    return {
      id: sessionId,
      createdAt: Date.now(),
      lastAccess: Date.now(),
      state: {},
    };
  }
  
  // 例: データベーストランザクションの管理
  private async executeWithState(
    toolName: string,
    args: any,
    session: ToolSession
  ): Promise<any> {
    switch (toolName) {
      case "db_transaction_begin":
        session.state.transaction = await this.db.beginTransaction();
        return { success: true };
        
      case "db_transaction_commit":
        if (session.state.transaction) {
          await session.state.transaction.commit();
          delete session.state.transaction;
        }
        return { success: true };
        
      case "db_query":
        // トランザクション内でクエリを実行
        const tx = session.state.transaction || this.db;
        return await tx.query(args.sql, args.params);
        
      default:
        throw new Error(`Unknown tool: ${toolName}`);
    }
  }
  
  // セッションのクリーンアップ
  cleanupSessions(maxAge: number = 3600000) {
    const now = Date.now();
    for (const [id, session] of this.sessions.entries()) {
      if (now - session.lastAccess > maxAge) {
        this.sessions.delete(id);
      }
    }
  }
}

3. クライアント側データ管理:効率性と応答性

3.1. クライアント側で管理すべきデータ

AIエージェント(クライアント)側では、以下のデータを管理します:

データタイプ 役割と目的 実装の考慮点
会話コンテキスト 現在の会話の履歴とステート メモリ効率、コンテキストウィンドウの制限
一時キャッシュ 頻繁にアクセスされるリソースのキャッシュ TTL、キャッシュ無効化戦略
ユーザー設定 ユーザー固有の設定や好み ローカルストレージ、プライバシー保護
UIステート アプリケーションのUI状態 リアクティビティ、状態管理

3.2. クライアント側実装パターン

例: リソースのクライアント側キャッシング

import { Client } from "@modelcontextprotocol/sdk/client/index.js";

class CachedMCPClient {
  private client: Client;
  private cache: Map<string, CacheEntry> = new Map();
  private cacheTTL: number = 300000; // 5分
  
  constructor(client: Client) {
    this.client = client;
  }
  
  async readResource(uri: string, useCache: boolean = true): Promise<any> {
    // キャッシュチェック
    if (useCache) {
      const cached = this.cache.get(uri);
      if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
        return cached.data;
      }
    }
    
    // サーバーから取得
    const response = await this.client.request({
      method: "resources/read",
      params: { uri },
    });
    
    // キャッシュに保存
    this.cache.set(uri, {
      data: response.contents,
      timestamp: Date.now(),
    });
    
    return response.contents;
  }
  
  // キャッシュの無効化
  invalidateCache(uri?: string) {
    if (uri) {
      this.cache.delete(uri);
    } else {
      this.cache.clear();
    }
  }
  
  // プリフェッチング
  async prefetchResources(uris: string[]) {
    await Promise.all(
      uris.map(uri => this.readResource(uri, false))
    );
  }
}

4. ハイブリッド戦略による最適なデータフロー

効率的なMCPアプリケーションは、両レイヤーの責任を明確に分離しながら、適切に連携させます。

4.1. シナリオ別のデータフロー

シナリオ1: 頻繁にアクセスされるリソース

1. クライアント: キャッシュをチェック
   ├─ ヒット → キャッシュから返す
   └─ ミス → サーバーにリクエスト
2. サーバー: リソースを取得(アクセス制御あり)
3. サーバー: アクセスログを記録
4. クライアント: レスポンスをキャッシュ

シナリオ2: ステートフルなツール実行

1. クライアント: ツール呼び出しをリクエスト
2. サーバー: セッションを取得/作成
3. サーバー: セッションステートを使用してツール実行
4. サーバー: セッションステートを更新
5. サーバー: 結果を返す
6. クライアント: UIを更新

シナリオ3: 大量データの処理

1. クライアント: 処理をサーバーに委譲
2. サーバー: バックグラウンドジョブを開始
3. サーバー: 進捗をイベントで通知(サブスクリプション)
4. クライアント: 進捗をUIに表示
5. サーバー: 完了時に結果を通知
6. クライアント: 結果を取得・表示

4.2. 実装例: リアルタイム進捗通知

// サーバー側
class LongRunningTaskServer {
  async handleToolCall(
    toolName: string,
    args: any,
    sessionId: string,
    progressCallback: (progress: number) => void
  ): Promise<any> {
    if (toolName === "process_large_dataset") {
      const totalItems = args.items.length;
      const results = [];
      
      for (let i = 0; i < totalItems; i++) {
        // 処理
        const result = await this.processItem(args.items[i]);
        results.push(result);
        
        // 進捗を通知
        const progress = ((i + 1) / totalItems) * 100;
        progressCallback(progress);
      }
      
      return { results };
    }
  }
}

// クライアント側
class ProgressTrackingClient {
  async executeWithProgress(
    toolName: string,
    args: any,
    onProgress: (progress: number) => void
  ): Promise<any> {
    // WebSocketやServer-Sent Eventsで進捗を受信
    const progressStream = this.subscribeToProgress();
    
    progressStream.on("progress", (data) => {
      onProgress(data.progress);
    });
    
    const result = await this.client.request({
      method: "tools/call",
      params: { name: toolName, arguments: args },
    });
    
    progressStream.close();
    return result;
  }
}

5. パフォーマンス最適化戦略

5.1. キャッシング戦略の比較

戦略 適用場面 メリット デメリット
クライアント側キャッシュ 頻繁にアクセスされる小さなリソース レイテンシ最小、サーバー負荷軽減 一貫性の管理が必要
サーバー側キャッシュ 計算コストの高いクエリ 複数クライアント間で共有可能 キャッシュ無効化の複雑さ
CDNキャッシュ 静的な公開リソース グローバルな高速配信 更新の遅延

5.2. レイテンシ削減のテクニック

class OptimizedMCPClient {
  // バッチリクエスト
  async batchRead(uris: string[]): Promise<Map<string, any>> {
    // 複数のリソースを1回のRPCで取得
    const responses = await this.client.request({
      method: "resources/read_batch",
      params: { uris },
    });
    
    return new Map(
      responses.results.map((r: any) => [r.uri, r.contents])
    );
  }
  
  // 投機的プリフェッチング
  async speculativePrefetch(context: ConversationContext) {
    // 会話の文脈から次に必要になりそうなリソースを予測
    const likelyUris = this.predictNextResources(context);
    
    // バックグラウンドでプリフェッチ
    this.prefetchResources(likelyUris);
  }
  
  // 並列リクエスト
  async parallelToolCalls(tools: ToolCall[]): Promise<any[]> {
    return await Promise.all(
      tools.map(tool => 
        this.client.request({
          method: "tools/call",
          params: tool,
        })
      )
    );
  }
}

6. セキュリティとプライバシーの考慮

6.1. データの保護レイヤー

レイヤー 保護対象 実装方法
サーバー側 認証情報、アクセス制御、監査ログ 暗号化、RBAC、セキュアなストレージ
通信層 MCP通信の内容 TLS/SSL、トークンベース認証
クライアント側 ユーザー設定、キャッシュデータ ローカル暗号化、メモリ保護

6.2. 実装例: セキュアなリソースアクセス

class SecureMCPServer {
  async handleResourceRead(
    uri: string,
    authToken: string
  ): Promise<any> {
    // 認証トークンの検証
    const user = await this.authenticateToken(authToken);
    if (!user) {
      throw new Error("Authentication failed");
    }
    
    // アクセス権限の確認
    const hasAccess = await this.checkPermission(user, uri, "read");
    if (!hasAccess) {
      // 監査ログに記録
      await this.logAccessDenied(user, uri);
      throw new Error("Access denied");
    }
    
    // リソースの取得
    const content = await this.fetchResource(uri);
    
    // 成功を監査ログに記録
    await this.logAccessSuccess(user, uri);
    
    return content;
  }
}

7. ベストプラクティスまとめ

サーバー側の責任

  • ✅ 永続データの管理と整合性の保証
  • ✅ アクセス制御と認証の実装
  • ✅ 監査ログの記録
  • ✅ ビジネスロジックの実装

クライアント側の責任

  • ✅ ユーザーエクスペリエンスの最適化
  • ✅ 一時的なキャッシングによる高速化
  • ✅ UIステートの管理
  • ✅ エラーハンドリングとリトライ

データフローの設計

  • ✅ 頻繁にアクセスされるデータはキャッシュ
  • ✅ 重い処理はサーバー側で実行
  • ✅ セキュリティが重要なデータはサーバー側で保護
  • ✅ レスポンシブな体験のための非同期処理

まとめ

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?