MCPとは何か、そしてJava開発者が気にすべき理由
あらゆるAIチュートリアルでは、チャットボットの作り方が紹介されています。システムプロンプトを渡して、LLMに接続して、返事をしてもらう。けれど、在庫を確認したり、支払い状況を問い合わせたりといった「実際のこと」をAIにやらせる必要が出た瞬間、独自のHTTPクライアントを書いて、つぎはぎのコードを組み上げることになります。
MCP(Model Context Protocol)がそれを解決します。これは、AIエージェントがHTTP経由で外部ツールを発見して呼び出せるようにする標準プロトコルです。USB for AIだと思ってください。1つ挿せば、どんなデバイスでも使える。どんなエージェントでも、どんなMCPサーバーに接続しても、独自の統合コードなしでそのツールを利用できます。
この連載は、Java開発者の視点からMCPを扱います。理論ではありません。私はマイクロサービス環境で、本番稼働しているMCPサーバーを4つ運用しており、実際のコードをお見せします。
MCPが解決する問題
Kafkaを使って、5つのマイクロサービスをサーガパターンで連携させています。私は、それらすべてにまたがるデータを問い合わせる必要があるAIエージェントを作りました。MongoDBから注文の詳細、PostgreSQLから支払い状況、別のPostgreSQLから在庫レベル、さらに4つ目のデータベースから製品カタログです。
MCPがなければ、選択肢は良くありません。各サービス向けにHTTPクライアントを書けます。クライアントが4つ、DTOのセットが4つ、エラーハンドリング戦略が4つ。サービスが新しい問い合わせを追加するたびに、そのクライアントを更新する必要があります。エージェントと、通信するすべてのサービスとの間に強い結合が生まれます。
あるいは、LangChain4jの@Toolアノテーションを使うこともできます。ですが@Toolは、同じJVM内のメソッドでしか動きません。私のエージェントは別のサービスに存在しています。在庫データは別にあります。
MCPの仕組み
MCPは、接続ライフサイクルにServer-Sent Events(SSE)を用い、HTTP上でJSON-RPCを使います。フローには3つのフェーズがあります。
発見(Discovery)。 クライアントがサーバーのSSEエンドポイントに接続します。initializeリクエストと、tools/listリクエストを送信します。サーバーは、利用可能なツールをすべて返します。ツール名、説明、パラメータのスキーマを含みます。
呼び出し(Invocation)。 エージェントがデータを必要とすると判断したとき、LLMがツール呼び出しを生成します。MCPクライアントは、ツール名と引数を一緒にしてtools/callリクエストを送ります。サーバーがツールを実行し、結果を返します。
ステートレス(Statelessness)。 各ツール呼び出しは独立しています。サーバーは呼び出し間で状態を保持しません。セッションが存在するのは、SSE接続のライフサイクル中だけです。
具体的には、次のように見えます:
Agent → SSE connect to http://localhost:8092/sse → gets sessionId
Agent → POST /mcp/message?sessionId=xxx → {"method": "initialize", ...}
Agent → POST /mcp/message?sessionId=xxx → {"method": "tools/list", ...}
Server responds → ["getStockByProduct", "getLowStockAlert", "checkReservationExists"]
Agent → POST /mcp/message?sessionId=xxx → {"method": "tools/call", "params": {"name": "getStockByProduct", "arguments": {"productCode": "COMIC_BOOKS"}}}
Server responds → {"available": 600}
クライアント側にSDKは不要です。これはJSONボディを伴うHTTPリクエストです。curlでテストすることもできます。
MCPとREST APIの違い
目を細めれば、MCPは追加のステップがあるREST APIのように見えます。どちらもHTTP経由で機能を公開しています。どちらもJSONを使います。では、RESTのエンドポイントを直接呼べばいいのでは?
理由は3つあります。
ツールの発見(Tool discovery)。 REST APIはドキュメントが必要です。誰かがSwaggerの仕様を読み、クライアントを書きます。MCPサーバーは実行時に自分の機能を宣伝します。エージェントが接続すると、利用可能なツール、受け取るパラメータ、そしてそれをいつ使うべきか(descriptionフィールド経由)を即座に理解できます。
LLM統合(LLM integration)。 LangChain4jは、MCPのツールスキーマをLLMが理解できるfunctionDeclarationオブジェクトに変換します。LLMはツールの説明を読み、それを使うタイミングを判断し、正しい引数を生成します。ルーティングのロジックを書く必要はありません。
疎結合(Decoupling)。 REST APIに新しいツールを追加するには、クライアントを更新する必要があります。MCPサーバーに新しいツールを追加する場合、次回の接続時にエージェントがそれを見つけます。エージェント側での変更はゼロです。
詳細:プロトコル
MCPは3つのJSON-RPCメソッドを使います。
initializeは接続を確立し、機能をネゴシエーションします:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"clientInfo": { "name": "my-agent",
"version": "1.0.0" },
"capabilities": {} }
}
tools/listは利用可能なすべてのツールを返します:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
tools/callは特定のツールを呼び出します:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "getStockByProduct",
"arguments": { "productCode": "COMIC_BOOKS" }
}
}
}
レスポンスは常に、content が TextContent オブジェクトの配列である CallToolResult として返ってきます。シンプルで予測しやすいです。
Java のエコシステム
Java では、MCP を扱う 2 つのライブラリがあります。
サーバー側: io.modelcontextprotocol.sdk:mcp:1.1.0。これは公式の MCP Java SDK です。transport、tools、server info を使って McpSyncServer を作成します。JSON-RPC プロトコルと SSE のライフサイクルを処理します。
クライアント側: LangChain4j の langchain4j-mcp モジュールです。各サーバーの SSE URL を指す McpClient インスタンスを作成します。次に、それらを McpToolProvider でラップして、エージェントビルダーに渡します。
私の経験では、どちらも安定していて、本番投入に対応しています。サーバー SDK は軽量です(依存関係が 1 つだけ)。クライアント側は LangChain4j が担当するので、すでにエージェントのためにそれを使っているなら、追加でインストールするものはありません。
次に何をするか
次回の記事では、Spring Boot のマイクロサービスを MCP サーバーにする手順を、ステップごとに説明します。payment-service からの実際のコードです。ツールの定義、transport のセットアップ、そして AI エージェントに接続する前に curl でテストするところまで示します。
