use std::sync::Arc;
use rmcp::model::{ErrorCode, ErrorData as McpError, Implementation};
use serde::{Deserialize, Serialize};
use solo_storage::{TenantHandle, TenantRegistry};
use crate::mcp::SoloMcpServer;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
#[serde(default)]
pub id: Option<serde_json::Value>,
pub method: String,
#[serde(default)]
pub params: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcSuccess {
pub jsonrpc: String,
pub id: serde_json::Value,
pub result: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcErrorResponse {
pub jsonrpc: String,
pub id: serde_json::Value,
pub error: JsonRpcErrorBody,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcErrorBody {
pub code: i32,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum JsonRpcResponse {
Success(JsonRpcSuccess),
Error(JsonRpcErrorResponse),
}
impl JsonRpcResponse {
pub fn success(id: serde_json::Value, result: serde_json::Value) -> Self {
Self::Success(JsonRpcSuccess {
jsonrpc: "2.0".to_string(),
id,
result,
})
}
pub fn error(id: serde_json::Value, code: i32, message: impl Into<String>) -> Self {
Self::Error(JsonRpcErrorResponse {
jsonrpc: "2.0".to_string(),
id,
error: JsonRpcErrorBody {
code,
message: message.into(),
data: None,
},
})
}
pub fn from_mcp_error(id: serde_json::Value, err: McpError) -> Self {
Self::error(id, err.code.0, err.message.to_string())
}
}
#[derive(Clone)]
pub struct McpDispatcher {
server: SoloMcpServer,
}
impl McpDispatcher {
pub fn new(
registry: Arc<TenantRegistry>,
tenant: Arc<TenantHandle>,
user_aliases: Vec<String>,
audit_principal: Option<String>,
) -> Self {
let server = SoloMcpServer::new_for_tenant_with_principal(
registry,
tenant,
user_aliases,
audit_principal,
);
Self { server }
}
pub fn from_server(server: SoloMcpServer) -> Self {
Self { server }
}
pub async fn dispatch(&self, request: JsonRpcRequest) -> Option<JsonRpcResponse> {
let Some(id) = request.id.clone() else {
tracing::debug!(
method = %request.method,
"mcp-http: notification received (no id; no reply)"
);
return None;
};
let params = request.params.unwrap_or(serde_json::Value::Null);
let response = match request.method.as_str() {
"initialize" => self.handle_initialize(id.clone(), params),
"tools/list" => self.handle_tools_list(id.clone()),
"tools/call" => self.handle_tools_call(id.clone(), params).await,
"ping" => JsonRpcResponse::success(id.clone(), serde_json::json!({})),
other => JsonRpcResponse::error(
id.clone(),
ErrorCode::METHOD_NOT_FOUND.0,
format!("unknown method `{other}`"),
),
};
Some(response)
}
fn handle_initialize(
&self,
id: serde_json::Value,
_params: serde_json::Value,
) -> JsonRpcResponse {
let server_info = Implementation::new(
"solo".to_string(),
env!("CARGO_PKG_VERSION").to_string(),
);
let result = serde_json::json!({
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
},
"serverInfo": server_info,
});
JsonRpcResponse::success(id, result)
}
fn handle_tools_list(&self, id: serde_json::Value) -> JsonRpcResponse {
let tools = self.server.dispatch_list_tools();
let result = serde_json::json!({ "tools": tools });
JsonRpcResponse::success(id, result)
}
async fn handle_tools_call(
&self,
id: serde_json::Value,
params: serde_json::Value,
) -> JsonRpcResponse {
let name = match params.get("name").and_then(|v| v.as_str()) {
Some(n) => n.to_string(),
None => {
return JsonRpcResponse::error(
id,
ErrorCode::INVALID_PARAMS.0,
"tools/call: missing `name` field",
);
}
};
let arguments = params
.get("arguments")
.cloned()
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
match self.server.dispatch_tool(&name, arguments).await {
Ok(call_result) => {
let result = match serde_json::to_value(&call_result) {
Ok(v) => v,
Err(e) => {
return JsonRpcResponse::error(
id,
ErrorCode::INTERNAL_ERROR.0,
format!("serialize tool result: {e}"),
);
}
};
JsonRpcResponse::success(id, result)
}
Err(e) => JsonRpcResponse::from_mcp_error(id, e),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn jsonrpc_success_serialises_with_jsonrpc_field() {
let resp =
JsonRpcResponse::success(serde_json::json!(1), serde_json::json!({"ok": true}));
let s = serde_json::to_string(&resp).unwrap();
assert!(s.contains(r#""jsonrpc":"2.0""#));
assert!(s.contains(r#""id":1"#));
assert!(s.contains(r#""result":{"ok":true}"#));
assert!(!s.contains(r#""error":"#));
}
#[test]
fn jsonrpc_error_serialises_with_error_field() {
let resp = JsonRpcResponse::error(
serde_json::json!(7),
ErrorCode::METHOD_NOT_FOUND.0,
"unknown method `foo`",
);
let s = serde_json::to_string(&resp).unwrap();
assert!(s.contains(r#""jsonrpc":"2.0""#));
assert!(s.contains(r#""id":7"#));
assert!(s.contains(r#""error":{"#));
assert!(s.contains(r#""code":-32601"#));
assert!(!s.contains(r#""result":"#));
}
#[test]
fn jsonrpc_notification_has_no_id() {
let raw = r#"{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}"#;
let req: JsonRpcRequest = serde_json::from_str(raw).unwrap();
assert_eq!(req.method, "notifications/initialized");
assert!(req.id.is_none());
}
#[test]
fn jsonrpc_request_with_null_id_parses_as_notification() {
let raw = r#"{"jsonrpc":"2.0","id":null,"method":"ping"}"#;
let req: JsonRpcRequest = serde_json::from_str(raw).unwrap();
assert!(req.id.is_none());
}
}