1use std::collections::BTreeMap;
45use std::sync::Arc;
46
47use serde_json::{Map, Value};
48
49use crate::application::bridge::cross_protocol::{
50 CrossProtocolCodec, CrossProtocolError, ProtocolSide, RequestCodecReport, RequestCodecStep,
51};
52use crate::domain::bridge::a2a_mcp::a2a_message_to_mcp_content;
53
54const BRIDGE_SKILL_KEY: &str = "bridge_skill";
61
62#[derive(Debug, Default, Clone, Copy)]
70pub struct A2aMcpCodec;
71
72impl A2aMcpCodec {
73 #[must_use]
75 pub const fn new() -> Self {
76 Self
77 }
78}
79
80impl CrossProtocolCodec for A2aMcpCodec {
81 fn downstream_protocol(&self) -> ProtocolSide {
82 ProtocolSide::Mcp
89 }
90
91 fn encode_request(
92 &self,
93 downstream_side: ProtocolSide,
94 ir_method: &str,
95 raw_envelope: &Map<String, Value>,
96 report: &mut RequestCodecReport,
97 ) -> Result<Map<String, Value>, CrossProtocolError> {
98 let Some(upstream_method) = downstream_side.wire_method(ir_method) else {
99 return Err(CrossProtocolError::MethodNotBridged(
100 ir_method.to_owned(),
101 downstream_side,
102 ));
103 };
104
105 let mut outbound = raw_envelope.clone();
106 if let Some(params) = outbound.get_mut("params") {
112 *params = translate_params_outbound(downstream_side, ir_method, params, report);
113 } else if downstream_side != ir_method_side(ir_method) {
114 return Err(CrossProtocolError::MethodNotBridged(
120 ir_method.to_owned(),
121 downstream_side,
122 ));
123 }
124
125 if let Some(method) = outbound.get_mut("method") {
126 *method = Value::String(upstream_method.to_owned());
127 }
128
129 report.record(RequestCodecStep::MethodRenamed {
130 from: ir_method_static(ir_method),
131 to: method_name_static(upstream_method),
132 });
133 Ok(outbound)
134 }
135
136 fn decode_reply(
137 &self,
138 downstream_side: ProtocolSide,
139 ir_method: &str,
140 reply: &Map<String, Value>,
141 report: &mut RequestCodecReport,
142 ) -> Result<Map<String, Value>, CrossProtocolError> {
143 if downstream_side.wire_method(ir_method).is_none() {
144 return Err(CrossProtocolError::MethodNotBridged(
145 ir_method.to_owned(),
146 downstream_side,
147 ));
148 }
149
150 let mut outbound = reply.clone();
151 let Some(result) = outbound.get_mut("result") else {
152 return Ok(outbound);
155 };
156
157 *result = match downstream_side {
158 ProtocolSide::A2a => mcp_to_a2a_result(result),
165 ProtocolSide::Mcp => a2a_to_mcp_result(result),
166 };
167
168 report.record(RequestCodecStep::ReplyTranslated {
169 from: reply_label(downstream_side, "upstream"),
170 to: reply_label(downstream_side, "downstream"),
171 });
172
173 Ok(outbound)
174 }
175}
176
177fn ir_method_side(ir_method: &str) -> ProtocolSide {
178 if ir_method == "SendMessage" {
179 ProtocolSide::A2a
180 } else if ir_method == "tools/call" {
181 ProtocolSide::Mcp
182 } else {
183 ProtocolSide::A2a
186 }
187}
188
189fn ir_method_static(method: &str) -> &'static str {
190 match method {
195 "SendMessage" => "SendMessage",
196 "tools/call" => "tools/call",
197 _ => "unknown",
199 }
200}
201
202fn method_name_static(method: &str) -> &'static str {
203 match method {
205 "SendMessage" => "SendMessage",
206 "SendStreamingMessage" => "SendStreamingMessage",
207 "GetTask" => "GetTask",
208 "tools/call" => "tools/call",
209 "tools/list" => "tools/list",
210 _ => "unknown",
212 }
213}
214
215fn reply_label(side: ProtocolSide, direction: &str) -> &'static str {
216 if direction == "upstream" {
220 if side == ProtocolSide::A2a {
221 "A2A Task/Message"
222 } else {
223 "MCP CallToolResult"
224 }
225 } else if side == ProtocolSide::A2a {
226 "MCP CallToolResult"
227 } else {
228 "A2A Task/Message"
229 }
230}
231
232fn translate_params_outbound(
233 side: ProtocolSide,
234 ir_method: &str,
235 params: &Value,
236 report: &mut RequestCodecReport,
237) -> Value {
238 match (side, ir_method) {
239 (ProtocolSide::A2a, "SendMessage") => a2a_to_mcp_tools_call_params(params, report),
240 (ProtocolSide::Mcp, "tools/call") => mcp_to_a2a_send_message_params(params, report),
241 _ => params.clone(),
242 }
243}
244
245fn mcp_to_a2a_send_message_params(params: &Value, report: &mut RequestCodecReport) -> Value {
246 let Some(object) = params.as_object() else {
248 return params.clone();
249 };
250 let skill = object.get("name").and_then(Value::as_str).unwrap_or("");
251 let arguments = object.get("arguments").cloned().unwrap_or(Value::Null);
252 let mut metadata = BTreeMap::new();
253 if !skill.is_empty() {
254 metadata.insert(BRIDGE_SKILL_KEY.to_owned(), Value::String(skill.to_owned()));
255 }
256 let mut message = serde_json::Map::new();
257 message.insert("role".to_owned(), Value::String("user".to_owned()));
258 if arguments.is_null() {
259 message.insert("parts".to_owned(), Value::Array(Vec::new()));
260 } else {
261 let mut data_part = serde_json::Map::new();
262 data_part.insert("data".to_owned(), arguments);
263 let mut part = serde_json::Map::new();
264 part.insert("kind".to_owned(), Value::String("data".to_owned()));
265 part.insert("data".to_owned(), Value::Object(data_part));
266 message.insert("parts".to_owned(), Value::Array(vec![Value::Object(part)]));
267 }
268 let mut out = serde_json::Map::new();
269 out.insert("message".to_owned(), Value::Object(message));
270 if !metadata.is_empty() {
271 let meta: Map<String, Value> = metadata.into_iter().collect();
272 out.insert("metadata".to_owned(), Value::Object(meta));
273 }
274 report.record(RequestCodecStep::ParamsTranslated {
275 from: "MCP tools/call{name, arguments}",
276 to: "A2A SendMessage{message, metadata}",
277 });
278 Value::Object(out)
279}
280
281fn a2a_to_mcp_tools_call_params(params: &Value, report: &mut RequestCodecReport) -> Value {
282 let Some(object) = params.as_object() else {
284 return params.clone();
285 };
286 let Some(message) = object.get("message") else {
287 return params.clone();
288 };
289 let skill = object
290 .get("metadata")
291 .and_then(Value::as_object)
292 .and_then(|meta| meta.get(BRIDGE_SKILL_KEY))
293 .and_then(Value::as_str)
294 .unwrap_or("");
295 let arguments = first_message_data(message);
296 let mut out = serde_json::Map::new();
297 if !skill.is_empty() {
298 out.insert("name".to_owned(), Value::String(skill.to_owned()));
299 }
300 out.insert("arguments".to_owned(), arguments);
301 report.record(RequestCodecStep::ParamsTranslated {
302 from: "A2A SendMessage{message, metadata}",
303 to: "MCP tools/call{name, arguments}",
304 });
305 Value::Object(out)
306}
307
308fn first_message_data(message: &Value) -> Value {
309 let Some(parts) = message
310 .as_object()
311 .and_then(|object| object.get("parts"))
312 .and_then(Value::as_array)
313 else {
314 return Value::Null;
315 };
316 for part in parts {
317 if let Some(data) = part.as_object().and_then(|object| object.get("data")) {
318 return data.clone();
319 }
320 }
321 Value::Null
322}
323
324fn a2a_to_mcp_result(result: &Value) -> Value {
325 let Some(object) = result.as_object() else {
329 return result.clone();
330 };
331 let mut mapping_report = crate::domain::bridge::a2a_mcp::CrossProtocolReport::new();
332 let mut content = if object.contains_key("id") {
333 a2a_task_content(object, &mut mapping_report)
334 } else {
335 let message = clone_message(object);
336 a2a_message_to_mcp_content(&message, &mut mapping_report)
337 };
338 if content.is_empty() {
339 if let Some(state) = object
343 .get("status")
344 .and_then(Value::as_object)
345 .and_then(|status| status.get("state"))
346 .and_then(Value::as_str)
347 {
348 content = vec![crate::domain::ir::Content::Text(
349 crate::domain::ir::TextContent::new(state),
350 )];
351 }
352 }
353 let mut out = serde_json::Map::new();
354 out.insert(
355 "content".to_owned(),
356 Value::Array(content_to_values(&content)),
357 );
358 let is_error = object
359 .get("status")
360 .and_then(Value::as_object)
361 .and_then(|status| status.get("state"))
362 .and_then(Value::as_str)
363 .is_some_and(|state| state == "failed" || state == "rejected");
364 if is_error {
365 out.insert("isError".to_owned(), Value::Bool(true));
366 }
367 Value::Object(out)
368}
369
370fn a2a_task_content(
371 object: &Map<String, Value>,
372 report: &mut crate::domain::bridge::a2a_mcp::CrossProtocolReport,
373) -> Vec<crate::domain::ir::Content> {
374 let history = object
376 .get("history")
377 .and_then(Value::as_array)
378 .cloned()
379 .unwrap_or_default();
380 let last_message: Option<crate::domain::a2a::message::Message> =
381 history.iter().rev().find_map(|m| {
382 let is_agent = m
383 .as_object()
384 .and_then(|obj| obj.get("role"))
385 .and_then(Value::as_str)
386 .is_some_and(|role| role != "user");
387 if is_agent {
388 serde_json::from_value(m.clone()).ok()
389 } else {
390 None
391 }
392 });
393 last_message.map_or_else(Vec::new, |message| {
394 a2a_message_to_mcp_content(&message, report)
395 })
396}
397
398fn mcp_to_a2a_result(result: &Value) -> Value {
399 let Some(object) = result.as_object() else {
401 return result.clone();
402 };
403 let content = object.get("content").and_then(Value::as_array);
404 let parts = content.map_or_else(Vec::new, |items| {
405 items.iter().map(value_to_a2a_part_value).collect()
406 });
407 let mut message = serde_json::Map::new();
408 message.insert("role".to_owned(), Value::String("assistant".to_owned()));
409 message.insert("parts".to_owned(), Value::Array(parts));
410 let mut out = serde_json::Map::new();
411 out.insert("message".to_owned(), Value::Object(message));
412 if object.get("isError").and_then(Value::as_bool) == Some(true) {
413 let mut status = serde_json::Map::new();
414 status.insert("state".to_owned(), Value::String("failed".to_owned()));
415 out.insert("status".to_owned(), Value::Object(status));
416 }
417 Value::Object(out)
418}
419
420fn value_to_a2a_part_value(value: &Value) -> Value {
421 let Some(object) = value.as_object() else {
427 return value.clone();
428 };
429 let kind = object.get("type").and_then(Value::as_str).unwrap_or("data");
430 match kind {
431 "text" => {
432 let text = object
433 .get("text")
434 .and_then(Value::as_str)
435 .unwrap_or("")
436 .to_owned();
437 let mut part = serde_json::Map::new();
438 part.insert("kind".to_owned(), Value::String("text".to_owned()));
439 part.insert("text".to_owned(), Value::String(text));
440 Value::Object(part)
441 }
442 "image" | "audio" | "resource_link" | "resource" => {
443 let mut part = serde_json::Map::new();
444 part.insert("kind".to_owned(), Value::String("file".to_owned()));
445 part.insert("file".to_owned(), value.clone());
446 Value::Object(part)
447 }
448 _ => {
449 let mut part = serde_json::Map::new();
450 part.insert("kind".to_owned(), Value::String("data".to_owned()));
451 part.insert("data".to_owned(), value.clone());
452 Value::Object(part)
453 }
454 }
455}
456
457fn clone_message(object: &Map<String, Value>) -> crate::domain::a2a::message::Message {
458 let value = Value::Object(object.clone());
466 serde_json::from_value(value).unwrap_or_default()
467}
468
469fn content_to_values(content: &[crate::domain::ir::Content]) -> Vec<Value> {
470 content.iter().map(content_to_value).collect()
471}
472
473fn content_to_value(content: &crate::domain::ir::Content) -> Value {
474 use crate::domain::ir::Content;
475 match content {
476 Content::Text(text) => {
477 let mut map = serde_json::Map::new();
478 map.insert("type".to_owned(), Value::String("text".to_owned()));
479 map.insert("text".to_owned(), Value::String(text.text.clone()));
480 Value::Object(map)
481 }
482 Content::Image(image) => {
483 let mut map = serde_json::Map::new();
484 map.insert("type".to_owned(), Value::String("image".to_owned()));
485 map.insert("data".to_owned(), Value::String(image.data.clone()));
486 map.insert(
487 "mimeType".to_owned(),
488 Value::String(image.mime_type.clone()),
489 );
490 Value::Object(map)
491 }
492 Content::Audio(audio) => {
493 let mut map = serde_json::Map::new();
494 map.insert("type".to_owned(), Value::String("audio".to_owned()));
495 map.insert("data".to_owned(), Value::String(audio.data.clone()));
496 map.insert(
497 "mimeType".to_owned(),
498 Value::String(audio.mime_type.clone()),
499 );
500 Value::Object(map)
501 }
502 Content::ResourceLink(link) => {
503 let mut map = serde_json::Map::new();
504 map.insert("type".to_owned(), Value::String("resource_link".to_owned()));
505 map.insert("uri".to_owned(), Value::String(link.uri.clone()));
506 map.insert("name".to_owned(), Value::String(link.name.clone()));
507 if let Some(description) = link.description.as_ref() {
508 map.insert("description".to_owned(), Value::String(description.clone()));
509 }
510 if let Some(mime) = link.mime_type.as_ref() {
511 map.insert("mimeType".to_owned(), Value::String(mime.clone()));
512 }
513 Value::Object(map)
514 }
515 Content::EmbeddedResource(resource) => {
516 let mut map = serde_json::Map::new();
517 map.insert("type".to_owned(), Value::String("resource".to_owned()));
518 let mut resource_map = serde_json::Map::new();
519 resource_map.insert("uri".to_owned(), Value::String(resource.uri.clone()));
520 if let Some(text) = resource.text.as_ref() {
521 resource_map.insert("text".to_owned(), Value::String(text.clone()));
522 }
523 if let Some(blob) = resource.blob.as_ref() {
524 resource_map.insert("blob".to_owned(), Value::String(blob.clone()));
525 }
526 if let Some(mime) = resource.mime_type.as_ref() {
527 resource_map.insert("mimeType".to_owned(), Value::String(mime.clone()));
528 }
529 map.insert("resource".to_owned(), Value::Object(resource_map));
530 Value::Object(map)
531 }
532 Content::Unknown(unknown) => match serde_json::from_str(&unknown.raw.to_string()) {
533 Ok(Value::Object(map)) => Value::Object(map),
534 Ok(other) => other,
535 Err(_) => Value::Null,
536 },
537 }
538}
539
540impl CrossProtocolCodec for Arc<A2aMcpCodec> {
541 fn downstream_protocol(&self) -> ProtocolSide {
542 (**self).downstream_protocol()
543 }
544
545 fn encode_request(
546 &self,
547 downstream_side: ProtocolSide,
548 ir_method: &str,
549 raw_envelope: &Map<String, Value>,
550 report: &mut RequestCodecReport,
551 ) -> Result<Map<String, Value>, CrossProtocolError> {
552 (**self).encode_request(downstream_side, ir_method, raw_envelope, report)
553 }
554
555 fn decode_reply(
556 &self,
557 downstream_side: ProtocolSide,
558 ir_method: &str,
559 reply: &Map<String, Value>,
560 report: &mut RequestCodecReport,
561 ) -> Result<Map<String, Value>, CrossProtocolError> {
562 (**self).decode_reply(downstream_side, ir_method, reply, report)
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use std::sync::Arc;
569
570 use serde_json::json;
571
572 use super::*;
573
574 fn report() -> RequestCodecReport {
575 RequestCodecReport::empty()
576 }
577
578 fn object_of(input: &Value) -> Map<String, Value> {
579 input.as_object().cloned().unwrap_or_default()
580 }
581
582 #[test]
583 fn a2a_send_message_becomes_mcp_tools_call() -> Result<(), Box<dyn std::error::Error>> {
584 let codec = A2aMcpCodec::new();
585 let mut rep = report();
586 let raw = object_of(&json!({
587 "jsonrpc": "2.0",
588 "id": 1,
589 "method": "SendMessage",
590 "params": {
591 "message": {
592 "role": "user",
593 "parts": [
594 { "kind": "data", "data": { "query": "weather" } }
595 ]
596 },
597 "metadata": { "bridge_skill": "lookup" }
598 }
599 }));
600 let out = codec.encode_request(ProtocolSide::A2a, "SendMessage", &raw, &mut rep)?;
601 assert_eq!(
602 out.get("method").and_then(Value::as_str),
603 Some("tools/call")
604 );
605 let params = out
606 .get("params")
607 .and_then(Value::as_object)
608 .ok_or("missing params")?;
609 assert_eq!(params.get("name").and_then(Value::as_str), Some("lookup"));
610 assert_eq!(
611 params.get("arguments"),
612 Some(&json!({ "query": "weather" }))
613 );
614 Ok(())
615 }
616
617 #[test]
618 fn mcp_tools_call_becomes_a2a_send_message() -> Result<(), Box<dyn std::error::Error>> {
619 let codec = A2aMcpCodec::new();
620 let mut rep = report();
621 let raw = object_of(&json!({
622 "jsonrpc": "2.0",
623 "id": 1,
624 "method": "tools/call",
625 "params": {
626 "name": "lookup",
627 "arguments": { "query": "weather" }
628 }
629 }));
630 let out = codec.encode_request(ProtocolSide::Mcp, "tools/call", &raw, &mut rep)?;
631 assert_eq!(
632 out.get("method").and_then(Value::as_str),
633 Some("SendMessage")
634 );
635 let params = out
636 .get("params")
637 .and_then(Value::as_object)
638 .ok_or("missing params")?;
639 let metadata = params
640 .get("metadata")
641 .and_then(Value::as_object)
642 .ok_or("missing metadata")?;
643 assert_eq!(
644 metadata.get("bridge_skill").and_then(Value::as_str),
645 Some("lookup")
646 );
647 Ok(())
648 }
649
650 #[test]
651 fn an_unbridged_method_is_refused() {
652 let codec = A2aMcpCodec::new();
653 let mut rep = report();
654 let raw = object_of(&json!({ "jsonrpc": "2.0", "id": 1, "method": "GetTask" }));
655 let outcome = codec.encode_request(ProtocolSide::A2a, "GetTask", &raw, &mut rep);
656 assert!(matches!(
657 outcome,
658 Err(CrossProtocolError::MethodNotBridged(_, ProtocolSide::A2a))
659 ));
660 }
661
662 #[test]
663 fn a_task_reply_becomes_a_mcp_tool_result() -> Result<(), Box<dyn std::error::Error>> {
664 let codec = A2aMcpCodec::new();
665 let mut rep = report();
666 let reply = object_of(&json!({
667 "jsonrpc": "2.0",
668 "id": 1,
669 "result": {
670 "id": "task-1",
671 "status": { "state": "completed" },
672 "history": [
673 { "role": "user", "parts": [{ "kind": "text", "text": "hi" }] },
674 { "role": "agent", "parts": [{ "kind": "text", "text": "hello" }] }
675 ]
676 }
677 }));
678 let out = codec.decode_reply(ProtocolSide::Mcp, "tools/call", &reply, &mut rep)?;
682 let result = out
683 .get("result")
684 .and_then(Value::as_object)
685 .ok_or("missing result")?;
686 let content = result
687 .get("content")
688 .and_then(Value::as_array)
689 .ok_or("missing content")?;
690 assert!(!content.is_empty());
691 Ok(())
692 }
693
694 #[test]
695 fn an_mcp_tool_result_becomes_an_a2a_message() -> Result<(), Box<dyn std::error::Error>> {
696 let codec = A2aMcpCodec::new();
697 let mut rep = report();
698 let reply = object_of(&json!({
699 "jsonrpc": "2.0",
700 "id": 1,
701 "result": {
702 "content": [{ "type": "text", "text": "hello" }],
703 "isError": false
704 }
705 }));
706 let out = codec.decode_reply(ProtocolSide::A2a, "SendMessage", &reply, &mut rep)?;
711 let result = out
712 .get("result")
713 .and_then(Value::as_object)
714 .ok_or("missing result")?;
715 let message = result.get("message").ok_or("missing message")?;
716 assert!(message.is_object());
717 Ok(())
718 }
719
720 #[test]
721 fn the_codec_is_send_and_sync_via_arc() {
722 fn assert_send_sync<T: Send + Sync>(_: &T) {}
723 let codec: Arc<A2aMcpCodec> = Arc::new(A2aMcpCodec::new());
724 assert_send_sync(&codec);
725 }
726}