Upgrade to Pro — share decks privately, control downloads, hide ads and more …

フルKotlinで作る! MCPサーバー、AIエージェント、UIまで一気 通貫したAIエージェ...

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →
Avatar for Shuzo Takahashi Shuzo Takahashi
November 01, 2025
3k

フルKotlinで作る! MCPサーバー、AIエージェント、UIまで一気 通貫したAIエージェントシステム

Avatar for Shuzo Takahashi

Shuzo Takahashi

November 01, 2025

Transcript

  1. 今回話すこと Kotlinで以下を全部一気通貫で作る話 MCPサーバー (公式MCP SDK + Ktor) AIエージェント (Koog) UI

    (Compose+Jetpack系ライブラリ) 今回はUIデザイン〜アプリ実装までのプロセスを包括的にサポートする、チャット 形式のAIエージェントを題材にします ドメイン知識、デザインデータやコードを参照し、チャット形式でユーザーの質問 に回答するシステムを例にします 3 / 29
  2. MCPツールのスキーマの実装 Kotlinらしく実装したいので、ピュアなデータクラスでスキーマを表現 それを内部的にプロトコルに準拠したJsonObjectに変換する。 # 例:FigmaからVariable(色などのトークン定義)の情報を取得する # ツールのdescriptionなどはアノテーションで定義できるように実装。 @Serializable data class

    FigmaGetVariablesInput( @ToolParam(description = "FigmaファイルのURL") val figmaUrl: String ) @Serializable data class FigmaGetVariablesOutput( @ToolParam(description = "Figmaファイルの変数情報") val variables: FigmaVariablesResponse, @ToolParam(description = "取得したファイルキー") val fileKey: String, @ToolParam(description = "取得日時") val retrievedAt: String ) MCPサーバーの実装 8 / 29
  3. MCPツールのロジックの実装 MCPツールを表す基底クラス(自分で)を定義し、各ツールはそれを実装する設計 class FigmaGetVariablesTool( private val figmaRepository: FigmaRepository ) :

    MCPTool<FigmaGetVariablesInput, FigmaGetVariablesOutput>() { override val toolSchema = FigmaGetVariablesToolSchema override suspend fun doExecute(params: FigmaGetVariablesInput): Result<FigmaGetVariablesOutput> { return figmaRepository.getFigmaVariablesByUrl(params.figmaUrl).map { variablesResponse -> val fileKey = figmaRepository.extractFileKey(params.figmaUrl) FigmaGetVariablesOutput( variables = variablesResponse, fileKey = fileKey ?: "", retrievedAt = Instant.now().toString() ) } } } MCPサーバーの実装 9 / 29
  4. MCPツールのロジックの実装 # 基底クラスの定義(注:SDKのコードではありません) abstract class MCPTool<TInput : Any, TOutput :

    Any> { protected abstract suspend fun doExecute(params: TInput): Result<TOutput> # 実行結果を取得してCallToolResult(SDKに定義されている)に変換する。 suspend fun execute(request: CallToolRequest): CallToolResult = try { val inputSerializer = serializer(inputClass.java) val params = Json.decodeFromJsonElement(inputSerializer, request.arguments) as TInput // 実装されたロジックを実行 val result = doExecute(params) result.fold( onSuccess = { output -> // 出力をJSONにシリアライズ val outputSerializer = serializer(outputClass.java) val outputJson = Json.encodeToJsonElement(outputSerializer, output) // CallToolResultとして返す CallToolResult( content = listOf(TextContent(outputJson.toString())), isError = false ) }, onFailure = { error -> /*エラーのとき*/} ) } catch (e: SerializationException) { /*エラーハンドリング*/ } } MCPサーバーの実装 10 / 29
  5. MCPツールの登録 // 複数のツールをまとめて登録する関数 private fun registerTools(server: Server, tools: List<MCPTool<*, *>>)

    { tools.forEach { tool -> server.addTool( name = tool.toolSchema.name, description = tool.toolSchema.description, inputSchema = tool.inputSchema, handler = tool::execute ) } } MCPサーバーの実装 11 / 29
  6. 基本的な実装 LLMのモデルや利用可能ツール、ワークフローのグラフなどを設定する AIAgent.run() で実行する val agent = AIAgent( executor =

    simpleGoogleAIExecutor(apiKey), systemPrompt = "あなたはアプリ開発/デザインの専門家です。定められた情報を参照し、ツールを使用してユーザーのリクエストに回答してください。", llmModel = GoogleModels.Gemini2_5Pro // その他必要に応じて設定 ) val result = agent.run("Repositoryの実装方針について教えて") println(result) AIエージェントの実装 16 / 29
  7. AIAgentServiceの実装 AIAgent/AIAgentServiceにはインプットとアウトプットを型で指定できる。 // 例として返答結果を受け取るFlowを定義 val agentResultFlow = MutableSharedFlow<String>() // String以外にも独自のクラスを定義可能

    val agentService: GraphAIAgentService<String, String> = AIAgentService( promptExecutor = simpleGoogleAIExecutor(apiKey), systemPrompt = "あなたはアプリ開発/デザインの専門家です。定められた情報を参照し、ツールを使用してユーザーのリクエストに回答してください。", llmModel = GoogleModels.Gemini2_5Pro // その他設定 ) // ---- 以下、入力の都度実行する --- val agent = agentService.createManagedAgent(id = agentId) // 会話を継続的にするには、同一のIDを使用する launch { val result = agent.run("Repositoryの実装方針について教えて") agentResultFlow.emit(result) } AIエージェントの実装 18 / 29
  8. ツールの実装と登録 AIエージェントが実行可能な機能は「ツール」として実装・登録する。 任意のMCPサーバーのツールも同じ用に登録することができる。 // MCPサーバーと接続してToolRegistryに val httpClient = HttpClient() val

    baseUrl = "http://$host:$port" val transport = SseClientTransport(httpClient, baseUrl) val client = Client( clientInfo = Implementation( name = "mcp-client-name", version = "1.1.0" ) ) client.connect(transport) val toolRegistry = McpToolRegistryProvider.fromClient(mcpClient = client) // AIAgentServiceの作成 AIAgentService( toolRegistry = toolRegistry, // 中略 ) AIエージェントの実装 19 / 29
  9. ストリーミング AIAgentから実行途中のレスポンスをストリーミング(Flow)で受け取ることが できる。 クライアントサイドが結果を受け取る「ツール」を登録しておき、Flowでの受信毎 にツール呼び出しを行うことで、クライアントサイドに応答を伝える。 // node定義の中 llm.writeSession { updatePrompt

    { system("これまで収集した情報に元に、ユーザーのリクエストに正確かつ詳細に回答してください。") } val responseStream = requestLLMStreaming() // ストリームを収集し、各テキストチャンクをリアルタイムで送信します responseStream.collect { frame -> if (frame is StreamFrame.Append && frame.text.isNotEmpty()) { responseBuilder.append(frame.text) // ユーザーに応答結果を送るツールの呼び出し callTool(...) } } } AIエージェントの実装 22 / 29
  10. 会話履歴の永続化 interface PersistenceStorageProviderを継承して、 AgentCheckpointData を保 持する仕組みを実装する。 今回は永続化はKMP対応してるJetpack DataStoreを使用する。 それをAIAgentServiceの作成時にinstallすることで、自動的に保存される。 //

    storageProviderをインスタンス化しておく AIAgentService( // その他設定... installFeatures = { install(Persistence) { storage = storageProvider enableAutomaticPersistence = true rollbackStrategy = RollbackStrategy.MessageHistoryOnly } }, ) AIエージェントの実装 24 / 29
  11. 会話履歴の永続化:ハマったこと 同一のagentIdで会話を再開しようとしても、会話履歴が保持されていないという 状況が発生。 対処法 install時に rollbackStrategy = RollbackStrategy.MessageHistoryOnly にしてお く。

    Defaultだと、会話以外のメタ情報も全て保持するが、以前のセッションでエージ ェントを破棄した(TombStone)状態としてマークされるため、会話履歴も復元 できない。 MessageHistoryOnly にすることで、会話履歴のみを復元し、会話を継続させるこ とができる。 AIエージェントの実装 25 / 29