MCPサーバー開発記

MCPサーバー開発記

はじめに

AIチャットボット機能を追加するプロジェクトが始まり、私はその中でも「LLMからシステムデータを参照できるようにする」部分を担当することになりました。LLMがユーザーの質問に答えるには、結局のところ私たちのシステムが持つリアルタイムデータを参照する必要があります。それをどのような方法で接続するかが、私の役割でした。

MCP(Model Context Protocol)という概念自体は、すでに知っていました。LLMに「ツール(Tool)」を追加して外部システムと相互作用させる標準プロトコルであることや、最近では複数のAIサービスがこれを採用していることも、ニュースやドキュメントで目にしたことはありました。しかし、それは「知っている」のであって、「作った」わけではありませんでした。概念を説明できることと、実際にSpring Bootプロジェクトを1つMCPサーバーとして構築し、運用可能なレベルまで仕上げることの間には、思った以上に大きな隔たりがありました。

この記事は、その隔たりを埋めていく中で学んだこと、そしてその過程でSpring AIが実際にどれほど多くのことを肩代わりしてくれるのかをまとめた記録です。

1. 問題 — 概念は分かるが、どこから作り始めればよいのか分からない

1-1. このプロジェクトで私が解決しなければならなかった問題

私たちのチームのAIチャットボットアーキテクチャは、次のように整理されていました。

UI → AI 서버 → LLM
        ↓ (MCP)
     MCP 서버
        ↓
MSA로 각각의 데이터를 수집하고 있는 서비스들

ユーザーが「今、データを収集できていない機器はある?」と質問すると、AIサーバーがLLMを通じて「この質問に答えるにはサーバー一覧を取得するツールが必要だ」と判断し、そのツール呼び出しをMCP経由で私が作ったMCPサーバーに渡します。MCPサーバーはそのリクエストを実際のサーバー管理サービスへのREST API呼び出しに変換して実行し、結果をLLMが理解しやすい形に整理して返します。

私が担当したのは、まさにこのMCPサーバーでした。つまり、「既存の 社内 システムの 機能を, AIが 使え る 形 に 新たに 公開する サーバー」をゼロから作る仕事でした。

1-2. 開発しなければならないもの

MCP仕様書を読むと、initialize、tools/list、tools/callといったJSON-RPCメッセージがやり取りされるプロトコルであることは理解できます。しかし、実際に開発を始めようとすると、具体的な疑問が次々に出てきました。

  • このプロトコルをSpring Bootのコントローラーとして直接実装しなければならないのか? JSON-RPCのルーティング、セッション管理(Mcp-Session-Id)、Streamable HTTPトランスポートまで、すべて手作業で実装するなら、本末転倒になりかねません。
  • 「ツール(Tool)」は、コード上でどのように表現すればよいのか? 単純に1つのメソッドが1つのツールになるのか、それとも別途インターフェースを実装しなければならないのか?
  • ツールの入出力仕様(JSON Schema)は誰が作成するのか? MCPクライアント(LLM側)は、ツールを呼び出す前に、そのツールがどのようなパラメータを受け取るのかを知る必要があります。これを毎回手作業でスキーマとして記述しなければならないなら、ツールを1つ追加するたびに膨大なボイラープレートが発生しそうでした。
  • 私たちのサービスには、すでにサーバー管理や機器管理など複数のバックエンドサービスがあり、それぞれが独自のREST APIの規約とクライアントライブラリを持っています。これらをMCPツールでラップする際、このプロジェクト独自の構造をどのように設計すれば、後から他のチームメンバーが別のサービスを追加するときにも混乱しないでしょうか?

まとめると、「MCPとは何か」が問題だったわけではありません。「Spring Boot エコシステム の中で これを どのように 構造化し 設計し 実装し なければならないのか」が本当の問題でした。

2. 解決策 — Spring AIによるMCP Serverのサポート

この問題を、Spring AI(Spring陣営による公式のAI統合プロジェクト)というフレームワークを使って解決することにしました。Spring AIはMCPサーバーを実装するためのスターターを提供しており、大きく2つのことを肩代わりしてくれます。

  • プロトコル レベル の処理: initialize、tools/list、tools/callといったMCPプロトコルメッセージのルーティングとセッション管理を、フレームワークが担ってくれます。
  • アノテーション 基盤 ツール など録:開発者は通常のメソッドに「これは1つのツールである」というアノテーションを付けるだけで、残り(スキーマ生成、登録、呼び出しの接続)はフレームワークが自動的に処理します。

なぜこれを選んだのかは明確でした。

  • 既存 スタックとの 自然な 統合:私たちのバックエンドはすべてSpring Boot + Gradleで、各サービスのクライアントライブラリもSpringエコシステム内で動作します。MCPサーバーも同じスタックにすれば、チームメンバーは参入障壁なくすぐに理解できます。
  • アノテーション ベースなので ツール 追加 コストが 低い:新しいツールを1つ追加する作業が「メソッドを1つ作成 + アノテーションをいくつか付ける」だけで終わるなら、今後ほかのチームメンバーが別のドメインのツールを追加するときも、同じパターンを繰り返すだけで済みます。
  • プロトコルの 細部から 自由であること:JSON-RPCメッセージのフォーマット、セッション管理、エラー応答のフォーマットなどは、仕様に従って正確に実装しなければクライアント(LLM側のMCPクライアント)と正常に通信できません。このような低レベルの詳細をフレームワークが代わりに処理してくれることが決め手でした。

