use serde_json::{Map, Value};
use crate::application::bridge::content::{ToolResultCodec, Translator};
use crate::domain::Agreement;
use crate::domain::ir::lower::LowerError;
use crate::domain::ir::{
EnvelopeError, IrMessage, MethodName, TranslationReport, lift_envelope, lower_envelope,
};
use crate::domain::{DegradePolicy, Feature, ProtocolVersion};
pub type SendFuture<'a, T> = std::pin::Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait BridgePeer: Send + Sync {
fn send(
&self,
envelope: Map<String, Value>,
) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>>;
fn notify(&self, envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>>;
fn close(&self) -> SendFuture<'_, Result<(), BridgeError>>;
fn take_pending(&self) -> Vec<Map<String, Value>> {
Vec::new()
}
fn wait_pending(&self) -> SendFuture<'static, ()> {
Box::pin(std::future::pending())
}
fn listen(&self) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async { Ok(()) })
}
}
impl<T: BridgePeer + ?Sized> BridgePeer for std::sync::Arc<T> {
fn send(
&self,
envelope: Map<String, Value>,
) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>> {
(**self).send(envelope)
}
fn notify(&self, envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>> {
(**self).notify(envelope)
}
fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
(**self).close()
}
fn wait_pending(&self) -> SendFuture<'static, ()> {
(**self).wait_pending()
}
fn take_pending(&self) -> Vec<Map<String, Value>> {
(**self).take_pending()
}
fn listen(&self) -> SendFuture<'_, Result<(), BridgeError>> {
(**self).listen()
}
}
#[derive(Debug, thiserror::Error)]
pub enum BridgeError {
#[error("the envelope could not be read: {0}")]
Unreadable(#[from] EnvelopeError),
#[error("neither peer can {feature:?}, so the bridge cannot forward it")]
FeatureUnavailable {
feature: Feature,
},
#[error("the upstream peer could not be reached: {reason}")]
Unreachable {
reason: String,
},
#[error("the connection did not close cleanly: {0}")]
Close(String),
#[error("the downstream client cannot read the reply: {0}")]
NotReadable(#[from] LowerError),
#[cfg(feature = "a2a")]
#[error("method {method:?} is not bridged on the {downstream:?} leg")]
MethodNotBridged {
method: String,
downstream: crate::application::bridge::ProtocolSide,
},
}
#[derive(Debug)]
pub struct Forwarded {
pub outbound: Map<String, Value>,
pub reply: Map<String, Value>,
pub report: TranslationReport,
}
#[derive(Debug)]
pub struct Bridge<U: BridgePeer, C> {
agreement: Agreement,
policy: DegradePolicy,
upstream: U,
translator: Translator<C>,
}
impl<U: BridgePeer, C: ToolResultCodec> Bridge<U, C> {
#[must_use]
pub const fn new(agreement: Agreement, policy: DegradePolicy, upstream: U, codec: C) -> Self {
Self {
agreement,
policy,
upstream,
translator: Translator::new(codec),
}
}
#[must_use]
pub const fn agreement(&self) -> &Agreement {
&self.agreement
}
#[must_use]
pub const fn upstream_version(&self) -> ProtocolVersion {
self.agreement.upstream().version
}
#[must_use]
pub const fn upstream(&self) -> &U {
&self.upstream
}
pub async fn forward(&self, envelope: &Map<String, Value>) -> Result<Forwarded, BridgeError> {
let message = lift_envelope(envelope)?;
self.check_method(&message)?;
let outbound = lower_envelope(&message);
let tool_call = message.method().map(MethodName::as_str) == Some(TOOL_CALL);
let mut report = TranslationReport::new(self.upstream_version());
if matches!(message, IrMessage::Notification(_)) {
self.upstream.notify(outbound).await?;
return Ok(Forwarded {
outbound: lower_envelope(&message),
reply: Map::new(),
report,
});
}
let sent = self.upstream.send(outbound.clone()).await?;
let reply = if tool_call {
let translated = self.translator.translate(
sent,
self.upstream_version(),
self.agreement.downstream(),
self.policy,
)?;
report.absorb(&translated.report);
translated.reply
} else {
sent
};
Ok(Forwarded {
outbound,
reply,
report,
})
}
fn check_method(&self, message: &IrMessage) -> Result<(), BridgeError> {
let Some(method) = message.method() else {
return Ok(());
};
let feature = feature_for(method.as_str());
match feature {
None => Ok(()),
Some(feature) if self.agreement.serves(feature) => Ok(()),
Some(feature) => Err(BridgeError::FeatureUnavailable { feature }),
}
}
pub async fn close(&self) -> Result<(), BridgeError> {
self.upstream.close().await
}
}
const TOOL_CALL: &str = "tools/call";
fn feature_for(method: &str) -> Option<Feature> {
let (feature, name) = match method {
name if name == "tools/list" => (Feature::Tools, name),
name if name == TOOL_CALL => (Feature::Tools, name),
name if name == "resources/list" => (Feature::Resources, name),
name if name == "resources/read" => (Feature::Resources, name),
name if name == "prompts/list" => (Feature::Prompts, name),
name if name == "prompts/get" => (Feature::Prompts, name),
_ => return None,
};
debug_assert!(!name.is_empty());
Some(feature)
}
#[cfg(test)]
mod tests {
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use serde_json::{Map, Value, json};
use crate::application::bridge::content::doubles::Counting;
use crate::domain::ir::{IrMessage, TranslationReport};
use crate::domain::{Agreement, Leg};
use crate::domain::{
DegradePolicy, Feature, FeatureProfile, Negotiation, ProtocolVersion, TransportKind,
resolve,
};
use super::{Bridge, BridgeError, BridgePeer, SendFuture};
type TestError = Box<dyn std::error::Error>;
fn codec() -> Arc<Counting> {
Arc::new(Counting::reading_nothing())
}
#[derive(Default)]
struct Recorder {
seen: Mutex<Vec<Map<String, Value>>>,
}
impl Recorder {
fn seen(&self) -> Result<Vec<Map<String, Value>>, BridgeError> {
self.seen
.lock()
.map(|seen| seen.clone())
.map_err(|_| BridgeError::Close("recorder poisoned".to_owned()))
}
}
type Shared = std::sync::Arc<Recorder>;
impl BridgePeer for Recorder {
fn send(
&self,
envelope: Map<String, Value>,
) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>> {
Box::pin(async move {
let id = envelope.get("id").cloned().unwrap_or(Value::Null);
let Ok(mut seen) = self.seen.lock() else {
return Err(BridgeError::Close("recorder poisoned".to_owned()));
};
seen.push(envelope);
let mut reply = Map::new();
reply.insert("jsonrpc".into(), json!("2.0"));
reply.insert("id".into(), id);
reply.insert("result".into(), json!({"content": []}));
Ok(reply)
})
}
fn notify(&self, envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async move {
let Ok(mut seen) = self.seen.lock() else {
return Err(BridgeError::Close("recorder poisoned".to_owned()));
};
seen.push(envelope);
Ok(())
})
}
fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async { Ok(()) })
}
}
struct Absent;
impl BridgePeer for Absent {
fn send(
&self,
_envelope: Map<String, Value>,
) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>> {
Box::pin(async {
Err(BridgeError::Unreachable {
reason: "connection refused".to_owned(),
})
})
}
fn notify(&self, _envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async {
Err(BridgeError::Unreachable {
reason: "connection refused".to_owned(),
})
})
}
fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async { Ok(()) })
}
}
fn negotiation(version: &str, transport: TransportKind) -> Result<Negotiation, TestError> {
Ok(Negotiation::bare(version.parse()?, transport))
}
fn serving(features: &[Feature]) -> FeatureProfile {
features
.iter()
.fold(FeatureProfile::empty(), |p, f| p.with(*f))
}
fn envelope(text: &str) -> Result<Map<String, Value>, TestError> {
let object: Map<String, Value> = serde_json::from_str(text)?;
Ok(object)
}
#[tokio::test]
async fn an_unmodelled_directive_reaches_the_upstream_peer() -> Result<(), TestError> {
let upstream: Shared = Recorder::default().into();
let sent = envelope(
r#"{"jsonrpc":"2.0","id":7,"method":"tools/call",
"params":{"name":"echo","_meta":{"com.example/trace":{"span":12}}},
"x-vendor-directive":{"anything":[1,2,3]}}"#,
)?;
let bridge = Bridge::new(
Agreement::between(
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
),
DegradePolicy::default(),
upstream.clone(),
codec(),
);
bridge.forward(&sent).await?;
let seen = upstream.seen()?;
let received = seen.first().ok_or("the upstream peer saw nothing")?;
let params = received
.get("params")
.and_then(Value::as_object)
.ok_or("no params")?;
assert_eq!(
params
.get("_meta")
.and_then(|meta| meta.get("com.example/trace")),
Some(&json!({"span": 12})),
"the _meta directive was stripped"
);
assert_eq!(
received.get("x-vendor-directive"),
Some(&json!({"anything": [1, 2, 3]})),
"the unmodelled top-level member was stripped"
);
Ok(())
}
#[tokio::test]
async fn a_feature_neither_side_has_never_reaches_the_wire() -> Result<(), TestError> {
let upstream: Shared = Recorder::default().into();
let bridge = Bridge::new(
Agreement::between(
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
),
DegradePolicy::default(),
upstream.clone(),
codec(),
);
let refusal = bridge
.forward(&envelope(
r#"{"jsonrpc":"2.0","id":1,"method":"prompts/list"}"#,
)?)
.await
.map(|_| ());
assert!(
matches!(
refusal,
Err(BridgeError::FeatureUnavailable {
feature: Feature::Prompts
})
),
"expected FeatureUnavailable(Prompts), got {refusal:?}"
);
assert!(
upstream.seen()?.is_empty(),
"the doomed request was sent anyway"
);
Ok(())
}
#[tokio::test]
async fn an_unmodelled_method_is_forwarded_untouched() -> Result<(), TestError> {
let upstream: Shared = Recorder::default().into();
let bridge = Bridge::new(
Agreement::between(
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
),
DegradePolicy::default(),
upstream.clone(),
codec(),
);
bridge
.forward(&envelope(
r#"{"jsonrpc":"2.0","id":2,"method":"experimental/quantum"}"#,
)?)
.await?;
let seen = upstream.seen()?;
let received = seen.first().ok_or("nothing was forwarded")?;
assert_eq!(received.get("method"), Some(&json!("experimental/quantum")));
Ok(())
}
#[tokio::test]
async fn a_notification_is_delivered_without_inventing_a_reply() -> Result<(), TestError> {
let upstream: Shared = Recorder::default().into();
let bridge = Bridge::new(
Agreement::between(
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
),
DegradePolicy::default(),
upstream.clone(),
codec(),
);
let sent = envelope(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#)?;
let forwarded = bridge.forward(&sent).await?;
assert!(
forwarded.reply.is_empty(),
"a notification came back with a reply envelope: {:?}",
forwarded.reply
);
let seen = upstream.seen()?;
assert_eq!(seen.len(), 1, "the notification did not reach the peer");
Ok(())
}
#[tokio::test]
async fn a_request_still_brings_an_answer_back() -> Result<(), TestError> {
let bridge = Bridge::new(
Agreement::between(
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
),
DegradePolicy::default(),
Recorder::default(),
codec(),
);
let sent = envelope(r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#)?;
let forwarded = bridge.forward(&sent).await?;
assert!(
forwarded.reply.get("result").is_some(),
"a request came back with no result: {:?}",
forwarded.reply
);
Ok(())
}
#[tokio::test]
async fn an_unreachable_peer_is_an_error_not_a_silent_drop() -> Result<(), TestError> {
let bridge = Bridge::new(
Agreement::between(
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
),
DegradePolicy::default(),
Absent,
codec(),
);
let outcome = bridge
.forward(&envelope(
r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#,
)?)
.await;
match outcome {
Err(BridgeError::Unreachable { reason }) => {
assert_eq!(reason, "connection refused");
Ok(())
}
other => Err(format!("expected Unreachable, got {other:?}").into()),
}
}
#[tokio::test]
async fn a_peer_refusal_comes_back_as_an_envelope() -> Result<(), TestError> {
struct Refusing;
impl BridgePeer for Refusing {
fn send(
&self,
envelope: Map<String, Value>,
) -> Pin<Box<dyn Future<Output = Result<Map<String, Value>, BridgeError>> + Send + '_>>
{
Box::pin(async move {
let id = envelope.get("id").cloned().unwrap_or(Value::Null);
let mut reply = Map::new();
reply.insert("jsonrpc".into(), json!("2.0"));
reply.insert("id".into(), id);
reply.insert(
"error".into(),
json!({"code": -32601, "message": "no such method"}),
);
Ok(reply)
})
}
fn notify(
&self,
_envelope: Map<String, Value>,
) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async { Ok(()) })
}
fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async { Ok(()) })
}
}
let bridge = Bridge::new(
Agreement::between(
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
),
DegradePolicy::default(),
Refusing,
codec(),
);
let forwarded = bridge
.forward(&envelope(
r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#,
)?)
.await?;
let error = forwarded
.reply
.get("error")
.and_then(Value::as_object)
.ok_or("the refusal was not forwarded")?;
assert_eq!(error.get("code"), Some(&json!(-32601)));
Ok(())
}
#[tokio::test]
async fn a_lossless_forward_reports_nothing() -> Result<(), TestError> {
let bridge = Bridge::new(
Agreement::between(
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
),
DegradePolicy::default(),
Recorder::default(),
codec(),
);
let forwarded = bridge
.forward(&envelope(
r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#,
)?)
.await?;
assert!(
forwarded.report.is_empty(),
"a lossless forward reported {:?}",
forwarded.report.steps()
);
Ok(())
}
#[test]
fn independent_negotiations_stay_independent() -> Result<(), TestError> {
let older = negotiation("2024-11-05", TransportKind::LegacySse)?;
let newer = negotiation("2025-03-26", TransportKind::StreamableHttp)?;
let agreement = Agreement::between(
Leg::new(older.version, resolve(older)),
Leg::new(newer.version, resolve(newer)),
);
assert_eq!(agreement.downstream().version.as_wire(), "2024-11-05");
assert_eq!(agreement.upstream().version.as_wire(), "2025-03-26");
Ok(())
}
#[tokio::test]
async fn an_envelope_survives_the_handler_intact() -> Result<(), TestError> {
let sent = envelope(
r#"{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"x"},"tail":1}"#,
)?;
let bridge = Bridge::new(
Agreement::between(
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
Leg::new(ProtocolVersion::latest_known(), serving(&[Feature::Tools])),
),
DegradePolicy::default(),
Recorder::default(),
codec(),
);
let forwarded = bridge.forward(&sent).await?;
let rebuilt = forwarded.outbound;
assert!(matches!(lift(&rebuilt)?, IrMessage::Request(_)));
assert_eq!(rebuilt.get("tail"), Some(&json!(1)), "a member was lost");
Ok(())
}
use crate::domain::ir::lift_envelope as lift;
fn _report_is_used(report: &TranslationReport) -> bool {
report.is_empty()
}
}