を作成 02 実際に起きたこと AI がツールを使いこなせていない 03 辿り着いた答え 原因はモデルではなく ツール設計 の問題 ツールは AI ファースト で設計すべ き 具体的に AI ファーストで設計するとはどういうことか —— 本日はそれを共有します。 はじめに 3 / 54 S P R I N G A I × M C P
AI がそれぞれ Starter として提供。 サーバー — 機能を提供する側 例: Gmail / Slack / PostgreSQL MCP Server クライアント — 機能を利用する側 例: Claude Desktop / Codex / GitHub Copilot ▼ ▼ Spring AI Spring AI MCP Server Boot Starter Spring AI MCP Client Boot Starter Starter 概要 12 / 54 S P R I N G A I × M C P
系の通信制御 リクエスト → メソッド引数のバインディング Spring Boot ライフサイクルへの統合 開発者が書く 設計とビジネスロジック ツールのビジネスロジック AIエージェント向けインターフェース設計 name / description の設計 返却値・エラー設計 監査ログ 役割分担は Starter に限らず、どの言語のフレームワークでも共通。 Starter 概要 14 / 54 S P R I N G A I × M C P
のスキルセットがその まま使える。 02 アノテーションを用いた宣言的 定義 @McpTool / @McpToolParam を 付けるだけ。 03 Spring AI フレームワークとの連 携 ChatClient / VectorStore / RAG と同じ世界で構築できる。 — 本日は省略 Starter 概要 15 / 54 S P R I N G A I × M C P
ルーティングまで自動。 @McpTool(name = "search_tasks", description = "タスクを検索"...) public List<TaskDto> searchTasks( @McpToolParam(description = "検索キーワード") String keyword, ... ) { ... } Starter 概要 17 / 54 S P R I N G A I × M C P
API 決定論的 ↔ 開発者 決定論的 前提が一致 AI 向けツール設計 提供側 利用側 ツール 決定論的 ↔ AI 確率的 前提が片側だけずれる API・ツール・開発者は挙動・解釈が安定。 一方 AI は出力が確率的。 結果、AI 向けツール設計は前提が片側だけずれる。 境界設計×実装 19 / 54 S P R I N G A I × M C P
副作用の有無 回復可能性 説明文の目的 開発者への仕様説明 モデルへのプロンプト 返却値の設計 クライアントが使いやすい形 次の推論を壊さない形 ログの目的 障害診断 AI の行動の事後検証 境界設計×実装 20 / 54 S P R I N G A I × M C P この 4 つを設計原則として整理する ▶
意味 デフォルト readOnlyHint 読み取り専用か false destructiveHint 破壊的操作(=不可逆)か true idempotentHint 冪等か false openWorldHint サーバの外側と相互作用するか true ① 公開機能は可逆 実装 26 / 54 S P R I N G A I × M C P
= 読み取り専用や可逆なツールは、デフォルトを明示的に打ち消す。 annotations = @McpTool.McpAnnotations( readOnlyHint = true, // 読み取り専用であることを宣言 destructiveHint = false, // デフォルト(true)を打ち消す idempotentHint = true, openWorldHint = false ) ※ Tool Annotations はMCP仕様上 「信頼できないヒント」 という位置づけ AIエージェントにとって、安全性を保証する根拠にはならないが、実行判断に影響するため正確な記述は必要 ① 公開機能は可逆 実装 27 / 54 S P R I N G A I × M C P
③ 副作用宣言 @McpTool( name = "archive_task", description = "タスク ID を指定して、タスクを論理アーカイブ(非表示化)します。" // ① 使用条件 + " 完全に削除(物理削除)したい場合は delete_task_with_elicit を使ってください。" // ② 否定的境界 + " 副作用: 担当者にアーカイブの通知メールが送信されます。", // ③ 副作用宣言 annotations = @McpTool.McpAnnotations(...) ) public String archiveTask( @McpToolParam(description = "1 以上の整数", required = true) final int taskId ) { ... } 「書き方」そのものが設計。 ② 説明文はプロンプト 実装 31 / 54 S P R I N G A I × M C P
999 は存在しません search_tasks で類似名のタスクを検索してください → モデルは正しい次アクションに誘導される。 「何が起きたか」だけでなく「次に何をすべきか」まで返し、AI の暴走・迷走を防ぐ。 ③ 返却値は次の推論インプット 方針 35 / 54 S P R I N G A I × M C P
= "get_task_detail", description = "タスク ID からタスクを取得します。...") public TaskDto getTaskDetail( @McpToolParam(...) final int taskId, @McpToolParam(...) final List<String> fields ) { if (!taskService.isValidFields(fields)) { throw new IllegalArgumentException( "不正なフィールドが含まれています。" + "指定可能なフィールド一覧は list_task_fields で確認してください。"); } return taskService.selectFieldsById(taskId, fields) .orElseThrow(() -> new IllegalArgumentException( "タスク " + taskId + " は存在しません。一覧から探す場合は search_tasks を使ってください。")); } ③ 返却値は次の推論インプット 実装 36 / 54 S P R I N G A I × M C P
description = "タスクに添付された画像を取得します。...") public CallToolResult getTaskAttachment( @McpToolParam(...) int taskId ) { Attachment file = taskService.findAttachment(taskId); String base64 = Base64.getEncoder().encodeToString(file.bytes()); return CallToolResult.builder() .addTextContent("タスク " + taskId + " の添付画像です。") // 自然言語の補足 .addContent(new ImageContent(null, base64, file.mimeType())) // 型付きコンテンツとして返す .build(); } ③ 返却値は次の推論インプット 実装 37 / 54 S P R I N G A I × M C P
deleteTaskWithElicit( final McpSyncRequestContext context, // ① @McpToolParam(description = "1 以上の整数。") final int taskId ) { if (!context.elicitEnabled()) { // ① return "Elicitation 未対応のため削除しません"; } ... } ① 第一引数に McpSyncRequestContext ツール実行中に MCP クライアントと同期でやり取り するときに付ける。Starter が自動注入。 未対応クライアントへのフォールバック Elicitation は比較的新しい機能。未対応なら 「実 行しない」に倒す。 補足 ① Elicitation 44 / 54 S P R I N G A I × M C P
approveDeletion ) {} @McpTool(name = "delete_task_with_elicit", ...) public String deleteTaskWithElicit(...) { ... String elicitMessage = "タスク " + taskId + " を削除しますか?"; StructuredElicitResult<ApprovalInput> result = context.elicit( s -> s.message(elicitMessage), ApprovalInput.class ); ... } 回答受け取り型 ユーザーの回答をマッピングする Java の型を .class で指定する。 確認メッセージ ユーザーに送る文言。操作の重大さに合わせて丁寧 に書く。雑に書くと意味がない。 補足 ① Elicitation 45 / 54 S P R I N G A I × M C P
{ ... StructuredElicitResult<ApprovalInput> result = ... if (result.action() != Action.ACCEPT || !result.structuredContent().approveDeletion()) { return "削除はキャンセルされました"; } return taskService.delete(taskId) ? "削除しました" : "..."; } action() 確認ダイアログへの応答(承認 / キャンセル / 拒 否)。 structuredContent() Step ② で指定した回答型に入ったユーザーの回答 内容。 両方の確認が必要 ー 承認かつ確認済みのときだけ実行する 補足 ① Elicitation 46 / 54 S P R I N G A I × M C P
ID-1 を削除しますか? → なぜ削除するのかが分からないと、 ユーザーは安心して判断できない。 Sampling で理由を補強 タスク ID-1 を削除しますか? 削除背景(AI推定): 他タスクと重複して いるため → なぜ呼ばれたのかを、 クライアント側モデルに問い合わせる。 補足 ② Sampling 48 / 54 S P R I N G A I × M C P
taskId) { if (!context.sampleEnabled()) { return ""; } CreateMessageResult res = context.sample(spec -> spec .message("タスク " + taskId + " の削除が要求されました。" + "会話の文脈から理由を1文で説明してください。") .includeContextStrategy( ContextInclusionStrategy.ALL_SERVERS) .maxTokens(100)); if (res != null && res.content() instanceof TextContent tc) { return "削除背景(AI推定): " + tc.text(); } return ""; } 留意点 ① クライアント未対応の場合があるため、 sampleEnabled() でフォールバックする。 留意点 ② クライアント側モデルの利用枠を消費するた め、maxTokens でコストを制御する。 補足 ② Sampling 49 / 54 S P R I N G A I × M C P
の判断の質を上げる誘導を仕込む 03 返却値は次の推論インプット AI のコンテキストに入る情報を制御する 04 ログは行動の証跡 AI の行動を後から検証可能にする AI エージェントとツールの境界が存在する限り、普遍的に問われる考え方。 まとめ 52 / 54 S P R I N G A I × M C P
MCP Annotations Server (@McpTool / Elicitation / Sampling のサンプル実装) docs.spring.io/spring-ai/reference/api/mcp/mcp-annotations-server.html MCP 仕様 • MCP 公式サイト modelcontextprotocol.io • Tool Annotations の定義 (仕様 — Schema Reference) modelcontextprotocol.io/specification/2025-11-25/schema#toolannotations 参考リンク 53 / 54 S P R I N G A I × M C P