turul-mcp-json-rpc-server 0.2.0

Pure JSON-RPC 2.0 server implementation with type-safe domain/protocol separation
Documentation

Turul JSON-RPC Server

A generic JSON-RPC 2.0 server implementation that provides the foundation for the MCP protocol transport layer.

Features

  • JSON-RPC 2.0 Compliant: Full specification support with proper error handling
  • Type-Safe Error Handling: Domain errors with automatic protocol conversion
  • Session Support: Optional session context for stateful operations
  • Generic Handler Interface: Flexible handler registration system
  • Unified Error Architecture: Clean domain/protocol separation (0.2.0+)

Key Architecture (0.2.0+)

The framework uses a clean domain/protocol separation where:

  1. Handlers return domain errors only (Result<Value, DomainError>)
  2. Dispatcher owns protocol conversion (domain → JSON-RPC errors)
  3. No double-wrapping or protocol structures in business logic

Quick Start

use turul_mcp_json_rpc_server::{
    JsonRpcHandler, JsonRpcDispatcher, RequestParams,
    r#async::{SessionContext, ToJsonRpcError},
    dispatch::parse_json_rpc_message,
    error::JsonRpcErrorObject,
};
use serde_json::{Value, json};
use async_trait::async_trait;

/// Calculator error type - handlers return domain errors only
#[derive(thiserror::Error, Debug)]
enum CalculatorError {
    #[error("Missing parameters: {0}")]
    MissingParameters(String),
    #[error("Invalid parameter: {0}")]
    InvalidParameter(String),
    #[error("Unknown method: {0}")]
    UnknownMethod(String),
}

impl ToJsonRpcError for CalculatorError {
    fn to_error_object(&self) -> JsonRpcErrorObject {
        match self {
            CalculatorError::MissingParameters(msg) => JsonRpcErrorObject::invalid_params(msg),
            CalculatorError::InvalidParameter(msg) => JsonRpcErrorObject::invalid_params(msg),
            CalculatorError::UnknownMethod(method) => JsonRpcErrorObject::method_not_found(method),
        }
    }
}

struct CalculatorHandler;

#[async_trait]
impl JsonRpcHandler for CalculatorHandler {
    type Error = CalculatorError;  // Handlers specify their error type

    async fn handle(
        &self,
        method: &str,
        params: Option<RequestParams>,
        _session: Option<SessionContext>
    ) -> Result<Value, Self::Error> {  // Return domain errors only
        match method {
            "add" => {
                let params = params.ok_or_else(|| {
                    CalculatorError::MissingParameters("Missing parameters for add operation".to_string())
                })?;

                let map = params.to_map();
                let a = map.get("a").and_then(|v| v.as_f64()).ok_or_else(|| {
                    CalculatorError::InvalidParameter("Parameter 'a' is required and must be a number".to_string())
                })?;
                let b = map.get("b").and_then(|v| v.as_f64()).ok_or_else(|| {
                    CalculatorError::InvalidParameter("Parameter 'b' is required and must be a number".to_string())
                })?;

                Ok(json!({"result": a + b}))
            }
            "subtract" => {
                let params = params.ok_or_else(|| {
                    CalculatorError::MissingParameters("Missing parameters for subtract operation".to_string())
                })?;

                let map = params.to_map();
                let a = map.get("a").and_then(|v| v.as_f64()).ok_or_else(|| {
                    CalculatorError::InvalidParameter("Parameter 'a' is required and must be a number".to_string())
                })?;
                let b = map.get("b").and_then(|v| v.as_f64()).ok_or_else(|| {
                    CalculatorError::InvalidParameter("Parameter 'b' is required and must be a number".to_string())
                })?;

                Ok(json!({"result": a - b}))
            }
            _ => Err(CalculatorError::UnknownMethod(
                format!("{}. Supported methods: add, subtract", method)
            ))
        }
    }

    fn supported_methods(&self) -> Vec<String> {
        vec!["add".to_string(), "subtract".to_string()]
    }
}

