Claude Media
JavaでMCPサーバーを書く手順とSpring AIとの関係

JavaでMCPサーバーを書く手順とSpring AIとの関係

MCP Java SDKでツールを実装する手順と、Spring AI 2.0でSpring連携が本体から分離された経緯を、動くコード付きで解説します。

JavaでMCPサーバーを書くと何ができるか

MCP(Model Context Protocol)のJava実装はio.modelcontextprotocol.sdk配下の公式SDKとして提供されています。社内APIやデータベースをツールとして公開するサーバーを、純粋なJavaだけで書けます。SpringのWebフレームワークは必須ではありません。Mavenの依存関係を1つ足せば、stdio(標準入出力)方式のサーバーがものの数十行で動きます。

一方で、Spring Bootのアプリケーションに組み込みたい場合はSpring AIのMCP Boot Starterとアノテーションを使う道があります。この2つは別物です。Spring AI 2.0からは、Spring向けのトランスポート実装がMCP Java SDK本体から分離され、Spring AI側のモジュールに移りました。本記事は、①素のSDKでサーバーを書く手順、②Spring AIでの実装方法、③両者の役割分担、の3点を順に扱います。

MCPそのものの仕組みや他言語SDKとの比較はMCP実用ガイドが扱っています。TypeScript・PythonでのMCPサーバー自作はMCPサーバー自作ガイドにあります。本記事はJavaとSpring AIの組み合わせに絞ります。

前提条件と3つの実装経路

Java SDKの動作要件はJava 17以上です。GitHub上の公式リポジトリのバッジがこれを明記しています。Mavenまたはgradleのビルド環境があれば準備は揃います。Spring Bootプロジェクトの新規作成はClaude CodeでSpring Bootアプリを作るにまとめています。

実装経路は3つに分かれ、フレームワークへの依存度で選びます。

経路

3つの実装経路

  • 素のJava(stdio)

    依存はio.modelcontextprotocol.sdk:mcpだけです。ローカル起動のCLIツールや、フレームワークに依存させたくないサーバーに向きます。

  • 素のJava(Servlet HTTP)

    依存は同じmcpモジュールです。Spring以外のサーブレットコンテナでHTTP公開したいときに使います。

  • Spring AI Boot Starter

    依存はorg.springframework.ai:spring-ai-starter-mcp-server(HTTPなら-webmvcか-webflux)です。既存のSpring Bootアプリに組み込む場合に向きます。

Mavenで依存関係を揃える

最小構成は、コア実装とJackson 3系のJSONシリアライズをまとめたmcpモジュール1つです。バージョン管理はmcp-bom(Bill of Materials)をインポートして揃えます。

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.modelcontextprotocol.sdk</groupId>
      <artifactId>mcp-bom</artifactId>
      <version>2.0.1</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
 
<dependencies>
  <dependency>
    <groupId>io.modelcontextprotocol.sdk</groupId>
    <artifactId>mcp</artifactId>
  </dependency>
</dependencies>

2.0.1はMaven Centralで公開されている値の一例です。BOMのバージョンは更新されるため、実際に指定する前にMaven Central上のmcp-bomページで最新値を確認してください。Jackson 2系を使いたいプロジェクトは、mcp-coreとmcp-json-jackson2を個別に組み合わせる構成も用意されています。

STDIOサーバーを実装する

stdioはローカル利用の第一候補です。クライアント(Claude Code)がサーバーをサブプロセスとして起動します。やり取りは標準入出力上のJSON-RPCメッセージです。

手順1: ツールを1つ定義する

ツールの入力スキーマはJSON Schema文字列で渡します。Tool.builder(name, jsonMapper, schema)がスキーマ文字列をMapへ変換してくれるため、手書きのMap構築は不要です。

import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool;
 
import java.util.List;
 
public class DocsServer {
 
    public static void main(String[] args) {
        var transportProvider = new StdioServerTransportProvider(McpJsonDefaults.getMapper());
 
        var schema = """
            {
              "type": "object",
              "properties": {
                "query": { "type": "string", "description": "検索キーワード" }
              },
              "required": ["query"]
            }
            """;
 
        var searchDocsTool = SyncToolSpecification.builder()
            .tool(Tool.builder("search-docs", McpJsonDefaults.getMapper(), schema)
                .description("社内ドキュメントを検索し、該当箇所を返す")
                .build())
            .callHandler((exchange, request) -> {
                String query = (String) request.arguments().get("query");
                String result = searchInternalDocs(query); // 自前の検索処理
                return CallToolResult.builder()
                    .content(List.of(TextContent.builder(result).build()))
                    .build();
            })
            .build();
 
        McpSyncServer server = McpServer.sync(transportProvider)
            .serverInfo("docs-server", "0.1.0")
            .capabilities(ServerCapabilities.builder()
                .tools(true)
                .build())
            .build();
 
        server.addTool(searchDocsTool);
    }
 
