はじめに
今日は、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の実装に進みましょう!