use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
#[serde(default)]
pub id: Option<RequestId>,
pub method: String,
#[serde(default)]
pub params: Option<Value>,
}
#[derive(Debug, Serialize)]
pub struct JsonRpcResponse {
pub jsonrpc: &'static str,
pub id: RequestId,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<RpcError>,
}
#[derive(Debug, Serialize)]
pub struct JsonRpcNotification {
pub jsonrpc: &'static str,
pub method: &'static str,
pub params: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RequestId {
Number(i64),
String(String),
}
#[derive(Debug, Serialize)]
pub struct RpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl RpcError {
pub const PARSE_ERROR: i32 = -32_700;
pub const INVALID_REQUEST: i32 = -32_600;
pub const METHOD_NOT_FOUND: i32 = -32_601;
pub const INVALID_PARAMS: i32 = -32_602;
pub const INTERNAL_ERROR: i32 = -32_603;
#[must_use]
pub fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
data: None,
}
}
}
impl JsonRpcResponse {
#[must_use]
pub fn ok(id: RequestId, result: Value) -> Self {
Self {
jsonrpc: "2.0",
id,
result: Some(result),
error: None,
}
}
#[must_use]
pub fn err(id: RequestId, error: RpcError) -> Self {
Self {
jsonrpc: "2.0",
id,
result: None,
error: Some(error),
}
}
}
#[derive(Debug, Deserialize)]
pub struct InitializeParams {
#[serde(rename = "protocolVersion")]
pub protocol_version: String,
pub capabilities: ClientCapabilities,
#[serde(rename = "clientInfo")]
pub client_info: ClientInfo,
}
#[derive(Debug, Default, Deserialize)]
pub struct ClientCapabilities {
pub roots: Option<Value>,
pub sampling: Option<Value>,
pub elicitation: Option<Value>,
}
impl ClientCapabilities {
#[must_use]
pub fn supports_elicitation(&self) -> bool {
self.elicitation.is_some()
}
#[must_use]
pub fn supports_sampling(&self) -> bool {
self.sampling.is_some()
}
}
#[derive(Debug, Deserialize)]
pub struct ClientInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Serialize)]
pub struct InitializeResult {
#[serde(rename = "protocolVersion")]
pub protocol_version: &'static str,
pub capabilities: ServerCapabilities,
#[serde(rename = "serverInfo")]
pub server_info: ServerInfo,
pub instructions: &'static str,
}
#[derive(Debug, Serialize)]
pub struct ServerCapabilities {
pub tools: ToolsCapability,
pub logging: LoggingCapability,
pub resources: ResourcesCapability,
pub prompts: PromptsCapability,
pub elicitation: ElicitationCapability,
pub tasks: TasksCapability,
pub sampling: SamplingCapability,
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental: Option<Value>,
}
#[derive(Debug, Serialize)]
pub struct SamplingCapability {}
#[derive(Debug, Serialize)]
pub struct ElicitationCapability {}
#[derive(Debug, Serialize)]
pub struct TasksCapability {}
#[derive(Debug, Serialize)]
pub struct ToolsCapability {
#[serde(rename = "listChanged")]
pub list_changed: bool,
}
#[derive(Debug, Serialize)]
pub struct LoggingCapability {}
#[derive(Debug, Serialize)]
pub struct ResourcesCapability {
pub subscribe: bool,
#[serde(rename = "listChanged")]
pub list_changed: bool,
}
#[derive(Debug, Serialize)]
pub struct PromptsCapability {
#[serde(rename = "listChanged")]
pub list_changed: bool,
}
#[derive(Debug, Serialize)]
pub struct ServerInfo {
pub name: &'static str,
pub version: &'static str,
pub title: &'static str,
}
#[derive(Debug, Clone, Serialize)]
pub struct Tool {
pub name: &'static str,
pub title: &'static str,
pub description: &'static str,
#[serde(rename = "inputSchema")]
pub input_schema: Value,
#[serde(rename = "outputSchema")]
pub output_schema: Value,
pub annotations: ToolAnnotations,
}
#[derive(Debug, Clone, Serialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct ToolAnnotations {
#[serde(rename = "readOnlyHint")]
pub read_only: bool,
#[serde(rename = "destructiveHint")]
pub destructive: bool,
#[serde(rename = "idempotentHint")]
pub idempotent: bool,
#[serde(rename = "openWorldHint")]
pub open_world: bool,
}
#[derive(Debug, Serialize)]
pub struct ToolListResult {
pub tools: Vec<Tool>,
}
#[derive(Debug, Deserialize)]
pub struct ToolCallParams {
pub name: String,
#[serde(default)]
pub arguments: Option<Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ContentItem {
#[serde(rename = "type")]
pub kind: &'static str,
pub text: String,
}
impl ContentItem {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self {
kind: "text",
text: text.into(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ToolCallResult {
pub content: Vec<ContentItem>,
#[serde(rename = "isError")]
pub is_error: bool,
}
impl ToolCallResult {
#[must_use]
pub fn ok(text: impl Into<String>) -> Self {
Self {
content: vec![ContentItem::text(text)],
is_error: false,
}
}
#[must_use]
pub fn error(text: impl Into<String>) -> Self {
Self {
content: vec![ContentItem::text(text)],
is_error: true,
}
}
}
#[derive(Debug, Serialize)]
pub struct PingResult {}
#[derive(Debug, Deserialize)]
pub struct ResourceSubscribeParams {
pub uri: String,
}
#[derive(Debug, Deserialize)]
pub struct ResourceUnsubscribeParams {
pub uri: String,
}
#[derive(Debug, Serialize)]
pub struct ResourceSubscribeResult {}
#[derive(Debug, Clone, Serialize)]
pub struct Resource {
pub uri: &'static str,
pub name: &'static str,
pub title: &'static str,
pub description: &'static str,
#[serde(rename = "mimeType")]
pub mime_type: &'static str,
}
#[derive(Debug, Clone, Serialize)]
pub struct ResourceTemplate {
#[serde(rename = "uriTemplate")]
pub uri_template: &'static str,
pub name: &'static str,
pub title: &'static str,
pub description: &'static str,
#[serde(rename = "mimeType")]
pub mime_type: &'static str,
}
#[derive(Debug, Serialize)]
pub struct ResourceListResult {
pub resources: Vec<Resource>,
}
#[derive(Debug, Serialize)]
pub struct ResourceTemplateListResult {
#[serde(rename = "resourceTemplates")]
pub resource_templates: Vec<ResourceTemplate>,
}
#[derive(Debug, Deserialize)]
pub struct ResourceReadParams {
pub uri: String,
}
#[derive(Debug, Serialize)]
pub struct ResourceContents {
pub uri: String,
#[serde(rename = "mimeType")]
pub mime_type: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blob: Option<String>,
}
impl ResourceContents {
#[must_use]
pub fn text(uri: impl Into<String>, mime_type: &'static str, text: impl Into<String>) -> Self {
Self {
uri: uri.into(),
mime_type,
text: Some(text.into()),
blob: None,
}
}
#[must_use]
pub fn blob(uri: impl Into<String>, mime_type: &'static str, blob: impl Into<String>) -> Self {
Self {
uri: uri.into(),
mime_type,
text: None,
blob: Some(blob.into()),
}
}
}
#[derive(Debug, Serialize)]
pub struct ResourceReadResult {
pub contents: Vec<ResourceContents>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Prompt {
pub name: &'static str,
pub title: &'static str,
pub description: &'static str,
pub arguments: Vec<PromptArgument>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PromptArgument {
pub name: &'static str,
pub description: &'static str,
pub required: bool,
}
#[derive(Debug, Serialize)]
pub struct PromptListResult {
pub prompts: Vec<Prompt>,
}
#[derive(Debug, Deserialize)]
pub struct PromptGetParams {
pub name: String,
#[serde(default)]
pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum PromptRole {
User,
Assistant,
}
#[derive(Debug, Serialize)]
pub struct PromptMessage {
pub role: PromptRole,
pub content: PromptContent,
}
#[derive(Debug, Serialize)]
pub struct PromptContent {
#[serde(rename = "type")]
pub kind: &'static str,
pub text: String,
}
impl PromptContent {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self {
kind: "text",
text: text.into(),
}
}
}
#[derive(Debug, Serialize)]
pub struct PromptGetResult {
pub description: String,
pub messages: Vec<PromptMessage>,
}
pub mod task_status {
pub const WORKING: &str = "working";
pub const DONE: &str = "done";
pub const FAILED: &str = "failed";
pub const CANCELLED: &str = "cancelled";
}
#[derive(Debug, Clone, Serialize)]
pub struct TaskInfo {
#[serde(rename = "taskId")]
pub task_id: String,
pub status: &'static str,
#[serde(rename = "statusMessage", skip_serializing_if = "Option::is_none")]
pub status_message: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct TaskResultParams {
#[serde(rename = "taskId")]
pub task_id: String,
}
#[derive(Debug, Deserialize)]
pub struct TaskCancelParams {
#[serde(rename = "taskId")]
pub task_id: String,
}
#[derive(Debug, Serialize)]
pub struct TasksListResult {
pub tasks: Vec<TaskInfo>,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum TaskResultResponse {
Complete(ToolCallResult),
Pending { task: TaskInfo },
}
#[derive(Debug, Serialize)]
pub struct TaskCancelResult {}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn request_id_round_trips_number() {
let id = RequestId::Number(42);
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "42");
}
#[test]
fn request_id_round_trips_string() {
let id = RequestId::String("abc".into());
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, r#""abc""#);
}
#[test]
fn rpc_response_ok_omits_error_field() {
let resp = JsonRpcResponse::ok(RequestId::Number(1), json!({"status": "ok"}));
let v: Value = serde_json::to_value(&resp).unwrap();
assert!(v.get("error").is_none());
assert_eq!(v["result"]["status"], "ok");
}
#[test]
fn rpc_response_err_omits_result_field() {
let resp = JsonRpcResponse::err(
RequestId::Number(1),
RpcError::new(RpcError::METHOD_NOT_FOUND, "not found"),
);
let v: Value = serde_json::to_value(&resp).unwrap();
assert!(v.get("result").is_none());
assert_eq!(v["error"]["code"], RpcError::METHOD_NOT_FOUND);
}
#[test]
fn tool_call_result_ok_is_not_error() {
let r = ToolCallResult::ok("done");
assert!(!r.is_error);
assert_eq!(r.content[0].text, "done");
}
#[test]
fn tool_call_result_error_is_error() {
let r = ToolCallResult::error("boom");
assert!(r.is_error);
assert_eq!(r.content[0].text, "boom");
}
#[test]
fn rpc_error_codes_are_correct_jsonrpc_values() {
assert_eq!(RpcError::PARSE_ERROR, -32_700);
assert_eq!(RpcError::METHOD_NOT_FOUND, -32_601);
}
#[test]
fn supports_elicitation_false_by_default() {
let caps = ClientCapabilities::default();
assert!(!caps.supports_elicitation());
}
#[test]
fn supports_elicitation_true_when_set() {
let mut caps = ClientCapabilities::default();
caps.elicitation = Some(json!({}));
assert!(caps.supports_elicitation());
}
#[test]
fn supports_sampling_false_by_default() {
let caps = ClientCapabilities::default();
assert!(!caps.supports_sampling());
}
#[test]
fn supports_sampling_true_when_set() {
let mut caps = ClientCapabilities::default();
caps.sampling = Some(json!({"createMessage": {}}));
assert!(caps.supports_sampling());
}
}