Java×Spring AIで始めるAIプログラミングの入門連載。前回はLLMを通じて外部ツールの実行が可能なTool Callingとその外部実行ツール自体を独立させて実装が可能なMCPという仕組みの流れについて説明しました。今回は、MCPサーバ自体の機能とSpring AIでMCPサーバを実装する流れについて解説します。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
前回説明したように、MCPサーバ(Model Context Protocol Server)は、LLM(大規模言語モデル)が外部の機能やデータにアクセスするための「共通の窓口」です。ここでは、その技術的な仕組みと通信の特性に絞って整理します。
MCPの仕様は公式ドキュメントで定義されており、通信メッセージの形式としてJSON-RPC(Remote Procedure Call)を採用しています。JSON-RPCは、「機能ごとにURL(エンドポイント)を分ける」REST APIとは異なり、「単一の窓口に対して、実行したいメソッド名とともに指定したJSONを送る」という考え方です。この違いを理解しておくことで、開発時の設計ミスやトラブル発生時の切り分けがスムーズになります。
MCPは現在普及の初期段階にあります。クライアント側の実装状況にはばらつきがあり、仕様に含まれる全ての機能が常に利用できるとは限らない点に注意が必要です。
MCPクライアントとMCPサーバがやりとりする方法は、大きく分けてSTDIO(Standard Input/Output)とHTTPの2種類があります(表1)。
| 方式 | 特徴 |
|---|---|
| STDIO | 標準入力・標準出力を利用。ローカル環境での実行に適しており、実装が極めてシンプル |
| HTTP | ネットワーク越しでの通信が可能。Web開発者になじみがあり、通信内容の可視化がしやすい |
| 表1 通信プロトコルの違い | |
HTTP方式を選択する場合、以下の2つの技術要素が組み合わせられます。
図1は、その通信の流れを示したものです。
もちろん、全てのMCPサーバが必ずしもSSEによる非同期通知を実装する必要はありません。従来のようなシンプルな「1リクエスト=1レスポンス」の形式でも運用可能です。特にSpring AIを利用する場合、これらの通信形態を柔軟に選択できる分、「どのレベルまで実装すべきか」という設計・運用の判断が求められ、開発をやや複雑に感じさせる要因にもなっています。
SSEを利用する際、一般的には「HTTP GET」を用いるケースが多いですが、MCPの仕様では「HTTP POST」でSSEを利用します。「SSE=GET」という先入観があると、パケットキャプチャーやデバッグ時に混乱を招く可能性があります。MCPサーバ開発においては、「POSTでSSEを確立する」という特殊な挙動を念頭に置いておきましょう。
MCPは単なる「外部ツールの実行手段」ではありません。LLMと外部世界をつなぐ共通プロトコルとして、役割に応じた3つの機能が定義されています(表2)。
| 機能 | 役割 | 具体的なイメージ |
|---|---|---|
| Tool(ツール) | 処理の実行 | 計算、API呼び出し、ファイル書き込みなど、具体的な「アクション」を行う |
| Resource(リソース) | 情報の提供 | データベースのレコードやログファイルなど、LLMが参照すべき「データ」を読み込む |
| Prompt(プロンプト) | 定型文の管理 | 特定のタスクに最適な指示文(テンプレート)を管理・提供する |
| 表2 MCPの役割 | ||
このように役割を分けることで、LLMに対して「これは実行すべき処理なのか、それとも参照すべきデータなのか」という意図を明確に伝えることができ、外部連携の設計が整理しやすくなります。
ただし、MCPサーバがこれら3つの機能を持っていても、実際に使えるかどうかはMCPクライアント(利用側のアプリ)の実装に依存します。現時点では、MCPクライアントの多くは「Tool」の実行のみに対応しています。「Resource」や「Prompt」をUI(ユーザーインタフェース)でどう表現し、LLMにどう扱わせるかというノウハウは、今まさに蓄積されている段階です。
Spring AIでMCPサーバを開発する際、各種アノテーションを使用してこれらの機能を実装します。ここで重要なのは、設定項目(nameやdescriptionなど)や制約が、Spring AI独自の都合というよりも、MCPの仕様に強く由来している点です。Spring AIはあくまで「MCP仕様に沿って実装するための枠組みを提供している」という理解を持っておくと、混乱しにくくなります。
Spring AIでは、アプリケーションのWeb基盤として「Spring Web(WebMVC)」と「Spring WebFlux」のどちらかを選択できます。MCPサーバにおいても、基本的には用途や経験に合わせて選んで問題ありませんが、HTTP方式でSSEをどう扱うかが選択のポイントとなります。
ビルド設定(build.gradle.kts)では、リスト1のように共通のスターターに加えて、選択した基盤に応じたライブラリを組み込みます。
dependencies {
implementation("org.springframework.ai:spring-ai-starter-mcp-server")
// 同期型のアプリケーションを作りたい場合に向いている
implementation("org.springframework.ai:spring-ai-starter-mcp-server-webmvc")
// ノンブロッキングIO型のアプリケーションを作りたい場合に向いている
// implementation("org.springframework.ai:spring-ai-starter-mcp-server-webflux")
}
依存関係としては、MCPサーバ本体に加えて、WebMVC向けかWebFlux向けかを選ぶ形になります。同期的な処理でコードを分かりやすく書きたい場合はWebMVC、ストリーミングを含む長時間接続や同時接続を効率的にさばきたい場合にはWebFluxを選択するとよいでしょう。
Spring AIのMCPサーバでは、通信プロトコルやメソッドの実行スタイル(同期・非同期)を、設定ファイル(application.propertiesやyml)で柔軟に切り替えることができます。リスト2のプロパティが、MCPサーバの振る舞いを決定する重要なキーとなります。
spring.ai.mcp.server.protocol=STATELESS #spring.ai.mcp.server.protocol=STREAMABLE spring.ai.mcp.server.type=SYNC #spring.ai.mcp.server.type=ASYNC
spring.ai.mcp.server.protocolが通信プロトコルを、spring.ai.mcp.server.typeが提供するメソッドの実行スタイル(同期・非同期)を、それぞれ表します。spring.ai.mcp.server.protocolに設定できる値は以下の通りです。
設定値としてのSSEは現在では非推奨であり、基本的に選ぶ必要はありません。MCPサーバのストリーミング特性を生かす設定はSTREAMABLEです。一方、STATELESSはレスポンスがSSEやセッション管理などもなく、リクエストとレスポンスが対になるため、デバッグや通信の追跡が非常に容易です。
spring.ai.mcp.server.typeには、SYNCかASYNCかを指定します。ASYNCではReactiveな戻り値(Mono/Fluxなど)を使うメソッドが有効になり、SYNCでは同期型の戻り値を使うメソッドが有効になります。
一般的には、開発の目的に応じて、主に以下の2つの組み合わせが選ばれます。
本記事のサンプルは後者(WebMVC、STATELESS、SYNC)を前提として説明します。
Spring AIでは、MCPサーバを手早く構築するために、MCP向けのアノテーションが用意されています。用意されているのは以下の3つです。
これらのアノテーションをメソッドに付与すると、Spring Bootがそれらを検出し、MCPサーバが提供する機能として自動的に公開します。つまり、開発者は「何を提供したいか」をアノテーションで宣言するだけで、公開に必要なコードを細かく書かずに済みます。前回までに紹介した方法と比べても、MCPサーバの実装が一段シンプルになります。
アノテーションによる自動検出は非常に便利ですが、「設定(type/protocol)とメソッドのシグネチャ(戻り値)が一致していること」が前提となります。以下のように条件に合わないメソッドは、たとえアノテーションが付いていても自動的にスキップされるため注意が必要です。
「アノテーションを付けたのに機能が公開されない」という場合は、アプリケーション起動時のログを確認してください。不一致がある場合、以下のようなWARN(警告)ログが出力されます。
WARN [main] o.s.mcp.McpPredicates - SYNC Providers don't support reactive return types. Skipping method public reactor.core.publisher.Mono jp.enbind.mcp.service.FileToolAsyncService.getDirectoryAsync() with reactive return type class reactor.core.publisher.Mono
それでは、実際にアノテーションを使ってMCPサーバの機能を実装してみましょう。今回のサンプルでは、指定したフォルダ配下のファイルを検索し、その結果(見つかったファイル)を返すMCPツールを作成します。実装の全体はサンプルコードに譲り、ここではリスト3を通じて「MCPとして公開するために、どのようにメソッドを定義するか」に焦点を当てます。
// 各アノテーションのクラスが使えるようにする
import org.springaicommunity.mcp.annotation.*;
import org.springframework.stereotype.Component;
// (1) Spring Bootから見えるようにする
@Component
public class FileToolService {
// 省略
// (2) McpToolアノテーションの定義
@McpTool(
name = "find_file_by_name_pattern_sync",
title = "ファイルの検索",
description = "ワークスペースや業務に関するファイル名に関して検索があった場合に、その条件に一致するファイルURI(file-content://で始まる)のリストを提供します。"
)
public List<String> findFileByNamePatternSync(
// (3) 引数の使い方
@McpToolParam(description = "ファイル名に含まれれる文字列やパターン。**/*.txtなどのようにして検索する事も可能")
String pattern){
var dir = config.getDir();
try {
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);
List<String> fileList = Files.walk(dir.toPath())
.filter(Files::isRegularFile)
.filter(path -> matcher.matches(dir.toPath().relativize(path)))
.map(Path::toString)
.toList();
List<String> fileIdList = new ArrayList<>();
for(String file : fileList){
String filename = dir.toPath().relativize(Path.of(file)).toString();
fileIdList.add("file-content://" + filename);
}
return fileIdList;
} catch (IOException e) {
log.error("Error searching files: ", e);
return List.of();
}
}
}
}
ポイントは大きく3つです。
まず、クラスに@Componentを付与してSpring Bootの管理下に置きます。これにより、起動時にアノテーションが付いたメソッドが自動的にスキャン・登録されます。
@McpTool内の項目は、開発者のためのメモではなく、LLMがツールを選ぶための判断材料です。
引数に@McpToolParamを付与することで、LLMがその引数にどのような値を渡すべきかを指示できます。「どのようなパターン(例:**/*.txt)が使えるか」を明記することで、LLMが正しい形式で値を生成しやすくなります。これによって、LLMが引数を埋める精度が上がり、想定外の値で呼ばれるリスクも下げられます。
続いて、MCPサーバが提供できる機能のうち「Resource(リソース)」を試してみます。Toolが「処理の実行」だとすると、Resourceは「参照すべきデータを取り出すための機能」です。例えば、ファイル内容や設定情報、ナレッジの断片など、LLMが参照したい情報を「リソース」として公開するイメージです。それらの役割を想定して、ファイルの内容をMCPリソースとして取得するためのコードを作成してみたのが、リスト4です。
@Service
public class FileResourceService {
// (1) McpResourceアノテーションの定義
@McpResource(
uri = "file-content://{fileId}",
name = "get_file_content",
description = "ファイルIDからファイル内容を取得する")
public ReadResourceResult getFileContent(String fileId){
String fileData = "ファイルの内容の参照を実際にはここで実装";
// (2) コンテンツタイプなども含めて返す事ができる
return new ReadResourceResult(List.of(
new McpSchema.TextResourceContents(
"file-content://" + fileId,
"text/plain",
fileData
)
));
}
}
@McpResourceアノテーションを付与することで、メソッドをMCPリソースとして公開します。nameやdescriptionは、LLMが「このリソースには何の情報が含まれているか」を判断するための重要な手掛かりとなります。
uri属性にはfile-content://{fileId}のようなテンプレート形式を使用できます。これにより、特定のIDに基づいた動的なリソース参照が可能になります。前段のToolが返す識別子とこのURI形式を統一しておくと、Toolの出力とResourceの入力が自然につながり、システム全体の設計の見通しが良くなります。
取得したデータは、このクラスにラップして返却します。テキストデータだけでなく、MIMEタイプ(例:text/plain)を含めることができ、LLMがデータの形式を正しく解釈する助けとなります。
ここまでMCPリソースを説明してきましたが、現時点ではMCPクライアント側が、Resourceを十分に活用できる環境が整っていない場合があります。本稿では参考として取り上げましたが、現時点では、これらの機能はMcpToolとして実装した方が利用しやすいでしょう。
MCPサーバ側にToolやResourceを実装できたら、次はMCPクライアント側から正しく呼び出せるかを確認しましょう。動作確認の手順は幾つかありますが、まずはHTTPリクエストを直接送信して、サーバの挙動をダイレクトに観察するのが最も確実です。
MCPの通信メッセージ形式はJSON-RPCです。一般的なREST APIのようにパス(URL)を機能ごとに分けるのではなく、単一のエンドポイントに対してJSONを送信し、methodとparamsによって「どの機能を実行するか」を指示します(リスト5)。
{
"jsonrpc": "2.0",
"id": 20,
"method": "tools/call",
"params": {
"name": "find_file_by_name_pattern_sync",
"arguments": {
"pattern": "**/*.java"
}
}
}
methodが「MCPとしての操作名」を表します。「tools/call」でToolの実行を意味します。また、paramsで実行対象(ツール名など)と引数を表します。
ここまで用意できたら、以下のCURLコマンドでリクエストを実行してみましょう。
$ curl -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d @client/find_file.json http://127.0.0.1:8080/mcp
レスポンスは状況に応じてSSEとして返します。よって、リクエスト時のAcceptヘッダにはtext/event-streamを含めておく必要があります(注)。
注:今回は設定によりSSEになりませんが、リクエストを受け付ける際の制限として必要なようです。
ツールが正常に実行されると、以下のようなJSON-RPC形式のレスポンスが返ってきます。
{
"jsonrpc": "2.0",
"id": 20,
"result": {
"content": [
{
"type": "text",
"text": "[\"file-content://src/main/java/jp/enbind/mcp/MainApplication.java\",
\"file-content://src/main/java/jp/enbind/mcp/config/FileServiceConfiguration.java\"
,// (省略)
}
],
"isError": false
}
}
このように、実行結果がresult.contentの中に格納されていれば成功です。ただし、このHTTPによる確認は、あくまで「疎通と最低限のロジック実行」にフォーカスした簡易的なものです。本来、MCPプロトコルでは正式な通信を開始する前に、クライアントとサーバ間で互いの機能を通知し合う「初期化フェーズ(initialize)」などのシーケンスが必要です。しかし、開発中のデバッグにおいては、このように単発のリクエストで「まず動かして確かめる」ことができる点は、実装のサイクルを速める大きな助けとなります。
本稿では、MCPが過渡期にある中で開発者が迷わないよう、まずは「動くものを作り、仕組みを体感する」ための最短ルートを提示しました。具体的には、以下の3点に絞って「動くものを作る」ための最短ルートを提示しています。
こうした土台を固めることで、複雑なMCPの構造をシンプルに捉えられるようになります。
構築の難しさはSpring AIそのものよりも、「周辺の利用環境がまだ発展途上である」という点にあります。ベストプラクティスが確立されるのはこれからのフェーズですが、未成熟な今だからこそ、開発者の試行錯誤がダイレクトに成果につながる面白さがあります。どの情報を公開し、どうLLMに最適化するか。正解が定まり切っていない中で自分なりの最適解を模索するプロセスにこそ、MCPに取り組む醍醐味(だいごみ)が詰まっています。
次回は、作成したMCPサーバを外部ツールから利用する場合の注意点や、MCPクライアント側の実装コードについて解説します。
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・X: @WingsPro_info(https://x.com/WingsPro_info)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.