MCP (Model Context Protocol) とは?Java開発者向け解説

1. 概要

MCP (Model Context Protocol) はオープン標準で、LLM (large language model) が外部データ、tool、システムに統一的な方法で接続できます。個々のAIアプリケーションが各データソースのための個別の統合コードを書く必要はありません。この記事では、MCPとは何か、そのアーキテクチャはどうなっているか、そしてSpring Boot(Spring AI経由)で簡単なMCP serverを書いてLLMが呼び出せるようにtoolをexposeする方法について解説します。最後には、いつMCPを使うべきか、そしていつそれが単なるover-engineeringなのかを理解できるようになります。

2. MCPが解決する問題

MCPが出現する前は、AIアプリケーション(chatbot、agent)がデータベース、ファイルシステム、内部API などにアクセスしたいと思ったら、それぞれ専用のアダプターを書く必要がありました。

M 個のAIアプリケーションと N 個のデータソースがあれば、M × N 個の異なる統合コードが必要になります。

MCPはこの通信層を標準化します。AIアプリケーション(Host と呼ばれる)はMCP標準について一度だけ理解すれば、MCP serverをexposeする任意のシステムと「会話」できます。これは電子機器のUSB-Cのようなもので、数十種類の異なるポートの代わりに、1つの標準コネクタを使うようなものです。

注意: MCPは通常のservice-to-service通信のためのRESTやgRPCの代わりとなるフレームワークではありません。LLM/agentがtoolを呼び出すために作られたもので、2つのmicroserviceバックエンド同士が通信するためのものではありません。

3. MCPのアーキテクチャ

MCPには3つの主な役割があります:

  • Host: LLMを含むアプリケーション(例:Claude Desktop、自分で書いたagent)
  • Client: Host内のコンポーネント。1つのMCP serverとの1対1の接続を保持します。
  • Server: MCPに従ってcapability(tool、resource、prompt)をexposeするプロセス。
┌──────────────┐        ┌──────────────┐        ┌──────────────────┐
│     Host     │  1-1   │  MCP Client  │  JSON-RPC  │   MCP Server   │
│  (chứa LLM)  │◄──────►│              │◄──────────►│ (DB, API, file) │
└──────────────┘        └──────────────┘        └──────────────────┘

clientとserver間の通信は JSON-RPC 2.0 を使用し、2つの一般的なtransportの1つを通じて送信されます:

  • stdio: serverは子プロセスとして実行され、client/serverはstdin/stdoutで通信します。serverがローカルで実行されている場合に適しています。
  • SSE/HTTP (Streamable HTTP): serverがリモートで実行されている場合、ネットワーク経由で使用されます。

3.1. MCP serverがexposeできる3種類のcapability

Capability 意味 例
Tools LLMが呼び出して動作を実行できる関数 getWeather(city), createOrder(...)
Resources LLMが読み取ることができるデータ(GETのような) ログファイル、データベースレコード
Prompts serverで事前に定義されたpromptテンプレート “パターンXに従って注文を要約してください”

ほとんどの実際のユースケースは Tools を中心に回っているため、以下の例はそれに焦点を当てています。

4. Spring AIでMCP serverを書く

Spring AI(バージョン1.0.0-M6から)はMCP用の専用starterで対応しており、annotationを使ってJava beanをMCP toolとしてexposeでき、JSON-RPC自体を実装する必要がありません。

4.1. 依存関係を追加する

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
    <version>1.0.0-M6</version>
</dependency>

4.2. toolを定義する

@Service
public class WeatherService {

    @Tool(description = "都市の現在の気温を取得します")
    public String getTemperature(String city) {
        return switch (city.toLowerCase()) {
            case "ha noi" -> "28°C、曇り";
            case "sai gon" -> "32°C、晴天";
            default -> "都市のデータはありません: " + city;
        };
    }
}
  • @Tool はこのメソッドがMCP toolであることを示しています。属性 description は 非常に重要 です。LLMはこの説明を使ってtoolを呼び出すべきかどうか、どのパラメータで呼び出すかを判断します。
  • getTemperature は String city を受け取り、天気の説明文字列を返します。parameterと戻り値の型からMCP serverが自動的にJSON schemaを生成し、LLMが理解できるようにします。

4.3. MCPサーバーにtoolを登録する

@Configuration
public class McpServerConfig {

    @Bean
    public ToolCallbackProvider weatherTools(WeatherService weatherService) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(weatherService)
                .build();
    }
}

MethodToolCallbackProvider は weatherService 内の @Tool アノテーションを持つメソッドをスキャンし、アプリケーションコンテキストの起動時に自動的にそれらをMCP serverに登録します。

4.4. サーバーを設定する

spring.ai.mcp.server.name=weather-mcp-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.stdio=true

stdio=true でserverは独立したプロセスとして実行され、stdin/stdoutで通信します。Claude DesktopまたはMCP clientを自分で書いて素早くテストするのに適しています。serverをリモートで実行される独立したサービスとしてデプロイしたい場合は、transport をSSE/HTTPに切り替えて、stdioではなくHTTPエンドポイント経由でexposeします。

