Problem Statement
Design a Model Context Protocol (MCP) Tool Router (similar to the integration bridges in Claude Desktop or modern IDE agents). The protocol Router must allow developers to register custom tools (capabilities) with specified parameter schemas, parse incoming standardized JSON-RPC 2.0 requests from an AI client, validate input arguments, route the execution dynamically to the matching tool, and return a structured content response following the MCP specification.
Design Decisions & Patterns Used
The Model Context Protocol (MCP) is an open standard designed to let AI models connect securely to tools and data. An MCP server exposes capabilities (Tools) using a standard message format over JSON-RPC 2.0. To build this in Java, we need a registry that registers tools by name, validates input maps against declared parameter types (e.g., asserting that an integer parameter was not passed as a string), and executes the target logic dynamically.
We will utilize the following Design Patterns:
- Registry Pattern: Maintaining a central map (registry) matching tool names to their execution strategies.
- Command Pattern: Modeling each tool as an executable command object that accepts arguments and returns standardized results.
Functional Requirements
- Define a standard tool interface (McpTool) containing schema definitions and execution hooks.
- Support registering tools with dynamic parameter types (e.g., String, Integer).
- Validate JSON-RPC 2.0 request structures and verify parameter types before executing tools.
- Execute tools dynamically and return standardized content payloads (McpResponse).
Objects Required
McpContent(Value object wrapping raw text response blocks)McpRequest&McpResponse(JSON-RPC protocol envelope structures)McpTool(The execution interface defining schemas and code actions)McpRouter(The coordinator class handling registrations, validation checks, and routing execution)
McpContent, Request & Response Classes
These classes represent the data structures used to communicate over the Model Context Protocol.
public class McpContent {
private final String type; // usually "text"
private final String text;
public McpContent(String type, String text) {
this.type = type;
this.text = text;
}
public String getType() { return type; }
public String getText() { return text; }
}
Let's define the McpRequest envelope:
import java.util.Map;
public class McpRequest {
private final String jsonrpc;
private final String method; // e.g. "tools/call"
private final String id;
private final Map<String, Object> params;
public McpRequest(String id, String method, Map<String, Object> params) {
this.jsonrpc = "2.0";
this.method = method;
this.id = id;
this.params = params;
}
public String getJsonrpc() { return jsonrpc; }
public String getMethod() { return method; }
public String getId() { return id; }
public Map<String, Object> getParams() { return params; }
}
Let's define the McpResponse envelope:
import java.util.List;
public class McpResponse {
private final String jsonrpc;
private final String id;
private final List<McpContent> content;
private final String error;
public McpResponse(String id, List<McpContent> content, String error) {
this.jsonrpc = "2.0";
this.id = id;
this.content = content;
this.error = error;
}
public String getJsonrpc() { return jsonrpc; }
public String getId() { return id; }
public List<McpContent> getContent() { return content; }
public String getError() { return error; }
}
These classes match the standard MCP specification, allowing clients to parse payload messages uniformly.
McpTool Interface & Concrete Implementations
The McpTool interface defines the schema and execution hooks for tools. Using the **Command Pattern** keeps our tools pluggable and decoupled from the router.
import java.util.Map;
public interface McpTool {
String getName();
String getDescription();
Map<String, Class<?>> getParameterSchema(); // maps argument names to types
McpContent execute(Map<String, Object> arguments);
}
Let's write two implementations: CalculatorTool and SystemInfoTool.
import java.util.HashMap;
import java.util.Map;
public class CalculatorTool implements McpTool {
@Override
public String getName() { return "calculate_sum"; }
@Override
public String getDescription() { return "Calculates the sum of two integers."; }
@Override
public Map<String, Class<?>> getParameterSchema() {
Map<String, Class<?>> schema = new HashMap<>();
schema.put("a", Integer.class);
schema.put("b", Integer.class);
return schema;
}
@Override
public McpContent execute(Map<String, Object> arguments) {
// Safe casting after router validation checks
int a = (Integer) arguments.get("a");
int b = (Integer) arguments.get("b");
int sum = a + b;
return new McpContent("text", "The calculated sum is: " + sum);
}
}
The CalculatorTool registers two integer parameters (a, b) and returns the sum formatted as text.
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Map;
public class SystemTimeTool implements McpTool {
@Override
public String getName() { return "get_system_time"; }
@Override
public String getDescription() { return "Retrieves the current local system date and time."; }
@Override
public Map<String, Class<?>> getParameterSchema() {
return Collections.emptyMap(); // No parameters required
}
@Override
public McpContent execute(Map<String, Object> arguments) {
String now = LocalDateTime.now().toString();
return new McpContent("text", "Current local system time is: " + now);
}
}
The SystemTimeTool requires no parameters and returns the current timestamp.
McpRouter Class
The McpRouter class acts as the central router, registering tools, validating parameters, and executing tools.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class McpRouter {
private final Map<String, McpTool> toolRegistry;
public McpRouter() {
this.toolRegistry = new ConcurrentHashMap<>();
}
public void registerTool(McpTool tool) {
toolRegistry.put(tool.getName(), tool);
System.out.println("Registered MCP Tool: " + tool.getName() + " - " + tool.getDescription());
}
public McpResponse routeRequest(McpRequest request) {
if (!"2.0".equals(request.getJsonrpc())) {
return new McpResponse(request.getId(), null, "Invalid JSON-RPC version. Expected '2.0'");
}
if (!"tools/call".equals(request.getMethod())) {
return new McpResponse(request.getId(), null, "Unknown method: '" + request.getMethod() + "'");
}
Map<String, Object> params = request.getParams();
if (params == null || !params.containsKey("name")) {
return new McpResponse(request.getId(), null, "Missing required parameter: 'name'");
}
String toolName = (String) params.get("name");
McpTool tool = toolRegistry.get(toolName);
if (tool == null) {
return new McpResponse(request.getId(), null, "Tool '" + toolName + "' not found in registry.");
}
@SuppressWarnings("unchecked")
Map<String, Object> arguments = (Map<String, Object>) params.get("arguments");
if (arguments == null) {
arguments = Collections.emptyMap();
}
// Validate arguments against parameter schema
try {
validateArguments(arguments, tool.getParameterSchema());
} catch (IllegalArgumentException e) {
return new McpResponse(request.getId(), null, "Argument Validation Error: " + e.getMessage());
}
// Execute the tool and wrap the results
try {
McpContent result = tool.execute(arguments);
List<McpContent> contentList = new ArrayList<>();
contentList.add(result);
return new McpResponse(request.getId(), contentList, null);
} catch (Exception e) {
return new McpResponse(request.getId(), null, "Execution Error: " + e.getMessage());
}
}
private void validateArguments(Map<String, Object> args, Map<String, Class<?>> schema) {
for (Map.Entry<String, Class<?>> entry : schema.entrySet()) {
String argName = entry.getKey();
Class<?> expectedType = entry.getValue();
if (!args.containsKey(argName)) {
throw new IllegalArgumentException("Missing parameter: '" + argName + "'");
}
Object value = args.get(argName);
if (value == null || !expectedType.isInstance(value)) {
throw new IllegalArgumentException("Parameter '" + argName + "' is invalid. Expected type: " + expectedType.getSimpleName());
}
}
}
}
Let's break down the logic of every method in the McpRouter class:
McpRouter(): The constructor initializes the thread-safe concurrent registry map.registerTool(tool): Adds a tool instance to the registry, indexing it by its name.routeRequest(request): Validates JSON-RPC metadata, checks the method type, locates the requested tool in the registry, validates the input arguments, routes the execution, and wraps the content in a protocol-compliant response object.validateArguments(args, schema): Checks that all required parameters are present in the input arguments and verifies that their types match the expected types in the schema, protecting the system from type-casting exceptions during execution.
Main Driver Class
This class tests our MCP router by simulating JSON-RPC requests, checking parameter validations, and verifying execution routing.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
McpRouter router = new McpRouter();
// Register tools
router.registerTool(new CalculatorTool());
router.registerTool(new SystemTimeTool());
System.out.println("\n==========================================");
System.out.println("Scenario 1: Executing Tool (calculate_sum)");
System.out.println("==========================================");
// Build valid JSON-RPC parameters
Map<String, Object> args1 = new HashMap<>();
args1.put("a", 10);
args1.put("b", 15);
Map<String, Object> params1 = new HashMap<>();
params1.put("name", "calculate_sum");
params1.put("arguments", args1);
McpRequest req1 = new McpRequest("req-101", "tools/call", params1);
// Route the request
McpResponse res1 = router.routeRequest(req1);
System.out.println("Response ID: " + res1.getId());
if (res1.getError() == null) {
System.out.println("Result text: " + res1.getContent().get(0).getText());
} else {
System.err.println("Error: " + res1.getError());
}
System.out.println("\n==========================================");
System.out.println("Scenario 2: Parameter Validation Error (Invalid Type)");
System.out.println("==========================================");
// Pass 'b' as a String instead of an Integer
Map<String, Object> args2 = new HashMap<>();
args2.put("a", 10);
args2.put("b", "15"); // Invalid type
Map<String, Object> params2 = new HashMap<>();
params2.put("name", "calculate_sum");
params2.put("arguments", args2);
McpRequest req2 = new McpRequest("req-102", "tools/call", params2);
McpResponse res2 = router.routeRequest(req2);
System.out.println("Response ID: " + res2.getId());
System.out.println("Expected Error: " + res2.getError());
}
}
The main() driver configures the router, registers tools, simulates valid and invalid JSON-RPC requests, and verifies that the router routes executions and validates inputs correctly.
Comments
Post a Comment