use serde::{Deserialize, Serialize};
pub const JSONRPC_VERSION: &str = "2.0";
pub const PARSE_ERROR: i64 = -32700;
pub const INVALID_REQUEST: i64 = -32600;
pub const METHOD_NOT_FOUND: i64 = -32601;
pub const INVALID_PARAMS: i64 = -32602;
pub const INTERNAL_ERROR: i64 = -32603;
pub const METHOD_INITIALIZE: &str = "initialize";
pub const METHOD_PING: &str = "ping";
pub const METHOD_LOGGING_SET_LEVEL: &str = "logging/setLevel";
pub const METHOD_NOTIFICATIONS_INITIALIZED: &str = "notifications/initialized";
pub const METHOD_INITIALIZED: &str = "initialized";
pub const METHOD_NOTIFICATIONS_CANCELLED: &str = "notifications/cancelled";
pub const METHOD_TOOLS_LIST: &str = "tools/list";
pub const METHOD_TOOLS_CALL: &str = "tools/call";
pub const METHOD_RESOURCES_LIST: &str = "resources/list";
pub const METHOD_RESOURCES_TEMPLATES_LIST: &str = "resources/templates/list";
pub const METHOD_RESOURCES_READ: &str = "resources/read";
pub const METHOD_PROMPTS_LIST: &str = "prompts/list";
pub const METHOD_PROMPTS_GET: &str = "prompts/get";
pub const METHOD_COMPLETION_COMPLETE: &str = "completion/complete";
pub const METHOD_NOTIFICATIONS_PROGRESS: &str = "notifications/progress";
pub const METHOD_NOTIFICATIONS_MESSAGE: &str = "notifications/message";
#[derive(Debug, Deserialize)]
pub struct JsonRpcRequest {
#[serde(rename = "jsonrpc")]
pub version: String,
pub id: Option<serde_json::Value>,
pub method: String,
#[serde(default)]
pub params: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct JsonRpcResponse {
pub jsonrpc: &'static str,
pub id: Option<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>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct JsonRpcError {
pub code: i64,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
impl JsonRpcResponse {
#[must_use]
pub fn success(id: Option<serde_json::Value>, result: impl Serialize) -> Self {
Self {
jsonrpc: "2.0",
id,
result: Some(
serde_json::to_value(result).expect("MCP result type must be JSON-serializable"),
),
error: None,
}
}
#[must_use]
pub fn error(id: Option<serde_json::Value>, code: i64, message: impl Into<String>) -> Self {
Self {
jsonrpc: "2.0",
id,
result: None,
error: Some(JsonRpcError {
code,
message: message.into(),
data: None,
}),
}
}
#[must_use]
pub fn error_with_data(
id: Option<serde_json::Value>,
code: i64,
message: impl Into<String>,
data: serde_json::Value,
) -> Self {
Self {
jsonrpc: "2.0",
id,
result: None,
error: Some(JsonRpcError {
code,
message: message.into(),
data: Some(data),
}),
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResult {
pub protocol_version: &'static str,
pub server_info: ServerInfo,
pub capabilities: Capabilities,
}
#[derive(Debug, Serialize)]
pub struct ServerInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Serialize)]
pub struct Capabilities {
pub tools: ToolCapabilities,
pub resources: ResourceCapabilities,
pub prompts: PromptCapabilities,
}
#[derive(Debug, Default, Serialize)]
pub struct ToolCapabilities {}
#[derive(Debug, Default, Serialize)]
pub struct ResourceCapabilities {}
#[derive(Debug, Default, Serialize)]
pub struct PromptCapabilities {}
#[derive(Clone, Debug, Serialize)]
pub struct ToolsListResult {
pub tools: Vec<McpToolSchema>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct McpToolSchema {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
#[derive(Debug, Deserialize)]
pub struct ToolCallParams {
pub name: String,
#[serde(default = "empty_object")]
pub arguments: serde_json::Value,
}
fn empty_object() -> serde_json::Value {
serde_json::Value::Object(serde_json::Map::new())
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolCallResult {
pub content: Vec<ContentItem>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub is_error: bool,
}
impl ToolCallResult {
#[must_use]
pub fn text(&self) -> Option<&str> {
self.content.first().map(|item| item.text.as_str())
}
}
pub const CONTENT_TYPE_TEXT: &str = "text";
#[derive(Debug, Serialize)]
pub struct ContentItem {
#[serde(rename = "type")]
pub content_type: &'static str,
pub text: String,
}
impl ContentItem {
pub fn text(text: impl Into<String>) -> Self {
Self {
content_type: CONTENT_TYPE_TEXT,
text: text.into(),
}
}
}
#[derive(Clone, Debug, Serialize)]
pub struct PromptsListResult {
pub prompts: Vec<PromptDefinition>,
}
pub use llm_tool::{PromptArgumentDefinition, PromptDefinition};
#[derive(Debug, Deserialize)]
pub struct GetPromptParams {
pub name: String,
#[serde(default = "empty_object")]
pub arguments: serde_json::Value,
}
#[derive(Debug, Serialize)]
pub struct GetPromptResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub messages: Vec<PromptMessage>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct PromptMessage {
pub role: String,
pub content: PromptMessageContent,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type")]
pub enum PromptMessageContent {
#[serde(rename = "text")]
Text {
text: String,
},
#[serde(rename = "resource")]
Resource {
resource: ResourceContent,
},
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct McpResource {
pub uri: String,
pub name: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
pub use McpResource as Resource;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ResourcesListResult {
pub resources: Vec<McpResource>,
}
pub use llm_tool::ResourceDefinition;
#[derive(Debug, Deserialize)]
pub struct ReadResourceParams {
pub uri: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ReadResourceResult {
pub contents: Vec<ResourceContent>,
}
pub use llm_tool::ResourceOutputContent as ResourceContent;
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct EmptyResult {}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceTemplatesListResult {
pub resource_templates: Vec<ResourceDefinition>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct CompletionCompleteResult {
pub completion: CompletionResultData,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CompletionResultData {
pub values: Vec<String>,
pub total: usize,
pub has_more: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_request_with_params() {
let json = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add"}}"#;
let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.version, "2.0");
assert_eq!(req.id, Some(serde_json::json!(1)));
assert_eq!(req.method, "tools/call");
assert!(req.params.is_some());
}
#[test]
fn deserialize_request_without_params() {
let json = r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#;
let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
assert!(req.params.is_none());
}
#[test]
fn deserialize_notification_without_id() {
let json = r#"{"jsonrpc":"2.0","method":"initialized"}"#;
let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
assert!(req.id.is_none());
}
#[test]
fn serialize_success_response() {
let resp =
JsonRpcResponse::success(Some(serde_json::json!(1)), serde_json::json!({"ok": true}));
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains(r#""jsonrpc":"2.0""#));
assert!(json.contains(r#""result":{""#));
assert!(!json.contains("error"));
}
#[test]
fn serialize_error_response() {
let resp = JsonRpcResponse::error(Some(serde_json::json!(1)), PARSE_ERROR, "bad json");
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains(r#""code":-32700"#));
assert!(json.contains(r#""message":"bad json""#));
assert!(!json.contains("result"));
}
#[test]
fn serialize_error_omits_null_id() {
let resp = JsonRpcResponse::error(None, METHOD_NOT_FOUND, "no such method");
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains(r#""id":null"#));
}
#[test]
fn response_jsonrpc_field_is_static() {
let resp = JsonRpcResponse::success(None, serde_json::json!(null));
assert_eq!(resp.jsonrpc, "2.0");
}
#[test]
fn error_without_data_omits_data_field() {
let resp = JsonRpcResponse::error(Some(serde_json::json!(1)), PARSE_ERROR, "bad");
let json = serde_json::to_string(&resp).unwrap();
assert!(!json.contains("data"));
}
#[test]
fn error_with_data_includes_data_field() {
let resp = JsonRpcResponse::error_with_data(
Some(serde_json::json!(1)),
INTERNAL_ERROR,
"boom",
serde_json::json!({"detail": "stack trace"}),
);
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains(r#""data":{"detail":"stack trace"}"#));
}
#[test]
fn jsonrpc_version_constant() {
assert_eq!(JSONRPC_VERSION, "2.0");
}
#[test]
fn method_consts_match_wire_strings() {
assert_eq!(METHOD_INITIALIZE, "initialize");
assert_eq!(METHOD_PING, "ping");
assert_eq!(METHOD_LOGGING_SET_LEVEL, "logging/setLevel");
assert_eq!(
METHOD_NOTIFICATIONS_INITIALIZED,
"notifications/initialized"
);
assert_eq!(METHOD_INITIALIZED, "initialized");
assert_eq!(METHOD_NOTIFICATIONS_CANCELLED, "notifications/cancelled");
assert_eq!(METHOD_TOOLS_LIST, "tools/list");
assert_eq!(METHOD_TOOLS_CALL, "tools/call");
assert_eq!(METHOD_RESOURCES_LIST, "resources/list");
assert_eq!(METHOD_RESOURCES_TEMPLATES_LIST, "resources/templates/list");
assert_eq!(METHOD_RESOURCES_READ, "resources/read");
assert_eq!(METHOD_PROMPTS_LIST, "prompts/list");
assert_eq!(METHOD_PROMPTS_GET, "prompts/get");
assert_eq!(METHOD_COMPLETION_COMPLETE, "completion/complete");
assert_eq!(METHOD_NOTIFICATIONS_PROGRESS, "notifications/progress");
assert_eq!(METHOD_NOTIFICATIONS_MESSAGE, "notifications/message");
}
#[test]
fn content_item_text_constructor_sets_type() {
let item = ContentItem::text("hello");
assert_eq!(item.content_type, CONTENT_TYPE_TEXT);
assert_eq!(item.content_type, "text");
assert_eq!(item.text, "hello");
}
#[test]
fn tool_call_result_text_returns_first_block() {
let result = ToolCallResult {
content: vec![ContentItem::text("first"), ContentItem::text("second")],
is_error: false,
};
assert_eq!(result.text(), Some("first"));
let empty = ToolCallResult {
content: vec![],
is_error: true,
};
assert_eq!(empty.text(), None);
}
}