use serde::{Deserialize, Serialize};
pub const PROTOCOL_VERSION: u32 = 1;
pub const MAX_FRAME_BYTES: usize = 64 * 1024;
const _: () = assert!(MAX_FRAME_BYTES * 6 < 1024 * 1024);
pub mod error_codes {
pub const PARSE_ERROR: i32 = -32700;
pub const INVALID_REQUEST: i32 = -32600;
pub const METHOD_NOT_FOUND: i32 = -32601;
pub const INVALID_PARAMS: i32 = -32602;
pub const INTERNAL_ERROR: i32 = -32603;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsonRpcMessage {
pub jsonrpc: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
impl JsonRpcMessage {
pub fn response(id: serde_json::Value, result: &impl Serialize) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id: Some(id),
method: None,
params: None,
result: Some(serde_json::to_value(result).unwrap_or(serde_json::Value::Null)),
error: None,
}
}
pub fn error_response(id: serde_json::Value, code: i32, message: impl Into<String>) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id: Some(id),
method: None,
params: None,
result: None,
error: Some(JsonRpcError {
code,
message: message.into(),
}),
}
}
pub fn notification(method: impl Into<String>, params: &impl Serialize) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id: None,
method: Some(method.into()),
params: Some(serde_json::to_value(params).unwrap_or(serde_json::Value::Null)),
result: None,
error: None,
}
}
pub fn request(
id: serde_json::Value,
method: impl Into<String>,
params: &impl Serialize,
) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id: Some(id),
method: Some(method.into()),
params: Some(serde_json::to_value(params).unwrap_or(serde_json::Value::Null)),
result: None,
error: None,
}
}
pub fn is_notification(&self) -> bool {
self.id.is_none() && self.method.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContentBlock {
#[serde(rename = "type")]
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resource: Option<EmbeddedResource>,
}
impl ContentBlock {
pub fn text(text: impl Into<String>) -> Self {
Self {
kind: "text".to_string(),
text: Some(text.into()),
resource: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EmbeddedResource {
pub uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeParams {
#[serde(default)]
pub protocol_version: u32,
#[serde(default)]
pub client_capabilities: Option<ClientCapabilities>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientCapabilities {
#[serde(default)]
pub fs: Option<serde_json::Value>,
#[serde(default)]
pub terminal: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResult {
pub protocol_version: u32,
pub agent_capabilities: AgentCapabilities,
pub agent_info: AgentInfo,
pub auth_methods: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCapabilities {
pub load_session: bool,
pub prompt_capabilities: PromptCapabilities,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptCapabilities {
pub image: bool,
pub audio: bool,
pub embedded_context: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AgentInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionNewParams {
#[serde(default)]
pub cwd: String,
#[serde(default)]
pub mcp_servers: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionNewResult {
pub session_id: String,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionPromptParams {
#[serde(default)]
pub session_id: String,
#[serde(default)]
pub prompt: Vec<ContentBlock>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionPromptResult {
pub stop_reason: StopReason,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
EndTurn,
MaxTokens,
MaxTurnRequests,
Refusal,
Cancelled,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionCancelParams {
#[serde(default)]
pub session_id: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionUpdateParams {
pub session_id: String,
pub update: SessionUpdate,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "sessionUpdate", rename_all = "snake_case")]
pub enum SessionUpdate {
AgentMessageChunk {
content: ContentBlock,
},
#[serde(rename_all = "camelCase")]
UsageUpdate {
used: usize,
size: usize,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RequestPermissionParams {
pub session_id: String,
pub tool_call: ToolCallRef,
pub options: Vec<PermissionOption>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolCallRef {
pub tool_call_id: String,
pub title: String,
pub kind: ToolKind,
pub status: ToolCallStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCallStatus {
Pending,
InProgress,
Completed,
Failed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolKind {
Read,
Edit,
Delete,
Move,
Search,
Execute,
Think,
Fetch,
Other,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionOption {
pub option_id: String,
pub name: String,
pub kind: PermissionOptionKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PermissionOptionKind {
AllowOnce,
AllowAlways,
RejectOnce,
RejectAlways,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RequestPermissionResult {
pub outcome: PermissionOutcome,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum PermissionOutcome {
#[serde(rename_all = "camelCase")]
Selected {
option_id: String,
},
Cancelled,
}
#[cfg(test)]
mod tests {
use super::*;
fn json(value: &impl Serialize) -> String {
serde_json::to_string(value).unwrap()
}
fn shape(value: &impl Serialize) -> serde_json::Value {
serde_json::from_str(&json(value)).unwrap()
}
#[test]
fn response_carries_id_and_result_and_omits_everything_else() {
let msg = JsonRpcMessage::response(
serde_json::json!(7),
&SessionNewResult {
session_id: "s1".to_string(),
},
);
assert_eq!(
json(&msg),
r#"{"jsonrpc":"2.0","id":7,"result":{"sessionId":"s1"}}"#
);
assert!(!msg.is_notification());
}
#[test]
fn error_response_carries_code_and_message() {
let msg = JsonRpcMessage::error_response(
serde_json::json!("abc"),
error_codes::METHOD_NOT_FOUND,
"no such method",
);
assert_eq!(
json(&msg),
r#"{"jsonrpc":"2.0","id":"abc","error":{"code":-32601,"message":"no such method"}}"#
);
assert!(!msg.is_notification());
}
#[test]
fn notification_has_no_id() {
let msg = JsonRpcMessage::notification(
"session/update",
&SessionUpdateParams {
session_id: "s1".to_string(),
update: SessionUpdate::AgentMessageChunk {
content: ContentBlock::text("hi"),
},
},
);
assert_eq!(
shape(&msg),
serde_json::json!({
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "s1",
"update": {
"sessionUpdate": "agent_message_chunk",
"content": {"type": "text", "text": "hi"},
},
},
})
);
assert!(!json(&msg).contains("\"id\""));
assert!(msg.is_notification());
}
#[test]
fn request_has_both_id_and_method() {
let msg = JsonRpcMessage::request(
serde_json::json!(1),
"session/request_permission",
&serde_json::json!({"sessionId": "s1"}),
);
assert_eq!(
json(&msg),
r#"{"jsonrpc":"2.0","id":1,"method":"session/request_permission","params":{"sessionId":"s1"}}"#
);
assert!(!msg.is_notification());
}
#[test]
fn a_response_with_neither_id_nor_method_is_not_a_notification() {
let msg: JsonRpcMessage = serde_json::from_str(r#"{"jsonrpc":"2.0"}"#).unwrap();
assert!(!msg.is_notification());
}
#[test]
fn usage_update_uses_camel_case_fields() {
let msg = JsonRpcMessage::notification(
"session/update",
&SessionUpdateParams {
session_id: "s1".to_string(),
update: SessionUpdate::UsageUpdate {
used: 10,
size: 200,
},
},
);
assert_eq!(
shape(&msg)["params"]["update"],
serde_json::json!({"sessionUpdate": "usage_update", "used": 10, "size": 200})
);
}
#[test]
fn session_update_round_trips() {
let update = SessionUpdate::AgentMessageChunk {
content: ContentBlock::text("out"),
};
assert_eq!(
serde_json::from_str::<SessionUpdate>(&json(&update)).unwrap(),
update
);
let usage = SessionUpdate::UsageUpdate { used: 1, size: 2 };
assert_eq!(
serde_json::from_str::<SessionUpdate>(&json(&usage)).unwrap(),
usage
);
}
#[test]
fn initialize_params_tolerate_a_bare_protocol_version() {
let params: InitializeParams = serde_json::from_str(
r#"{"protocolVersion":1,"clientInfo":{"name":"gc","version":"1.0"}}"#,
)
.unwrap();
assert_eq!(params.protocol_version, 1);
assert!(params.client_capabilities.is_none());
let full: InitializeParams = serde_json::from_str(
r#"{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":true},"terminal":true}}"#,
)
.unwrap();
let caps = full.client_capabilities.unwrap();
assert!(caps.terminal.unwrap());
assert!(caps.fs.is_some());
assert_eq!(
serde_json::from_str::<InitializeParams>("{}").unwrap(),
InitializeParams::default()
);
}
#[test]
fn initialize_result_serializes_the_spec_shape() {
let result = InitializeResult {
protocol_version: PROTOCOL_VERSION,
agent_capabilities: AgentCapabilities {
load_session: false,
prompt_capabilities: PromptCapabilities {
image: false,
audio: false,
embedded_context: true,
},
},
agent_info: AgentInfo {
name: "leviath".to_string(),
version: "0.1.0".to_string(),
},
auth_methods: vec![],
};
assert_eq!(
json(&result),
r#"{"protocolVersion":1,"agentCapabilities":{"loadSession":false,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":true}},"agentInfo":{"name":"leviath","version":"0.1.0"},"authMethods":[]}"#
);
assert_eq!(
serde_json::from_str::<InitializeResult>(&json(&result)).unwrap(),
result
);
}
#[test]
fn session_new_params_default_every_field() {
let params: SessionNewParams = serde_json::from_str("{}").unwrap();
assert_eq!(params, SessionNewParams::default());
assert_eq!(params.cwd, "");
assert!(params.mcp_servers.is_empty());
let populated: SessionNewParams =
serde_json::from_str(r#"{"cwd":"/w","mcpServers":[{"name":"x"}]}"#).unwrap();
assert_eq!(populated.cwd, "/w");
assert_eq!(populated.mcp_servers.len(), 1);
assert_eq!(
serde_json::from_str::<SessionNewParams>(&json(&populated)).unwrap(),
populated
);
}
#[test]
fn prompt_params_accept_unknown_block_kinds() {
let params: SessionPromptParams = serde_json::from_str(
r#"{"sessionId":"s","prompt":[{"type":"text","text":"hi"},{"type":"image","data":"..."}]}"#,
)
.unwrap();
assert_eq!(params.session_id, "s");
assert_eq!(params.prompt.len(), 2);
assert_eq!(params.prompt[1].kind, "image");
assert!(params.prompt[1].text.is_none());
assert!(params.prompt[1].resource.is_none());
assert_eq!(
serde_json::from_str::<SessionPromptParams>("{}").unwrap(),
SessionPromptParams::default()
);
}
#[test]
fn embedded_resource_round_trips_with_and_without_optionals() {
let full = EmbeddedResource {
uri: "file:///a.rs".to_string(),
mime_type: Some("text/rust".to_string()),
text: Some("fn main() {}".to_string()),
};
assert_eq!(
json(&full),
r#"{"uri":"file:///a.rs","mimeType":"text/rust","text":"fn main() {}"}"#
);
assert_eq!(
serde_json::from_str::<EmbeddedResource>(&json(&full)).unwrap(),
full
);
let bare = EmbeddedResource {
uri: "u".to_string(),
mime_type: None,
text: None,
};
assert_eq!(json(&bare), r#"{"uri":"u"}"#);
}
#[test]
fn content_block_text_constructor_and_round_trip() {
let block = ContentBlock::text("hello");
assert_eq!(json(&block), r#"{"type":"text","text":"hello"}"#);
assert_eq!(
serde_json::from_str::<ContentBlock>(&json(&block)).unwrap(),
block
);
let resource = ContentBlock {
kind: "resource".to_string(),
text: None,
resource: Some(EmbeddedResource {
uri: "u".to_string(),
mime_type: None,
text: Some("body".to_string()),
}),
};
assert_eq!(
serde_json::from_str::<ContentBlock>(&json(&resource)).unwrap(),
resource
);
}
#[test]
fn stop_reasons_use_snake_case() {
for (reason, wire) in [
(StopReason::EndTurn, r#""end_turn""#),
(StopReason::MaxTokens, r#""max_tokens""#),
(StopReason::MaxTurnRequests, r#""max_turn_requests""#),
(StopReason::Refusal, r#""refusal""#),
(StopReason::Cancelled, r#""cancelled""#),
] {
assert_eq!(json(&reason), wire);
assert_eq!(serde_json::from_str::<StopReason>(wire).unwrap(), reason);
}
assert_eq!(
json(&SessionPromptResult {
stop_reason: StopReason::EndTurn
}),
r#"{"stopReason":"end_turn"}"#
);
assert_eq!(
serde_json::from_str::<SessionPromptResult>(r#"{"stopReason":"refusal"}"#)
.unwrap()
.stop_reason,
StopReason::Refusal
);
}
#[test]
fn session_cancel_params_default_the_session_id() {
assert_eq!(
serde_json::from_str::<SessionCancelParams>("{}").unwrap(),
SessionCancelParams::default()
);
let params: SessionCancelParams = serde_json::from_str(r#"{"sessionId":"s"}"#).unwrap();
assert_eq!(params.session_id, "s");
assert_eq!(json(¶ms), r#"{"sessionId":"s"}"#);
}
#[test]
fn permission_request_serializes_the_spec_shape() {
let params = RequestPermissionParams {
session_id: "s1".to_string(),
tool_call: ToolCallRef {
tool_call_id: "t1".to_string(),
title: "run tests".to_string(),
kind: ToolKind::Execute,
status: ToolCallStatus::Pending,
},
options: vec![PermissionOption {
option_id: "allow-once".to_string(),
name: "Allow".to_string(),
kind: PermissionOptionKind::AllowOnce,
}],
};
assert_eq!(
json(¶ms),
r#"{"sessionId":"s1","toolCall":{"toolCallId":"t1","title":"run tests","kind":"execute","status":"pending"},"options":[{"optionId":"allow-once","name":"Allow","kind":"allow_once"}]}"#
);
assert_eq!(
serde_json::from_str::<RequestPermissionParams>(&json(¶ms)).unwrap(),
params
);
}
#[test]
fn every_tool_and_permission_enum_value_round_trips() {
for (kind, wire) in [
(ToolKind::Read, r#""read""#),
(ToolKind::Edit, r#""edit""#),
(ToolKind::Delete, r#""delete""#),
(ToolKind::Move, r#""move""#),
(ToolKind::Search, r#""search""#),
(ToolKind::Execute, r#""execute""#),
(ToolKind::Think, r#""think""#),
(ToolKind::Fetch, r#""fetch""#),
(ToolKind::Other, r#""other""#),
] {
assert_eq!(json(&kind), wire);
assert_eq!(serde_json::from_str::<ToolKind>(wire).unwrap(), kind);
}
for (status, wire) in [
(ToolCallStatus::Pending, r#""pending""#),
(ToolCallStatus::InProgress, r#""in_progress""#),
(ToolCallStatus::Completed, r#""completed""#),
(ToolCallStatus::Failed, r#""failed""#),
] {
assert_eq!(json(&status), wire);
assert_eq!(
serde_json::from_str::<ToolCallStatus>(wire).unwrap(),
status
);
}
for (kind, wire) in [
(PermissionOptionKind::AllowOnce, r#""allow_once""#),
(PermissionOptionKind::AllowAlways, r#""allow_always""#),
(PermissionOptionKind::RejectOnce, r#""reject_once""#),
(PermissionOptionKind::RejectAlways, r#""reject_always""#),
] {
assert_eq!(json(&kind), wire);
assert_eq!(
serde_json::from_str::<PermissionOptionKind>(wire).unwrap(),
kind
);
}
}
#[test]
fn permission_outcomes_round_trip() {
let selected = RequestPermissionResult {
outcome: PermissionOutcome::Selected {
option_id: "allow-once".to_string(),
},
};
assert_eq!(
json(&selected),
r#"{"outcome":{"outcome":"selected","optionId":"allow-once"}}"#
);
assert_eq!(
serde_json::from_str::<RequestPermissionResult>(&json(&selected)).unwrap(),
selected
);
let cancelled = RequestPermissionResult {
outcome: PermissionOutcome::Cancelled,
};
assert_eq!(json(&cancelled), r#"{"outcome":{"outcome":"cancelled"}}"#);
assert_eq!(
serde_json::from_str::<RequestPermissionResult>(&json(&cancelled)).unwrap(),
cancelled
);
}
#[test]
fn agent_capability_defaults_are_all_false() {
assert_eq!(
json(&AgentCapabilities::default()),
r#"{"loadSession":false,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}}"#
);
}
}