    private static String searchInternalDocs(String query) {
        return "Result: " + query; // 実装は自前の検索処理に置き換える
    }
}

callHandlerはexchangeと呼び出しリクエストを受け取りCallToolResultを返すハンドラーです。引数のexchangeからはクライアントへのログ通知や進捗報告ができ、request.arguments()がMap<String, Object>としてツール呼び出しの引数を返します。同期(McpServer.sync)と非同期(McpServer.async)の両APIが用意されています。非同期版の戻り値はMonoベースで、Reactorに慣れたチームには既存コードと馴染みます。

手順2: 起動してInspectorで確認する

ビルドしたjarをそのまま実行するか、Mavenのexecプラグインで起動します。stdioサーバーは起動しても画面に何も出さず、標準入力を待ち続けるのが正常な状態です。動作確認はMCP Inspectorに任せると効率的です。手順はMCPサーバー自作ガイドのInspector節と同じ流れが使えます。

npx @modelcontextprotocol/inspector java -jar target/docs-server.jar

手順3: Claude Codeに接続する

登録はclaude mcp addコマンドで行います。実行コマンドを--より後ろに書く点はTypeScript・Pythonのサーバーと同じです。

claude mcp add --transport stdio docs-server \
  -- java -jar /absolute/path/to/docs-server.jar

--scopeを省くとlocalスコープに保存されます。チームで共有するなら--scope projectを付けます。v2.1.287で、一時ディレクトリをHOMEにして--scope projectで実行したところ、そのディレクトリの.mcp.jsonに次の内容が書かれました。

{
  "mcpServers": {
    "docs-server": {
      "type": "stdio",
      "command": "java",
      "args": ["-jar", "/absolute/path/to/docs-server.jar"],
      "env": {}
    }
  }
}

--の前までがclaudeのオプション、後ろがcommandとargsに分かれて入ります。HTTPで公開したサーバーは--transport httpでURLを渡します。次のコマンドなら、"type": "http"と"url"だけのエントリが追記されます。

claude mcp add --transport http spring-docs http://localhost:8080/mcp

HTTPで公開する場合(Spring以外)

Spring Bootを使わないサーブレット環境向けに、mcpモジュールはHttpServletStreamableServerTransportProviderを単体で提供しています。Spring依存を増やさずにStreamable HTTP方式のサーバーを組めるため、「Springは使っていないがHTTPで公開したい」というチームの選択肢になります。

HttpServletStreamableServerTransportProvider transportProvider =
    HttpServletStreamableServerTransportProvider.builder()
        .jsonMapper(McpJsonDefaults.getMapper())
        .mcpEndpoint("/mcp")
        .build();

生成したインスタンスはサーブレットそのものなので、サーブレットとして登録できます。Spring Bootのサーブレット環境なら、ServletRegistrationBeanを@Beanとして返す形でも登録できます。Java SDKの公式ドキュメントにこの登録例があり、Spring AI Boot Starterを使わずに素のSDKだけでHTTPサーバーを組み込めます。

HTTPで公開するときは、認証を別に用意する必要があります。Spring AIのBoot Starterも、HTTP系のトランスポート(SSE・Streamable HTTP・Stateless)では認証なしのエンドポイントを開きます。既定はPOST /mcpです。到達できるクライアントは登録済みのツールをすべて一覧・実行できます。localhostの外へ出す前に、Spring Securityなどの認証層を前段に置く前提です。stdioはクライアントが起動した子プロセスと標準入出力でやり取りするだけで、ネットワークのポートは開きません。

Spring AIでの実装 — Boot StarterとAnnotations

既存のSpring Bootアプリに組み込むなら、Spring AIのMCP Boot Starterが近道です。依存関係を1つ足すだけで、トランスポートの配線とSpringのDIコンテナへの統合が済みます。

サーバー種別依存設定
STDIO依存spring-ai-starter-mcp-server設定spring.ai.mcp.server.stdio=true
Streamable HTTP(Servlet)依存spring-ai-starter-mcp-server-webmvc設定spring.ai.mcp.server.protocol=STREAMABLE
Streamable HTTP(Reactive)依存spring-ai-starter-mcp-server-webflux設定spring.ai.mcp.server.protocol=STREAMABLE
セッションを持たない(WebMVC/WebFlux)依存上記のwebmvc/webflux設定spring.ai.mcp.server.protocol=STATELESS

SSE(protocol=SSE)も選べますが、Spring AI 2.0.0で非推奨になり、公式はSTREAMABLEへの移行を示しています。同期か非同期かはspring.ai.mcp.server.type=SYNCまたはASYNCで切り替えます。

