use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
pub const PROTOCOL_VERSION: &str = "1.0";
pub fn version_compatible(server_version: &str) -> bool {
fn major(v: &str) -> Option<&str> {
v.split('.').next().filter(|m| !m.is_empty())
}
match (major(PROTOCOL_VERSION), major(server_version)) {
(Some(ours), Some(theirs)) => ours == theirs,
_ => false,
}
}
#[derive(Debug)]
pub enum Incoming {
Response {
id: u64,
result: Result<Value, ErrorObject>,
},
Request {
id: u64,
method: String,
params: Value,
},
Notification {
method: String,
params: Value,
},
}
pub fn classify_line(line: &str) -> Option<Incoming> {
let value: Value = serde_json::from_str(line).ok()?;
let obj = value.as_object()?;
let id = obj.get("id").and_then(Value::as_u64);
let method = obj.get("method").and_then(Value::as_str);
let params = obj.get("params").cloned().unwrap_or(Value::Null);
match (method, id) {
(Some(method), Some(id)) => Some(Incoming::Request {
id,
method: method.to_string(),
params,
}),
(Some(method), None) => Some(Incoming::Notification {
method: method.to_string(),
params,
}),
(None, Some(id)) => {
let result = match obj.get("error") {
Some(error) => Err(serde_json::from_value(error.clone()).unwrap_or_else(|_| {
ErrorObject::message("malformed error object on the wire")
})),
None => Ok(obj.get("result").cloned().unwrap_or(Value::Null)),
};
Some(Incoming::Response { id, result })
}
(None, None) => None,
}
}
pub fn request_line(id: u64, method: &str, params: &Value) -> String {
let mut obj = Map::new();
obj.insert("id".into(), json!(id));
obj.insert("method".into(), json!(method));
if !params.is_null() {
obj.insert("params".into(), params.clone());
}
Value::Object(obj).to_string()
}
pub fn notification_line(method: &str, params: &Value) -> String {
let mut obj = Map::new();
obj.insert("method".into(), json!(method));
if !params.is_null() {
obj.insert("params".into(), params.clone());
}
Value::Object(obj).to_string()
}
pub fn response_error_line(id: u64, error: &ErrorObject) -> String {
json!({ "id": id, "error": error }).to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorObject {
#[serde(default)]
pub code: i64,
pub message: String,
#[serde(default)]
pub retryable: bool,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub data: Value,
}
impl ErrorObject {
pub fn message(message: impl Into<String>) -> Self {
Self {
code: 0,
message: message.into(),
retryable: false,
data: Value::Null,
}
}
pub fn method_not_found(method: &str) -> Self {
Self {
code: -32601,
message: format!("method not supported by this host: {method}"),
retryable: false,
data: Value::Null,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct InitializeParams {
pub protocol_version: String,
pub session_id: String,
pub workspace_root: String,
pub config: Value,
pub capabilities: Vec<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct InitializeResult {
#[serde(default)]
pub protocol_version: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default)]
pub capability_params: CapabilityParams,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct CapabilityParams {
#[serde(default)]
pub prompt: Option<PromptParams>,
#[serde(default)]
pub tools: Vec<ServedTool>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct PromptParams {
#[serde(default, rename = "static")]
pub static_text: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ServedTool {
pub name: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ToolCallParams {
pub tool_call_id: String,
pub name: String,
pub args: Value,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ToolUpdateParams {
#[serde(default)]
pub request_id: u64,
#[serde(default)]
pub output: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classification_is_field_based() {
match classify_line(r#"{"id":3,"method":"tool/call","params":{}}"#) {
Some(Incoming::Request { id: 3, method, .. }) => assert_eq!(method, "tool/call"),
other => panic!("expected request, got {other:?}"),
}
match classify_line(r#"{"method":"tool/update","params":{"request_id":3}}"#) {
Some(Incoming::Notification { method, .. }) => assert_eq!(method, "tool/update"),
other => panic!("expected notification, got {other:?}"),
}
match classify_line(r#"{"id":3,"result":{"ok":true}}"#) {
Some(Incoming::Response {
id: 3,
result: Ok(v),
}) => assert_eq!(v["ok"], true),
other => panic!("expected response, got {other:?}"),
}
assert!(classify_line(r#"{"neither":true}"#).is_none());
assert!(classify_line("not json").is_none());
}
#[test]
fn bare_error_message_parses_with_defaults() {
match classify_line(r#"{"id":1,"error":{"message":"boom"}}"#) {
Some(Incoming::Response {
result: Err(err), ..
}) => {
assert_eq!(err.message, "boom");
assert_eq!(err.code, 0);
assert!(!err.retryable);
}
other => panic!("expected error response, got {other:?}"),
}
}
#[test]
fn initialize_result_ignores_unknown_and_defaults_missing() {
let result: InitializeResult = serde_json::from_value(json!({
"protocol_version": "1.3",
"name": "lsp",
"future_field": {"x": 1},
"capability_params": { "tools": [{"name": "lsp_definition", "extra": true}] }
}))
.expect("lenient parse");
assert_eq!(result.protocol_version, "1.3");
assert_eq!(result.capability_params.tools[0].name, "lsp_definition");
assert!(result.capability_params.prompt.is_none());
}
#[test]
fn version_compat_is_by_major() {
assert!(version_compatible("1.0"));
assert!(version_compatible("1.7"));
assert!(!version_compatible("2.0"));
assert!(!version_compatible(""));
}
}