use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::commands::mcp::error::{McpError, RpcError};
pub(crate) const JSONRPC_VERSION: &str = "2.0";
pub(crate) const MAX_METHOD_LEN: usize = 128;
pub(crate) const MAX_TOOL_NAME_LEN: usize = 128;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub(crate) enum RequestId {
Num(i64),
Str(String),
Null,
}
#[derive(Debug, Deserialize)]
pub(crate) struct Request {
pub(crate) jsonrpc: String,
#[serde(default)]
pub(crate) id: Option<RequestId>,
pub(crate) method: String,
#[serde(default)]
pub(crate) params: Value,
}
#[derive(Debug, Serialize)]
pub(crate) struct Response {
pub(crate) jsonrpc: &'static str,
pub(crate) id: ResponseId,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) error: Option<RpcError>,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub(crate) enum ResponseId {
Num(i64),
Str(String),
Null,
}
impl Response {
pub(crate) fn ok(id: ResponseId, result: Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION,
id,
result: Some(result),
error: None,
}
}
pub(crate) fn error(id: ResponseId, error: McpError) -> Self {
Self {
jsonrpc: JSONRPC_VERSION,
id,
result: None,
error: Some(RpcError::from_mcp(&error)),
}
}
}
pub(crate) fn validate(request: Request) -> Result<ParsedRequest, McpError> {
if request.jsonrpc != JSONRPC_VERSION {
return Err(McpError::UnknownMethod("(bad jsonrpc version)".to_owned()));
}
if request.method.len() > MAX_METHOD_LEN {
return Err(McpError::ArgumentTooLarge {
field: "method",
limit: MAX_METHOD_LEN,
observed: request.method.len(),
});
}
Ok(ParsedRequest {
id: request.id,
method: request.method,
params: request.params,
})
}
#[derive(Debug)]
pub(crate) struct ParsedRequest {
pub(crate) id: Option<RequestId>,
pub(crate) method: String,
pub(crate) params: Value,
}
impl RequestId {
pub(crate) fn to_response(&self) -> ResponseId {
match self {
Self::Num(n) => ResponseId::Num(*n),
Self::Str(s) => ResponseId::Str(s.clone()),
Self::Null => ResponseId::Null,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_a_full_request() {
let json = r#"{"jsonrpc":"2.0","id":7,"method":"tools/list","params":{}}"#;
let req: Request = serde_json::from_str(json).expect("parse");
assert_eq!(req.id, Some(RequestId::Num(7)));
assert_eq!(req.method, "tools/list");
}
#[test]
fn parses_a_notification_without_id() {
let json = r#"{"jsonrpc":"2.0","method":"initialized","params":{}}"#;
let req: Request = serde_json::from_str(json).expect("parse");
assert!(req.id.is_none());
}
#[test]
fn parses_a_string_id() {
let json = r#"{"jsonrpc":"2.0","id":"abc","method":"ping","params":{}}"#;
let req: Request = serde_json::from_str(json).expect("parse");
assert_eq!(req.id, Some(RequestId::Str("abc".to_owned())));
}
#[test]
fn validate_rejects_wrong_jsonrpc_version() {
let req = Request {
jsonrpc: "1.0".to_owned(),
id: Some(RequestId::Num(1)),
method: "x".to_owned(),
params: Value::Null,
};
assert!(validate(req).is_err());
}
#[test]
fn validate_rejects_oversized_method() {
let huge = "x".repeat(MAX_METHOD_LEN + 1);
let req = Request {
jsonrpc: JSONRPC_VERSION.to_owned(),
id: Some(RequestId::Num(1)),
method: huge,
params: Value::Null,
};
let err = validate(req).expect_err("oversized method");
assert!(
matches!(
err,
McpError::ArgumentTooLarge {
field: "method",
..
}
),
"{err}"
);
}
#[test]
fn validate_accepts_well_formed_request() {
let req = Request {
jsonrpc: JSONRPC_VERSION.to_owned(),
id: Some(RequestId::Num(1)),
method: "tools/list".to_owned(),
params: Value::Object(Default::default()),
};
let parsed = validate(req).expect("ok");
assert_eq!(parsed.method, "tools/list");
}
#[test]
fn response_ok_serializes_result_and_omits_error() {
let resp = Response::ok(ResponseId::Num(1), Value::String("ok".into()));
let json = serde_json::to_string(&resp).expect("serialize");
assert!(json.contains("\"result\":\"ok\""));
assert!(!json.contains("\"error\""));
}
#[test]
fn response_error_serializes_error_and_omits_result() {
let resp = Response::error(ResponseId::Num(1), McpError::UnknownTool("x".into()));
let json = serde_json::to_string(&resp).expect("serialize");
assert!(json.contains("\"error\""));
assert!(json.contains("\"code\":-32602"));
assert!(!json.contains("\"result\""));
}
#[test]
fn malformed_json_is_a_typed_error_not_a_panic() {
let result: Result<Request, _> = serde_json::from_str("{ not json");
assert!(result.is_err());
}
}