Skip to main content

Design a Model Context Protocol (MCP) Router

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.

Asked In Companies
Anthropic Vercel Slack

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.


Also See

Comments

Popular posts from this blog

Designing a Parking Lot - Low Level Design

Problem Statement Design a parking lot that can handle vehicles entering and leaving while managing parking across multiple floors. Each vehicle should be assigned a suitable parking spot based on its type, and the spot should be freed once the vehicle exits. The design should also support generating a ticket at entry and optionally calculating the parking fee based on the duration of stay. Asked In Companies Amazon Google Microsoft Uber Walmart Flipkart Meta PayPal Oracle Salesforce Adobe Apple Intuit LinkedIn Atlassian Functional Requirements The design should support multiple vehicle types such as bikes, cars, and trucks A vehicle must be assigned a parking spot compatible with its type A parking spot cannot be assigned to more than one vehicle at a time The parking lot should support multiple levels (floors) The design should search and allocate an availa...

Most Frequently Asked Low Level Design(LLD) Interview Questions

Below are the curated list of most commonly asked Low Level Design (LLD) interview problems. Each problem includes a short description and a link to the complete solution with code and class diagrams. Design Parking Lot System The system should handle parking for different vehicle types such as bikes, cars, and trucks. It should manage slot allocation, availability tracking, and entry/exit flow. The design also ensures efficient usage of parking space under varying load conditions. View Solution Design Elevator / Lift System The system should support multiple elevators operating across floors with request handling logic. It focuses on scheduling algorithms to minimize wait time and optimize movement. It also manages direction control and concurrent floor requests. View Solution Design Movie Ticket Booking System The system should allow users to browse movies, select shows, and book seats. It handles seat ...

Software Design Patterns for LLD Interviews: A Complete Guide

Software Design Patterns for LLD Interviews: A Complete Guide In Software Development Engineer (SDE) interviews—especially for mid-level and senior roles—low-level design (LLD) rounds assess your ability to write clean, reusable, maintainable, and extensible code. The foundation of resolving these architectural challenges lies in the standard Gang of Four (GoF) Design Patterns. Rather than memorizing theoretical definitions, interviewers expect you to apply these patterns to real-world scenarios, identifying the trade-offs of each. Below is a comprehensive guide to the 12 most frequently asked design patterns in LLD interviews, categorized by their classification (Creational, Structural, and Behavioral). Each pattern contains a concrete, real-world Java implementation and a detailed breakdown of design decisions. Creational Design Patterns Creational design patterns deal with object creation mechanisms. They abstract the instantiation process, making a system independent of how...