TypeScript - TECH PLAY - TECH PLAY

TECH PLAY

TypeScript

イベント

該当するコンテンツが見つかりませんでした

マガジン

技術ブログ

前提:楽楽明細とは別のアプリとして作った 起きたこと:12機能を1本のプルリクにまとめた 原因:4層の名前までしか決めていなかった 対策:機械に判定させる範囲を広げ、人が見る範囲を絞る 実例:レビューで通せなかった箇所 次にやるなら:プルリクの切り方から変える チェックリスト:着手前に決めておく項目 編集後記:インタビューを終えて ラクス技術広報です。 開発本部では、各部署のAI活用の取り組みを技術広報がインタビューし、記事にしています。今回は電子請求書発行システム「楽楽明細」の新しいオプション機能を、AIにほぼ実装させて約2か月でリリースした事例です。 本記事は、AIを使って順調に機能開発が進んだ話だけでなく、うまくいかなかったこと・その対策をまとめています。 AIに任せる範囲を広げる前に、正しさを機械が判定できる状態を作る。 今回はこの状態を作らないまま進めました。その結果、機能ごとに実装の仕方がばらばらになり、1機能のレビューで入った指摘を他の機能に横展開できず、同じ指摘を機能の数だけ受けることになりました。 納期の都合で12機能を1本のプルリクエストにまとめたことも重なり、変更ファイルは700を超え、サーバーサイドのレビュー担当が全量を確認し終えるまでに5営業日かかっています。 新規プロダクトでAI駆動開発を始める方 AIが書いたコードのレビューが追いつかないと感じている方 に向けて、着手前に決めておく項目を記事の最後にチェックリストとして置きました。是非ご参考ください。 [図1:任せる範囲と、機械が判定できる範囲] 前提:楽楽明細とは別のアプリとして作った   楽楽明細は、請求書や支払明細などの帳票を電子発行するクラウドサービスです。帳票のもとになる売上データは、お客様が販売管理システムやExcelで管理しているものを取り込みます。 販売管理システムを使っていないお客様は、売上データをExcelで管理しています。 ・営業や拠点ごとにファイルが分かれ、どれが最新かわからなくなる ・転記のときにミスも起きる ここを解決するために作ったのが、今回の 売上登録オプション です。専用画面で売上データを入力すると、そのまま請求データとしてCSV出力できます。 仕様はプロダクトマネージャーが決めました。AI(Claude)でプロトタイプを作り、動くものをお客様に見せながら40社に話を聞いています。話を聞くうちに最小限の機能で導入いただけるとわかり、最初のプロトタイプから機能を約30%削って、CSV出力と税額計算に必要な設定に絞りました。 楽楽明細本体には手を入れず、別のアプリとして切り出しています。長く運用してきた本体にAIを入れると既存機能を壊す可能性があるため、影響範囲を限定してAIに任せる範囲を広げる判断です。 項目 内容 期間 開発決定4月7日 → リリース5月末(実質2か月未満) 体制 設計・実装は2026年1月入社の川島卓大さんがほぼ一人。 サーバーサイドとフロントエンドで各1名がレビュー。 技術 Java、Spring Boot、React、TypeScript、PostgreSQL AIの使い方 仕様の書き起こしはClaude Code、kiro(cc-sdd)でspec化してエージェントに読ませる。 実装の主軸はCodex。 git worktreeで機能ごとに作業を隔離し、複数セッションを同時に走らせる。 起きたこと:12機能を1本のプルリクにまとめた   実装に使える期間は実質1か月弱で、機能は12ありました。機能ごとにプルリクエスト(以下プルリク)を切ってレビューを待つ余裕がないため、まず全機能をつなげて動かし、そこから改善する方針を選びました。 項目 実績 変更ファイル数 700超 サーバーサイドのレビュー 機能単位で半日〜1日。 最初のプルリクを全量見切るまでに5営業日(期間で2週間弱)。 フロントエンドのレビュー 大型プルリクと、機能単位に分割された後続15件のフロント部分を合わせて2人日。 GitHub Copilotによるレビューも入れていましたが、人手で見きれる量を超えています。機能ごとに切り分けてレビューできたのは、パッケージ構成をfeature-firstで先に決めていたためです。 原因:4層の名前までしか決めていなかった   プロダクトマネージャーからの要求仕様書は、お客様の求めるものが整理された状態で渡されています。ただ、そのままAIに渡せる粒度ではなかったと川島さんは語ります。 「そのままでは渡せませんでした。特に受け入れ条件と例外系が足りませんでした。」 そこで要件を「WHEN 条件 THEN 結果」のEARS形式で1アクション単位に分解し、「IF 制約 THEN 拒否」の例外パスを正常系と並べて書いてからspecに落としました。 実装側で先に決めていたのは、feature-firstのパッケージ構成と、DDD(オニオンアーキテクチャ)の4層の名前までです。層の中で誰が何を担うのか、検証はどこでやるのか、副作用はどこに置くのかは決めていませんでした。 その状態で機能ごとに並列でエージェントを走らせると、1機能のレビューで入った指摘を他の機能に横展開できず、同じ指摘を機能の数だけ受けることになりました。 「仕様自体は要件通り作成できているものが多かったが、責務が分離できていないものが多く、レイヤー間の妥当性を見る時間が多かった。」 要求がテストできる形になっていても、実装方針が決まっていなければ、AIはそれぞれの解釈で書きます。 対策:機械に判定させる範囲を広げ、人が見る範囲を絞る   [図2:判定を3段に分ける] CIとpre-commitは最初から入れていました。ルールの書き出しとマージゲートの追加は、機能ごとに実装がばらばらになった反省から足したものです。 「一番効いているのは『できました』と言わせず証拠を出させることです。」 川島さんは、サブエージェントの「テストが通った」という報告も鵜呑みにせず、親のエージェントがdiffとテスト出力を自分で確認します。 レビューは一次をGitHub CopilotとClaude Codeのスキルで出し、人が二次で見ます。観点が複数あるときは同じエージェントに全部任せず、観点ごとにサブエージェントを分けて並列で走らせます。指摘にはmust、should、ask、imo、nitのラベルを付け、対応するかどうかを人が判断しやすくしています。 実例:レビューで通せなかった箇所   本機能にて、サーバーサイドとフロントエンドのレビューを担当した2人に「レビューで通せなかった箇所」についてお伺いしました。 領域 レビューで通せなかった箇所 サーバーサイド メール送信のように非同期で構わない処理が、トランザクションの中に入っていた。 初期のプルリクでは、メールは送信されたのにDBがロールバックされ、無効なパスワード初期化用URLがユーザーに届く実装になっていた(リリース前に修正)。 サーバーサイド 業務ロジックがユースケースやインフラの層に流れ出す責務違反。 サーバーサイド N+1が起きやすい構造。 明細行20行の売上データを取得するのに、最初はSQLが23本走っていた。 フロントエンド 方針はバックエンドで検証して送信時に表示することだったが、全画面にリアルタイムバリデーションが入っていた(全画面から削除)。 フロントエンド すでにある共通コンポーネントを使わず画面ごとに直書きし、ドラッグ・アンド・ドロップも複雑に自前実装していた(ライブラリを使う形に置き換え)。 フロントエンド テーブル内のテキストフィールドに1文字打つごとに、テーブル全体が再レンダリングされていた。 レビューを担当した2人が、共通して口にしたことがあります。 「『動く』のと『そのまま出せる』の間には距離がある」 また、これから同じ進め方を始めるチームへ、2人からのコメントです。 サーバーサイド レビュー担当者 「設計、実装の前にどうやってAIをハンドリングをしていくかという工程をちゃんと設けるべきではあった。期日までが短く急ぎ早で始めたものの手戻りも多く、事前準備をちゃんと設けていても間に合わせられていたのではと思う。 」 フロントエンド レビュー担当者 「レビューする側もAIを使っていいということ。作る速度が上がる分、見る側もAIで理解スピードを早めないとレビューが追いつかなくボトルネックになる。」 次にやるなら:プルリクの切り方から変える   [図3:プルリクの切り方] 切れ目を先に決めておけば、レビューは内側から外側へ積み上げる順に流れます。 一人でフルスタックに進めた体制では、フロントとバックエンドの間で調整が発生しないため、動くものを作る速度は出ました。一方で、一人にかかる負担は大きくなりました。API規約を先に確定させれば、フロントとバックエンドを分担できます。着手前にAIのハンドリングを詰める工程を置くことも、次の案件に向けた課題として残りました。 チェックリスト:着手前に決めておく項目   冒頭でもお伝えした通り、本取り組みから得た知見を基に着手前に決めておく項目をまとめました。 ご自身のプロジェクトに当ててみてください。 仕様と設計で決めておくこと (→ 原因の章 ) □ 受け入れ条件と例外系を、1アクション単位で書き出したか(EARS形式など) □ 各層で誰が何を担うか、検証はどこでやるか、副作用はどこに置くかを決めたか 機械に判定させる仕組み (→ 対策の章 ) □ CIで必須にする項目を並べたか(lint、format、型チェック、単体テスト、secret scanning、依存パッケージの実在チェック) □ テストを実データに近い環境で流せるか(Testcontainersなど) □ 機械では拾えない観点を、プロジェクト固有のレビュー観点として文字にしたか □ エージェントに、テスト出力とdiffを証拠として出させる運用にしたか 分割の単位 (→ 次にやるならの章 ) □ API規約(インターフェース)を、実装より先に確定させたか □ プルリクの切れ目を、レビューできる大きさで先に決めたか 最後に、川島さんからのメッセージです。 「AIに任せる範囲を広げる前に、正しさを機械が判定できる環境を先に作ることが大事だと思いました。最初の壁はコードが書けないことではなく、動いているように見えて設計方針から外れたコードが、レビューの追いつかない速度で積み上がることです。AI駆動開発で変わるのは、コードを書く仕事の比重です。良し悪しを定義し検証する仕事へ重心が移ります。ツールは半年で入れ替わりますが、この土台はどのツールに乗り換えても効き続けます。」 編集後記:インタビューを終えて   取材していて印象に残ったのは、川島さんもレビュー担当の2人も、うまくいった話より「次はこうする」を具体的に話してくれたことでした。700ファイルのプルリクは、社外に出すには気の重い話だと思います。それでも数字と経緯をそのまま出してくれたので、この記事が書けました。 開発本部では、うまくいったところとやり直したいところの両方を、これからも技術広報が聞いてご紹介していきます。
はじめに 1. 注目した次世代ツールの魅力 1.1 Oxlint ── 「今すぐ、設定ゼロで爆速化」 1.2 Biome ── 「これ 1 つで全てが片付く、究極の一本化」 1.3 Deno ── 「セットアップ不要、最高にクリーンな開発体験」 1.4 Ezno ── 「型チェックまで Rust 化する、超未来のコンパイラ」 2. Vue特有の壁: の静的解析問題 3. チームでの議論:「設定ファイル2つは本当に必要か?」 4. 結果 5. 今後の展望 まとめ はじめに フロントエンドエンジニアをしている伊藤と申します。 昨今は、Rust 製のツールが数多く生み出されており、Linter もその波に乗っていると思います。 弊社でも移行をしたブログを書いており、是非下記の記事も読んでみてください。 https://tech.revcomm.co.jp/ai-lint-formatter-oxc 史上稀に見る大波が来ている時代に、私も颯爽と波に乗ろうとしました。 そしてどういった結論に至ったかご紹介させていただきます。 注: 本記事の検証は2026年9月10日時点、Oxlint v1.82.0 / Biome v2.5.12 で行っています。この領域は変化が速いため、お読みいただく時点では状況が変わっている可能性があります。 1. 注目した次世代ツールの魅力 1.1 Oxlint ── 「今すぐ、設定ゼロで爆速化」 最大の魅力:既存のプロジェクトにノーリスクで追加できる メリット ESLint の 50〜100 倍高速 : 数万行のコードも一瞬でスキャンが終わります。 設定ファイルが不要 : インストールして実行するだけで、プロ級のバグ検出ルールが最初から動きます。 既存の ESLint と併用可能 : 重い処理(バグチェック)は Oxlint に任せ、複雑なカスタムルールだけ ESLint に残す「いいとこ取り」ができます。 1.2 Biome ── 「これ 1 つで全てが片付く、究極の一本化」 最大の魅力:Linter と Formatter が融合したオールインワン構造 メリット ESLint と Prettier の多くの用途を一本化できる : 個別にプラグインを組み合わせる煩わしさから解放されます(ただし後述の通り、Vue の <template> 部分は現状 ESLint との併用が前提になる場面が残っています)。 設定の衝突ゼロ : Linter と Formatter が同じ思想で作られているため、「Linter と Prettier のルールがバッティングして動かない」という定番のストレスがありません。 圧倒的な構文解析の正確さ : エラーがあっても壊れたコードを器用にパースし、親切なエラーメッセージを出してくれます。 1.3 Deno ── 「セットアップ不要、最高にクリーンな開発体験」 最大の魅力:最初からすべてが揃っている「環境」そのもの メリット : ツール選定やインストールの手間がゼロ : deno lint と deno fmt が最初から内蔵されています。 package.json に大量の依存関係を書く必要がありません。 標準で Web 標準に準拠 : ブラウザと同じ API をそのままサポートしているため、環境ごとの差異に悩まされません。 Node.js との互換性も強力 : 近年は Node.js のパッケージもそのまま動くため、敷居が非常に低くなっています。 注意: Deno は Linter 単体のツールではなく、ランタイムとツールチェーンそのものです。既存の Node.js プロジェクトに deno lint だけを差し込むのではなく実行環境ごと移行する話になるため、Oxlint / Biome / Ezno とは比較の土俵・移行規模が異なる点に注意してください。 1.4 Ezno ── 「型チェックまで Rust 化する、超未来のコンパイラ」 最大の魅力:JavaScript の限界を超える、圧倒的に賢く高速な型解析 メリット : TypeScript(tsc)より遥かに高速 : 現在、フロントエンド開発で一番時間がかかる「型チェック」のプロセスを Rust で爆速にします。 型推論がスマート : コードの型をより深く、正確に推論してくれるため、開発者が型定義を細かく書く手間を減らせます。 バグを未然に防ぐ先進性 : 副作用(Side Effects)の検知など、従来の TypeScript よりもさらに一歩進んだ安全なコード検証が可能です。 注意: ただし Ezno は本記事執筆時点(2026年9月)でまだ feature-complete ではなく、tsc との完全互換を目指しているわけでもありません。既存の大規模プロジェクトの型チェックをそのまま置き換えられる段階ではなく、将来性に注目したい実験的プロジェクトという位置付けです。 どれも高速性を売りにしており、React では非常に乗り換えやすく恩恵も受けられます! 実際に私もローカルでBiomeの設定を試してみましたが、かなり早かったです。 ではそのまま導入して、Rust の恩恵を受けようとしました。 ただ、Vue になると少し話が変わってくるんです・・・ 2. Vue特有の壁: の静的解析問題 Vue は、HTML5 の標準仕様である <template> を、独自の便利な形に拡張して使っています。 要素に v-if や v-for を付けたり、 <template> を .vue ファイルの枠組みとして使ったりしています。 普段の開発をしている中で、 v-if や v-for を使わないケースはほぼないと思います。 当然ですが、それらの値もチェックしてくれると思っていました。 ただ、 <template> 内は、Oxlint, Biome ともにチェックしてくれないんです。 実際に最新版(Oxlint v1.82.0 / Biome v2.5.12、2026年9月時点)で試した結果を、以下のサンプルコードとあわせてご紹介します。 <template > < div > <!-- ❌ 1. 壁:Linterがここを見ないため、存在しない変数(タイポ)をスルーする --> < p > {{ userNema }} </ p > <!-- ❌ 2. 壁:Linterがここを見ないため、「下で使われている」と認識できない --> < p > {{ unusedMessage }} </ p > </ div > < / template> <script setup lang="ts"> import { ref } from 'vue' const userMessage = ref ( 'こんにちは' ) // 🔴 <script> 内は見ているので、「定義したのに使われていない!」と怒られる // (実際には上の template で使いたいのに、Linterが template を読めないせいで孤立する) const unusedMessage = ref ( 'これはエラーになります' ) < / s cript> Oxlint(v1.82.0) デフォルトでも vue-plugin オプションを付けても、このサンプルに対しては何も警告を出しませんでした。 userNema のタイポも unusedMessage の誤検知も発生しない代わりに、テンプレート側の静的解析自体がまだ行われていない、というのが実態でした。 Biome(v2.5.12) デフォルト設定ではスクリプト側の noUnusedVariables ルールが .vue ファイルにも適用され、 userMessage・unusedMessage の両方を「未使用」としてエラーにします( unusedMessage は実際にはテンプレート側で使われているので、これは誤検知です)。 ただし html.experimentalFullSupportEnabled を有効にすると、Biomeはテンプレート内での参照を認識できるようになり、 unusedMessage の誤検知は解消されました(本当に未使用な userMessage は引き続き正しく検出されます)。 一方で、フラグを有効にしてもテンプレート内の userNema のようなタイポを検出するルールはまだ無く、不正な変数参照を見逃す問題は解消されません。 上記の検証からわかる通り、テンプレート内の静的解析はRust製Linter単体ではまだ完結できません。Biomeは実験的フラグ付きで一部の誤検知を解消できるようになってきていますが、テンプレート内のタイポ検出のような踏み込んだチェックまでは、依然として eslint-plugin-vue に頼る必要があります。 つまり、必要なVue固有のルールと各ツールの対応状況次第では、OxlintまたはBiomeとESLintを併用する必要があるということです。 3. チームでの議論:「設定ファイル2つは本当に必要か?」 Rust製Linter を導入するか、一旦は ESLint のままにするか。 どちらを選択するか検討するため、メリット・デメリットを出してみました。 メリット 圧倒的な高速化 : 従来のJavaScriptなどで書かれたLinter(ESLintなど)に比べ、数倍〜数十倍高速に動作します。大規模なコードベースでも ミリ秒単位 で処理が終わるため、CI/CDの実行時間を大幅に短縮し、開発者の待ち時間をほぼゼロにします。 開発の快適化 : 保存時の自動整形や pre-commit フックでの実行が劇的に速くなります。 簡単な導入 : Rust製ツールはコンパイル済みのシングルバイナリとして配布されることが多いため、環境構築やCIでのセットアップが非常にシンプルになります。 デメリット 二重管理 :(一番の懸念) Rust製フロントエンドLinter(BiomeやOxcなど)は、 .vue ファイルの構文(HTMLテンプレートやVue固有の構文ルール)を完全にサポートしていません。 そのため、Vue固有のルール( vue/valid-template-root など)をチェックするには、結局従来の ESLint(eslint-plugin-vue) を残さなければならなくなります TypeScript/JavaScript部分: 爆速のRust製Linter(Biomeなど)でチェック Vueファイル・テンプレート部分: 従来のESLintでチェック プラグインエコシステムの未熟さ : 歴史のあるESLintなどのように、コミュニティが作った無数のサードパーティ製プラグインを自由に追加することが難しい場合があります。独自のルールを追加したい場合のハードルが高めです。 ルールや構文の追従ラグ: 言語(特にJavaScript/TypeScriptやPython)の新機能や新しい構文が登場した際、Rust側でのパーサ(構文解析器)の対応にわずかなタイムラグが生じることがあります。 4. 結果 結果は、デメリット部分の二重管理の懸念部分が大きく、現状のESLint(eslint-plugin-vue)で管理する方を選びました。 手元の計測で速度については、申し分ないことが立証済みでした。 導入するメリットも大いに感じているものの、2つのLinterを併用する点を上回るほどではないと判断しました。 5. 今後の展望 Biome は既に html.experimentalFullSupportEnabled フラグで Vue SFC の実験的サポートを始めており、Oxlint も vue-plugin オプションなどVue向けの機能を増やしてきています。 「サポートされるのを待つ」というよりも、「実験的サポートがどこまで安定するか・テンプレート内のタイポ検出のような踏み込んだチェックまでカバーされるか」を継続的にウォッチしていく、というのが今の立ち位置です。 各プロジェクトの現状を、ご紹介いたします。 Biome v2.4(2026年2月)で Vue・Svelte・Astro 対応を実験的機能からプロダクション標準に近づける取り組みが発表されました。既存のリンティングルールを強化し、HTMLライクな言語でも機能するようにする方針です。 https://biomejs.dev/ja/blog/roadmap-2026/#2026-roadmap Oxlint 公式ドキュメント上の制限の記載に、Vue も含まれており、改善してくれるはずだと思います。そして、改善のための issue があり開発が進められているのがわかります。 https://github.com/oxc-project/oxc/issues/23207 https://oxc.rs/compatibility.html そしてなんといっても、Oxlint の開発を支援しているのが Vue の開発者である Evan You さんが立ち上げた VoidZero という会社なんです。 現在のフロントエンドの「ツールの断片化(Linter、Formatter、テストツール、ビルドツールがバラバラで設定が面倒かつ遅い)」という課題を根本から解決するためVite+(ヴィートプラス)が開発され、その中に Oxlint も含まれています。 Vue の開発者が、Vue を見放すはずがないんです!弊社が Oxlint にするのも近いですね! まとめ Rust 製のツールは非常に高速で、すぐにでも乗り換えたいと思ってしまいます。 ただ、メリット・デメリットや今後の展望を踏まえて、検討するのが大事だなと今回の移行作業を通じて感じました。 どのツールにするかもしっかり検討したいです。 AI で開発を行うと、簡単に実行できるため、検討するのをサボりがちになってしまいます。 そういう意味でも今回はいい勉強になったと思います。 お読みいただきありがとうございました。 次は、Oxlint に移行した話でお会いしましょう。
この記事でわかること MCPサーバーはPythonやTypeScriptだけでなく、Spring AIを使えばJava(Spring Boot)でも構築できる MCPサーバー機能は、Spring AIの公式スターターを使えば簡単に既存のSpring Bootアプリに組み込める(サーバー設定はapplication.ymlのみ) 既存アプリがSpring SecurityのOAuth2 Resource Serverで認証していれば、MCPの認可に必要なメタデータ公開(RFC 9728)も含めてSpring Securityの標準機能で対応できる AIエージェントがCMDBの構成情報を自分で参照できるようになると、Atlassian MCPなど他のMCPとも組み合わさって、脆弱性対応やEOL対応といった業務がエージェントとの1つの会話で完結する はじめに こんにちは。KINTOテクノロジーズ(以下、KTC) プラットフォームグループ Platform Engineeringチームの山田です。普段は社内向けのCMDBを内製開発していて、ここ最近はCMDBと生成AIを組み合わせる取り組みを続けています。 CMDBそのものやSpring AIについては過去の記事で紹介しているので、あわせてご覧ください!(CMDBの記事は少し古いですが、雰囲気は伝わるかと思います) https://blog.kinto-technologies.com/posts/2023-12-14-CMDB/ https://blog.kinto-technologies.com/posts/2025-06-11-springAI/ 今回はその続きとして、CMDB本体(Spring Boot製)にMCPサーバー機能を追加し、AIエージェントから社内の構成情報を直接検索できるようにしました。 背景 Claude CodeなどのAIエージェントだけで業務を進める場面が、この1年で一気に増えてきていると思います。社内でも各種MCPサーバーやスキルなどの整備が進み、調査・実装・ドキュメント作成の多くをエージェントに任せられるようになっています。 一方で、困っていたのが、今回の主役であるCMDB (Configuration Management Database: 構成管理データベース) のデータです。CMDBは、プロダクト・チーム・GitHubリポジトリ・脆弱性・SBOMといった社内の構成情報を一元管理するデータベースで、KTCでは内製開発しています(詳しくは冒頭の過去記事をご覧ください)。せっかく構成情報を集約しているのに、これまでの参照手段はWebアプリだけでした。そのため、エージェントで作業をしていてCMDBのデータが必要になるたびに、以下の作業が発生していました。 CMDBのWebアプリを開いてキーワード検索する 検索結果のテキストをコピーして、エージェントのプロンプトに貼り付ける この間、エージェントの作業は止まり、人がWebアプリとエージェントの間でデータを受け渡すだけの作業に時間を取られます。せっかく構成情報を一元化したのに、AIエージェントからは参照できない状態でした。 この手作業での受け渡しをなくすため、CMDBをMCPサーバー化することにしました。 CMDBのMCPサーバーで何ができるようになったか 先に、できるようになったことの紹介です。 たとえば、あるライブラリに脆弱性が見つかったとします。SBOM・GitHubリポジトリ・プロダクト・チーム・ユーザーなどを管理するCMDBのMCPサーバーができたことで、こうした脆弱性対応をAIエージェントとの1つの会話の中で完結できるようになりました。具体的には、SBOM情報から脆弱性のあるバージョンのライブラリを使っているGitHubリポジトリを洗い出し、リポジトリに紐づくプロダクトと管理チーム・担当者を特定し、担当者向けのJiraチケットの起票、さらには脆弱性を解消するPRの作成まで進められます。 ①〜③と⑤がCMDBのMCPツール、④はAtlassian MCP、⑥はエージェント自身のコーディング機能です。 ポイントは、CMDBのMCPサーバー単体で完結するのではなく、他のMCPやエージェントの機能と組み合わさることで業務が一気通貫になることです。AIエージェントは、参照できるコンテキストが増えるほど、対応できる業務の幅が広がります。CMDBにはエージェントが持っていない社内の構成情報が集まっています。これまで人が都度補っていたこれらの情報をMCPで提供することで、エージェントに任せられる範囲そのものを広げることができました。 脆弱性対応の他にも、たとえばEOL(サポート期限)対応にも同じ要領で使えます。期限切れが近いコンポーネントとそれを使っているプロダクトをCMDBのMCPツールで洗い出し、担当チームの特定からJiraチケットの起票までを、そのまま1つの会話で進められます。 MCPサーバーの実装にSpring AIを選んだ理由 MCPサーバーの構築というと、PythonやTypeScriptのイメージが強いかと思います。公式SDKの充実度やFastMCPのようなフレームワークの存在もあり、実装記事もその2つに偏っている印象です。 ただ、今回MCPサーバー化したい対象は、Spring Bootで実装済みの既存アプリでした。別プロセスでPythonやTypeScriptのMCPサーバーを立ててAPIを中継させる構成も考えられますが、Spring AIを使って既存アプリにMCPサーバー機能を組み込めば、以下の資産をそのまま使い回せます。 REST APIのために実装してきたビジネスロジック(service層) Spring Securityを使った認証認可の仕組み ECSへのデプロイや監視などの運用の仕組み 唯一の不安は「Entra IDを使った既存の認証認可が、AIエージェント相手でも同じように使えるのか」でした。MCPの認可仕様はOAuth 2.1ベースで比較的新しく、検証するまでは正直手探りでしたが、結論から言うとSpring Securityの標準機能で対応できました(詳細は実装の節でお話しします)。 もしSpring Bootでアプリケーションを開発・運用しているチームであれば、MCPサーバーの構築にSpring AIを選ぶメリットは大きいと思います。 技術スタック Java 21 Spring Boot 4.0 Spring AI 2.0 (spring-ai-starter-mcp-server-webmvc) Spring Security (OAuth2 Resource Server) Microsoft Entra ID (認可サーバー) AWS (CloudFront + ALB + ECS など) システム構成 既存のCMDBバックエンド(Spring Boot)に、MCPエンドポイント /mcp を追加した構成です。MCPサーバーのために新しく追加したインフラはありません。なお、図はMCPサーバーに関係する部分に絞っており、フロントエンドの配信経路などは省略しています。 実装 ここからは実装内容を紹介します。なお、記事中のコードは一部、省略・編集しています。 依存関係の設定 build.gradleにSpring AIのBOMとMCPサーバー用スターターを追加します。追加する依存関係はこれだけです。 ext { set('springAiVersion', "2.0.0") } dependencies { implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc' } dependencyManagement { imports { mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}" } } サーバー設定はapplication.ymlのみ MCPサーバーとしての設定はapplication.ymlに書くだけで、Javaの設定クラスは1つも作っていません。あわせて、JWT検証(OAuth2 Resource Server)の設定も抜粋します。 spring: security: oauth2: resourceserver: jwt: issuer-uri: https://login.microsoftonline.com/${TENANT_ID}/v2.0 ai: mcp: server: name: cmdb-mcp version: 0.0.1 instructions: >- CMDB は KINTO Technologies (略: KTC) 社内の構成管理データベースのこと。 プロダクト・チーム・リポジトリ・脆弱性などの社内構成情報を検索できる。 protocol: STATELESS type: SYNC stateless: mcp-endpoint: /mcp annotation-scanner: enabled: true 設定のポイントを2つ紹介します。 1つ目は instructions です。ここに書いた文章はMCPの初期化時に一度だけエージェントへ渡されます。「CMDBとは何か」のような前提知識をここに置いておくと、各ツールのdescriptionで同じ説明を繰り返さずに済み、ツール定義のトークンを節約できます。 2つ目は annotation-scanner です。これを有効にすると、後述する @McpTool の付いたメソッドが自動でツールとして登録されるため、ツール登録用のBean定義も不要になります。 @McpToolでツールの実装 ツールは @McpTool を付けたメソッドとして実装します。プロダクト検索ツールを例に、実装の全体像を紹介します。 @Component public class ProductMcpTools { private final ProductService productService; public ProductMcpTools(ProductService productService) { this.productService = productService; } @McpTool( name = "searchProducts", description = """ プロダクトを検索します。一覧・絞り込み・詳細取得はすべてこのツール\ (productId 指定で1件の詳細、未指定で一覧)。""", annotations = @McpTool.McpAnnotations( readOnlyHint = true, destructiveHint = false, idempotentHint = true, openWorldHint = false)) public ProductSearchMcpResult searchProducts( @McpToolParam(description = "プロダクトID。指定すると1件の詳細を返す。一覧結果の productId を渡す", required = false) Integer productId, @McpToolParam(description = "プロダクトの所属部署(グループID)で絞り込み", required = false) String groupId, @McpToolParam(description = "true で削除済みプロダクトを検索。削除済みはこの指定時のみ取得可(既定: false)", required = false) Boolean deleteFlag) { SearchProductServiceParameter parameter = new SearchProductServiceParameter(); parameter.setProductId(productId); parameter.setGroupId(groupId); parameter.setDeleteFlag(Boolean.TRUE.equals(deleteFlag)); // 既存のREST API用serviceをそのまま呼び出す SearchProductServiceResult result = productService.searchProduct(parameter); return productId != null ? ProductSearchMcpResult.ofDetail(result) : ProductSearchMcpResult.ofList(result); } } このクラスの役割はControllerと同じです。引数を組み立てて既存のserviceを呼び、結果をエージェント向けに整形するだけで、ビジネスロジックは置きません。検索ロジック本体は、REST APIのために実装済みの ProductService をそのまま呼んでいます。 これだけでMCPサーバーのツールが実装できてしまいます。 今回実装した検索ツール12個のほとんどは、既存のservice層を変更することなく再利用できました。REST APIのために実装してきたservice層を、MCPツールからそのまま再利用できるのが、既存アプリに組み込む構成のいちばんのメリットだと思います。 ツール 概要 searchProducts プロダクトの一覧・詳細検索 searchGroups 部署(グループ)の一覧取得 searchTeams チームとメンバーの検索 searchUsers ユーザーの検索 searchDomains ドメインの検索 searchGithubRepositories GitHubリポジトリの検索 searchVulnerabilitySummaries 脆弱性サマリの横断検索 searchVulnerabilityDetails 脆弱性詳細の検索 searchSbom SBOM(ソフトウェア部品表)の検索 searchEol EOL(サポート期限)の検索 searchArn AWSリソース(ARN)の検索 searchSchedules 開発環境のコスト削減を目的とした、AWSリソース起動停止スケジュールの検索 実装にあたって、3点補足します。 1つ目はツールヒント(annotations)です。MCPの仕様では、ヒントが未指定のツールは readOnlyHint=false ・ destructiveHint=true 、つまり「破壊的な操作をするかもしれないツール」というデフォルトで扱われます。エージェントが検索ツールの利用をためらわないよう、読み取り専用であることをヒントで明示しています。 2つ目は引数の形です。引数は1つのオブジェクトにまとめず、フラットに並べています。Spring AIはメソッドの引数をそれぞれinputSchemaのプロパティに変換するため、REST APIのようにパラメータクラスへまとめると、エージェントから見えるJSONスキーマに余計な階層ができてしまいます。既存のパラメータクラスの流儀とは意図的に変えている部分です。 3つ目はレスポンスの件数です。CMDBは、SBOMのように件数が膨大なデータも持っています(執筆時点で50万件超)。こうしたデータをそのまま返すと、エージェントのコンテキストを圧迫し、MCPの結果サイズ上限にも引っかかります。件数の多いデータを扱うツールでは絞り込み条件を必須にし、1ページ50件のページングと総件数(totalCount)の返却をあわせて実装しています。ページを分けても、データが足りなければエージェントがpageを変えて自分でツールを呼び直してくれるため、複数回の呼び出しで必要なデータを集められます。 認証認可 一番不安だった認証認可ですが、結論としては自前の認可サーバーを実装する必要はなく、既存のEntra IDをそのまま認可サーバーとして使えました。MCPの認可仕様の登場人物と流れは以下のとおりです。 sequenceDiagram participant Agent as AIエージェント<br/>(Claude Code等) participant MCP as CMDBバックエンド<br/>(MCPサーバー) participant Entra as Microsoft Entra ID Agent->>MCP: ① POST /mcp(トークンなし) MCP-->>Agent: ② 401 + WWW-Authenticate<br/>(メタデータの場所を案内) Agent->>MCP: ③ GET /.well-known/oauth-protected-resource MCP-->>Agent: ④ メタデータ応答<br/>(認可サーバー = Entra ID の場所) Agent->>Entra: ⑤ 認可コードフロー(PKCE)<br/>ブラウザでSSOログイン Entra-->>Agent: ⑥ アクセストークン(JWT) Agent->>MCP: ⑦ Bearerトークン付きでツール実行 MCP->>MCP: ⑧ JWT検証(署名・issuer・有効期限) MCP-->>Agent: ⑨ 検索結果 重要なのが③④のProtected Resource Metadata (RFC 9728)です。MCPサーバーは /.well-known/oauth-protected-resource でメタデータを公開し、「このサーバーの認可サーバーはどこか」をエージェントに教えます。エージェントはこれを読んで、自動的にEntra IDのログイン画面(ブラウザのSSO)へユーザーを誘導してくれます。 Spring側の実装は、ほぼ標準機能の組み合わせで済みました。 JWT検証: Spring SecurityのOAuth2 Resource Server。application.ymlの issuer-uri の設定だけで、Entra IDが発行したJWTの検証が動きます メタデータの公開: Spring Securityが標準でRFC 9728をサポートしており( OAuth2ProtectedResourceMetadataFilter )、認可サーバーの場所などをJavaの設定コード数行のカスタマイズだけで公開できます 1点だけインフラ側の対応が必要でした。RFC 9728のメタデータはドメインルートの /.well-known/ 配下で公開する必要がありますが、バックエンドは /api のコンテキストパスで動いています。そこで、CloudFrontで /.well-known/oauth-protected-resource* へのリクエストをALB(オリジンパス /api )にルーティングして解決しました。 ちなみに、Spring AIのドキュメントには、MCPサーバーの認証認可をコミュニティモジュールの mcp-security で実現する方法も紹介されています。 なお、AIエージェントによってOAuth周りの実装には微妙に差があり、あるエージェントでは問題なく接続できても、別のエージェントではエラーになることがありました。MCPサーバーを社内に展開する場合は、利用が想定されるエージェントで早めに接続確認をしておくと安心です。 AOPによる利用状況ログ 社内展開後の利用状況を把握するため、ツール呼び出しのログも仕込みました。 @McpTool アノテーションを対象にしたAOPのアスペクトを1つ書くだけで、ツールクラスが今後増えても自動でログ対象になります。ログにはツール名・ユーザーID(JWTのクレームから取得)・検索条件を構造化フィールドとして出力し、ダッシュボードで「誰がどのツールをどんな条件で使っているか」を集計できるようにしています。 実行結果 最後に、社内情報のためお見せできる部分が少なくなってしまいますが、実際の使用感をご紹介します。 接続 (Claude Codeの場合) 以下のコマンドでClaude CodeにCMDBのMCPを登録します。 claude mcp add --transport http cmdb-mcp https://xxxxx/api/mcp \ --client-id xxx \ --callback-port 29352 サーバー一覧でCMDBのMCPを選択し、認証を開始します。 ブラウザで認証をします。 ツールを呼び出してみる 試しにSpring Bootで構築しているプロダクト一覧を出してみます。 「Spring Bootで構築しているプロダクトを洗い出して」と聞いてみると、CMDBのMCPサーバーにあるSBOM検索ツールを spring-boot-autoconfigure のコンポーネントを検索条件に入れて呼び出します。 こちらがレスポンスの一部になります。全部で31プロダクトがSpring Bootを採用しているそうです。多いですね。 次のステップ 更新系ツールへの拡張: 現在は検索専用ですが、登録・更新系のツールも要望があります。誤操作のリスクが検索とは段違いなので、権限制御や実行前の確認の仕組みとセットで検討していきたいと思います さいごに 今回は、Spring Boot製の社内CMDBをSpring AIでMCPサーバー化し、AIエージェントから社内の構成情報を検索できるようにした取り組みについてお話ししました。 MCPサーバーの構築はPythonやTypeScriptの情報が多いですが、既存のSpring Bootアプリを持っているなら、service層も認証もインフラもそのまま使い回せるSpring AIは十分有力な選択肢です。この記事が、JavaでのMCPサーバー構築を検討している方の参考になれば嬉しいです。 AIエージェントを中心とした働き方はまだまだ進化していくと思うので、キャッチアップと実践を繰り返しながら、Platform Engineeringチームとしてツールの整備を進めていきたいと思います。

動画

書籍