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