use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
pub const JSONRPC_VERSION: &str = "2.0";
pub const MCP_VERSION: &str = "2024-11-05";
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RequestId {
Number(i64),
String(String),
}
impl From<i64> for RequestId {
fn from(n: i64) -> Self {
Self::Number(n)
}
}
impl From<String> for RequestId {
fn from(s: String) -> Self {
Self::String(s)
}
}
impl From<&str> for RequestId {
fn from(s: &str) -> Self {
Self::String(s.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub id: RequestId,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
impl JsonRpcRequest {
pub fn new(id: impl Into<RequestId>, method: impl Into<String>) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
id: id.into(),
method: method.into(),
params: None,
}
}
pub fn with_params(mut self, params: Value) -> Self {
self.params = Some(params);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
pub id: RequestId,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
impl JsonRpcResponse {
pub fn success(id: impl Into<RequestId>, result: Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
id: id.into(),
result: Some(result),
error: None,
}
}
pub fn error(id: impl Into<RequestId>, error: JsonRpcError) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
id: id.into(),
result: None,
error: Some(error),
}
}
pub fn is_error(&self) -> bool {
self.error.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl JsonRpcError {
pub fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
data: None,
}
}
pub fn parse_error(message: impl Into<String>) -> Self {
Self::new(-32700, message)
}
pub fn invalid_request(message: impl Into<String>) -> Self {
Self::new(-32600, message)
}
pub fn method_not_found(message: impl Into<String>) -> Self {
Self::new(-32601, message)
}
pub fn invalid_params(message: impl Into<String>) -> Self {
Self::new(-32602, message)
}
pub fn internal_error(message: impl Into<String>) -> Self {
Self::new(-32603, message)
}
}
impl std::fmt::Display for JsonRpcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.code, self.message)
}
}
impl std::error::Error for JsonRpcError {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcNotification {
pub jsonrpc: String,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
impl JsonRpcNotification {
pub fn new(method: impl Into<String>) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
method: method.into(),
params: None,
}
}
pub fn with_params(mut self, params: Value) -> Self {
self.params = Some(params);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpTool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "inputSchema")]
pub input_schema: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpResource {
pub uri: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpResourceTemplate {
#[serde(rename = "uriTemplate")]
pub uri_template: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpPrompt {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<McpPromptArgument>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpPromptArgument {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ServerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<ToolCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourceCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompts: Option<PromptCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logging: Option<LoggingCapabilities>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ToolCapabilities {
#[serde(rename = "listChanged", skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResourceCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub subscribe: Option<bool>,
#[serde(rename = "listChanged", skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PromptCapabilities {
#[serde(rename = "listChanged", skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LoggingCapabilities {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub roots: Option<RootCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sampling: Option<SamplingCapabilities>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RootCapabilities {
#[serde(rename = "listChanged", skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SamplingCapabilities {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitializeParams {
#[serde(rename = "protocolVersion")]
pub protocol_version: String,
pub capabilities: ClientCapabilities,
#[serde(rename = "clientInfo")]
pub client_info: ClientInfo,
}
impl Default for InitializeParams {
fn default() -> Self {
Self {
protocol_version: MCP_VERSION.to_string(),
capabilities: ClientCapabilities::default(),
client_info: ClientInfo::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientInfo {
pub name: String,
pub version: String,
}
impl Default for ClientInfo {
fn default() -> Self {
Self {
name: "liteforge".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitializeResult {
#[serde(rename = "protocolVersion")]
pub protocol_version: String,
pub capabilities: ServerCapabilities,
#[serde(rename = "serverInfo")]
pub server_info: ServerInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallToolParams {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<HashMap<String, Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallToolResult {
pub content: Vec<ToolResultContent>,
#[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ToolResultContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image")]
Image { data: String, mime_type: String },
#[serde(rename = "resource")]
Resource { resource: McpResource, text: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListToolsResult {
pub tools: Vec<McpTool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListResourcesResult {
pub resources: Vec<McpResource>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListPromptsResult {
pub prompts: Vec<McpPrompt>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadResourceResult {
pub contents: Vec<ResourceContent>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceContent {
pub uri: String,
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blob: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetPromptResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub messages: Vec<PromptMessage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptMessage {
pub role: String,
pub content: PromptContent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum PromptContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image")]
Image { data: String, mime_type: String },
#[serde(rename = "resource")]
Resource { resource: McpResource },
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_request_id_from() {
let id: RequestId = 123.into();
assert_eq!(id, RequestId::Number(123));
let id: RequestId = "abc".into();
assert_eq!(id, RequestId::String("abc".to_string()));
}
#[test]
fn test_jsonrpc_request() {
let req = JsonRpcRequest::new(1, "tools/list");
assert_eq!(req.jsonrpc, "2.0");
assert_eq!(req.method, "tools/list");
assert!(req.params.is_none());
let req = req.with_params(json!({"cursor": null}));
assert!(req.params.is_some());
}
#[test]
fn test_jsonrpc_response_success() {
let resp = JsonRpcResponse::success(1, json!({"tools": []}));
assert!(!resp.is_error());
assert!(resp.result.is_some());
}
#[test]
fn test_jsonrpc_response_error() {
let err = JsonRpcError::method_not_found("Unknown method");
let resp = JsonRpcResponse::error(1, err);
assert!(resp.is_error());
assert!(resp.error.is_some());
assert_eq!(resp.error.unwrap().code, -32601);
}
#[test]
fn test_jsonrpc_error_display() {
let err = JsonRpcError::invalid_params("Missing required field");
assert_eq!(err.to_string(), "[-32602] Missing required field");
}
#[test]
fn test_mcp_tool_serialization() {
let tool = McpTool {
name: "read_file".to_string(),
description: Some("Read a file".to_string()),
input_schema: json!({
"type": "object",
"properties": {
"path": {"type": "string"}
},
"required": ["path"]
}),
};
let json_str = serde_json::to_string(&tool).unwrap();
assert!(json_str.contains("inputSchema"));
}
#[test]
fn test_initialize_params_default() {
let params = InitializeParams::default();
assert_eq!(params.protocol_version, MCP_VERSION);
assert_eq!(params.client_info.name, "liteforge");
}
#[test]
fn test_tool_result_content() {
let content = ToolResultContent::Text {
text: "Hello".to_string(),
};
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json["type"], "text");
assert_eq!(json["text"], "Hello");
}
}