MCPサーバの機能と実装の肝――仕組みと構築の流れをSpring AIで理解するSpring AIで始める生成AIプログラミング(9)

Java×Spring AIで始めるAIプログラミングの入門連載。前回はLLMを通じて外部ツールの実行が可能なTool Callingとその外部実行ツール自体を独立させて実装が可能なMCPという仕組みの流れについて説明しました。今回は、MCPサーバ自体の機能とSpring AIでMCPサーバを実装する流れについて解説します。

» 2026年01月29日 05時00分 公開
「Spring AIで始める生成AIプログラミング」のインデックス

連載:Spring AIで始める生成AIプログラミング

本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。


MCPサーバについて

 前回説明したように、MCPサーバ(Model Context Protocol Server)は、LLM(大規模言語モデル)が外部の機能やデータにアクセスするための「共通の窓口」です。ここでは、その技術的な仕組みと通信の特性に絞って整理します。

通信の基本仕様

 MCPの仕様は公式ドキュメントで定義されており、通信メッセージの形式としてJSON-RPC(Remote Procedure Call)を採用しています。JSON-RPCは、「機能ごとにURL(エンドポイント)を分ける」REST APIとは異なり、「単一の窓口に対して、実行したいメソッド名とともに指定したJSONを送る」という考え方です。この違いを理解しておくことで、開発時の設計ミスやトラブル発生時の切り分けがスムーズになります。

【補足】発展途上の技術であることの留意点

  MCPは現在普及の初期段階にあります。クライアント側の実装状況にはばらつきがあり、仕様に含まれる全ての機能が常に利用できるとは限らない点に注意が必要です。

MCPサーバの通信方式

 MCPクライアントとMCPサーバがやりとりする方法は、大きく分けてSTDIO(Standard Input/Output)とHTTPの2種類があります(表1)。

方式 特徴
STDIO 標準入力・標準出力を利用。ローカル環境での実行に適しており、実装が極めてシンプル
HTTP ネットワーク越しでの通信が可能。Web開発者になじみがあり、通信内容の可視化がしやすい
表1 通信プロトコルの違い

 HTTP方式を選択する場合、以下の2つの技術要素が組み合わせられます。

  • JSON-RPC:単一のエンドポイントURLに対し、実行内容を指示するメッセージを送信する
  • SSE(Server-Sent Events):「1リクエストに対して1レスポンス」で終わらない場合(処理の進捗通知やストリーミング回答など)に使用される。サーバからクライアントへリアルタイムに情報をプッシュ配信する仕組み

 図1は、その通信の流れを示したものです。

図1 MCPサーバとクライアントの通信シーケンス図 図1 MCPサーバとクライアントの通信シーケンス図

 もちろん、全てのMCPサーバが必ずしもSSEによる非同期通知を実装する必要はありません。従来のようなシンプルな「1リクエスト=1レスポンス」の形式でも運用可能です。特にSpring AIを利用する場合、これらの通信形態を柔軟に選択できる分、「どのレベルまで実装すべきか」という設計・運用の判断が求められ、開発をやや複雑に感じさせる要因にもなっています。

【補足】POSTでSSEを確立する

 SSEを利用する際、一般的には「HTTP GET」を用いるケースが多いですが、MCPの仕様では「HTTP POST」でSSEを利用します。「SSE=GET」という先入観があると、パケットキャプチャーやデバッグ時に混乱を招く可能性があります。MCPサーバ開発においては、「POSTでSSEを確立する」という特殊な挙動を念頭に置いておきましょう。

MCPサーバが提供する機能

 MCPは単なる「外部ツールの実行手段」ではありません。LLMと外部世界をつなぐ共通プロトコルとして、役割に応じた3つの機能が定義されています(表2)。

