Lesson 3 - Step 5 of 5

Building a Router

Create a method router that dispatches JSON-RPC messages to handlers.

What is a Router?

A router maps incoming JSON-RPC method names to handler functions. When a message arrives, the router looks up the method name and calls the right handler:

text
Message: {"method": "tools/list", ...}
                    |
                    v
         Router: "tools/list" -> handle_tools_list()
                    |
                    v
         Response: {"result": {"tools": [...]}, ...}

Edit: jsonrpc.py

Step 1: Add an Exception Class

Add this class after the ERROR_MESSAGES dict:

python jsonrpc.py - Add after ERROR_MESSAGES
class JsonRpcError(Exception):
    """Exception that converts to a JSON-RPC error response."""

    def __init__(
        self,
        code: JsonRpcErrorCode,
        message: str | None = None,
        data: Any = None
    ):
        self.code = code
        self.message = message or ERROR_MESSAGES.get(code, "Unknown error")
        self.data = data
        super().__init__(self.message)

Step 2: Add the Router Class

Add this class after all your functions (before the test block):

python jsonrpc.py - Add before the test block
from typing import Callable, cast


class JsonRpcRouter:
    """
    Routes JSON-RPC messages to handler functions.

    Usage:
        router = JsonRpcRouter()

        @router.method("tools/list")
        def handle_tools_list(params):
            return {"tools": [...]}

        response = router.handle(request)
    """

    def __init__(self):
        self._request_handlers: dict[str, Callable] = {}
        self._notification_handlers: dict[str, Callable] = {}

    def method(self, name: str):
        """Decorator to register a request handler."""
        def decorator(func: Callable):
            self._request_handlers[name] = func
            return func
        return decorator

    def notification(self, name: str):
        """Decorator to register a notification handler."""
        def decorator(func: Callable):
            self._notification_handlers[name] = func
            return func
        return decorator

    def handle(self, message: dict) -> dict | None:
        """
        Process a JSON-RPC message and return a response.

        Args:
            message: Parsed JSON-RPC message

        Returns:
            Response dict for requests, None for notifications
        """
        if is_notification(message):
            self._handle_notification(message)
            return None

        if is_request(message):
            return self._handle_request(message)

        # Invalid message
        return create_error_response(
            request_id=message.get("id"),
            code=JsonRpcErrorCode.INVALID_REQUEST,
            message="Invalid JSON-RPC message"
        )

    def _handle_request(self, request: dict) -> dict:
        """Handle a request and return a response."""
        request_id = cast(int | str, request.get("id"))
        method = request.get("method", "")
        params = request.get("params", {})

        # Find handler
        handler = self._request_handlers.get(method)
        if not handler:
            return create_error_response(
                request_id=request_id,
                code=JsonRpcErrorCode.METHOD_NOT_FOUND,
                message=f"Method not found: {method}",
                data={"method": method}
            )

        # Execute handler
        try:
            result = handler(params)
            return create_success_response(request_id, result)
        except JsonRpcError as e:
            return create_error_response(
                request_id=request_id,
                code=e.code,
                message=e.message,
                data=e.data
            )
        except Exception as e:
            return create_error_response(
                request_id=request_id,
                code=JsonRpcErrorCode.INTERNAL_ERROR,
                message=str(e)
            )

    def _handle_notification(self, notification: dict):
        """Handle a notification silently (no response)."""
        method = notification.get("method", "")
        params = notification.get("params", {})

        handler = self._notification_handlers.get(method)
        if handler:
            try:
                handler(params)
            except Exception as e:
                # Log but never respond
                print(f"Notification error ({method}): {e}")

Step 3: Update the Test Block

Replace your test block with this router demo:

python jsonrpc.py - Replace the test block
# =============================================================================
# Try it out
# =============================================================================

