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

```rust
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:**
```rust
#[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:**
```rust
// 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:**
```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {"calculation": 42}
}
```

**Error Response:**
```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "Invalid parameter: missing required field"
  }
}
```

## Advanced Usage

### Session Context

```rust
#[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

```rust
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<YourError>** 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.