Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
フルKotlinで作る! MCPサーバー、AIエージェント、UIまで一気 通貫したAIエージェ...
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Shuzo Takahashi
November 01, 2025
2.2k
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
フルKotlinで作る! MCPサーバー、AIエージェント、UIまで一気 通貫したAIエージェントシステム
Shuzo Takahashi
November 01, 2025
More Decks by Shuzo Takahashi
See All by Shuzo Takahashi
Figma102_デザインからアプリ実装まで一貫したデザインシステムを構築するベストプラクティス
shuzo
9
4.7k
デザインからアプリ実装まで一貫したデザインシステムを構築するベストプラクティス
shuzo
16
11k
Featured
See All Featured
Neural Spatial Audio Processing for Sound Field Analysis and Control
skoyamalab
0
330
Heart Work Chapter 1 - Part 1
lfama
PRO
7
36k
Design in an AI World
tapps
1
230
The Director’s Chair: Orchestrating AI for Truly Effective Learning
tmiket
1
190
Product Roadmaps are Hard
iamctodd
PRO
55
12k
Scaling GitHub
holman
464
140k
How People are Using Generative and Agentic AI to Supercharge Their Products, Projects, Services and Value Streams Today
helenjbeal
1
210
The browser strikes back
jonoalderson
0
1.2k
Mind Mapping
helmedeiros
PRO
1
240
AI Search: Implications for SEO and How to Move Forward - #ShenzhenSEOConference
aleyda
1
1.3k
Sam Torres - BigQuery for SEOs
techseoconnect
PRO
0
280
GraphQLとの向き合い方2022年版
quramy
50
15k
Transcript
フルKotlinで作る! MCPサーバー、AIエージェント、UIまで一気 通貫したAIエージェントシステム Shuzo Takahashi(しゅーぞー) 1 / 29
自己紹介 高橋 柊蔵(しゅーぞー) LINEヤフー株式会社所属 2020年新卒入社 Androidアプリエンジニア&UIデザイナー X: shuzo_create 2 /
29
今回話すこと Kotlinで以下を全部一気通貫で作る話 MCPサーバー (公式MCP SDK + Ktor) AIエージェント (Koog) UI
(Compose+Jetpack系ライブラリ) 今回はUIデザイン〜アプリ実装までのプロセスを包括的にサポートする、チャット 形式のAIエージェントを題材にします ドメイン知識、デザインデータやコードを参照し、チャット形式でユーザーの質問 に回答するシステムを例にします 3 / 29
MCPサーバーの実装 MCPサーバーの実装 4 / 29
Kotlin MCP SDK MCP公式でKotlinのSDKが提供されている。 https://github.com/modelcontextprotocol/kotlin-sdk MCPサーバーの実装 5 / 29
MCPサーバーの設計 MCPサーバーは個別の機能を「ツール」として提供する MCPツールはステートレスで冪等かつ不透明な副作用がないようにする アプリ実装で言うとUseCaseに近い ツールはLLMが呼び出して使うため、解釈容易性が高く「LLMにとって」わかりや すいIFにする MCPサーバーの実装 6 / 29
MCPツールのスキーマの実装 MCPツールの定義で、インプットとアウトプットのスキーマを定義する SDKではJsonObjectでスキーマ定義を受け取る用になっていて非常に扱いづらい # SDKのインプットの定義 @Serializable public data class Input(
val properties: JsonObject = EmptyJsonObject, val required: List<String>? = null, ) MCPサーバーの実装 7 / 29
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
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
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
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
MCPツールの公開 MCPサーバーとクライアントが疎通するプロトコル STDIO:サーバーとクライアント同一マシン内で起動してプロセス通信する SSE:サーバーからクライアントへ一方的にデータをプッシュ通信するプロトコル Streamable HTTP:HTTPレスポンスを分割して順次送信する方式 現状では、公式SDKのサーバーサイド実装はSTDIOとSSEにしか対応していない。 SSEはKtorで非常に楽に実装できる。 MCPサーバーの実装 12
/ 29
MCPサーバー実装のまとめ この一連の実装でMCPクライアント(後述するKoogでのAIエージェントや Calude Desktop、Clineなど)から接続することができるようになる。 その他所感 正直、思ったよりきれいに実装できない(JsonObjectを扱う必要があったり) Kotlinらしくスマートに安全に実装するために、自分でそのあたりを隠蔽する設計 が必要になる。 公式SDKがStreamableHTTPをサポートしていないなど、最新のMCPプロトコル を追従しきれていない。
MCPサーバーの実装 13 / 29
AIエージェントの実装 AIエージェントの実装 14 / 29
Koogフレームワークの概要 JetBrainsが公開しているAIエージェント構築フレームワーク「Koog」 https://github.com/JetBrains/koog MCPツールを始めとした様々な機能を組み合わせて、ワークフローを構築すること ができる KMP対応(JVM、JS、WasmJS、Android、iOS) 会話履歴の永続化、履歴圧縮 主要なLLMのサポート LLMのシームレスな切り替え ストリーミング機能
AIエージェントの実装 15 / 29
基本的な実装 LLMのモデルや利用可能ツール、ワークフローのグラフなどを設定する AIAgent.run() で実行する val agent = AIAgent( executor =
simpleGoogleAIExecutor(apiKey), systemPrompt = "あなたはアプリ開発/デザインの専門家です。定められた情報を参照し、ツールを使用してユーザーのリクエストに回答してください。", llmModel = GoogleModels.Gemini2_5Pro // その他必要に応じて設定 ) val result = agent.run("Repositoryの実装方針について教えて") println(result) AIエージェントの実装 16 / 29
AIエージェントの管理 AIAgentインスタンスはシングルショットで動作し、生存期間は短い 基本的には1セッションの実行(プロンプトの送信、結果の受信)で破棄される 会話コンテキストの保持などは、後述するPersistenceの実装に委ねる Koog 0.5.0から「AIAgentService」が提供 同一の構成のAIAgentのインスタンスをまとめて効率的に管理できるようなった AIエージェントの実装 17 /
29
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
ツールの実装と登録 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
ツールの実装と登録 純粋ロジックではない、LLMを使用した機能もツールとして実装することも可能 ツールや後述するグラフNodeでは、インプットとアウトプットも型で指定でき、 LLMからの応答をデータクラスのオブジェクトなどで受け取ることができる。 パースロジックの実装も不要かつ型安全 LLMに応答品質も格段に向上する、非常に強力な機能 @Serializable @SerialName("FigmaLinterResult") @LLMDescription("FigmaLinterの検出結果") data
class FigmaLinterResult( @property:LLMDescription("チェック結果のリスト") val linterIssues: linterIssues, ) AIエージェントの実装 20 / 29
ワークフローの構築 Koogではワークフローをグラフベースで定義することができる。 ユーザーのプロンプトのコンテキストに応じて、必要なツールを呼び出しを判断・ 実行して、要求に答える一連のフローを定義する。 AIエージェントの実装 21 / 29
ストリーミング 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
ストリーミング:ハマったこと ストリーミングでLLMのリクエストをしたとき、LLMがツールの実行を一切してく れなくなってしまった。 原因 メソッドのIFとかは問題なさそうだったが、最後の最後のLLMへのリクエストで Toolを引数として渡さない内部実装になっていた。 issueをよく見たらストリーミング実行時のツール実行は現状サポートされていなか った AIエージェントの実装 23
/ 29
会話履歴の永続化 interface PersistenceStorageProviderを継承して、 AgentCheckpointData を保 持する仕組みを実装する。 今回は永続化はKMP対応してるJetpack DataStoreを使用する。 それをAIAgentServiceの作成時にinstallすることで、自動的に保存される。 //
storageProviderをインスタンス化しておく AIAgentService( // その他設定... installFeatures = { install(Persistence) { storage = storageProvider enableAutomaticPersistence = true rollbackStrategy = RollbackStrategy.MessageHistoryOnly } }, ) AIエージェントの実装 24 / 29
会話履歴の永続化:ハマったこと 同一のagentIdで会話を再開しようとしても、会話履歴が保持されていないという 状況が発生。 対処法 install時に rollbackStrategy = RollbackStrategy.MessageHistoryOnly にしてお く。
Defaultだと、会話以外のメタ情報も全て保持するが、以前のセッションでエージ ェントを破棄した(TombStone)状態としてマークされるため、会話履歴も復元 できない。 MessageHistoryOnly にすることで、会話履歴のみを復元し、会話を継続させるこ とができる。 AIエージェントの実装 25 / 29
クライアントのUIの実装 クライアントのUIの実装 26 / 29
クライアントのUIの実装の構成 UI部分は真新しい話ではありませんが、今回はCompose Desktopで作成していま す。 設計は一般的なUDFパターン ライブラリは Ktor, Flow, ViewModel, DataStoreなどを使用
クライアントのUIの実装 27 / 29
クライアントのUIの実装の留意点 AIAgentServiceを保持するAIAgentServiceProviderを定義して、このインスタ ンスはシングルトンとしています。 KoinなどでDI LLMからの応答は、AIAgentServiceProviderからストリーミングのレスポンス + runの実行結果をFlowとして受け取る。 ViewModelで上記を購読して、UiStateのモデルに変換して、Composeに公開し ます。 クライアントのUIの実装
28 / 29
ご清聴ありがとうございました! 29 / 29