Slide 1

Slide 1 text

フルKotlinで作る! MCPサーバー、AIエージェント、UIまで一気 通貫したAIエージェントシステム Shuzo Takahashi(しゅーぞー) 1 / 29

Slide 2

Slide 2 text

自己紹介 高橋 柊蔵(しゅーぞー) LINEヤフー株式会社所属 2020年新卒入社 Androidアプリエンジニア&UIデザイナー X: shuzo_create 2 / 29

Slide 3

Slide 3 text

今回話すこと Kotlinで以下を全部一気通貫で作る話 MCPサーバー (公式MCP SDK + Ktor) AIエージェント (Koog) UI (Compose+Jetpack系ライブラリ) 今回はUIデザイン〜アプリ実装までのプロセスを包括的にサポートする、チャット 形式のAIエージェントを題材にします ドメイン知識、デザインデータやコードを参照し、チャット形式でユーザーの質問 に回答するシステムを例にします 3 / 29

Slide 4

Slide 4 text

MCPサーバーの実装 MCPサーバーの実装 4 / 29

Slide 5

Slide 5 text

Kotlin MCP SDK MCP公式でKotlinのSDKが提供されている。 https://github.com/modelcontextprotocol/kotlin-sdk MCPサーバーの実装 5 / 29

Slide 6

Slide 6 text

MCPサーバーの設計 MCPサーバーは個別の機能を「ツール」として提供する MCPツールはステートレスで冪等かつ不透明な副作用がないようにする アプリ実装で言うとUseCaseに近い ツールはLLMが呼び出して使うため、解釈容易性が高く「LLMにとって」わかりや すいIFにする MCPサーバーの実装 6 / 29

Slide 7

Slide 7 text

MCPツールのスキーマの実装 MCPツールの定義で、インプットとアウトプットのスキーマを定義する SDKではJsonObjectでスキーマ定義を受け取る用になっていて非常に扱いづらい # SDKのインプットの定義 @Serializable public data class Input( val properties: JsonObject = EmptyJsonObject, val required: List? = null, ) MCPサーバーの実装 7 / 29

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

