use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const JSONRPC_VERSION: &str = "2.0";
#[derive(Debug, Clone, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
#[serde(default)]
pub id: Option<Value>,
pub method: String,
#[serde(default)]
pub params: Option<Value>,
}
impl JsonRpcRequest {
pub fn validate(&self) -> Result<(), JsonRpcError> {
if self.jsonrpc != JSONRPC_VERSION {
return Err(JsonRpcError::invalid_request(format!(
"Unsupported JSON-RPC version: {}",
self.jsonrpc
)));
}
Ok(())
}
pub fn is_notification(&self) -> bool {
self.id.is_none()
}
pub fn extract_tool_call(&self) -> Result<ToolCallParams, JsonRpcError> {
let params = self
.params
.as_ref()
.ok_or_else(|| JsonRpcError::invalid_params("Missing params"))?;
let name = params
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| JsonRpcError::invalid_params("Missing or invalid 'name' field"))?;
let arguments = params
.get("arguments")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
Ok(ToolCallParams {
name: name.to_string(),
arguments,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct JsonRpcNotification {
pub jsonrpc: String,
pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
impl JsonRpcNotification {
pub fn new(method: impl Into<String>) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
method: method.into(),
params: None,
}
}
pub fn with_params(method: impl Into<String>, params: Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
method: method.into(),
params: Some(params),
}
}
pub fn notification_type(&self) -> NotificationType {
match self.method.as_str() {
"notifications/initialized" => NotificationType::Initialized,
"notifications/cancelled" => NotificationType::Cancelled,
"notifications/progress" => NotificationType::Progress,
"notifications/message" => NotificationType::Message,
"notifications/resources/updated" => NotificationType::ResourcesUpdated,
"notifications/resources/list_changed" => NotificationType::ResourcesListChanged,
"notifications/tools/list_changed" => NotificationType::ToolsListChanged,
"notifications/prompts/list_changed" => NotificationType::PromptsListChanged,
"notifications/roots/list_changed" => NotificationType::RootsListChanged,
_ => NotificationType::Unknown(self.method.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NotificationType {
Initialized,
Cancelled,
Progress,
Message,
ResourcesUpdated,
ResourcesListChanged,
ToolsListChanged,
PromptsListChanged,
RootsListChanged,
Unknown(String),
}
impl std::fmt::Display for NotificationType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NotificationType::Initialized => write!(f, "notifications/initialized"),
NotificationType::Cancelled => write!(f, "notifications/cancelled"),
NotificationType::Progress => write!(f, "notifications/progress"),
NotificationType::Message => write!(f, "notifications/message"),
NotificationType::ResourcesUpdated => write!(f, "notifications/resources/updated"),
NotificationType::ResourcesListChanged => {
write!(f, "notifications/resources/list_changed")
}
NotificationType::ToolsListChanged => write!(f, "notifications/tools/list_changed"),
NotificationType::PromptsListChanged => write!(f, "notifications/prompts/list_changed"),
NotificationType::RootsListChanged => write!(f, "notifications/roots/list_changed"),
NotificationType::Unknown(method) => write!(f, "{}", method),
}
}
}
#[derive(Debug, Clone)]
pub enum JsonRpcMessage {
Request(JsonRpcRequest),
Notification(JsonRpcNotification),
}
impl JsonRpcMessage {
pub fn from_json(json: &str) -> Result<Self, JsonRpcError> {
let raw: serde_json::Value = serde_json::from_str(json)
.map_err(|e| JsonRpcError::parse_error(format!("Invalid JSON: {}", e)))?;
if !raw.is_object() {
return Err(JsonRpcError::invalid_request(
"Message must be a JSON object",
));
}
let obj = raw.as_object().unwrap();
if let Some(jsonrpc) = obj.get("jsonrpc") {
if jsonrpc.as_str() != Some(JSONRPC_VERSION) {
return Err(JsonRpcError::invalid_request(format!(
"Unsupported JSON-RPC version: {:?}",
jsonrpc
)));
}
} else {
return Err(JsonRpcError::invalid_request("Missing jsonrpc field"));
}
if !obj.contains_key("method") {
return Err(JsonRpcError::invalid_request("Missing method field"));
}
if obj.contains_key("id") {
let request: JsonRpcRequest = serde_json::from_value(raw)
.map_err(|e| JsonRpcError::invalid_request(format!("Invalid request: {}", e)))?;
Ok(JsonRpcMessage::Request(request))
} else {
let notification: JsonRpcNotification = serde_json::from_value(raw).map_err(|e| {
JsonRpcError::invalid_request(format!("Invalid notification: {}", e))
})?;
Ok(JsonRpcMessage::Notification(notification))
}
}
pub fn is_notification(&self) -> bool {
matches!(self, JsonRpcMessage::Notification(_))
}
pub fn is_request(&self) -> bool {
matches!(self, JsonRpcMessage::Request(_))
}
pub fn method(&self) -> &str {
match self {
JsonRpcMessage::Request(req) => &req.method,
JsonRpcMessage::Notification(notif) => ¬if.method,
}
}
pub fn id(&self) -> Option<&Value> {
match self {
JsonRpcMessage::Request(req) => req.id.as_ref(),
JsonRpcMessage::Notification(_) => None,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
pub id: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
impl JsonRpcResponse {
pub fn success(id: Value, result: Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
id,
result: Some(result),
error: None,
}
}
pub fn success_opt(id: Option<Value>, result: Value) -> Self {
Self::success(id.unwrap_or(Value::Null), result)
}
pub fn error(id: Value, error: JsonRpcError) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
id,
result: None,
error: Some(error),
}
}
pub fn error_opt(id: Option<Value>, error: JsonRpcError) -> Self {
Self::error(id.unwrap_or(Value::Null), error)
}
pub fn from_result(id: Value, result: Result<Value, JsonRpcError>) -> Self {
match result {
Ok(value) => Self::success(id, value),
Err(err) => Self::error(id, err),
}
}
pub fn from_result_opt(id: Option<Value>, result: Result<Value, JsonRpcError>) -> Self {
Self::from_result(id.unwrap_or(Value::Null), result)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl JsonRpcError {
pub fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
data: None,
}
}
pub fn with_data(code: i32, message: impl Into<String>, data: Value) -> Self {
Self {
code,
message: message.into(),
data: Some(data),
}
}
pub fn parse_error(msg: impl Into<String>) -> Self {
Self::new(error_codes::PARSE_ERROR, msg)
}
pub fn invalid_request(msg: impl Into<String>) -> Self {
Self::new(error_codes::INVALID_REQUEST, msg)
}
pub fn method_not_found(method: String) -> Self {
Self::with_data(
error_codes::METHOD_NOT_FOUND,
"Method not found",
serde_json::json!({ "method": method }),
)
}
pub fn invalid_params(msg: impl Into<String>) -> Self {
Self::new(error_codes::INVALID_PARAMS, msg)
}
pub fn invalid_params_with_suggestion(
msg: impl Into<String>,
suggestion: impl Into<String>,
) -> Self {
Self::with_data(
error_codes::INVALID_PARAMS,
msg,
serde_json::json!({ "suggestion": suggestion.into() }),
)
}
pub fn internal_error(msg: impl Into<String>) -> Self {
Self::new(error_codes::INTERNAL_ERROR, msg)
}
pub fn project_not_found(project: String) -> Self {
Self::with_data(
error_codes::PROJECT_NOT_FOUND,
"Project not found",
serde_json::json!({ "project": project }),
)
}
pub fn project_not_indexed(project: String) -> Self {
Self::with_data(
error_codes::PROJECT_NOT_INDEXED,
"Project not indexed — call leindex_index or pass project_path to auto-index",
serde_json::json!({
"project": project,
"suggestion": "Pass project_path to any tool to auto-index on first use, or call leindex_index explicitly.",
"error_type": "project_not_indexed"
}),
)
}
pub fn init_failed(path: &str, inner: &str) -> Self {
Self::with_data(
error_codes::INIT_FAILED,
format!(
"Failed to initialize LeIndex for '{}'.\n\
Inner error: {}\n\n\
Remediation:\n\
1. Check write permissions on {}\n\
2. Set LEINDEX_HOME=/writable/path env var\n\
3. Delete .leindex/ directory and retry\n\
4. Check available disk space",
path, inner, path
),
serde_json::json!({
"error_type": "init_failed",
"inner_error": inner,
}),
)
}
pub fn indexing_failed(msg: impl Into<String>) -> Self {
let m = msg.into();
Self::with_data(
error_codes::INDEXING_FAILED,
format!(
"{}\n\n\
Remediation:\n\
1. Verify project_path is a valid directory with source files\n\
2. Try force_reindex=true to rebuild the index\n\
3. Delete .leindex/ directory and retry\n\
4. Check disk space and permissions",
m
),
serde_json::json!({
"error_type": "indexing_failed",
}),
)
}
pub fn search_failed(msg: impl Into<String>) -> Self {
let m = msg.into();
Self::with_data(
error_codes::SEARCH_FAILED,
format!(
"{}\n\n\
Remediation:\n\
1. Ensure the project is indexed (call leindex_index)\n\
2. Try a simpler query (single term instead of complex pattern)\n\
3. Use leindex_grep_symbols for exact/regex pattern matching\n\
4. Try force_reindex=true if the index may be stale",
m
),
serde_json::json!({
"error_type": "search_failed",
}),
)
}
pub fn context_expansion_failed(msg: impl Into<String>) -> Self {
let m = msg.into();
Self::with_data(
error_codes::CONTEXT_EXPANSION_FAILED,
format!(
"{}\n\n\
Remediation:\n\
1. Check that the node_id exists in the indexed project\n\
2. Use leindex_grep_symbols to find valid symbol names\n\
3. Try force_reindex=true if the index may be stale",
m
),
serde_json::json!({
"error_type": "context_expansion_failed",
}),
)
}
pub fn memory_limit_exceeded() -> Self {
Self::with_data(
error_codes::MEMORY_LIMIT_EXCEEDED,
"Memory limit exceeded",
serde_json::json!({
"error_type": "memory_limit_exceeded",
"suggestion": "Reduce token_budget, use pagination (offset/limit), or re-index with a smaller scope."
}),
)
}
}
impl std::fmt::Display for JsonRpcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.code, self.message)
}
}
impl std::error::Error for JsonRpcError {}
#[derive(Debug, Clone, Deserialize)]
pub struct ToolCallParams {
pub name: String,
#[serde(default)]
pub arguments: Value,
}
#[derive(Debug, Clone, Serialize)]
pub struct ProgressEvent {
#[serde(rename = "type")]
pub event_type: String,
pub stage: String,
pub current: usize,
pub total: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
pub timestamp_ms: u64,
}
impl ProgressEvent {
pub fn progress(
stage: impl Into<String>,
current: usize,
total: usize,
message: impl Into<String>,
) -> Self {
Self {
event_type: "progress".to_string(),
stage: stage.into(),
current,
total,
message: Some(message.into()),
timestamp_ms: {
let duration = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap();
duration.as_millis() as u64
},
}
}
pub fn complete(stage: impl Into<String>, message: impl Into<String>) -> Self {
Self {
event_type: "complete".to_string(),
stage: stage.into(),
current: 0,
total: 0,
message: Some(message.into()),
timestamp_ms: {
let duration = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap();
duration.as_millis() as u64
},
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
event_type: "error".to_string(),
stage: "error".into(),
current: 0,
total: 0,
message: Some(message.into()),
timestamp_ms: {
let duration = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap();
duration.as_millis() as u64
},
}
}
}
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;
pub const PROJECT_NOT_FOUND: i32 = -32001;
pub const PROJECT_NOT_INDEXED: i32 = -32002;
pub const INDEXING_FAILED: i32 = -32003;
pub const SEARCH_FAILED: i32 = -32004;
pub const CONTEXT_EXPANSION_FAILED: i32 = -32005;
pub const MEMORY_LIMIT_EXCEEDED: i32 = -32006;
pub const INDEX_STALE: i32 = -32007;
pub const INIT_FAILED: i32 = -32008;
pub const FILESYSTEM_ERROR: i32 = -32009;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_jsonrpc_request_valid() {
let json = r#"{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {"name": "test", "arguments": {}}
}"#;
let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
assert!(req.validate().is_ok());
assert_eq!(req.method, "tools/call");
assert!(!req.is_notification());
assert_eq!(req.id, Some(serde_json::json!(1)));
}
#[test]
fn test_jsonrpc_request_notification() {
let json = r#"{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}"#;
let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
assert!(req.validate().is_ok());
assert!(req.is_notification());
assert!(req.id.is_none());
}
#[test]
fn test_jsonrpc_notification_parsing() {
let json = r#"{
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
}"#;
let notif: JsonRpcNotification = serde_json::from_str(json).unwrap();
assert_eq!(notif.method, "notifications/initialized");
assert_eq!(notif.notification_type(), NotificationType::Initialized);
}
#[test]
fn test_jsonrpc_message_request() {
let json = r#"{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {"name": "test"}
}"#;
let msg = JsonRpcMessage::from_json(json).unwrap();
assert!(msg.is_request());
assert!(!msg.is_notification());
assert_eq!(msg.method(), "tools/call");
assert_eq!(msg.id(), Some(&serde_json::json!(42)));
}
#[test]
fn test_jsonrpc_message_notification() {
let json = r#"{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}"#;
let msg = JsonRpcMessage::from_json(json).unwrap();
assert!(msg.is_notification());
assert!(!msg.is_request());
assert_eq!(msg.method(), "notifications/initialized");
assert!(msg.id().is_none());
}
#[test]
fn test_jsonrpc_request_invalid_version() {
let json = r#"{
"jsonrpc": "1.0",
"id": 1,
"method": "tools/call"
}"#;
let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
assert!(req.validate().is_err());
}
#[test]
fn test_jsonrpc_message_invalid_version() {
let json = r#"{
"jsonrpc": "1.0",
"id": 1,
"method": "tools/call"
}"#;
let result = JsonRpcMessage::from_json(json);
assert!(result.is_err());
}
#[test]
fn test_jsonrpc_message_missing_method() {
let json = r#"{
"jsonrpc": "2.0",
"id": 1
}"#;
let result = JsonRpcMessage::from_json(json);
assert!(result.is_err());
}
#[test]
fn test_jsonrpc_response_success() {
let response =
JsonRpcResponse::success(serde_json::json!(1), serde_json::json!({"result": "ok"}));
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"result\""));
assert!(!json.contains("\"error\""));
}
#[test]
fn test_jsonrpc_response_error() {
let error = JsonRpcError::invalid_params("Missing required field");
let response = JsonRpcResponse::error(serde_json::json!(1), error);
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"error\""));
assert!(!json.contains("\"result\""));
}
#[test]
fn test_jsonrpc_response_with_null_id() {
let response =
JsonRpcResponse::success(serde_json::Value::Null, serde_json::json!({"result": "ok"}));
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"id\":null"));
}
#[test]
fn test_extract_tool_call() {
let json = r#"{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "leindex_search",
"arguments": {"query": "test"}
}
}"#;
let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
let tool_call = req.extract_tool_call().unwrap();
assert_eq!(tool_call.name, "leindex_search");
assert_eq!(tool_call.arguments["query"], "test");
}
#[test]
fn test_error_codes() {
assert_eq!(error_codes::PARSE_ERROR, -32700);
assert_eq!(error_codes::INVALID_REQUEST, -32600);
assert_eq!(error_codes::METHOD_NOT_FOUND, -32601);
assert_eq!(error_codes::INVALID_PARAMS, -32602);
assert_eq!(error_codes::INTERNAL_ERROR, -32603);
assert_eq!(error_codes::PROJECT_NOT_FOUND, -32001);
assert_eq!(error_codes::PROJECT_NOT_INDEXED, -32002);
}
#[test]
fn test_notification_types() {
assert_eq!(
JsonRpcNotification::new("notifications/initialized").notification_type(),
NotificationType::Initialized
);
assert_eq!(
JsonRpcNotification::new("notifications/cancelled").notification_type(),
NotificationType::Cancelled
);
assert_eq!(
JsonRpcNotification::new("notifications/progress").notification_type(),
NotificationType::Progress
);
assert_eq!(
JsonRpcNotification::new("notifications/message").notification_type(),
NotificationType::Message
);
assert_eq!(
JsonRpcNotification::new("notifications/resources/updated").notification_type(),
NotificationType::ResourcesUpdated
);
assert_eq!(
JsonRpcNotification::new("unknown/notification").notification_type(),
NotificationType::Unknown("unknown/notification".to_string())
);
}
#[test]
fn test_backwards_compatibility() {
let json = r#"{
"jsonrpc": "2.0",
"id": "test-id-123",
"method": "initialize",
"params": {}
}"#;
let msg = JsonRpcMessage::from_json(json).unwrap();
assert!(msg.is_request());
if let JsonRpcMessage::Request(req) = msg {
assert_eq!(req.id, Some(serde_json::json!("test-id-123")));
assert!(!req.is_notification());
} else {
panic!("Expected request");
}
}
}