if __name__ == "__main__":
    # Create a router
    router = JsonRpcRouter()

    # Register request handlers using decorators
    @router.method("initialize")
    def handle_initialize(params: dict) -> dict:
        return {
            "protocolVersion": "2024-11-05",
            "capabilities": {"tools": {}},
            "serverInfo": {"name": "demo-server", "version": "1.0.0"}
        }

    @router.method("tools/list")
    def handle_tools_list(params: dict) -> dict:
        return {
            "tools": [
                {
                    "name": "calculator",
                    "description": "Basic math operations",
                    "inputSchema": {"type": "object"}
                }
            ]
        }

    @router.method("tools/call")
    def handle_tools_call(params: dict) -> dict:
        tool_name = params.get("name", "")
        if tool_name != "calculator":
            raise JsonRpcError(
                JsonRpcErrorCode.INVALID_PARAMS,
                f"Unknown tool: {tool_name}"
            )
        return {"content": [{"type": "text", "text": "4"}]}

    # Register notification handlers
    @router.notification("notifications/initialized")
    def on_initialized(params: dict):
        print("  [Server] Client initialization complete!")

    # Test the router
    print("=" * 60)
    print("JSON-RPC Router Demo")
    print("=" * 60)

    test_messages = [
        # Request: initialize
        {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}},
        # Notification: initialized (no response expected)
        {"jsonrpc": "2.0", "method": "notifications/initialized"},
        # Request: tools/list
        {"jsonrpc": "2.0", "id": 2, "method": "tools/list"},
        # Request: unknown method
        {"jsonrpc": "2.0", "id": 3, "method": "unknown/method"},
        # Request: tools/call with invalid params
        {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "invalid"}},
    ]

    for msg in test_messages:
        print(f"\n-> Incoming: {json.dumps(msg)}")
        response = router.handle(msg)
        if response:
            print(f"<- Response: {json.dumps(response, indent=2)}")
        else:
            print("<- (no response - notification)")

    print("\n" + "=" * 60)
    print("Router demo complete!")
    print("=" * 60)

Try It

bash
python jsonrpc.py

Expected Output

text
============================================================
JSON-RPC Router Demo
============================================================

-> Incoming: {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}
<- Response: {
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "tools": {}
    },
    "serverInfo": {
      "name": "demo-server",
      "version": "1.0.0"
    }
  }
}

-> Incoming: {"jsonrpc": "2.0", "method": "notifications/initialized"}
  [Server] Client initialization complete!
<- (no response - notification)

-> Incoming: {"jsonrpc": "2.0", "id": 2, "method": "tools/list"}
<- Response: {
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [...]
  }
}

-> Incoming: {"jsonrpc": "2.0", "id": 3, "method": "unknown/method"}
<- Response: {
  "jsonrpc": "2.0",
  "id": 3,
  "error": {
    "code": -32601,
    "message": "Method not found: unknown/method"
  }
}

-> Incoming: {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "invalid"}}
<- Response: {
  "jsonrpc": "2.0",
  "id": 4,
  "error": {
    "code": -32602,
    "message": "Unknown tool: invalid"
  }
}

============================================================
Router demo complete!
============================================================

Understanding the Router

The Decorator Pattern

The @router.method("tools/list") syntax is a Python decorator. It registers the function with the router, so when a message with "method": "tools/list" arrives, the router knows to call that function.

python
# These two are equivalent:

# Using decorator
@router.method("tools/list")
def handle_tools_list(params):
    return {"tools": []}

# Manual registration
def handle_tools_list(params):
    return {"tools": []}
router._request_handlers["tools/list"] = handle_tools_list

Error Handling Flow

The router handles errors at multiple levels:

  1. Unknown method → METHOD_NOT_FOUND error
  2. Handler raises JsonRpcError → That error is returned
  3. Handler raises other exception → INTERNAL_ERROR
Ready for MCP!

This router is exactly what you'll use for MCP servers. In the next lesson, you'll add the MCP handshake (initialize + initialized) to make it a real MCP server.

Your Complete Module

Your jsonrpc.py now has everything you need:

Component Purpose
JsonRpcErrorCode Standard error codes
JsonRpcError Exception for handler errors
create_request() Build request messages
create_success_response() Build success responses
create_error_response() Build error responses
create_notification() Build notifications
is_request(), etc. Detect message types
JsonRpcRouter Dispatch messages to handlers

What You've Learned

In this lesson, you mastered JSON-RPC 2.0:

  • Protocol basics — requests, responses, and notifications
  • Message creation — building valid JSON-RPC messages
  • Error handling — standard codes and error responses
  • Message routing — dispatching to handler functions

What's Next

In Lesson 4, you'll implement the MCP handshake — the initialize request and initialized notification that establish an MCP session. You'll learn about protocol version negotiation and capability exchange.