use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
#[serde(default)]
pub id: serde_json::Value,
pub method: String,
#[serde(default)]
pub params: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
pub id: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
impl JsonRpcResponse {
pub fn success(id: serde_json::Value, result: serde_json::Value) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: Some(result),
error: None,
}
}
pub fn error(id: serde_json::Value, code: ErrorCode, message: String) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: Some(JsonRpcError {
code: code.as_i32(),
message,
data: None,
}),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
ParseError,
InvalidRequest,
MethodNotFound,
InvalidParams,
InternalError,
}
impl ErrorCode {
pub fn as_i32(self) -> i32 {
match self {
Self::ParseError => -32700,
Self::InvalidRequest => -32600,
Self::MethodNotFound => -32601,
Self::InvalidParams => -32602,
Self::InternalError => -32603,
}
}
}
pub trait McpTransport {
fn read_line(
&mut self,
) -> impl std::future::Future<Output = std::io::Result<Option<String>>> + Send;
fn write_line(
&mut self,
line: &str,
) -> impl std::future::Future<Output = std::io::Result<()>> + Send;
fn flush(&mut self) -> impl std::future::Future<Output = std::io::Result<()>> + Send;
}
pub struct StdioTransport {
reader: tokio::io::Lines<tokio::io::BufReader<tokio::io::Stdin>>,
writer: tokio::io::Stdout,
}
impl Default for StdioTransport {
fn default() -> Self {
use tokio::io::AsyncBufReadExt;
Self {
reader: tokio::io::BufReader::new(tokio::io::stdin()).lines(),
writer: tokio::io::stdout(),
}
}
}
impl StdioTransport {
pub fn new() -> Self {
Self::default()
}
}
impl McpTransport for StdioTransport {
async fn read_line(&mut self) -> std::io::Result<Option<String>> {
self.reader.next_line().await
}
async fn write_line(&mut self, line: &str) -> std::io::Result<()> {
use tokio::io::AsyncWriteExt;
self.writer.write_all(line.as_bytes()).await
}
async fn flush(&mut self) -> std::io::Result<()> {
use tokio::io::AsyncWriteExt;
self.writer.flush().await
}
}
#[cfg(any(test, feature = "test-transport"))]
pub struct ChannelTransport {
rx: tokio::sync::mpsc::UnboundedReceiver<String>,
tx: tokio::sync::mpsc::UnboundedSender<String>,
}
#[cfg(any(test, feature = "test-transport"))]
impl ChannelTransport {
pub fn new() -> (
Self,
tokio::sync::mpsc::UnboundedSender<String>,
tokio::sync::mpsc::UnboundedReceiver<String>,
) {
let (input_tx, input_rx) = tokio::sync::mpsc::unbounded_channel();
let (output_tx, output_rx) = tokio::sync::mpsc::unbounded_channel();
(
Self {
rx: input_rx,
tx: output_tx,
},
input_tx,
output_rx,
)
}
}
#[cfg(any(test, feature = "test-transport"))]
impl McpTransport for ChannelTransport {
async fn read_line(&mut self) -> std::io::Result<Option<String>> {
Ok(self.rx.recv().await)
}
async fn write_line(&mut self, line: &str) -> std::io::Result<()> {
self.tx
.send(line.to_string())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::BrokenPipe, e.to_string()))
}
async fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_jsonrpc_request() {
let msg = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
});
let request: JsonRpcRequest = serde_json::from_value(msg).unwrap();
assert_eq!(request.method, "tools/list");
assert_eq!(request.id, serde_json::Value::Number(1.into()));
}
#[test]
fn test_parse_notification_without_id() {
let msg = json!({
"jsonrpc": "2.0",
"method": "initialized"
});
let request: JsonRpcRequest = serde_json::from_value(msg).unwrap();
assert_eq!(request.method, "initialized");
assert!(request.id.is_null());
assert!(request.params.is_none());
}
#[test]
fn test_serialize_success_response() {
let response =
JsonRpcResponse::success(serde_json::Value::Number(1.into()), json!({"tools": []}));
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"jsonrpc\":\"2.0\""));
assert!(json.contains("\"tools\":[]"));
assert!(!json.contains("\"error\""));
}
#[test]
fn test_serialize_error_response() {
let response = JsonRpcResponse::error(
serde_json::Value::Number(1.into()),
ErrorCode::MethodNotFound,
"Method not found".to_string(),
);
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("-32601"));
assert!(json.contains("Method not found"));
assert!(!json.contains("\"result\""));
}
#[test]
fn test_error_codes() {
assert_eq!(ErrorCode::ParseError.as_i32(), -32700);
assert_eq!(ErrorCode::InvalidRequest.as_i32(), -32600);
assert_eq!(ErrorCode::MethodNotFound.as_i32(), -32601);
assert_eq!(ErrorCode::InvalidParams.as_i32(), -32602);
assert_eq!(ErrorCode::InternalError.as_i32(), -32603);
}
#[test]
fn test_request_with_string_id() {
let msg = json!({
"jsonrpc": "2.0",
"id": "abc-123",
"method": "ping"
});
let request: JsonRpcRequest = serde_json::from_value(msg).unwrap();
assert_eq!(request.id, serde_json::Value::String("abc-123".to_string()));
}
}