- Replace hardcoded i32 enum matches with ProtoEnum::try_from() in 8 command files (cluster, config, data, model, risk, service, train, tune) - Replace hardcoded i32 literals with enum variants (EmergencyStopType, ExportFormat, TrainingMode) - Add JSON-RPC 2.0 PARSE_ERROR (-32700) for malformed JSON input - Validate jsonrpc:"2.0" version on incoming MCP requests - Change unreachable execute_tool catch-all from Ok to Err - Fix TUI teardown to not mask original event_loop error - Rename config_cmd module to config (commands::config) - 77 tests pass, 0 clippy warnings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
136 lines
4.2 KiB
Rust
136 lines
4.2 KiB
Rust
//! JSON-RPC 2.0 message types for the MCP protocol.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
|
|
/// A JSON-RPC 2.0 request (or notification, when `id` is `None`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct JsonRpcRequest {
|
|
/// Must be `"2.0"`.
|
|
pub jsonrpc: String,
|
|
/// Request identifier. `None` for notifications (no response expected).
|
|
pub id: Option<Value>,
|
|
/// The method to invoke (e.g. `"initialize"`, `"tools/list"`, `"tools/call"`).
|
|
pub method: String,
|
|
/// Method parameters. Defaults to `null` when absent.
|
|
#[serde(default)]
|
|
pub params: Value,
|
|
}
|
|
|
|
/// A JSON-RPC 2.0 response.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct JsonRpcResponse {
|
|
/// Always `"2.0"`.
|
|
pub jsonrpc: String,
|
|
/// Mirrors the `id` from the request.
|
|
pub id: Value,
|
|
/// The result on success.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub result: Option<Value>,
|
|
/// The error on failure.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub error: Option<JsonRpcError>,
|
|
}
|
|
|
|
/// A JSON-RPC 2.0 error object.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct JsonRpcError {
|
|
/// Numeric error code (see JSON-RPC 2.0 spec).
|
|
pub code: i64,
|
|
/// Human-readable error message.
|
|
pub message: String,
|
|
/// Optional structured error data.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub data: Option<Value>,
|
|
}
|
|
|
|
// ── Standard JSON-RPC 2.0 error codes ───────────────────────────────
|
|
|
|
/// The JSON sent is not valid JSON (parse error).
|
|
pub const PARSE_ERROR: i64 = -32700;
|
|
|
|
/// The JSON sent is not a valid JSON-RPC request.
|
|
pub const INVALID_REQUEST: i64 = -32600;
|
|
|
|
/// The method does not exist or is not available.
|
|
pub const METHOD_NOT_FOUND: i64 = -32601;
|
|
|
|
/// Invalid method parameters.
|
|
pub const INVALID_PARAMS: i64 = -32602;
|
|
|
|
/// Internal server error.
|
|
pub const INTERNAL_ERROR: i64 = -32603;
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
|
|
/// Build a successful response.
|
|
pub fn success_response(id: Value, result: Value) -> JsonRpcResponse {
|
|
JsonRpcResponse {
|
|
jsonrpc: "2.0".into(),
|
|
id,
|
|
result: Some(result),
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
/// Build an error response.
|
|
pub fn error_response(id: Value, code: i64, message: &str) -> JsonRpcResponse {
|
|
JsonRpcResponse {
|
|
jsonrpc: "2.0".into(),
|
|
id,
|
|
result: None,
|
|
error: Some(JsonRpcError {
|
|
code,
|
|
message: message.to_owned(),
|
|
data: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn deserialize_request_with_id() {
|
|
let raw = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#;
|
|
let req: JsonRpcRequest = serde_json::from_str(raw).unwrap();
|
|
assert_eq!(req.jsonrpc, "2.0");
|
|
assert_eq!(req.method, "initialize");
|
|
assert_eq!(req.id, Some(json!(1)));
|
|
}
|
|
|
|
#[test]
|
|
fn deserialize_notification_no_id() {
|
|
let raw = r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#;
|
|
let req: JsonRpcRequest = serde_json::from_str(raw).unwrap();
|
|
assert!(req.id.is_none());
|
|
assert_eq!(req.params, Value::Null);
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_success_response() {
|
|
let resp = success_response(json!(1), json!({"ok": true}));
|
|
let s = serde_json::to_string(&resp).unwrap();
|
|
assert!(s.contains(r#""result":"#));
|
|
assert!(!s.contains(r#""error""#));
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_error_response() {
|
|
let resp = error_response(json!(2), METHOD_NOT_FOUND, "Method not found");
|
|
let s = serde_json::to_string(&resp).unwrap();
|
|
assert!(s.contains(r#""error""#));
|
|
assert!(!s.contains(r#""result""#));
|
|
}
|
|
|
|
#[test]
|
|
fn deserialize_request_string_id() {
|
|
let raw = r#"{"jsonrpc":"2.0","id":"abc","method":"tools/list"}"#;
|
|
let req: JsonRpcRequest = serde_json::from_str(raw).unwrap();
|
|
assert_eq!(req.id, Some(json!("abc")));
|
|
}
|
|
}
|