1use serde::{Deserialize, Deserializer, Serialize};
7use std::collections::HashMap;
8
9pub const PROTOCOL_VERSION: &str = "2024-11-05";
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct JsonRpcRequest {
15 pub jsonrpc: String,
16 pub id: u64,
17 pub method: String,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub params: Option<serde_json::Value>,
20}
21
22impl JsonRpcRequest {
23 pub fn new(id: u64, method: &str, params: Option<serde_json::Value>) -> Self {
24 Self {
25 jsonrpc: "2.0".to_string(),
26 id,
27 method: method.to_string(),
28 params,
29 }
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct JsonRpcResponse {
36 pub jsonrpc: String,
37 pub id: Option<u64>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub result: Option<serde_json::Value>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub error: Option<JsonRpcError>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct JsonRpcError {
47 pub code: i32,
48 pub message: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub data: Option<serde_json::Value>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct JsonRpcNotification {
56 pub jsonrpc: String,
57 pub method: String,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub params: Option<serde_json::Value>,
60}
61
62impl JsonRpcNotification {
63 pub fn new(method: &str, params: Option<serde_json::Value>) -> Self {
64 Self {
65 jsonrpc: "2.0".to_string(),
66 method: method.to_string(),
67 params,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78pub struct ClientCapabilities {
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub roots: Option<RootsCapability>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub sampling: Option<SamplingCapability>,
83}
84
85#[derive(Debug, Clone, Default, Serialize, Deserialize)]
86pub struct RootsCapability {
87 #[serde(default)]
88 pub list_changed: bool,
89}
90
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct SamplingCapability {}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ClientInfo {
97 pub name: String,
98 pub version: String,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct InitializeParams {
105 pub protocol_version: String,
106 pub capabilities: ClientCapabilities,
107 pub client_info: ClientInfo,
108}
109
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct ServerCapabilities {
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub tools: Option<ToolsCapability>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub resources: Option<ResourcesCapability>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub prompts: Option<PromptsCapability>,
119 #[serde(skip_serializing_if = "Option::is_none")]
120 pub logging: Option<LoggingCapability>,
121}
122
123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct ToolsCapability {
126 #[serde(default)]
127 pub list_changed: bool,
128}
129
130#[derive(Debug, Clone, Default, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct ResourcesCapability {
133 #[serde(default)]
134 pub subscribe: bool,
135 #[serde(default)]
136 pub list_changed: bool,
137}
138
139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct PromptsCapability {
142 #[serde(default)]
143 pub list_changed: bool,
144}
145
146#[derive(Debug, Clone, Default, Serialize, Deserialize)]
147pub struct LoggingCapability {}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ServerInfo {
152 pub name: String,
153 pub version: String,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct InitializeResult {
160 pub protocol_version: String,
161 pub capabilities: ServerCapabilities,
162 pub server_info: ServerInfo,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct McpTool {
173 pub name: String,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub title: Option<String>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub description: Option<String>,
178 pub input_schema: serde_json::Value,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub output_schema: Option<serde_json::Value>,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub annotations: Option<McpToolAnnotations>,
183 #[serde(default, skip_serializing_if = "Vec::is_empty")]
184 pub icons: Vec<serde_json::Value>,
185 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
186 pub meta: Option<serde_json::Value>,
187}
188
189#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct McpToolAnnotations {
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub title: Option<String>,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub read_only_hint: Option<bool>,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub destructive_hint: Option<bool>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub idempotent_hint: Option<bool>,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub open_world_hint: Option<bool>,
206 #[serde(flatten)]
207 pub additional: HashMap<String, serde_json::Value>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct ListToolsResult {
213 pub tools: Vec<McpTool>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct CallToolParams {
219 pub name: String,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub arguments: Option<serde_json::Value>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226#[serde(tag = "type", rename_all = "lowercase")]
227pub enum ToolContent {
228 Text {
229 text: String,
230 },
231 Image {
232 data: String,
233 #[serde(rename = "mimeType")]
234 mime_type: String,
235 },
236 Resource {
237 resource: ResourceContent,
238 },
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243#[serde(rename_all = "camelCase")]
244pub struct ResourceContent {
245 pub uri: String,
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub mime_type: Option<String>,
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub text: Option<String>,
250 #[serde(skip_serializing_if = "Option::is_none")]
251 pub blob: Option<String>,
252}
253
254#[derive(Debug, Clone, Default, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub struct CallToolResult {
258 #[serde(default)]
259 pub content: Vec<ToolContent>,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub structured_content: Option<serde_json::Value>,
262 #[serde(default)]
263 pub is_error: bool,
264 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
265 pub meta: Option<serde_json::Value>,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct McpResource {
276 pub uri: String,
277 pub name: String,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub description: Option<String>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub mime_type: Option<String>,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct ListResourcesResult {
287 pub resources: Vec<McpResource>,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ReadResourceParams {
293 pub uri: String,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct ReadResourceResult {
299 pub contents: Vec<ResourceContent>,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct McpPrompt {
309 pub name: String,
310 #[serde(skip_serializing_if = "Option::is_none")]
311 pub description: Option<String>,
312 #[serde(skip_serializing_if = "Option::is_none")]
313 pub arguments: Option<Vec<PromptArgument>>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct PromptArgument {
319 pub name: String,
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub description: Option<String>,
322 #[serde(default)]
323 pub required: bool,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct ListPromptsResult {
329 pub prompts: Vec<McpPrompt>,
330}
331
332#[derive(Debug, Clone)]
338pub enum McpNotification {
339 ToolsListChanged,
340 ResourcesListChanged,
341 PromptsListChanged,
342 Progress {
343 progress_token: String,
344 progress: f64,
345 total: Option<f64>,
346 },
347 Log {
348 level: String,
349 logger: Option<String>,
350 data: serde_json::Value,
351 },
352 Unknown {
353 method: String,
354 params: Option<serde_json::Value>,
355 },
356}
357
358impl McpNotification {
359 pub fn from_json_rpc(notification: &JsonRpcNotification) -> Self {
360 match notification.method.as_str() {
361 "notifications/tools/list_changed" => McpNotification::ToolsListChanged,
362 "notifications/resources/list_changed" => McpNotification::ResourcesListChanged,
363 "notifications/prompts/list_changed" => McpNotification::PromptsListChanged,
364 "notifications/progress" => {
365 if let Some(params) = ¬ification.params {
366 let progress_token = params
367 .get("progressToken")
368 .and_then(|v| v.as_str())
369 .unwrap_or("")
370 .to_string();
371 let progress = params
372 .get("progress")
373 .and_then(|v| v.as_f64())
374 .unwrap_or(0.0);
375 let total = params.get("total").and_then(|v| v.as_f64());
376 McpNotification::Progress {
377 progress_token,
378 progress,
379 total,
380 }
381 } else {
382 McpNotification::Unknown {
383 method: notification.method.clone(),
384 params: notification.params.clone(),
385 }
386 }
387 }
388 "notifications/message" => {
389 if let Some(params) = ¬ification.params {
390 let level = params
391 .get("level")
392 .and_then(|v| v.as_str())
393 .unwrap_or("info")
394 .to_string();
395 let logger = params
396 .get("logger")
397 .and_then(|v| v.as_str())
398 .map(|s| s.to_string());
399 let data = params
400 .get("data")
401 .cloned()
402 .unwrap_or(serde_json::Value::Null);
403 McpNotification::Log {
404 level,
405 logger,
406 data,
407 }
408 } else {
409 McpNotification::Unknown {
410 method: notification.method.clone(),
411 params: notification.params.clone(),
412 }
413 }
414 }
415 _ => McpNotification::Unknown {
416 method: notification.method.clone(),
417 params: notification.params.clone(),
418 },
419 }
420 }
421}
422
423#[derive(Debug, Clone, Serialize)]
429pub struct McpServerConfig {
430 pub name: String,
432 pub transport: McpTransportConfig,
434 #[serde(default = "default_true")]
436 pub enabled: bool,
437 #[serde(default)]
439 pub env: HashMap<String, String>,
440 #[serde(skip_serializing_if = "Option::is_none")]
442 pub oauth: Option<OAuthConfig>,
443 #[serde(default = "default_tool_timeout")]
445 pub tool_timeout_secs: u64,
446}
447
448impl<'de> Deserialize<'de> for McpServerConfig {
449 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
450 use serde::de::Error;
451 use serde_json::Value;
452
453 let mut map = serde_json::Map::deserialize(deserializer)?;
454
455 let transport = if let Some(t) = map.remove("transport") {
458 match &t {
459 Value::String(kind) => {
460 match kind.as_str() {
462 "stdio" => {
463 let command = map
464 .remove("command")
465 .and_then(|v| v.as_str().map(String::from))
466 .ok_or_else(|| D::Error::missing_field("command"))?;
467 let args = map
468 .remove("args")
469 .and_then(|v| serde_json::from_value(v).ok())
470 .unwrap_or_default();
471 McpTransportConfig::Stdio { command, args }
472 }
473 "http" => {
474 let url = map
475 .remove("url")
476 .and_then(|v| v.as_str().map(String::from))
477 .ok_or_else(|| D::Error::missing_field("url"))?;
478 let headers = map
479 .remove("headers")
480 .and_then(|v| serde_json::from_value(v).ok())
481 .unwrap_or_default();
482 McpTransportConfig::Http { url, headers }
483 }
484 "streamable-http" | "streamable_http" => {
485 let url = map
486 .remove("url")
487 .and_then(|v| v.as_str().map(String::from))
488 .ok_or_else(|| D::Error::missing_field("url"))?;
489 let headers = map
490 .remove("headers")
491 .and_then(|v| serde_json::from_value(v).ok())
492 .unwrap_or_default();
493 McpTransportConfig::StreamableHttp { url, headers }
494 }
495 other => {
496 return Err(D::Error::unknown_variant(
497 other,
498 &["stdio", "http", "streamable-http"],
499 ));
500 }
501 }
502 }
503 Value::Object(_) => serde_json::from_value(t).map_err(D::Error::custom)?,
505 _ => return Err(D::Error::custom("transport must be a string or object")),
506 }
507 } else {
508 return Err(D::Error::missing_field("transport"));
509 };
510
511 let name = map
512 .remove("name")
513 .and_then(|v| v.as_str().map(String::from))
514 .ok_or_else(|| D::Error::missing_field("name"))?;
515 let enabled = map
516 .remove("enabled")
517 .and_then(|v| v.as_bool())
518 .unwrap_or(true);
519 let env = map
520 .remove("env")
521 .and_then(|v| serde_json::from_value(v).ok())
522 .unwrap_or_default();
523 let oauth = map
524 .remove("oauth")
525 .and_then(|v| serde_json::from_value(v).ok());
526 let tool_timeout_secs = map
527 .remove("tool_timeout_secs")
528 .or_else(|| map.remove("toolTimeoutSecs"))
529 .and_then(|v| v.as_u64())
530 .unwrap_or(60);
531
532 Ok(McpServerConfig {
533 name,
534 transport,
535 enabled,
536 env,
537 oauth,
538 tool_timeout_secs,
539 })
540 }
541}
542
543#[allow(dead_code)] fn default_tool_timeout() -> u64 {
545 60
546}
547
548#[allow(dead_code)]
549fn default_true() -> bool {
550 true
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize)]
555#[serde(tag = "type", rename_all = "kebab-case")]
556pub enum McpTransportConfig {
557 Stdio {
559 command: String,
560 #[serde(default)]
561 args: Vec<String>,
562 },
563 Http {
565 url: String,
566 #[serde(default)]
567 headers: HashMap<String, String>,
568 },
569 StreamableHttp {
574 url: String,
575 #[serde(default)]
576 headers: HashMap<String, String>,
577 },
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct OAuthConfig {
583 pub auth_url: String,
584 pub token_url: String,
585 pub client_id: String,
586 #[serde(skip_serializing_if = "Option::is_none")]
587 pub client_secret: Option<String>,
588 #[serde(default)]
589 pub scopes: Vec<String>,
590 pub redirect_uri: String,
591 #[serde(skip_serializing_if = "Option::is_none")]
594 pub access_token: Option<String>,
595}
596
597#[cfg(test)]
598#[path = "protocol/tests.rs"]
599mod tests;