機能 役割 具体的なイメージ
Tool(ツール) 処理の実行 計算、API呼び出し、ファイル書き込みなど、具体的な「アクション」を行う
Resource(リソース) 情報の提供 データベースのレコードやログファイルなど、LLMが参照すべき「データ」を読み込む
Prompt(プロンプト) 定型文の管理 特定のタスクに最適な指示文(テンプレート)を管理・提供する
表2 MCPの役割

 このように役割を分けることで、LLMに対して「これは実行すべき処理なのか、それとも参照すべきデータなのか」という意図を明確に伝えることができ、外部連携の設計が整理しやすくなります。

 ただし、MCPサーバがこれら3つの機能を持っていても、実際に使えるかどうかはMCPクライアント(利用側のアプリ)の実装に依存します。現時点では、MCPクライアントの多くは「Tool」の実行のみに対応しています。「Resource」や「Prompt」をUI(ユーザーインタフェース)でどう表現し、LLMにどう扱わせるかというノウハウは、今まさに蓄積されている段階です。

【補足】Spring AIでの実装における考え方

 Spring AIでMCPサーバを開発する際、各種アノテーションを使用してこれらの機能を実装します。ここで重要なのは、設定項目(nameやdescriptionなど)や制約が、Spring AI独自の都合というよりも、MCPの仕様に強く由来している点です。Spring AIはあくまで「MCP仕様に沿って実装するための枠組みを提供している」という理解を持っておくと、混乱しにくくなります。

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")
}
リスト1 依存関係の設定例(server/build.gradle.ktsの抜粋)

 依存関係としては、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
リスト2 設定例(server/src/main/resources/application.propertiesの抜粋)

 spring.ai.mcp.server.protocolが通信プロトコルを、spring.ai.mcp.server.typeが提供するメソッドの実行スタイル(同期・非同期)を、それぞれ表します。spring.ai.mcp.server.protocolに設定できる値は以下の通りです。

  • SSE(非推奨)
  • STATELESS(SSEを使わない方式)
  • STREAMABLE(SSEを使う方式)

 設定値としてのSSEは現在では非推奨であり、基本的に選ぶ必要はありません。MCPサーバのストリーミング特性を生かす設定はSTREAMABLEです。一方、STATELESSはレスポンスがSSEやセッション管理などもなく、リクエストとレスポンスが対になるため、デバッグや通信の追跡が非常に容易です。

 spring.ai.mcp.server.typeには、SYNCかASYNCかを指定します。ASYNCではReactiveな戻り値(Mono/Fluxなど)を使うメソッドが有効になり、SYNCでは同期型の戻り値を使うメソッドが有効になります。

 一般的には、開発の目的に応じて、主に以下の2つの組み合わせが選ばれます。

  • WebFlux×STREAMABLE×ASYNC:ストリーミングや非同期処理を前提とした、パフォーマンス重視の構成
  • WebMVC×STATELESS×SYNC:挙動がシンプルで、まずは動かして理解するのに最適

 本記事のサンプルは後者(WebMVC、STATELESS、SYNC)を前提として説明します。

MCPサーバ用のアノテーション

 Spring AIでは、MCPサーバを手早く構築するために、MCP向けのアノテーションが用意されています。用意されているのは以下の3つです。

  • @McpTool:「処理(Tool)」を公開
  • @McpResource:「リソース(Resource)」を公開
  • @McpPrompt:「プロンプトテンプレート(Prompt)」を公開

 これらのアノテーションをメソッドに付与すると、Spring Bootがそれらを検出し、MCPサーバが提供する機能として自動的に公開します。つまり、開発者は「何を提供したいか」をアノテーションで宣言するだけで、公開に必要なコードを細かく書かずに済みます。前回までに紹介した方法と比べても、MCPサーバの実装が一段シンプルになります。

