1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Android開発30日間マスターシリーズ - Day22: MCP(Model Context Protocol)の理解 - LLMとの効率的な連携基盤

Posted at

はじめに

今日は、LLM(大規模言語モデル)をアプリに組み込む上で、重要な概念であるMCP (Model Context Protocol) について正しく理解し、LLMとの効率的な連携方法を解説します。

1. LLM連携における課題

LLMは強力なツールですが、直接APIを叩くだけでは、以下のような課題に直面することがあります。

  • 応答時間の遅延: LLMの応答生成には時間がかかり、UIが固まってしまう可能性があります。
  • APIの多様性: LLMサービスごとにAPI仕様や認証方法が異なり、複数のサービスを利用する際に開発が複雑になります。
  • トークン管理とコスト最適化: リクエストのトークン数やコンテキストの管理が煩雑で、コストを意識した実装が必要になります。
  • 外部ツールとの連携: LLMが外部のデータベース、API、ファイルシステムにアクセスする必要がある場合の複雑さ。

これらの課題を解決し、よりスムーズなLLM連携を実現するのが**MCP(Model Context Protocol)**です。

2. MCP(Model Context Protocol)とは?

MCPは、Anthropic社が開発した、LLMアプリケーションと外部データソース・ツールを安全かつ標準化された方法で接続するためのオープンプロトコルです。

MCPの主な特徴

  • 標準化されたプロトコル: LLMクライアント(Claude Desktop、IDEなど)とMCPサーバー間の通信を標準化
  • リソース管理: ファイル、データベース、APIエンドポイントなどのリソースへの安全なアクセス
  • ツール提供: LLMが実行可能な関数(ツール)の定義と実行
  • プロンプト管理: 再利用可能なプロンプトテンプレートの提供
  • セキュリティ: 明示的な同意に基づくリソースアクセス制御

MCPアーキテクチャ

LLMクライアント ←→ MCP Protocol ←→ MCPサーバー ←→ 外部リソース
(Claude Desktop等)                    (ファイル, DB, API等)

3. MCPサーバーの実装例

MCPサーバーは、特定のリソースやツールへのアクセスを提供するコンポーネントです。

シンプルなファイルシステムMCPサーバー(Node.js)

#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import fs from 'fs/promises';
import path from 'path';

const server = new Server(
  {
    name: "filesystem-server",
    version: "0.1.0",
  },
  {
    capabilities: {
      resources: {},
      tools: {},
    },
  }
);

// ファイル読み取りツールの実装
server.setRequestHandler('tools/call', async (request) => {
  const { name, arguments: args } = request.params;
  
  if (name === 'read_file') {
    try {
      const content = await fs.readFile(args.path, 'utf-8');
      return {
        content: [
          {
            type: 'text',
            text: content
          }
        ]
      };
    } catch (error) {
      throw new Error(`Failed to read file: ${error.message}`);
    }
  }
  
  throw new Error(`Unknown tool: ${name}`);
});

// 利用可能なツール一覧
server.setRequestHandler('tools/list', async () => {
  return {
    tools: [
      {
        name: 'read_file',
        description: 'Read the contents of a file',
        inputSchema: {
          type: 'object',
          properties: {
            path: {
              type: 'string',
              description: 'Path to the file to read'
            }
          },
          required: ['path']
        }
      }
    ]
  };
});

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('Filesystem MCP server running on stdio');
}

main().catch(console.error);

4. ストリーミング応答の仕組み

LLMの応答は長いテキストになることが多く、すべての応答が生成されるまで待っているとユーザー体験が悪化します。これを解決するのが「ストリーミング応答」です。

ストリーミング応答では、LLMは生成したテキストをトークン単位で逐次的に送信します。

Server-Sent Events (SSE) を使用した実装例

// Retrofit2サービスインターフェース
@POST("/v1/chat/completions")
@Headers("Accept: text/event-stream")
fun streamCompletion(@Body requestBody: ChatCompletionRequest): Call<ResponseBody>

