1use serde::{Deserialize, Serialize};
13
14pub const PROTOCOL_VERSION: u32 = 1;
16
17pub const MAX_FRAME_BYTES: usize = 64 * 1024;
26
27const _: () = assert!(MAX_FRAME_BYTES * 6 < 1024 * 1024);
31
32pub mod error_codes {
34 pub const PARSE_ERROR: i32 = -32700;
36 pub const INVALID_REQUEST: i32 = -32600;
38 pub const METHOD_NOT_FOUND: i32 = -32601;
40 pub const INVALID_PARAMS: i32 = -32602;
42 pub const INTERNAL_ERROR: i32 = -32603;
44}
45
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54pub struct JsonRpcMessage {
55 pub jsonrpc: String,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub id: Option<serde_json::Value>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub method: Option<String>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub params: Option<serde_json::Value>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub result: Option<serde_json::Value>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub error: Option<JsonRpcError>,
72}
73
74impl JsonRpcMessage {
75 pub fn response(id: serde_json::Value, result: &impl Serialize) -> Self {
81 Self {
82 jsonrpc: "2.0".to_string(),
83 id: Some(id),
84 method: None,
85 params: None,
86 result: Some(serde_json::to_value(result).unwrap_or(serde_json::Value::Null)),
87 error: None,
88 }
89 }
90
91 pub fn error_response(id: serde_json::Value, code: i32, message: impl Into<String>) -> Self {
93 Self {
94 jsonrpc: "2.0".to_string(),
95 id: Some(id),
96 method: None,
97 params: None,
98 result: None,
99 error: Some(JsonRpcError {
100 code,
101 message: message.into(),
102 }),
103 }
104 }
105
106 pub fn notification(method: impl Into<String>, params: &impl Serialize) -> Self {
108 Self {
109 jsonrpc: "2.0".to_string(),
110 id: None,
111 method: Some(method.into()),
112 params: Some(serde_json::to_value(params).unwrap_or(serde_json::Value::Null)),
113 result: None,
114 error: None,
115 }
116 }
117
118 pub fn request(
120 id: serde_json::Value,
121 method: impl Into<String>,
122 params: &impl Serialize,
123 ) -> Self {
124 Self {
125 jsonrpc: "2.0".to_string(),
126 id: Some(id),
127 method: Some(method.into()),
128 params: Some(serde_json::to_value(params).unwrap_or(serde_json::Value::Null)),
129 result: None,
130 error: None,
131 }
132 }
133
134 pub fn is_notification(&self) -> bool {
137 self.id.is_none() && self.method.is_some()
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143pub struct JsonRpcError {
144 pub code: i32,
146 pub message: String,
148}
149
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct ContentBlock {
161 #[serde(rename = "type")]
163 pub kind: String,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub text: Option<String>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub resource: Option<EmbeddedResource>,
170}
171
172impl ContentBlock {
173 pub fn text(text: impl Into<String>) -> Self {
175 Self {
176 kind: "text".to_string(),
177 text: Some(text.into()),
178 resource: None,
179 }
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct EmbeddedResource {
187 pub uri: String,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub mime_type: Option<String>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub text: Option<String>,
195}
196
197#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct InitializeParams {
203 #[serde(default)]
205 pub protocol_version: u32,
206 #[serde(default)]
209 pub client_capabilities: Option<ClientCapabilities>,
210}
211
212#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub struct ClientCapabilities {
216 #[serde(default)]
218 pub fs: Option<serde_json::Value>,
219 #[serde(default)]
221 pub terminal: Option<bool>,
222}
223
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct InitializeResult {
228 pub protocol_version: u32,
230 pub agent_capabilities: AgentCapabilities,
232 pub agent_info: AgentInfo,
234 pub auth_methods: Vec<serde_json::Value>,
237}
238
239#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct AgentCapabilities {
243 pub load_session: bool,
245 pub prompt_capabilities: PromptCapabilities,
247}
248
249#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
251#[serde(rename_all = "camelCase")]
252pub struct PromptCapabilities {
253 pub image: bool,
255 pub audio: bool,
257 pub embedded_context: bool,
259}
260
261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
263pub struct AgentInfo {
264 pub name: String,
266 pub version: String,
268}
269
270#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct SessionNewParams {
276 #[serde(default)]
278 pub cwd: String,
279 #[serde(default)]
283 pub mcp_servers: Vec<serde_json::Value>,
284}
285
286#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct SessionNewResult {
290 pub session_id: String,
292}
293
294#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
298#[serde(rename_all = "camelCase")]
299pub struct SessionPromptParams {
300 #[serde(default)]
302 pub session_id: String,
303 #[serde(default)]
305 pub prompt: Vec<ContentBlock>,
306}
307
308#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
310#[serde(rename_all = "camelCase")]
311pub struct SessionPromptResult {
312 pub stop_reason: StopReason,
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
318#[serde(rename_all = "snake_case")]
319pub enum StopReason {
320 EndTurn,
322 MaxTokens,
324 MaxTurnRequests,
326 Refusal,
328 Cancelled,
330}
331
332#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct SessionCancelParams {
338 #[serde(default)]
340 pub session_id: String,
341}
342
343#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
347#[serde(rename_all = "camelCase")]
348pub struct SessionUpdateParams {
349 pub session_id: String,
351 pub update: SessionUpdate,
353}
354
355#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
362#[serde(tag = "sessionUpdate", rename_all = "snake_case")]
363pub enum SessionUpdate {
364 AgentMessageChunk {
366 content: ContentBlock,
368 },
369 #[serde(rename_all = "camelCase")]
371 UsageUpdate {
372 used: usize,
374 size: usize,
376 },
377}
378
379#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
383#[serde(rename_all = "camelCase")]
384pub struct RequestPermissionParams {
385 pub session_id: String,
387 pub tool_call: ToolCallRef,
389 pub options: Vec<PermissionOption>,
391}
392
393#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
395#[serde(rename_all = "camelCase")]
396pub struct ToolCallRef {
397 pub tool_call_id: String,
399 pub title: String,
401 pub kind: ToolKind,
403 pub status: ToolCallStatus,
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
409#[serde(rename_all = "snake_case")]
410pub enum ToolCallStatus {
411 Pending,
413 InProgress,
415 Completed,
417 Failed,
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
423#[serde(rename_all = "snake_case")]
424pub enum ToolKind {
425 Read,
427 Edit,
429 Delete,
431 Move,
433 Search,
435 Execute,
437 Think,
439 Fetch,
441 Other,
443}
444
445#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
447#[serde(rename_all = "camelCase")]
448pub struct PermissionOption {
449 pub option_id: String,
451 pub name: String,
453 pub kind: PermissionOptionKind,
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
459#[serde(rename_all = "snake_case")]
460pub enum PermissionOptionKind {
461 AllowOnce,
463 AllowAlways,
465 RejectOnce,
467 RejectAlways,
469}
470
471#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
473pub struct RequestPermissionResult {
474 pub outcome: PermissionOutcome,
476}
477
478#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
480#[serde(tag = "outcome", rename_all = "snake_case")]
481pub enum PermissionOutcome {
482 #[serde(rename_all = "camelCase")]
484 Selected {
485 option_id: String,
487 },
488 Cancelled,
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495
496 fn json(value: &impl Serialize) -> String {
498 serde_json::to_string(value).unwrap()
499 }
500
501 fn shape(value: &impl Serialize) -> serde_json::Value {
510 serde_json::from_str(&json(value)).unwrap()
511 }
512
513 #[test]
514 fn response_carries_id_and_result_and_omits_everything_else() {
515 let msg = JsonRpcMessage::response(
516 serde_json::json!(7),
517 &SessionNewResult {
518 session_id: "s1".to_string(),
519 },
520 );
521 assert_eq!(
522 json(&msg),
523 r#"{"jsonrpc":"2.0","id":7,"result":{"sessionId":"s1"}}"#
524 );
525 assert!(!msg.is_notification());
526 }
527
528 #[test]
529 fn error_response_carries_code_and_message() {
530 let msg = JsonRpcMessage::error_response(
531 serde_json::json!("abc"),
532 error_codes::METHOD_NOT_FOUND,
533 "no such method",
534 );
535 assert_eq!(
536 json(&msg),
537 r#"{"jsonrpc":"2.0","id":"abc","error":{"code":-32601,"message":"no such method"}}"#
538 );
539 assert!(!msg.is_notification());
540 }
541
542 #[test]
543 fn notification_has_no_id() {
544 let msg = JsonRpcMessage::notification(
545 "session/update",
546 &SessionUpdateParams {
547 session_id: "s1".to_string(),
548 update: SessionUpdate::AgentMessageChunk {
549 content: ContentBlock::text("hi"),
550 },
551 },
552 );
553 assert_eq!(
554 shape(&msg),
555 serde_json::json!({
556 "jsonrpc": "2.0",
557 "method": "session/update",
558 "params": {
559 "sessionId": "s1",
560 "update": {
561 "sessionUpdate": "agent_message_chunk",
562 "content": {"type": "text", "text": "hi"},
563 },
564 },
565 })
566 );
567 assert!(!json(&msg).contains("\"id\""));
569 assert!(msg.is_notification());
570 }
571
572 #[test]
573 fn request_has_both_id_and_method() {
574 let msg = JsonRpcMessage::request(
575 serde_json::json!(1),
576 "session/request_permission",
577 &serde_json::json!({"sessionId": "s1"}),
578 );
579 assert_eq!(
580 json(&msg),
581 r#"{"jsonrpc":"2.0","id":1,"method":"session/request_permission","params":{"sessionId":"s1"}}"#
582 );
583 assert!(!msg.is_notification());
584 }
585
586 #[test]
587 fn a_response_with_neither_id_nor_method_is_not_a_notification() {
588 let msg: JsonRpcMessage = serde_json::from_str(r#"{"jsonrpc":"2.0"}"#).unwrap();
589 assert!(!msg.is_notification());
590 }
591
592 #[test]
593 fn usage_update_uses_camel_case_fields() {
594 let msg = JsonRpcMessage::notification(
595 "session/update",
596 &SessionUpdateParams {
597 session_id: "s1".to_string(),
598 update: SessionUpdate::UsageUpdate {
599 used: 10,
600 size: 200,
601 },
602 },
603 );
604 assert_eq!(
605 shape(&msg)["params"]["update"],
606 serde_json::json!({"sessionUpdate": "usage_update", "used": 10, "size": 200})
607 );
608 }
609
610 #[test]
611 fn session_update_round_trips() {
612 let update = SessionUpdate::AgentMessageChunk {
613 content: ContentBlock::text("out"),
614 };
615 assert_eq!(
616 serde_json::from_str::<SessionUpdate>(&json(&update)).unwrap(),
617 update
618 );
619 let usage = SessionUpdate::UsageUpdate { used: 1, size: 2 };
620 assert_eq!(
621 serde_json::from_str::<SessionUpdate>(&json(&usage)).unwrap(),
622 usage
623 );
624 }
625
626 #[test]
627 fn initialize_params_tolerate_a_bare_protocol_version() {
628 let params: InitializeParams = serde_json::from_str(
630 r#"{"protocolVersion":1,"clientInfo":{"name":"gc","version":"1.0"}}"#,
631 )
632 .unwrap();
633 assert_eq!(params.protocol_version, 1);
634 assert!(params.client_capabilities.is_none());
635
636 let full: InitializeParams = serde_json::from_str(
638 r#"{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":true},"terminal":true}}"#,
639 )
640 .unwrap();
641 let caps = full.client_capabilities.unwrap();
642 assert!(caps.terminal.unwrap());
643 assert!(caps.fs.is_some());
644
645 assert_eq!(
647 serde_json::from_str::<InitializeParams>("{}").unwrap(),
648 InitializeParams::default()
649 );
650 }
651
652 #[test]
653 fn initialize_result_serializes_the_spec_shape() {
654 let result = InitializeResult {
655 protocol_version: PROTOCOL_VERSION,
656 agent_capabilities: AgentCapabilities {
657 load_session: false,
658 prompt_capabilities: PromptCapabilities {
659 image: false,
660 audio: false,
661 embedded_context: true,
662 },
663 },
664 agent_info: AgentInfo {
665 name: "leviath".to_string(),
666 version: "0.1.0".to_string(),
667 },
668 auth_methods: vec![],
669 };
670 assert_eq!(
671 json(&result),
672 r#"{"protocolVersion":1,"agentCapabilities":{"loadSession":false,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":true}},"agentInfo":{"name":"leviath","version":"0.1.0"},"authMethods":[]}"#
673 );
674 assert_eq!(
675 serde_json::from_str::<InitializeResult>(&json(&result)).unwrap(),
676 result
677 );
678 }
679
680 #[test]
681 fn session_new_params_default_every_field() {
682 let params: SessionNewParams = serde_json::from_str("{}").unwrap();
683 assert_eq!(params, SessionNewParams::default());
684 assert_eq!(params.cwd, "");
685 assert!(params.mcp_servers.is_empty());
686
687 let populated: SessionNewParams =
688 serde_json::from_str(r#"{"cwd":"/w","mcpServers":[{"name":"x"}]}"#).unwrap();
689 assert_eq!(populated.cwd, "/w");
690 assert_eq!(populated.mcp_servers.len(), 1);
691 assert_eq!(
693 serde_json::from_str::<SessionNewParams>(&json(&populated)).unwrap(),
694 populated
695 );
696 }
697
698 #[test]
699 fn prompt_params_accept_unknown_block_kinds() {
700 let params: SessionPromptParams = serde_json::from_str(
701 r#"{"sessionId":"s","prompt":[{"type":"text","text":"hi"},{"type":"image","data":"..."}]}"#,
702 )
703 .unwrap();
704 assert_eq!(params.session_id, "s");
705 assert_eq!(params.prompt.len(), 2);
706 assert_eq!(params.prompt[1].kind, "image");
707 assert!(params.prompt[1].text.is_none());
708 assert!(params.prompt[1].resource.is_none());
709
710 assert_eq!(
711 serde_json::from_str::<SessionPromptParams>("{}").unwrap(),
712 SessionPromptParams::default()
713 );
714 }
715
716 #[test]
717 fn embedded_resource_round_trips_with_and_without_optionals() {
718 let full = EmbeddedResource {
719 uri: "file:///a.rs".to_string(),
720 mime_type: Some("text/rust".to_string()),
721 text: Some("fn main() {}".to_string()),
722 };
723 assert_eq!(
724 json(&full),
725 r#"{"uri":"file:///a.rs","mimeType":"text/rust","text":"fn main() {}"}"#
726 );
727 assert_eq!(
728 serde_json::from_str::<EmbeddedResource>(&json(&full)).unwrap(),
729 full
730 );
731
732 let bare = EmbeddedResource {
733 uri: "u".to_string(),
734 mime_type: None,
735 text: None,
736 };
737 assert_eq!(json(&bare), r#"{"uri":"u"}"#);
738 }
739
740 #[test]
741 fn content_block_text_constructor_and_round_trip() {
742 let block = ContentBlock::text("hello");
743 assert_eq!(json(&block), r#"{"type":"text","text":"hello"}"#);
744 assert_eq!(
745 serde_json::from_str::<ContentBlock>(&json(&block)).unwrap(),
746 block
747 );
748
749 let resource = ContentBlock {
750 kind: "resource".to_string(),
751 text: None,
752 resource: Some(EmbeddedResource {
753 uri: "u".to_string(),
754 mime_type: None,
755 text: Some("body".to_string()),
756 }),
757 };
758 assert_eq!(
759 serde_json::from_str::<ContentBlock>(&json(&resource)).unwrap(),
760 resource
761 );
762 }
763
764 #[test]
765 fn stop_reasons_use_snake_case() {
766 for (reason, wire) in [
767 (StopReason::EndTurn, r#""end_turn""#),
768 (StopReason::MaxTokens, r#""max_tokens""#),
769 (StopReason::MaxTurnRequests, r#""max_turn_requests""#),
770 (StopReason::Refusal, r#""refusal""#),
771 (StopReason::Cancelled, r#""cancelled""#),
772 ] {
773 assert_eq!(json(&reason), wire);
774 assert_eq!(serde_json::from_str::<StopReason>(wire).unwrap(), reason);
775 }
776 assert_eq!(
777 json(&SessionPromptResult {
778 stop_reason: StopReason::EndTurn
779 }),
780 r#"{"stopReason":"end_turn"}"#
781 );
782 assert_eq!(
783 serde_json::from_str::<SessionPromptResult>(r#"{"stopReason":"refusal"}"#)
784 .unwrap()
785 .stop_reason,
786 StopReason::Refusal
787 );
788 }
789
790 #[test]
791 fn session_cancel_params_default_the_session_id() {
792 assert_eq!(
793 serde_json::from_str::<SessionCancelParams>("{}").unwrap(),
794 SessionCancelParams::default()
795 );
796 let params: SessionCancelParams = serde_json::from_str(r#"{"sessionId":"s"}"#).unwrap();
797 assert_eq!(params.session_id, "s");
798 assert_eq!(json(¶ms), r#"{"sessionId":"s"}"#);
799 }
800
801 #[test]
802 fn permission_request_serializes_the_spec_shape() {
803 let params = RequestPermissionParams {
804 session_id: "s1".to_string(),
805 tool_call: ToolCallRef {
806 tool_call_id: "t1".to_string(),
807 title: "run tests".to_string(),
808 kind: ToolKind::Execute,
809 status: ToolCallStatus::Pending,
810 },
811 options: vec![PermissionOption {
812 option_id: "allow-once".to_string(),
813 name: "Allow".to_string(),
814 kind: PermissionOptionKind::AllowOnce,
815 }],
816 };
817 assert_eq!(
818 json(¶ms),
819 r#"{"sessionId":"s1","toolCall":{"toolCallId":"t1","title":"run tests","kind":"execute","status":"pending"},"options":[{"optionId":"allow-once","name":"Allow","kind":"allow_once"}]}"#
820 );
821 assert_eq!(
822 serde_json::from_str::<RequestPermissionParams>(&json(¶ms)).unwrap(),
823 params
824 );
825 }
826
827 #[test]
828 fn every_tool_and_permission_enum_value_round_trips() {
829 for (kind, wire) in [
830 (ToolKind::Read, r#""read""#),
831 (ToolKind::Edit, r#""edit""#),
832 (ToolKind::Delete, r#""delete""#),
833 (ToolKind::Move, r#""move""#),
834 (ToolKind::Search, r#""search""#),
835 (ToolKind::Execute, r#""execute""#),
836 (ToolKind::Think, r#""think""#),
837 (ToolKind::Fetch, r#""fetch""#),
838 (ToolKind::Other, r#""other""#),
839 ] {
840 assert_eq!(json(&kind), wire);
841 assert_eq!(serde_json::from_str::<ToolKind>(wire).unwrap(), kind);
842 }
843 for (status, wire) in [
844 (ToolCallStatus::Pending, r#""pending""#),
845 (ToolCallStatus::InProgress, r#""in_progress""#),
846 (ToolCallStatus::Completed, r#""completed""#),
847 (ToolCallStatus::Failed, r#""failed""#),
848 ] {
849 assert_eq!(json(&status), wire);
850 assert_eq!(
851 serde_json::from_str::<ToolCallStatus>(wire).unwrap(),
852 status
853 );
854 }
855 for (kind, wire) in [
856 (PermissionOptionKind::AllowOnce, r#""allow_once""#),
857 (PermissionOptionKind::AllowAlways, r#""allow_always""#),
858 (PermissionOptionKind::RejectOnce, r#""reject_once""#),
859 (PermissionOptionKind::RejectAlways, r#""reject_always""#),
860 ] {
861 assert_eq!(json(&kind), wire);
862 assert_eq!(
863 serde_json::from_str::<PermissionOptionKind>(wire).unwrap(),
864 kind
865 );
866 }
867 }
868
869 #[test]
870 fn permission_outcomes_round_trip() {
871 let selected = RequestPermissionResult {
872 outcome: PermissionOutcome::Selected {
873 option_id: "allow-once".to_string(),
874 },
875 };
876 assert_eq!(
877 json(&selected),
878 r#"{"outcome":{"outcome":"selected","optionId":"allow-once"}}"#
879 );
880 assert_eq!(
881 serde_json::from_str::<RequestPermissionResult>(&json(&selected)).unwrap(),
882 selected
883 );
884
885 let cancelled = RequestPermissionResult {
886 outcome: PermissionOutcome::Cancelled,
887 };
888 assert_eq!(json(&cancelled), r#"{"outcome":{"outcome":"cancelled"}}"#);
889 assert_eq!(
890 serde_json::from_str::<RequestPermissionResult>(&json(&cancelled)).unwrap(),
891 cancelled
892 );
893 }
894
895 #[test]
896 fn agent_capability_defaults_are_all_false() {
897 assert_eq!(
898 json(&AgentCapabilities::default()),
899 r#"{"loadSession":false,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}}"#
900 );
901 }
902}