#[tokio::main]
async fn main() {
    // Create type-safe dispatcher
    let mut dispatcher: JsonRpcDispatcher<CalculatorError> = JsonRpcDispatcher::new();
    dispatcher.register_methods(
        vec!["add".to_string(), "subtract".to_string()],
        CalculatorHandler,
    );

    // Example request processing
    let request_json = r#"{"jsonrpc": "2.0", "method": "add", "params": {"a": 5, "b": 3}, "id": 1}"#;

    match parse_json_rpc_message(request_json) {
        Ok(turul_mcp_json_rpc_server::dispatch::JsonRpcMessage::Request(request)) => {
            let response = dispatcher.handle_request(request).await;
            println!("Response: {}", serde_json::to_string_pretty(&response).unwrap());
        }
        _ => println!("Invalid request"),
    }
}

Error Handling Architecture

🚨 Critical: Use Domain Errors Only

✅ CORRECT Pattern:

#[derive(thiserror::Error, Debug)]
enum MyError {
    #[error("Invalid input: {0}")]
    InvalidInput(String),
}

impl ToJsonRpcError for MyError {
    fn to_error_object(&self) -> JsonRpcErrorObject {
        match self {
            MyError::InvalidInput(msg) => JsonRpcErrorObject::invalid_params(msg),
        }
    }
}

impl JsonRpcHandler for MyHandler {
    type Error = MyError;

    async fn handle(&self, ...) -> Result<Value, MyError> {
        Err(MyError::InvalidInput("Bad data".to_string()))  // Domain error only
    }
}

❌ WRONG Pattern:

// DON'T DO THIS - JsonRpcProcessingError no longer exists
use turul_mcp_json_rpc_server::error::JsonRpcProcessingError;  // Removed!

// DON'T DO THIS - Never create protocol errors in handlers
impl JsonRpcHandler for MyHandler {
    async fn handle(&self, ...) -> Result<Value, JsonRpcError> {  // NO!
        Err(JsonRpcError::new(...))  // Wrong layer!
    }
}

JSON-RPC Error Conversion

The dispatcher automatically converts domain errors to proper JSON-RPC errors:

  • Domain Error: InvalidInput("missing field")
  • JSON-RPC Error: {"code": -32602, "message": "missing field"}

Response Format

Success Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {"calculation": 42}
}

Error Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "Invalid parameter: missing required field"
  }
}

Advanced Usage

Session Context

#[async_trait]
impl JsonRpcHandler for StatefulHandler {
    type Error = MyError;

    async fn handle(
        &self,
        method: &str,
        params: Option<RequestParams>,
        session: Option<SessionContext>
    ) -> Result<Value, Self::Error> {
        if let Some(ctx) = session {
            // Use async session operations
            let state = (ctx.get_state)("user_id").await;
            (ctx.set_state)("last_method", json!(method)).await;
        }
        Ok(json!({"status": "success"}))
    }
}

Multiple Handlers

let mut dispatcher: JsonRpcDispatcher<MyError> = JsonRpcDispatcher::new();

// Register specific methods
dispatcher.register_method("calculate".to_string(), CalculatorHandler);
dispatcher.register_method("validate".to_string(), ValidatorHandler);

// Register default handler for unmatched methods
dispatcher.set_default_handler(FallbackHandler);

Migration Guide

When upgrading from earlier versions:

  1. Remove JsonRpcProcessingError imports - no longer exists
  2. Add associated Error type to JsonRpcHandler implementations
  3. Implement ToJsonRpcError for your error types
  4. Return domain errors directly - no protocol layer creation
  5. Use JsonRpcDispatcher with explicit type parameter

Features

  • serde: JSON serialization/deserialization support
  • async: Async handler support with futures
  • session: Session context support for stateful operations

Dependencies

This crate depends on:

  • serde and serde_json for JSON handling
  • async-trait for async trait support
  • thiserror for domain error definitions
  • tokio for async runtime support

For MCP protocol support, use this with turul-mcp-server which provides the high-level MCP server framework.