use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Debug, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub id: Value,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
#[derive(Debug, Serialize, Deserialize)]
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>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JsonRpcNotification {
pub jsonrpc: String,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InitializeParams {
#[serde(rename = "protocolVersion")]
pub protocol_version: String,
pub capabilities: ClientCapabilities,
#[serde(rename = "clientInfo")]
pub client_info: ClientInfo,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ClientCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<ToolsCapability>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ToolsCapability {}
#[derive(Debug, Serialize, Deserialize)]
pub struct ClientInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InitializeResult {
#[serde(rename = "protocolVersion")]
pub protocol_version: String,
pub capabilities: ServerCapabilities,
#[serde(rename = "serverInfo")]
pub server_info: ServerInfo,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ServerCapabilities {
pub tools: ToolsCapability,
#[serde(skip_serializing_if = "Option::is_none")]
pub logging: Option<LoggingCapability>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoggingCapability {}
pub const ALERT_CAPACITY: usize = 64;
#[derive(Clone, Debug)]
pub struct Alerter {
tx: tokio::sync::mpsc::Sender<String>,
armed: std::sync::Arc<std::sync::atomic::AtomicBool>,
dropped: std::sync::Arc<std::sync::atomic::AtomicU64>,
}
impl Alerter {
pub fn new(tx: tokio::sync::mpsc::Sender<String>) -> Self {
Self {
tx,
armed: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
dropped: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
}
}
pub fn arm(&self) {
self.armed.store(true, std::sync::atomic::Ordering::Relaxed);
}
pub fn alert(&self, level: &str, data: &serde_json::Value) -> bool {
use std::sync::atomic::Ordering;
if !self.armed.load(Ordering::Relaxed) || !alerts_enabled() {
return false;
}
let missed = self.dropped.swap(0, Ordering::Relaxed);
let mut params = json!({ "level": level, "logger": "jdwp-mcp", "data": data });
if missed > 0 {
if let Some(o) = params.as_object_mut() {
o.insert("droppedSinceLast".to_string(), json!(missed));
}
}
let note = JsonRpcNotification {
jsonrpc: "2.0".to_string(),
method: "notifications/message".to_string(),
params: Some(params),
};
let Ok(line) = serde_json::to_string(¬e) else {
return false;
};
if self.tx.try_send(line).is_err() {
self.dropped.fetch_add(missed + 1, Ordering::Relaxed);
return false;
}
true
}
}
pub fn alerts_enabled() -> bool {
std::env::var("JDWP_ALERTS")
.map_or(true, |v| !matches!(v.trim().to_ascii_lowercase().as_str(), "0" | "false" | "no"))
}
#[cfg(test)]
mod tests {
use super::*;
fn notifier_with_capacity(n: usize) -> (Alerter, tokio::sync::mpsc::Receiver<String>) {
let (tx, rx) = tokio::sync::mpsc::channel(n);
(Alerter::new(tx), rx)
}
#[test]
fn nothing_is_pushed_before_the_handshake_completes() {
let (n, mut rx) = notifier_with_capacity(4);
assert!(!n.alert("warning", &json!({"event": "breakpoint"})), "must not push unarmed");
assert!(rx.try_recv().is_err(), "nothing should have been queued");
n.arm();
assert!(n.alert("warning", &json!({"event": "breakpoint"})));
let line = rx.try_recv().expect("armed alerter should queue");
assert!(line.contains("notifications/message"), "{line}");
assert!(line.contains("\"logger\":\"jdwp-mcp\""), "{line}");
assert!(line.contains("\"event\":\"breakpoint\""), "payload must survive: {line}");
assert!(!line.contains("\"id\""), "a notification must have no id: {line}");
}
#[test]
fn a_full_queue_drops_and_reports_what_it_dropped() {
let (n, mut rx) = notifier_with_capacity(2);
n.arm();
assert!(n.alert("warning", &json!({"i": 1})));
assert!(n.alert("warning", &json!({"i": 2})));
assert!(!n.alert("warning", &json!({"i": 3})));
assert!(!n.alert("warning", &json!({"i": 4})));
let _ = rx.try_recv().expect("first");
let _ = rx.try_recv().expect("second");
assert!(n.alert("warning", &json!({"i": 5})));
let recovered = rx.try_recv().expect("fifth");
assert!(recovered.contains("\"droppedSinceLast\":2"), "must report the gap: {recovered}");
assert!(n.alert("warning", &json!({"i": 6})));
let clean = rx.try_recv().expect("sixth");
assert!(!clean.contains("droppedSinceLast"), "drop count must reset: {clean}");
}
#[test]
fn a_dead_writer_is_survivable() {
let (n, rx) = notifier_with_capacity(2);
n.arm();
drop(rx);
assert!(!n.alert("warning", &json!({"i": 1})), "must report failure, not panic");
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ServerInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
pub description: String,
#[serde(rename = "inputSchema")]
pub input_schema: Value,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ListToolsResult {
pub tools: Vec<Tool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CallToolParams {
pub name: String,
pub arguments: Value,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CallToolResult {
pub content: Vec<ContentBlock>,
#[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ContentBlock {
#[serde(rename = "text")]
Text { text: String },
}
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;