結果として、build.gradleに依存関係を1つ、application.ymlに設定を数行追加するだけで、このプロジェクトのMCPサーバーの骨格がすべて整いました。

spring:
  ai:
    mcp:
      server:
        name: my-mcp-server
        protocol: STREAMABLE
        type: SYNC
        annotation-scanner:
          enabled: true

3. 実装例 — Spring AIが実際に代わりに処理してくれること

ここからは、実際にコードを書きながら「なるほど、だからフレームワークを使うのか」と感じたポイントを整理しました。

3-1. MCP Inspectorで直接確認する

理論上はプロトコルが自動処理されるとしても、実際に目で確認してみないと安心できません。このとき使えるのが MCP Inspectorです。Anthropicが公式に配布しているWebベースのMCPサーバーのテスト/デバッグツールで、別途インストールしなくても、以下の1行ですぐに実行できます。

npx @modelcontextprotocol/inspector

上記のコードで実行すると、Web UIから開発中のツールをテストおよび確認できます。Servers 画面で Add Serversをクリックし、Transport Typeに Streamable HTTPを選択した後、開発中のサーバーのMCPエンドポイントアドレス(例:http://localhost:8088/mcp)を入力すると接続できます。接続後、Toolsタブには、アノテーションで登録しておいたツールの一覧が、名前、説明、入力スキーマまでそのまま表示されます。ツールを1つ選び、パラメータを入力して直接呼び出すこともできます。

この画面で実際にクリックしながら、プロトコルレベルでは正確にこのような流れが行われていることを確認できました。

  1. initializeリクエストを送信すると、サーバーがセッションを作成し、Mcp-Session-Idをレスポンスヘッダーに載せて返します。
  2. 以降のリクエストでは、このセッションIDをヘッダーに載せて送信すればよく、
  3. tools/listを呼び出すと、アノテーションで登録しておいたツールの一覧が、名前、説明、入力スキーマとともに自動的に返されます。
  4. tools/callで実際にツールを呼び出すと、作成したJavaメソッドが実行され、戻り値がMCPのレスポンスフォーマット(content、isErrorなど)で自動的にラップされます。

私が作成したコードには、このようなプロトコル処理ロジックが1行もありません。これを自分で実装していたら、プロジェクトの初めの数週間は、純粋に「MCP仕様の再実装」だけに費やしていたと思います。

テスト中に実際に混乱した点も1つありました。配列(List)型のパラメータを受け取るツールを呼び出す際、Inspectorの入力欄に値を1つだけ入力したところ、「integerが検出されましたが、arrayが期待されます」というスキーマ検証エラーが発生しました。サーバーから返されたスキーマ自体はarrayで正常だったのですが、入力欄には["value1", "value2"]のようにJSON配列として入力する必要があることを知らなかったのです。このようなことはドキュメントを見るだけでは分かりにくく、実際にツールを1つずつ呼び出してみて初めて分かる細部でした。

3-2. @McpTool / @McpToolParam — 1つのメソッドが1つのツールになる構造

実際に作成したツールの1つを簡略化すると、次のようになります。

@McpTool(
        name = "findServers",
        description = "서버 목록을 조회합니다. "
                + "이름 부분 문자열로 필터링할 수 있습니다.",
        annotations = @McpTool.McpAnnotations(
                title = "서버 목록 조회",
                readOnlyHint = true,
                destructiveHint = false,
                idempotentHint = true,
                openWorldHint = false
        )
)
public List<GatewaySummary> findGateways(
        @McpToolParam(description = "서버 이름 필터용 문자열", required = false)
        String nameFilter
) {
    // 내부적으로는 서버 관리 서비스의 기존 API 클라이언트를 그대로 호출
}

このコードが実行されると、フレームワークが自動的に次の処理を行います。

  • メソッドシグネチャ(String nameFilter)を分析して、JSON Schema({"type": "string"})を自動生成
  • descriptionは、LLMが「このツールをいつ使うべきか」を判断する根拠になります。つまり、この説明をどれだけ具体的に書くかが、実際にLLMがツールを適切に選択して使えるかどうかに直結します。
  • McpAnnotationsのreadOnlyHint、destructiveHint、idempotentHint、openWorldHintは、MCP仕様で定義された「このツールがシステムにどのような影響を与えるか」に関するメタデータです。私たちはすべて参照専用ツールなので、readOnlyHint=true、destructiveHint=falseに統一しました。このようなヒントは、クライアントが「このツールは安全に繰り返し呼び出してよいか」を判断するために使われます。
  • 戻り値の型も、別途変換コードを書くことなくJSONにシリアライズされ、レスポンスに含まれます。

つまり、私が実際に書いたコードは 「既存の API クライアントを 呼び出して 結果を 整理する 普通の Java メソッド」 だけです。MCPらしい部分はアノテーション数行だけで、残りは従来のSpring開発者が慣れ親しんだ方法そのものでした。

3-3. 複数のバックエンドを一貫した構造でラップする

社内にはすでに、サーバー管理、機器管理、データ収集、アラート/モニタリングをそれぞれ担当する複数のバックエンドサービスがあり、それぞれが独自のクライアントライブラリを持っています。このプロジェクトでは、それらをそのまま再利用しながら、ドメインごとにフォルダを分けてツールを構成しました。

tools/
├── server/    ServerTools
├── device/     DeviceModelTools
├── collect/    DataCollectionTools
└── alert/      AlarmTools, ConditionTools

この構造にした理由は、実用性を重視したためです。後から別のチームメンバーが新しいドメインのツールを追加するときに、「このフォルダの下に、似たパターンでクラスをもう1つ作ればよい」とすぐに分かるようにしたいと考えました。Spring AI側では、アノテーションの付いたBeanを自動的にスキャンしてツールとして登録してくれるため、この構造をどれだけきれいに設計できるかが、そのまま「次の担当者がどれだけ簡単に拡張できるか」に直結しました。

4. 導入結果と今後の課題

結果

  • サーバー管理、機器管理、データ収集、アラート/モニタリングの4つのドメインに、複数のMCPツールを構築しました。
  • 各ツールについて、MCP Inspectorとcurlを使った実際のプロトコルハンドシェイク(initialize → tools/list → tools/call)により、正常に動作することを確認しました。
  • 当初の企画書に記載されていた主要ドメインとの連携は、クラスタインフラ関連の項目を除き、すべて完了している状態です。

商用化に向けて改善するとよい点

  • 認証: 現在、MCPエンドポイントには別途認証を設定していません。社内ネットワーク内からのみAIサーバーが呼び出すことを前提に、まずは開発を進めましたが、実際に運用環境へデプロイする前に必ず追加すべき部分です。
  • 拡張: 今回構築した構造(ドメイン別のフォルダ、クライアントライブラリの再利用パターン)を他のチームメンバーに共有し、残りのドメインについても引き続き拡張していく予定です。

5. Spring AIはこのような場合におすすめです(メリット・デメリットの整理)

Spring AIでMCPサーバーを作ってみようと考えている方のために、実際に使ってみて感じたメリットとデメリットを整理します。

メリット

  • ボイラープレートが ほとんど ありません.JSON-RPCのルーティング、セッション管理、Streamable HTTPトランスポート、JSON Schemaの生成まで、すべてフレームワークが処理してくれます。開発者は、ツールとなるメソッドとアノテーションだけに集中すれば済みます。
  • 既存の Spring Boot 資産を そのまま 再利用することが でき ます.DI、設定管理、すでに作成済みのクライアントライブラリ、ロギングなど、使い慣れた方法をそのまま引き継げます。新しいフレームワークを一から学ぶ必要はありません。
  • 拡張が 簡単です.ツールを1つ増やすのが「メソッド1つ+アノテーション」なので、複数人で分担し、それぞれが担当ドメインを並行して開発するのにも適した構造です。

デメリット

  • バージョンが 急速に 上がります.それだけAPIが変わる可能性もあるということなので、最新バージョンをすぐにプロダクションで使う場合は、マイナーアップグレード1つにも注意が必要です。
  • フレームワークが 隠してくれる 分だけ, 低レベル部分を 直接 制御して みたい ときは もどかしく感じる ことが あります.標準的なフローから外れた特殊な要件(例:カスタムトランスポート、プロトコル拡張)がある場合は、むしろフレームワークの抽象化を取り除くほうが難しくなる可能性があります。

結論として、 「Spring Boot ベースの サービスで 素早く MCP サーバーを 立ち上げ たい」という目的であれば十分で、参入障壁も低く、直感的で非常に便利だと思います。

6. おわりに

今回のプロジェクトを通じて感じたのは、MCPサーバーの開発で本当に難しいのはプロトコルそのものではないということでした。プロトコルについてはSpring AIがほとんど代わりに処理してくれました。実際に多くの時間を費やしたのは、「LLMがこのツールをうまく選んで使うには、名前と説明をどのように付ければよいのか」「複数のバックエンドシステムをどのような基準で分けてツールとして公開すれば、他の人も混乱しないのか」といった、フレームワークでは代わりに判断できない設計上の判断でした。

MCPという概念を知っていることと、それを実際のサービス構造に落とし込むことは、明らかに異なる経験でした。今では、私たちのシステムに新しい機能が追加されるたびに、「これをAIが使えるようにするには、どのようにツールとして公開すればよいか」を自然に考えるようになりました。AIを社内システムに安全かつ一貫した方法で接続できるようにするこのような抽象化レイヤーは、今後AI機能が増えるほど、より重要になると思います。

sauce0127

Site footer