サーバーを起動する:

mvn spring-boot:run
Started WeatherMcpApplication in 1.124 seconds
Registered MCP tool: getTemperature

5. clientからMCP serverを呼び出す

Host application(例えば内部agent)を書いていて、上記の getTemperature toolを呼び出す必要があると仮定しましょう。

5.1. client依存関係

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
    <version>1.0.0-M6</version>
</dependency>

5.2. toolを呼び出す

@Component
public class WeatherAgent {

    private final McpSyncClient mcpClient;

    public WeatherAgent(McpSyncClient mcpClient) {
        this.mcpClient = mcpClient;
    }

    public String askTemperature(String city) {
        CallToolRequest request = new CallToolRequest(
                "getTemperature",
                Map.of("city", city));

        CallToolResult result = mcpClient.callTool(request);
        return result.content().get(0).text();
    }
}
  • McpSyncClient はSpring AIのclientで、application.yml で宣言されたtransport設定(stdioまたはSSE)に基づいてbeanを自動的に作成します。
  • CallToolRequest は tool名(getTemperature)とパラメータ(city)をJSON-RPC callに含めます。
  • result.content() はレスポンス内容のリストを返します。ここでは最初の text 部分を取得します。

clientの設定(例:stdioを通じてserverに接続し、server jarをsubprocessとして実行):

spring:
  ai:
    mcp:
      client:
        stdio:
          connections:
            weather-server:
              command: java
              args: -jar weather-mcp-server.jar

6. JUnit 5 + AssertJでtoolをテストする

@Tool は通常のJavaメソッドの単なるannotationなので、MCPから独立して業務ロジックをテストできます。サーバー全体を構築する必要はありません:

class WeatherServiceTest {

    private final WeatherService weatherService = new WeatherService();

    @Test
    void shouldReturnTemperatureForKnownCity() {
        String result = weatherService.getTemperature("Ha Noi");

        assertThat(result).contains("28°C");
    }

    @Test
    void shouldReturnFallbackMessageForUnknownCity() {
        String result = weatherService.getTemperature("Tokyo");

        assertThat(result)
                .startsWith("都市のデータはありません:")
                .contains("Tokyo");
    }
}

MCPの統合部分もテストしたい場合(toolが正しい名前で正しいschemaで登録されているか確認したい場合)、@SpringBootTest レベルでテストを書き、ToolCallbackProvider を確認できます:

@SpringBootTest
class McpServerConfigTest {

    @Autowired
    private ToolCallbackProvider toolCallbackProvider;

    @Test
    void shouldExposeTemperatureTool() {
        var toolNames = toolCallbackProvider.getToolCallbacks().stream()
                .map(callback -> callback.getToolDefinition().name())
                .toList();

        assertThat(toolNames).contains("getTemperature");
    }
}

このテストは、WeatherService をリファクタリングするたびに、toolが正しい名前でexposeされていることを保証します。メソッド名を変更したが、LLMがそれを参照している箇所の更新を忘れるような状況を避けられます。

7. よくあるエラーとMCPを使うべきでない場合

toolの説明が曖昧すぎる。 description が単に「データを取得する」としか書かれていない場合、似たようなtoolが複数ある時にLLMが誤ったtoolを選択しやすくなります。具体的な説明を書き、期待されるinput/outputを明確にしてください。

内部REST/gRPCの代わりにMCPを使う。 MCPはJSON-RPCレイヤーと独自transportを追加し、LLMが動的にtoolを呼び出す文脈用に最適化されています。高頻度、低遅延の2つのバックエンドサービス間の通信には最適ではありません。この場合のMCP使用はover-engineeringです。

toolの権限を制御しない。 MCP serverは通常、ホストプロセスの権限で実行されます。ファイル削除やシェルコマンド実行が可能なtoolをexposeしておりながら、入力パラメータを制限していない場合、LLM(またはprompt injection)がそのtoolを悪用する可能性があります。常にtool内のparameterをvalidateし、LLMからの入力を盲目的に信頼しないでください。

serverless環境でstdio serverを実行する。 stdioのtransportは子プロセスを生成する必要があり、serverless/短期間のcontainer環境では正常に動作しません。このような場合、SSE/HTTPのtransportを使用してください。

client と server間でversionを同期しない。 MCPは急速に開発が進んでいる状態で(spec版間に多くのbreaking changeがあります)。SDKアップグレード時は、production環境にデプロイする前にchangelogを丁寧に確認してください。

8. 結論

MCPは、LLMがtoolを呼び出し、外部データを読み取る方法を標準化し、AIアプリケーションとデータソース間の M × N 統合問題を解決します。Spring AIを使えば、@Tool annotationだけでJavaサービスをMCP toolとしてexposeでき、JSON-RPC自体を実装する必要がありません。LLMの判断に基づいて動的にtoolを呼び出す必要があるagent/chatbotを構築している場合にMCPを使ってください。通常のservice-to-service通信にはRESTやgRPCがより適切な選択肢なので、そのような場合の使用は避けてください。

mcpspring-aillmjavaai-integration