MCPツールのロジックの実装 MCPツールを表す基底クラス(自分で)を定義し、各ツールはそれを実装する設計 class FigmaGetVariablesTool( private val figmaRepository: FigmaRepository ) : MCPTool() { override val toolSchema = FigmaGetVariablesToolSchema override suspend fun doExecute(params: FigmaGetVariablesInput): Result { 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

Slide 10

Slide 10 text

MCPツールのロジックの実装 # 基底クラスの定義(注:SDKのコードではありません) abstract class MCPTool { protected abstract suspend fun doExecute(params: TInput): Result # 実行結果を取得して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

Slide 11

Slide 11 text

MCPツールの登録 // 複数のツールをまとめて登録する関数 private fun registerTools(server: Server, tools: List>) { tools.forEach { tool -> server.addTool( name = tool.toolSchema.name, description = tool.toolSchema.description, inputSchema = tool.inputSchema, handler = tool::execute ) } } MCPサーバーの実装 11 / 29

Slide 12

Slide 12 text

MCPツールの公開 MCPサーバーとクライアントが疎通するプロトコル STDIO:サーバーとクライアント同一マシン内で起動してプロセス通信する SSE:サーバーからクライアントへ一方的にデータをプッシュ通信するプロトコル Streamable HTTP:HTTPレスポンスを分割して順次送信する方式 現状では、公式SDKのサーバーサイド実装はSTDIOとSSEにしか対応していない。 SSEはKtorで非常に楽に実装できる。 MCPサーバーの実装 12 / 29

Slide 13

Slide 13 text

MCPサーバー実装のまとめ この一連の実装でMCPクライアント(後述するKoogでのAIエージェントや Calude Desktop、Clineなど)から接続することができるようになる。 その他所感 正直、思ったよりきれいに実装できない(JsonObjectを扱う必要があったり) Kotlinらしくスマートに安全に実装するために、自分でそのあたりを隠蔽する設計 が必要になる。 公式SDKがStreamableHTTPをサポートしていないなど、最新のMCPプロトコル を追従しきれていない。 MCPサーバーの実装 13 / 29

Slide 14

Slide 14 text

AIエージェントの実装 AIエージェントの実装 14 / 29

Slide 15

Slide 15 text

Koogフレームワークの概要 JetBrainsが公開しているAIエージェント構築フレームワーク「Koog」 https://github.com/JetBrains/koog MCPツールを始めとした様々な機能を組み合わせて、ワークフローを構築すること ができる KMP対応(JVM、JS、WasmJS、Android、iOS) 会話履歴の永続化、履歴圧縮 主要なLLMのサポート LLMのシームレスな切り替え ストリーミング機能 AIエージェントの実装 15 / 29

Slide 16

Slide 16 text

基本的な実装 LLMのモデルや利用可能ツール、ワークフローのグラフなどを設定する AIAgent.run() で実行する val agent = AIAgent( executor = simpleGoogleAIExecutor(apiKey), systemPrompt = "あなたはアプリ開発/デザインの専門家です。定められた情報を参照し、ツールを使用してユーザーのリクエストに回答してください。", llmModel = GoogleModels.Gemini2_5Pro // その他必要に応じて設定 ) val result = agent.run("Repositoryの実装方針について教えて") println(result) AIエージェントの実装 16 / 29

Slide 17

Slide 17 text

AIエージェントの管理 AIAgentインスタンスはシングルショットで動作し、生存期間は短い 基本的には1セッションの実行(プロンプトの送信、結果の受信)で破棄される 会話コンテキストの保持などは、後述するPersistenceの実装に委ねる Koog 0.5.0から「AIAgentService」が提供 同一の構成のAIAgentのインスタンスをまとめて効率的に管理できるようなった AIエージェントの実装 17 / 29

Slide 18

Slide 18 text

AIAgentServiceの実装 AIAgent/AIAgentServiceにはインプットとアウトプットを型で指定できる。 // 例として返答結果を受け取るFlowを定義 val agentResultFlow = MutableSharedFlow() // String以外にも独自のクラスを定義可能 val agentService: GraphAIAgentService = 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

Slide 19

Slide 19 text

ツールの実装と登録 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

Slide 20

Slide 20 text

ツールの実装と登録 純粋ロジックではない、LLMを使用した機能もツールとして実装することも可能 ツールや後述するグラフNodeでは、インプットとアウトプットも型で指定でき、 LLMからの応答をデータクラスのオブジェクトなどで受け取ることができる。 パースロジックの実装も不要かつ型安全 LLMに応答品質も格段に向上する、非常に強力な機能 @Serializable @SerialName("FigmaLinterResult") @LLMDescription("FigmaLinterの検出結果") data class FigmaLinterResult( @property:LLMDescription("チェック結果のリスト") val linterIssues: linterIssues, ) AIエージェントの実装 20 / 29

Slide 21

Slide 21 text

ワークフローの構築 Koogではワークフローをグラフベースで定義することができる。 ユーザーのプロンプトのコンテキストに応じて、必要なツールを呼び出しを判断・ 実行して、要求に答える一連のフローを定義する。 AIエージェントの実装 21 / 29

Slide 22

Slide 22 text

ストリーミング 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

Slide 23

Slide 23 text

ストリーミング:ハマったこと ストリーミングでLLMのリクエストをしたとき、LLMがツールの実行を一切してく れなくなってしまった。 原因 メソッドのIFとかは問題なさそうだったが、最後の最後のLLMへのリクエストで Toolを引数として渡さない内部実装になっていた。 issueをよく見たらストリーミング実行時のツール実行は現状サポートされていなか った AIエージェントの実装 23 / 29

Slide 24

Slide 24 text

会話履歴の永続化 interface PersistenceStorageProviderを継承して、 AgentCheckpointData を保 持する仕組みを実装する。 今回は永続化はKMP対応してるJetpack DataStoreを使用する。 それをAIAgentServiceの作成時にinstallすることで、自動的に保存される。 // storageProviderをインスタンス化しておく AIAgentService( // その他設定... installFeatures = { install(Persistence) { storage = storageProvider enableAutomaticPersistence = true rollbackStrategy = RollbackStrategy.MessageHistoryOnly } }, ) AIエージェントの実装 24 / 29

Slide 25

Slide 25 text

会話履歴の永続化:ハマったこと 同一のagentIdで会話を再開しようとしても、会話履歴が保持されていないという 状況が発生。 対処法 install時に rollbackStrategy = RollbackStrategy.MessageHistoryOnly にしてお く。 Defaultだと、会話以外のメタ情報も全て保持するが、以前のセッションでエージ ェントを破棄した(TombStone)状態としてマークされるため、会話履歴も復元 できない。 MessageHistoryOnly にすることで、会話履歴のみを復元し、会話を継続させるこ とができる。 AIエージェントの実装 25 / 29

Slide 26

Slide 26 text

クライアントのUIの実装 クライアントのUIの実装 26 / 29

Slide 27

Slide 27 text

クライアントのUIの実装の構成 UI部分は真新しい話ではありませんが、今回はCompose Desktopで作成していま す。 設計は一般的なUDFパターン ライブラリは Ktor, Flow, ViewModel, DataStoreなどを使用 クライアントのUIの実装 27 / 29

Slide 28

Slide 28 text

クライアントのUIの実装の留意点 AIAgentServiceを保持するAIAgentServiceProviderを定義して、このインスタ ンスはシングルトンとしています。 KoinなどでDI LLMからの応答は、AIAgentServiceProviderからストリーミングのレスポンス + runの実行結果をFlowとして受け取る。 ViewModelで上記を購読して、UiStateのモデルに変換して、Composeに公開し ます。 クライアントのUIの実装 28 / 29

Slide 29

Slide 29 text

ご清聴ありがとうございました! 29 / 29