Spring Boot による MCP サーバーの実装とカスタマイズ

MCP(Model Context Protocol)は、AI プラグイン(例:Cursor、Continue、VS Code 拡張)とバックエンドサービス間の標準化された通信プロトコルです。JSON-RPC 2.0 を基盤とし、HTTP、WebSocket、stdio のいずれかのトランスポートを用いてツール呼び出しやコンテキストリソースの取得を可能にします。本稿では、Spring Boot を用いた HTTP ベースの MCP サーバーの構築手法を、実装からデプロイまで一貫して解説します。

アーキテクチャの概要

クライアント(IDE プラグイン)は `/mcp` エンドポイントに対して JSON-RPC 形式の POST リクエストを送信します。Spring Boot アプリケーションはこのリクエストを受信し、`method` フィールドに基づき処理を分岐させ、適切な応答を返します。この設計は、フル機能の JSON-RPC ライブラリを導入せずとも、必要な最小限のメソッド(`list_tools`/`call_tool`)のみを実装するシンプルなアプローチを採用しています。

データモデルの再設計

以下の DTO クラス群は、MCP の仕様に準拠しつつ、命名および構造を明確化・簡略化しました。
// JsonRpcEnvelope.java
package com.example.mcp.core;

import lombok.Data;

@Data
public class JsonRpcEnvelope {
    private String jsonrpc = "2.0";
    private String method;
    private Object params;
    private Object id;
}
// RpcResponse.java
package com.example.mcp.core;

import lombok.Data;

@Data
public class RpcResponse {
    private String jsonrpc = "2.0";
    private Object result;
    private RpcError error;
    private Object id;
}
// RpcError.java
package com.example.mcp.core;

import lombok.Data;

@Data
public class RpcError {
    private int code;
    private String message;
    private Object details;
}
// ToolDefinition.java
package com.example.mcp.model;

import lombok.Data;
import java.util.Map;

@Data
public class ToolDefinition {
    private String identifier;
    private String summary;
    private Map<String, Object> schema;
}
// ToolListPayload.java
package com.example.mcp.model;

import lombok.Data;
import java.util.List;

@Data
public class ToolListPayload {
    private List<ToolDefinition> availableTools;
}
// ToolInvocation.java
package com.example.mcp.model;

import lombok.Data;
import java.util.Map;

@Data
public class ToolInvocation {
    private String toolId;
    private Map<String, Object> inputs;
}

ビジネスロジックの実装

サービス層では、ツールの一覧提供と実行処理を責務分離し、拡張性を確保します。
// ToolRegistryService.java
package com.example.mcp.service;

import com.example.mcp.core.RpcError;
import com.example.mcp.model.*;
import org.springframework.stereotype.Service;
import java.time.ZonedDateTime;
import java.util.*;

@Service
public class ToolRegistryService {

    public ToolListPayload fetchAllTools() {
        var docsSearch = new ToolDefinition();
        docsSearch.setIdentifier("docs_search");
        docsSearch.setSummary("内部ドキュメント検索エンジン");
        docsSearch.setSchema(Map.of(
            "type", "object",
            "properties", Map.of("keyword", Map.of("type", "string")),
            "required", Arrays.asList("keyword")
        ));

        var clock = new ToolDefinition();
        clock.setIdentifier("server_clock");
        clock.setSummary("現在時刻を取得");
        clock.setSchema(Map.of("type", "object"));

        return new ToolListPayload(Arrays.asList(docsSearch, clock));
    }

    public Object execute(ToolInvocation invocation) {
        return switch (invocation.getToolId()) {
            case "docs_search" -> {
                String kw = (String) invocation.getInputs().get("keyword");
                if (kw == null || kw.trim().isEmpty()) {
                    throw new IllegalArgumentException("keyword must be non-empty");
                }
                yield Map.of("matches", 4, "sample", "MCP v1.2 spec reference");
            }
            case "server_clock" -> Map.of("utc", ZonedDateTime.now().toString());
            default -> throw new UnsupportedOperationException("tool not registered: " + invocation.getToolId());
        };
    }
}

HTTP エンドポイントの定義

コントローラーは、Jackson を活用して動的なパラメータ変換を行い、エラー状態も JSON-RPC 仕様に則って整形します。
// McpEndpoint.java
package com.example.mcp.web;

import com.example.mcp.core.*;
import com.example.mcp.model.ToolInvocation;
import com.example.mcp.service.ToolRegistryService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
public class McpEndpoint {

    private final ToolRegistryService registry;
    private final ObjectMapper mapper;

    @PostMapping("/mcp")
    public ResponseEntity<RpcResponse> dispatch(@RequestBody JsonRpcEnvelope req) {
        var res = new RpcResponse();
        res.setId(req.getId());

        try {
            return switch (req.getMethod()) {
                case "list_tools" -> {
                    res.setResult(registry.fetchAllTools());
                    yield ResponseEntity.ok(res);
                }
                case "call_tool" -> {
                    var params = mapper.convertValue(req.getParams(), ToolInvocation.class);
                    res.setResult(registry.execute(params));
                    yield ResponseEntity.ok(res);
                }
                default -> buildError(-32601, "Unsupported method: " + req.getMethod(), req.getId());
            };
        } catch (Exception e) {
            return buildError(-32603, "Execution failed: " + e.getMessage(), req.getId());
        }
    }

    private ResponseEntity<RpcResponse> buildError(int code, String msg, Object id) {
        var err = new RpcError();
        err.setCode(code);
        err.setMessage(msg);
        var res = new RpcResponse();
        res.setError(err);
        res.setId(id);
        return ResponseEntity.badRequest().body(res);
    }
}

統合テストの実行例

アプリケーション起動後、以下の cURL コマンドで動作確認が可能です。
# ツール一覧取得
curl -s -X POST http://localhost:8080/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":"t1","method":"list_tools"}' | jq

# 特定ツール実行
curl -s -X POST http://localhost:8080/mcp \
  -H 'Content-Type: application/json' \
  -d '{
        "jsonrpc":"2.0",
        "id":"t2",
        "method":"call_tool",
        "params":{"toolId":"server_clock","inputs":{}}
      }' | jq

本番環境向けの調整

- 認証制御:`@RequestHeader("X-API-Key")` を用いた簡易トークン検証を追加可能 - HTTPS 対応:Nginx または Spring Boot の `server.ssl.*` 設定で TLS 終端を実現 - 監査ログ:`@ControllerAdvice` を利用したリクエスト/レスポンスの記録 - ヘルスチェック:`/actuator/health` エンドポイントの有効化 MCP サーバーは、LLM インターフェースや社内ナレッジベースとの連携を容易にし、AI プラグインから直接呼び出せる「スマートなツール」を実現する基盤となります。

タグ: spring-boot json-rpc MCP Java rest-api

6月13日 21:58 投稿