1use axum::http::{HeaderName, HeaderValue};
29use futures::StreamExt as _;
30use serde_json::{json, Value};
31use std::collections::HashMap;
32use std::sync::Arc;
33use std::time::Instant;
34
35pub const SUPPORTED_PROTOCOL_VERSIONS: [&str; 4] = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
37
38pub const LATEST_PROTOCOL_VERSION: &str = "2025-11-25";
40
41const ACCEPT_BOTH: &str = "application/json, text/event-stream";
43
44pub const SESSION_ID_HEADER: &str = "mcp-session-id";
46
47pub const PROTOCOL_VERSION_HEADER: &str = "mcp-protocol-version";
49
50const MAX_BACKEND_SESSIONS: usize = 4096;
52
53pub(crate) const MAX_MCP_RESPONSE_BYTES: usize = 16 * 1024 * 1024;
56
57const MAX_DISCOVERY_PAGES: usize = 32;
59const MAX_DISCOVERED_TOOLS: usize = 2048;
60
61const MAX_RESUMPTIONS: usize = 3;
63
64const MAX_RETRY_WAIT_MS: u64 = 5_000;
66
67pub fn is_supported_version(version: &str) -> bool {
69 SUPPORTED_PROTOCOL_VERSIONS.contains(&version)
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct BackendSession {
75 pub id: Option<String>,
77 pub protocol_version: String,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum McpError {
86 NotDelivered(String),
89 AfterDelivery(String),
91}
92
93impl std::fmt::Display for McpError {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 Self::NotDelivered(detail) | Self::AfterDelivery(detail) => f.write_str(detail),
97 }
98 }
99}
100
101#[derive(Clone)]
103pub struct BackendEndpoint {
104 pub url: String,
106 pub auth: Option<(HeaderName, HeaderValue)>,
108 pub extra: Vec<(HeaderName, HeaderValue)>,
110 pub deadline: tokio::time::Instant,
112}
113
114impl BackendEndpoint {
115 fn remaining(&self) -> std::time::Duration {
116 self.deadline
117 .saturating_duration_since(tokio::time::Instant::now())
118 .max(std::time::Duration::from_millis(1))
119 }
120
121 fn post(&self, client: &reqwest::Client, session: Option<&BackendSession>) -> reqwest::RequestBuilder {
122 let request = client
123 .post(&self.url)
124 .timeout(self.remaining())
125 .header(axum::http::header::CONTENT_TYPE, "application/json")
126 .header(axum::http::header::ACCEPT, ACCEPT_BOTH);
127 self.decorate(request, session)
128 }
129
130 fn decorate(
131 &self,
132 mut request: reqwest::RequestBuilder,
133 session: Option<&BackendSession>,
134 ) -> reqwest::RequestBuilder {
135 if let Some(session) = session {
136 request = request.header(PROTOCOL_VERSION_HEADER, session.protocol_version.as_str());
137 if let Some(id) = &session.id {
138 request = request.header(SESSION_ID_HEADER, id.as_str());
139 }
140 }
141 if let Some((name, value)) = &self.auth {
142 request = request.header(name.clone(), value.clone());
143 }
144 for (name, value) in &self.extra {
145 request = request.header(name.clone(), value.clone());
146 }
147 request
148 }
149}
150
151fn classify(error: &reqwest::Error) -> McpError {
152 let category = crate::pipeline::classify_upstream_error(error).to_owned();
153 tracing::warn!(
154 category = %category,
155 error.status = ?error.status(),
156 error.is_timeout = error.is_timeout(),
157 error.is_connect = error.is_connect(),
158 "MCP backend request failed"
159 );
160 if error.is_connect() {
161 McpError::NotDelivered(category)
162 } else {
163 McpError::AfterDelivery(category)
164 }
165}
166
167fn session_id_of(response: &reqwest::Response) -> Result<Option<String>, String> {
169 let mut values = response.headers().get_all(SESSION_ID_HEADER).iter();
170 let Some(value) = values.next() else {
171 return Ok(None);
172 };
173 if values.next().is_some() {
174 return Err("backend returned more than one MCP-Session-Id".to_owned());
175 }
176 let text = value
177 .to_str()
178 .map_err(|_| "backend MCP-Session-Id is not visible ASCII".to_owned())?;
179 if text.is_empty() || text.len() > 1024 || !text.bytes().all(|b| (0x21..=0x7e).contains(&b)) {
180 return Err("backend MCP-Session-Id must be 1-1024 visible ASCII characters".to_owned());
181 }
182 Ok(Some(text.to_owned()))
183}
184
185fn is_event_stream(response: &reqwest::Response) -> bool {
186 response
187 .headers()
188 .get(axum::http::header::CONTENT_TYPE)
189 .and_then(|value| value.to_str().ok())
190 .and_then(|value| value.split(';').next())
191 .is_some_and(|media| media.trim().eq_ignore_ascii_case("text/event-stream"))
192}
193
194#[derive(Debug, Default)]
197pub struct SseDecoder {
198 buffer: Vec<u8>,
199 data: String,
200 has_data: bool,
201 pub last_event_id: Option<String>,
203 pub retry_ms: Option<u64>,
205}
206
207impl SseDecoder {
208 pub fn push(&mut self, chunk: &[u8]) -> Result<Vec<String>, String> {
210 self.buffer.extend_from_slice(chunk);
211 if self.buffer.len() > MAX_MCP_RESPONSE_BYTES {
212 return Err(format!(
213 "event stream line exceeds {MAX_MCP_RESPONSE_BYTES} bytes"
214 ));
215 }
216 let mut events = Vec::new();
217 while let Some(end) = self.buffer.iter().position(|b| *b == b'\n' || *b == b'\r') {
218 let terminator = self.buffer.get(end).copied();
219 let mut consumed = end.saturating_add(1);
220 if terminator == Some(b'\r') {
221 match self.buffer.get(end.saturating_add(1)) {
222 Some(b'\n') => consumed = end.saturating_add(2),
223 Some(_) => {}
224 None => break,
226 }
227 }
228 let line: Vec<u8> = self.buffer.drain(..consumed).take(end).collect();
229 let line = String::from_utf8(line).map_err(|_| "event stream is not valid UTF-8".to_owned())?;
230 if let Some(event) = self.line(&line) {
231 events.push(event);
232 }
233 }
234 Ok(events)
235 }
236
237 fn line(&mut self, line: &str) -> Option<String> {
238 if line.is_empty() {
239 if !self.has_data {
240 return None;
241 }
242 self.has_data = false;
243 let mut data = std::mem::take(&mut self.data);
244 if data.ends_with('\n') {
245 data.pop();
246 }
247 return Some(data);
248 }
249 if line.starts_with(':') {
250 return None;
251 }
252 let (field, value) = match line.split_once(':') {
253 Some((field, value)) => (field, value.strip_prefix(' ').unwrap_or(value)),
254 None => (line, ""),
255 };
256 match field {
257 "data" => {
258 self.data.push_str(value);
259 self.data.push('\n');
260 self.has_data = true;
261 }
262 "id" if !value.contains('\0') => self.last_event_id = Some(value.to_owned()),
263 "retry" if !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit()) => {
264 self.retry_ms = value.parse().ok();
265 }
266 _ => {}
267 }
268 None
269 }
270}
271
272enum Message {
274 Response(Vec<u8>),
276 ServerRequest { id: Value, method: String },
278 Other,
280}
281
282fn interpret(payload: &str, request_id: &Value) -> Result<Vec<Message>, String> {
283 if payload.trim().is_empty() {
284 return Ok(vec![Message::Other]);
285 }
286 let value: Value =
287 serde_json::from_str(payload).map_err(|_| "event stream carried a non-JSON message".to_owned())?;
288 let items = match value {
289 Value::Array(items) => items,
290 other => vec![other],
291 };
292 let mut messages = Vec::with_capacity(items.len());
293 for item in items {
294 let Some(object) = item.as_object() else {
295 messages.push(Message::Other);
296 continue;
297 };
298 let method = object.get("method").and_then(Value::as_str);
299 let id = object.get("id");
300 match (method, id) {
301 (Some(method), Some(id)) => messages.push(Message::ServerRequest {
302 id: id.clone(),
303 method: method.to_owned(),
304 }),
305 (None, Some(id))
306 if id == request_id && (object.contains_key("result") || object.contains_key("error")) =>
307 {
308 messages.push(Message::Response(
309 serde_json::to_vec(&item).map_err(|error| error.to_string())?,
310 ));
311 }
312 _ => messages.push(Message::Other),
313 }
314 }
315 Ok(messages)
316}
317
318pub struct McpBackendClient {
320 client: reqwest::Client,
321 sessions: parking_lot::Mutex<HashMap<(String, String), SessionSlot>>,
322}
323
324struct SessionSlot {
325 session: Arc<tokio::sync::Mutex<Option<BackendSession>>>,
326 last_used: Instant,
327}
328
329impl McpBackendClient {
330 pub fn new(client: reqwest::Client) -> Self {
332 Self {
333 client,
334 sessions: parking_lot::Mutex::new(HashMap::new()),
335 }
336 }
337
338 pub fn cached_sessions(&self) -> usize {
340 self.sessions.lock().len()
341 }
342
343 fn slot(&self, agent_session: &str, backend: &str) -> Arc<tokio::sync::Mutex<Option<BackendSession>>> {
344 let mut sessions = self.sessions.lock();
345 let key = (agent_session.to_owned(), backend.to_owned());
346 if let Some(slot) = sessions.get_mut(&key) {
347 slot.last_used = Instant::now();
348 return Arc::clone(&slot.session);
349 }
350 if sessions.len() >= MAX_BACKEND_SESSIONS {
351 let mut idle: Vec<((String, String), Instant)> = sessions
354 .iter()
355 .filter(|(_, slot)| Arc::strong_count(&slot.session) == 1)
356 .map(|(key, slot)| (key.clone(), slot.last_used))
357 .collect();
358 idle.sort_by_key(|(_, used)| *used);
359 let excess = sessions
360 .len()
361 .saturating_add(1)
362 .saturating_sub(MAX_BACKEND_SESSIONS);
363 for (key, _) in idle.into_iter().take(excess.max(1)) {
364 sessions.remove(&key);
365 }
366 }
367 let session = Arc::new(tokio::sync::Mutex::new(None));
368 sessions.insert(
369 key,
370 SessionSlot {
371 session: Arc::clone(&session),
372 last_used: Instant::now(),
373 },
374 );
375 session
376 }
377
378 pub fn forget_agent_session(&self, agent_session: &str) {
380 self.sessions
381 .lock()
382 .retain(|(agent, _), _| agent != agent_session);
383 }
384
385 pub async fn initialize(&self, endpoint: &BackendEndpoint) -> Result<BackendSession, McpError> {
387 let request_id = Value::String(format!("agentvisor-initialize-{}", av_core::ids::new_event_uid()));
388 let body = json!({
389 "jsonrpc": "2.0",
390 "id": request_id,
391 "method": "initialize",
392 "params": {
393 "protocolVersion": LATEST_PROTOCOL_VERSION,
394 "capabilities": {},
395 "clientInfo": {"name": "agentvisor-gateway", "version": "undisclosed"},
396 },
397 });
398 let bytes = serde_json::to_vec(&body).map_err(|error| McpError::NotDelivered(error.to_string()))?;
399 let response = endpoint
400 .post(&self.client, None)
401 .body(bytes)
402 .send()
403 .await
404 .map_err(|error| McpError::NotDelivered(format!("initialize: {}", classify(&error))))?;
407 let status = response.status();
408 if !status.is_success() {
409 return Err(McpError::NotDelivered(format!(
410 "backend refused initialize with HTTP {}",
411 status.as_u16()
412 )));
413 }
414 let assigned = session_id_of(&response).map_err(McpError::NotDelivered)?;
415 let provisional = BackendSession {
416 id: assigned.clone(),
417 protocol_version: LATEST_PROTOCOL_VERSION.to_owned(),
418 };
419 let reply = self
420 .read_reply(response, &request_id, endpoint, Some(&provisional))
421 .await
422 .map_err(|error| McpError::NotDelivered(format!("initialize: {error}")))?;
423 let reply: Value = serde_json::from_slice(&reply)
424 .map_err(|_| McpError::NotDelivered("initialize reply is not JSON".to_owned()))?;
425 if let Some(error) = reply.get("error") {
426 let message = error.get("message").and_then(Value::as_str).unwrap_or("error");
427 return Err(McpError::NotDelivered(format!(
428 "backend rejected initialize: {message}"
429 )));
430 }
431 let version = reply
432 .pointer("/result/protocolVersion")
433 .and_then(Value::as_str)
434 .ok_or_else(|| McpError::NotDelivered("initialize result has no protocolVersion".to_owned()))?;
435 if !is_supported_version(version) {
436 return Err(McpError::NotDelivered(format!(
437 "backend negotiated unsupported MCP protocol version {version:?}"
438 )));
439 }
440 let session = BackendSession {
441 id: assigned,
442 protocol_version: version.to_owned(),
443 };
444 let notification =
445 serde_json::to_vec(&json!({"jsonrpc": "2.0", "method": "notifications/initialized"}))
446 .map_err(|error| McpError::NotDelivered(error.to_string()))?;
447 let acknowledged = endpoint
448 .post(&self.client, Some(&session))
449 .body(notification)
450 .send()
451 .await
452 .map_err(|error| {
453 McpError::NotDelivered(format!("initialized notification: {}", classify(&error)))
454 })?;
455 if !acknowledged.status().is_success() {
456 return Err(McpError::NotDelivered(format!(
457 "backend refused the initialized notification with HTTP {}",
458 acknowledged.status().as_u16()
459 )));
460 }
461 Ok(session)
462 }
463
464 pub async fn session(
467 &self,
468 agent_session: &str,
469 backend: &str,
470 endpoint: &BackendEndpoint,
471 ) -> Result<BackendSession, McpError> {
472 let slot = self.slot(agent_session, backend);
473 let mut guard = slot.lock().await;
474 if let Some(session) = guard.as_ref() {
475 return Ok(session.clone());
476 }
477 let session = self.initialize(endpoint).await?;
478 *guard = Some(session.clone());
479 Ok(session)
480 }
481
482 async fn invalidate(&self, agent_session: &str, backend: &str, stale: &BackendSession) {
485 let slot = self.slot(agent_session, backend);
486 let mut guard = slot.lock().await;
487 if guard.as_ref() == Some(stale) {
488 *guard = None;
489 }
490 }
491
492 pub async fn request(
499 &self,
500 agent_session: &str,
501 backend: &str,
502 endpoint: &BackendEndpoint,
503 body: &[u8],
504 request_id: &Value,
505 ) -> Result<(u16, Vec<u8>, Option<String>), McpError> {
506 let mut session = self.session(agent_session, backend, endpoint).await?;
507 let mut retried = false;
508 loop {
509 let response = endpoint
510 .post(&self.client, Some(&session))
511 .body(body.to_vec())
512 .send()
513 .await
514 .map_err(|error| classify(&error))?;
515 let status = response.status();
516 if status == reqwest::StatusCode::NOT_FOUND && session.id.is_some() && !retried {
517 retried = true;
520 self.invalidate(agent_session, backend, &session).await;
521 session = self.session(agent_session, backend, endpoint).await?;
522 continue;
523 }
524 if let Some(refused) = refused_encoding(&response) {
525 return Err(McpError::AfterDelivery(refused));
526 }
527 if !status.is_success() {
528 let content_type = response
529 .headers()
530 .get(axum::http::header::CONTENT_TYPE)
531 .and_then(|value| value.to_str().ok())
532 .map(str::to_owned);
533 let bytes = read_bounded(response).await.map_err(McpError::AfterDelivery)?;
534 return Ok((status.as_u16(), bytes, content_type));
535 }
536 let bytes = self
537 .read_reply(response, request_id, endpoint, Some(&session))
538 .await
539 .map_err(McpError::AfterDelivery)?;
540 return Ok((status.as_u16(), bytes, Some("application/json".to_owned())));
541 }
542 }
543
544 pub async fn list_tools(
547 &self,
548 agent_session: &str,
549 backend: &str,
550 endpoint: &BackendEndpoint,
551 ) -> Result<Vec<Value>, McpError> {
552 let mut tools = Vec::new();
553 let mut cursor: Option<String> = None;
554 for page in 0..MAX_DISCOVERY_PAGES {
555 let request_id = Value::String(format!(
556 "agentvisor-tools-list-{page}-{}",
557 av_core::ids::new_event_uid()
558 ));
559 let mut request =
560 json!({"jsonrpc": "2.0", "id": request_id, "method": "tools/list", "params": {}});
561 if let (Some(cursor), Some(params)) = (&cursor, request.get_mut("params")) {
562 params["cursor"] = Value::String(cursor.clone());
563 }
564 let body =
565 serde_json::to_vec(&request).map_err(|error| McpError::NotDelivered(error.to_string()))?;
566 let (status, bytes, _) = self
567 .request(agent_session, backend, endpoint, &body, &request_id)
568 .await?;
569 if !(200..300).contains(&status) {
570 return Err(McpError::AfterDelivery(format!(
571 "tools/list answered HTTP {status}"
572 )));
573 }
574 let reply: Value = serde_json::from_slice(&bytes)
575 .map_err(|_| McpError::AfterDelivery("tools/list reply is not JSON".to_owned()))?;
576 if reply.get("error").is_some() {
577 return Err(McpError::AfterDelivery("backend rejected tools/list".to_owned()));
578 }
579 let page_tools = reply
580 .pointer("/result/tools")
581 .and_then(Value::as_array)
582 .ok_or_else(|| McpError::AfterDelivery("tools/list result has no tools array".to_owned()))?;
583 for tool in page_tools {
584 if tools.len() >= MAX_DISCOVERED_TOOLS {
585 return Ok(tools);
586 }
587 tools.push(tool.clone());
588 }
589 cursor = reply
590 .pointer("/result/nextCursor")
591 .and_then(Value::as_str)
592 .filter(|next| !next.is_empty())
593 .map(str::to_owned);
594 if cursor.is_none() {
595 return Ok(tools);
596 }
597 }
598 Ok(tools)
599 }
600
601 async fn read_reply(
604 &self,
605 response: reqwest::Response,
606 request_id: &Value,
607 endpoint: &BackendEndpoint,
608 session: Option<&BackendSession>,
609 ) -> Result<Vec<u8>, String> {
610 if !is_event_stream(&response) {
611 return read_bounded(response).await;
612 }
613 let mut decoder = SseDecoder::default();
614 let mut response = response;
615 let mut total = 0_usize;
616 let mut resumptions = 0_usize;
617 loop {
618 let mut stream = response.bytes_stream();
619 while let Some(chunk) = stream.next().await {
620 let chunk =
621 chunk.map_err(|error| crate::pipeline::classify_upstream_error(&error).to_owned())?;
622 total = total.saturating_add(chunk.len());
623 if total > MAX_MCP_RESPONSE_BYTES {
624 return Err(format!("backend response exceeds {MAX_MCP_RESPONSE_BYTES} bytes"));
625 }
626 for payload in decoder.push(&chunk)? {
627 for message in interpret(&payload, request_id)? {
628 match message {
629 Message::Response(bytes) => return Ok(bytes),
630 Message::ServerRequest { id, method } => {
631 self.answer_server_request(endpoint, session, id, &method).await;
632 }
633 Message::Other => {}
634 }
635 }
636 }
637 }
638 let Some(last_event_id) = decoder.last_event_id.clone() else {
639 return Err("backend event stream ended without a response".to_owned());
640 };
641 if resumptions >= MAX_RESUMPTIONS {
642 return Err("backend event stream ended without a response after resumption".to_owned());
643 }
644 resumptions = resumptions.saturating_add(1);
645 let wait = decoder.retry_ms.unwrap_or(0).min(MAX_RETRY_WAIT_MS);
646 if wait > 0 {
647 tokio::time::sleep(std::time::Duration::from_millis(wait)).await;
648 }
649 let resume = endpoint
650 .decorate(
651 self.client
652 .get(&endpoint.url)
653 .timeout(endpoint.remaining())
654 .header(axum::http::header::ACCEPT, "text/event-stream")
655 .header("last-event-id", last_event_id.as_str()),
656 session,
657 )
658 .send()
659 .await
660 .map_err(|error| crate::pipeline::classify_upstream_error(&error).to_owned())?;
661 if !resume.status().is_success() || !is_event_stream(&resume) {
662 return Err(format!(
663 "backend refused to resume its event stream (HTTP {})",
664 resume.status().as_u16()
665 ));
666 }
667 response = resume;
668 }
669 }
670
671 async fn answer_server_request(
674 &self,
675 endpoint: &BackendEndpoint,
676 session: Option<&BackendSession>,
677 id: Value,
678 method: &str,
679 ) {
680 let reply = if method == "ping" {
681 json!({"jsonrpc": "2.0", "id": id, "result": {}})
682 } else {
683 tracing::warn!(method, "MCP backend sent a request the gateway does not support");
684 json!({
685 "jsonrpc": "2.0",
686 "id": id,
687 "error": {"code": -32601, "message": "method not supported by the AgentVisor gateway"},
688 })
689 };
690 let Ok(bytes) = serde_json::to_vec(&reply) else {
691 return;
692 };
693 if let Err(error) = endpoint.post(&self.client, session).body(bytes).send().await {
694 let _ = classify(&error);
695 }
696 }
697}
698
699fn refused_encoding(response: &reqwest::Response) -> Option<String> {
700 for value in response.headers().get_all(axum::http::header::CONTENT_ENCODING) {
701 let raw = value.to_str().unwrap_or_default();
702 for token in raw.split(',') {
703 let token = token.trim();
704 if !token.is_empty() && !token.eq_ignore_ascii_case("identity") {
705 return Some(format!(
706 "tool upstream responded with unsupported Content-Encoding token {token:?} (full header: {raw:?})"
707 ));
708 }
709 }
710 }
711 None
712}
713
714async fn read_bounded(response: reqwest::Response) -> Result<Vec<u8>, String> {
715 let mut stream = response.bytes_stream();
716 let mut body = Vec::new();
717 while let Some(chunk) = stream.next().await {
718 let chunk = chunk.map_err(|error| crate::pipeline::classify_upstream_error(&error).to_owned())?;
719 if body.len().saturating_add(chunk.len()) > MAX_MCP_RESPONSE_BYTES {
720 return Err(format!("backend response exceeds {MAX_MCP_RESPONSE_BYTES} bytes"));
721 }
722 body.extend_from_slice(&chunk);
723 }
724 Ok(body)
725}
726
727#[cfg(test)]
728mod tests {
729 #![allow(
730 clippy::unwrap_used,
731 clippy::expect_used,
732 clippy::panic,
733 clippy::indexing_slicing
734 )]
735
736 use super::*;
737
738 #[test]
739 fn decoder_joins_multiline_data_and_tracks_ids() {
740 let mut decoder = SseDecoder::default();
741 let events = decoder
742 .push(b": comment\r\nid: 7\r\nretry: 250\r\ndata: {\"a\":\r\ndata: 1}\r\n\r\n")
743 .unwrap();
744 assert_eq!(events, vec!["{\"a\":\n1}".to_owned()]);
745 assert_eq!(decoder.last_event_id.as_deref(), Some("7"));
746 assert_eq!(decoder.retry_ms, Some(250));
747 }
748
749 #[test]
750 fn decoder_handles_split_crlf_and_lone_cr() {
751 let mut decoder = SseDecoder::default();
752 assert!(decoder.push(b"data: one\r").unwrap().is_empty());
753 let events = decoder.push(b"\n\rdata: two\r\r").unwrap();
754 assert_eq!(events, vec!["one".to_owned()]);
755 let events = decoder.push(b"x").unwrap();
756 assert_eq!(events, vec!["two".to_owned()]);
757 }
758
759 #[test]
760 fn empty_priming_event_is_ignored() {
761 let mut decoder = SseDecoder::default();
762 let events = decoder.push(b"id: 1\ndata:\n\n").unwrap();
763 assert_eq!(events, vec![String::new()]);
764 assert!(matches!(
765 interpret(&events[0], &json!(1)).unwrap()[0],
766 Message::Other
767 ));
768 }
769
770 #[test]
771 fn interpret_finds_matching_response_and_server_requests() {
772 let id = json!(5);
773 let messages = interpret(
774 r#"[{"jsonrpc":"2.0","method":"notifications/progress","params":{}},
775 {"jsonrpc":"2.0","id":"s1","method":"ping"},
776 {"jsonrpc":"2.0","id":4,"result":{}},
777 {"jsonrpc":"2.0","id":5,"result":{"ok":true}}]"#,
778 &id,
779 )
780 .unwrap();
781 assert!(matches!(messages[0], Message::Other));
782 assert!(matches!(&messages[1], Message::ServerRequest { method, .. } if method == "ping"));
783 assert!(
784 matches!(messages[2], Message::Other),
785 "a response with another id is not ours"
786 );
787 match &messages[3] {
788 Message::Response(bytes) => {
789 let value: Value = serde_json::from_slice(bytes).unwrap();
790 assert_eq!(value["result"]["ok"], true);
791 }
792 _ => panic!("expected the matching response"),
793 }
794 }
795
796 #[test]
797 fn non_json_event_is_an_error() {
798 assert!(interpret("not json", &json!(1)).is_err());
799 }
800
801 #[test]
802 fn supported_versions_include_latest() {
803 assert!(is_supported_version(LATEST_PROTOCOL_VERSION));
804 assert!(is_supported_version("2025-03-26"));
805 assert!(!is_supported_version("1.0.0"));
806 }
807}