【補足】設定とメソッド形式の「不一致」

 アノテーションによる自動検出は非常に便利ですが、「設定(type/protocol)とメソッドのシグネチャ(戻り値)が一致していること」が前提となります。以下のように条件に合わないメソッドは、たとえアノテーションが付いていても自動的にスキップされるため注意が必要です。

  • 設定がSYNC(同期)なのに、戻り値がMono/Flux(Reactive型)
  • 設定がASYNC(非同期)なのに、戻り値が通常のオブジェクト(Stringなど)

 「アノテーションを付けたのに機能が公開されない」という場合は、アプリケーション起動時のログを確認してください。不一致がある場合、以下のような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サーバの機能を実装してみましょう。今回のサンプルでは、指定したフォルダ配下のファイルを検索し、その結果(見つかったファイル)を返す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 MCPツールのAPI実装例(server/src/main/java/jp/enbind/mcp/service/FileToolService.javaの抜粋)

 ポイントは大きく3つです。

(1)Spring Beanとしての登録

 まず、クラスに@Componentを付与してSpring Bootの管理下に置きます。これにより、起動時にアノテーションが付いたメソッドが自動的にスキャン・登録されます。

(2)LLMのための「メタデータ」を設定

 @McpTool内の項目は、開発者のためのメモではなく、LLMがツールを選ぶための判断材料です。

  • name:プログラム上の識別子。アンダースコア区切りなどが一般的
  • description:最重要項目。LLMはこの文章を読んで「今、このツールを使うべきどうか」を判断する。正確かつ具体的に記述すること

(3)引数情報の具体化(@McpToolParam)

 引数に@McpToolParamを付与することで、LLMがその引数にどのような値を渡すべきかを指示できます。「どのようなパターン(例:**/*.txt)が使えるか」を明記することで、LLMが正しい形式で値を生成しやすくなります。これによって、LLMが引数を埋める精度が上がり、想定外の値で呼ばれるリスクも下げられます。

MCPリソースを設定してみる

 続いて、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
            )
        ));
    }
}
リスト4 MCPリソースのAPI実装例(server/src/main/java/jp/enbind/mcp/service/FileResourceService.javaの抜粋)

(1)リソースを定義する

 @McpResourceアノテーションを付与することで、メソッドをMCPリソースとして公開します。nameやdescriptionは、LLMが「このリソースには何の情報が含まれているか」を判断するための重要な手掛かりとなります。

 uri属性にはfile-content://{fileId}のようなテンプレート形式を使用できます。これにより、特定のIDに基づいた動的なリソース参照が可能になります。前段のToolが返す識別子とこのURI形式を統一しておくと、Toolの出力とResourceの入力が自然につながり、システム全体の設計の見通しが良くなります。

(2)実際のデータを生成する

 取得したデータは、このクラスにラップして返却します。テキストデータだけでなく、MIMEタイプ(例:text/plain)を含めることができ、LLMがデータの形式を正しく解釈する助けとなります。

 ここまでMCPリソースを説明してきましたが、現時点ではMCPクライアント側が、Resourceを十分に活用できる環境が整っていない場合があります。本稿では参考として取り上げましたが、現時点では、これらの機能はMcpToolとして実装した方が利用しやすいでしょう。

MCPサーバに接続して動作確認してみる

 MCPサーバ側にToolやResourceを実装できたら、次はMCPクライアント側から正しく呼び出せるかを確認しましょう。動作確認の手順は幾つかありますが、まずはHTTPリクエストを直接送信して、サーバの挙動をダイレクトに観察するのが最も確実です。

通信プロトコルとしてのJSON-RPC

 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"
    }
  }
}
リスト5 MCPサーバへのツール実行例(client/tools_call.json)

 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点に絞って「動くものを作る」ための最短ルートを提示しています。

  • 通信の基礎:JSON-RPCやSSEによるやりとりの理解
  • サーバの機能:Tool、Resource、Promptの役割と定義
  • 実装の肝:Spring AIによる依存関係とアノテーションの活用

 こうした土台を固めることで、複雑な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.

アイティメディアからのお知らせ

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2026
人に頼れない今こそ、本音で語るセキュリティ「モダナイズ」
4AI by @IT - AIを作り、動かし、守り、生かす
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。