Skip to main content

boost/
protocol.rs

1//! MCP wire types — JSON-RPC 2.0 over stdio, newline-delimited.
2//!
3//! We implement only what an MCP client (Claude Code, Cursor, Continue) actually
4//! sends: `initialize`, `tools/list`, `tools/call`, plus the
5//! `notifications/initialized` no-op. Resources and prompts are intentionally
6//! left out for v1 — every introspection point we'd surface as a Resource is
7//! already covered by a tool.
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12#[derive(Debug, Clone, Deserialize)]
13pub struct JsonRpcRequest {
14    pub jsonrpc: String,
15    pub id: Option<Value>,
16    pub method: String,
17    #[serde(default)]
18    pub params: Value,
19}
20
21#[derive(Debug, Clone, Serialize)]
22pub struct JsonRpcResponse {
23    pub jsonrpc: &'static str,
24    pub id: Value,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub result: Option<Value>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub error: Option<JsonRpcError>,
29}
30
31#[derive(Debug, Clone, Serialize)]
32pub struct JsonRpcError {
33    pub code: i32,
34    pub message: String,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub data: Option<Value>,
37}
38
39impl JsonRpcResponse {
40    pub fn ok(id: Value, result: Value) -> Self {
41        Self {
42            jsonrpc: "2.0",
43            id,
44            result: Some(result),
45            error: None,
46        }
47    }
48
49    pub fn err(id: Value, code: i32, message: impl Into<String>) -> Self {
50        Self {
51            jsonrpc: "2.0",
52            id,
53            result: None,
54            error: Some(JsonRpcError {
55                code,
56                message: message.into(),
57                data: None,
58            }),
59        }
60    }
61}
62
63// ─── MCP-specific shapes ───────────────────────────────────────────────────
64
65pub const PROTOCOL_VERSION: &str = "2024-11-05";
66
67#[derive(Debug, Serialize)]
68pub struct InitializeResult {
69    #[serde(rename = "protocolVersion")]
70    pub protocol_version: &'static str,
71    pub capabilities: ServerCapabilities,
72    #[serde(rename = "serverInfo")]
73    pub server_info: ServerInfo,
74}
75
76#[derive(Debug, Serialize)]
77pub struct ServerCapabilities {
78    pub tools: ToolsCapability,
79}
80
81#[derive(Debug, Serialize)]
82pub struct ToolsCapability {
83    #[serde(rename = "listChanged")]
84    pub list_changed: bool,
85}
86
87#[derive(Debug, Serialize)]
88pub struct ServerInfo {
89    pub name: &'static str,
90    pub version: &'static str,
91}
92
93#[derive(Debug, Serialize)]
94pub struct ToolDescriptor {
95    pub name: String,
96    pub description: String,
97    #[serde(rename = "inputSchema")]
98    pub input_schema: Value,
99}
100
101#[derive(Debug, Serialize)]
102pub struct ListToolsResult {
103    pub tools: Vec<ToolDescriptor>,
104}
105
106#[derive(Debug, Deserialize)]
107pub struct CallToolParams {
108    pub name: String,
109    #[serde(default)]
110    pub arguments: Value,
111}
112
113#[derive(Debug, Serialize)]
114pub struct CallToolResult {
115    pub content: Vec<ContentBlock>,
116    #[serde(rename = "isError", skip_serializing_if = "std::ops::Not::not")]
117    pub is_error: bool,
118}
119
120#[derive(Debug, Serialize)]
121#[serde(tag = "type")]
122pub enum ContentBlock {
123    #[serde(rename = "text")]
124    Text { text: String },
125}
126
127impl CallToolResult {
128    pub fn text(text: impl Into<String>) -> Self {
129        Self {
130            content: vec![ContentBlock::Text { text: text.into() }],
131            is_error: false,
132        }
133    }
134
135    pub fn json(value: &Value) -> Self {
136        Self::text(serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string()))
137    }
138
139    pub fn error(msg: impl Into<String>) -> Self {
140        Self {
141            content: vec![ContentBlock::Text { text: msg.into() }],
142            is_error: true,
143        }
144    }
145}