// ストリーミング処理
class LLMStreamingService {
    fun streamChatCompletion(
        message: String,
        onToken: (String) -> Unit,
        onComplete: () -> Unit,
        onError: (Throwable) -> Unit
    ) {
        val request = ChatCompletionRequest(
            messages = listOf(Message("user", message)),
            stream = true
        )
        
        val call = apiService.streamCompletion(request)
        call.enqueue(object : Callback<ResponseBody> {
            override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
                response.body()?.let { responseBody ->
                    try {
                        parseSSEStream(responseBody.byteStream(), onToken, onComplete)
                    } catch (e: Exception) {
                        onError(e)
                    }
                }
            }
            
            override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
                onError(t)
            }
        })
    }
    
    private fun parseSSEStream(
        inputStream: InputStream,
        onToken: (String) -> Unit,
        onComplete: () -> Unit
    ) {
        val reader = BufferedReader(InputStreamReader(inputStream, "UTF-8"))
        var line: String?
        
        while (reader.readLine().also { line = it } != null) {
            if (line?.startsWith("data: ") == true) {
                val jsonData = line.substring(6)
                if (jsonData == "[DONE]") {
                    onComplete()
                    break
                }
                
                try {
                    val response = Gson().fromJson(jsonData, StreamingResponse::class.java)
                    response.choices.firstOrNull()?.delta?.content?.let { content ->
                        onToken(content)
                    }
                } catch (e: Exception) {
                    // JSONパースエラーをログに記録
                    Log.w("Streaming", "Failed to parse: $jsonData", e)
                }
            }
        }
    }
}

data class StreamingResponse(
    val choices: List<StreamingChoice>
)

data class StreamingChoice(
    val delta: Delta
)

data class Delta(
    val content: String?
)

5. Coroutinesを活用した非同期ストリーミング

Kotlin Coroutinesを使用することで、よりエレガントなストリーミング実装が可能です。

class LLMRepository {
    suspend fun streamChatCompletion(message: String): Flow<String> = flow {
        val request = ChatCompletionRequest(
            messages = listOf(Message("user", message)),
            stream = true
        )
        
        val response = apiService.streamCompletionSuspend(request)
        
        response.body()?.let { responseBody ->
            val reader = BufferedReader(InputStreamReader(responseBody.byteStream()))
            var line: String?
            
            while (reader.readLine().also { line = it } != null) {
                if (line?.startsWith("data: ") == true) {
                    val jsonData = line.substring(6)
                    if (jsonData == "[DONE]") break
                    
                    try {
                        val streamResponse = Gson().fromJson(jsonData, StreamingResponse::class.java)
                        streamResponse.choices.firstOrNull()?.delta?.content?.let { content ->
                            emit(content)
                        }
                    } catch (e: Exception) {
                        // エラーハンドリング
                    }
                }
            }
        }
    }.flowOn(Dispatchers.IO)
}

// ViewModelでの使用例
class ChatViewModel : ViewModel() {
    fun sendMessage(message: String) {
        viewModelScope.launch {
            repository.streamChatCompletion(message)
                .collect { token ->
                    // UIを更新
                    _chatResponse.value += token
                }
        }
    }
}

まとめ

  • **MCP(Model Context Protocol)**は、LLMと外部リソースを安全かつ標準化された方法で接続するためのオープンプロトコルです。
  • ストリーミング応答は、LLMの応答をリアルタイムに表示し、ユーザー体験を大幅に向上させます。
  • Androidアプリでは、Retrofit2とCoroutines/Flowを組み合わせることで、効率的なストリーミング通信を実装できます。
  • MCPサーバーを実装することで、LLMが様々な外部リソースにアクセスできるようになります。

次回は、このMCPの概念を活用し、実際のLLM向けチャットUIの実装に進みましょう!

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?