Boot Starterに加えて、Spring AIはMCP Annotationsモジュールでアノテーションベースの実装も提供します。サーバー側は@McpTool @McpResource @McpPrompt @McpCompleteの4種で、Springの@Componentを書く感覚でツールを増やせます。

import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.stereotype.Component;
 
@Component
public class DocsTools {
 
    @McpTool(name = "search-docs", description = "社内ドキュメントを検索し、該当箇所を返す")
    public String searchDocs(@McpToolParam(description = "検索キーワード", required = true) String query) {
        return searchInternalDocs(query);
    }
}

依存関係はspring-ai-mcp-annotations(org.springframework.aiグループ)で、Boot Starterを使っていれば自動的に含まれます。引数の型と@McpToolParamの説明からJSON Schemaが自動生成されるため、手順1のような手書きスキーマは不要になります。

Spring AI 2.0でSDKとの役割分担がどう変わったか

Spring AI 2.0より前は、Spring WebFlux・WebMVC向けのトランスポート実装がMCP Java SDK本体のモジュールでした。mcp-spring-webfluxとmcp-spring-webmvcで、io.modelcontextprotocol.sdkグループに含まれていました。Spring AI 2.0でこの2つはSDK本体から切り離され、org.springframework.aiグループの下でSpring AIプロジェクト自身が保守する形に変わりました。

この変更は破壊的変更(breaking change)です。旧グループIDで直接この2アーティファクトを参照していたプロジェクトは、依存関係とimport文の両方を更新する必要があります。現在の役割分担は次のとおりです。

くらべる

2.0以降の保守範囲

io.modelcontextprotocol.sdk

MCP Java SDKチーム

プロトコルのコア実装(mcp-core)、stdioとServletのHTTPトランスポート、JSON処理を保守します。

org.springframework.ai

Spring AIチーム

Spring向けのトランスポート実装、Boot Starter、Annotationsモジュールを保守し、MCP Java SDKを依存先として使います。

素のSDKで書き始めたサーバーをあとからSpring Bootへ移植する場合、ハンドラーの中身(業務ロジック)は流用しやすいです。一方で登録方法は@McpToolに書き換えるか、ToolSpecificationをBeanとして登録する形になります。

ツールのエラーは2つの経路で返す

ハンドラーが失敗したときの返し方は2通りあり、LLMが自力で直せるかどうかで使い分けます。

くらべる

ツール実行時のエラー

LLMが再試行できる

CallToolResultのisError

メールアドレスの形式が違う、引数が足りないといった入力・業務上の失敗に使います。結果はふつうのツール応答としてLLMに届き、次の呼び出しで修正されます。

再試行できない

McpErrorを投げる

DBのタイムアウトなど想定外のインフラ障害だけに使います。ハンドラーから漏れた例外はJSON-RPCのエラー応答に変換されます。

if (!email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
    return CallToolResult.builder()
        .content(List.of(TextContent.builder("emailの形式が正しくありません").build()))
        .isError(true)
        .build();
}

入力スキーマに合わない引数は、ハンドラーに届く前にSDKが弾きます。validateToolInputsの既定値はtrueで、失敗した呼び出しはisError付きのCallToolResultで返ります。スキーマ文字列自体もJSON Schema 2020-12のメタスキーマで検証されます。壊れたスキーマはMcpServer.build()やaddTool()の時点でIllegalArgumentExceptionになります。

よくあるつまずき

ビルドファイルの新旧が混在してエラーになる

Spring AI 2.0への移行期は、旧グループIDと新グループIDの記事が混在して出回ります。旧はio.modelcontextprotocol.sdk:mcp-spring-webflux、新はorg.springframework.ai:mcp-spring-webfluxです。依存関係を追加する前に、実際に使うSpring AIのバージョンに対応するグループIDを公式ドキュメントで確認してください。

Tool.builderの非推奨警告が出る

Tool.builder(name)(スキーマなし)やTool.builder()(引数なし)は非推奨扱いです。現行の推奨形はTool.builder(name, inputSchema)またはTool.builder(name, jsonMapper, schemaString)です。コンパイル時に警告が出たら呼び出し形を見直してください。

Tier 2のSDKでビルダーの使い方を確かめる

ビルダーの引数や呼び出し順で迷ったときは、公式リポジトリのJavadocとテストコードで確かめられます。たとえばSyncToolSpecificationBuilderTestは、SyncToolSpecification.builder()の使い方をそのままコードで示すテストクラスです。

まとめ

経路の選び方より先に決めておきたいのは、HTTPで公開するかどうかです。公開するなら、Spring AIでも素のSDKでも認証は自前で前段に用意することになります。stdioで閉じるなら、この問題自体が生じません。

他言語SDKとの機能差やTierの詳細はMCP SDK Tierシステムとはを参照してください。実装したサーバーの権限設計はMCPセキュリティガイドにあります。

この記事を共有:XはてブLinkedIn