protocol_droid/adapters/secondary/transport/http/bridge_peer.rs
1//! `HttpClient` as one side of a bridge.
2//!
3//! ## Why this is an adapter and not a second client
4//!
5//! A bridge holds a live upstream session, and the thing that owns that
6//! session — the `Mcp-Session-Id` header, the cookie jar, the connection
7//! pool — is `HttpClient`. A second client would mean a second session
8//! lifecycle, and the two would drift the first time one of them learned a
9//! detail the other had not. So this is an `impl BridgePeer for HttpClient`,
10//! not a new type wrapping one.
11//!
12//! ## The two things that must not be simplified away
13//!
14//! **The id is the caller's, not ours.** `HttpClient::next_request_id` mints an
15//! id from this client's counter, which is right for a client making calls and
16//! wrong here: a proxy that renumbers the id leaves the downstream client
17//! waiting forever for a reply carrying an id it never sent. The envelope
18//! arrives with its id and leaves with it.
19//!
20//! **A refusal is not a failure.** A peer that declines a request answers with
21//! a well-formed envelope carrying an `error` member. That comes back through
22//! `Ok`, because it is a successful exchange. Only a transport fault is an
23//! `Err`. Conflating them would tell the downstream client "network error"
24//! about a request the server processed and deliberately declined.
25//!
26//! ## Notifications get their own method
27//!
28//! `BridgePeer` separates `notify` from `send` for the same reason
29//! `ClientTransport` does: a notification has no `id`, and a conforming peer
30//! correctly answers one with `202` and no body. A `send` that waited for a
31//! reply would wait forever. `post_envelope` returns `Ok(None)` for exactly
32//! that case rather than inventing an empty reply to hand downstream.
33
34use serde_json::{Map, Value};
35
36use crate::adapters::secondary::transport::http::{HttpClient, HttpClientError};
37use crate::application::bridge::{BridgeError, BridgePeer, SendFuture};
38
39/// Carries envelopes over one HTTP client's session.
40impl BridgePeer for HttpClient {
41 /// Delivers one envelope and yields the peer's answer.
42 ///
43 /// # Errors
44 ///
45 /// [`BridgeError::Unreachable`] when the request could not be completed at
46 /// all. A peer that answered with an `error` member is *not* an error here:
47 /// see the module docs.
48 fn send(
49 &self,
50 envelope: Map<String, Value>,
51 ) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>> {
52 Box::pin(async move {
53 // `None` here would mean the peer sent no body, which is only
54 // correct for a notification — and a notification has no reply to
55 // hand back. Reaching `None` on the `send` path means the caller
56 // used the wrong method, so it is reported rather than invented.
57 self.post_envelope(&envelope)
58 .await
59 .map_err(|error| unreachable(&error))?
60 .ok_or_else(|| BridgeError::Unreachable {
61 reason: "the peer accepted the request but sent no answer".to_owned(),
62 })
63 })
64 }
65
66 /// Delivers one envelope that expects no answer.
67 ///
68 /// # Errors
69 ///
70 /// [`BridgeError::Unreachable`] when the request could not be completed.
71 /// The inherent `take_pending` and not this one, which is what an
72 /// unqualified `Self::` resolves to. Naming it here because a reader
73 /// seeing the same method name twice has no other way to tell the two
74 /// apart.
75 fn take_pending(&self) -> Vec<Map<String, Value>> {
76 Self::take_pending(self)
77 }
78
79 fn wait_pending(&self) -> SendFuture<'static, ()> {
80 // The queue is where the frames are, so waiting on the queue is
81 // waiting on the peer. Nothing else to poll.
82 Box::pin(self.unsolicited.wait())
83 }
84
85 /// Opens the upstream's standing stream.
86 ///
87 /// This is the half that makes a server-initiated message reach the client
88 /// at all: without it the client POSTs, the server answers, and anything
89 /// the server volunteers in between goes to nobody.
90 fn listen(&self) -> SendFuture<'_, Result<(), BridgeError>> {
91 Box::pin(async move {
92 Self::listen(self)
93 .await
94 .map_err(|error| unreachable(&error))
95 })
96 }
97
98 fn notify(&self, envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>> {
99 Box::pin(async move {
100 self.notify_envelope(&envelope)
101 .await
102 .map_err(|error| unreachable(&error))?;
103 Ok(())
104 })
105 }
106
107 /// Ends the upstream session.
108 ///
109 /// # Errors
110 ///
111 /// [`BridgeError::Unreachable`] when the peer did not accept the teardown.
112 /// A peer that has already gone is reported as unreachable rather than
113 /// swallowed: pretending a shutdown succeeded would hide a leaked session.
114 fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
115 Box::pin(async move {
116 self.delete().await.map_err(|error| unreachable(&error))?;
117 Ok(())
118 })
119 }
120}
121
122/// Maps a transport failure onto the bridge's vocabulary.
123///
124/// One place, so the wording of "the peer could not be reached" is decided
125/// once. [`HttpClientError::NotAMessage`] and [`HttpClientError::Body`] land
126/// here too, and deliberately read as unreachable: from the downstream
127/// client's side, a peer that answered with something that is not a
128/// JSON-RPC message *is* unusable, and there is no more useful thing to tell
129/// it.
130fn unreachable(error: &HttpClientError) -> BridgeError {
131 BridgeError::Unreachable {
132 reason: error.to_string(),
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use crate::application::bridge::SendFuture;
139 use crate::application::bridge::{BridgeError, BridgePeer};
140 use serde_json::{Map, Value, json};
141
142 type TestError = Box<dyn std::error::Error>;
143
144 /// A client pointed at a port nothing is listening on. Constructed, never
145 /// dialled: these tests pin the port's shape, not the wire.
146 fn client() -> Result<crate::adapters::secondary::transport::http::HttpClient, TestError> {
147 let config = crate::adapters::secondary::transport::http::HttpClientConfig::new(
148 "http://127.0.0.1:1/mcp",
149 )?;
150 Ok(crate::adapters::secondary::transport::http::HttpClient::new(config)?)
151 }
152
153 /// The three `BridgePeer` methods are async, so the example below is a
154 /// compile-time check rather than a live call: it pins the signatures an
155 /// adapter has to satisfy, which is exactly where a drift would appear.
156 fn assert_port<T: BridgePeer>(peer: &T) {
157 let _ = |envelope: Map<String, Value>| {
158 let _: SendFuture<'_, Result<Map<String, Value>, BridgeError>> =
159 peer.send(envelope.clone());
160 let _: SendFuture<'_, Result<(), BridgeError>> = peer.notify(envelope);
161 let _: SendFuture<'_, Result<(), BridgeError>> = peer.close();
162 };
163 }
164
165 /// `HttpClient` satisfies the bridge port.
166 #[test]
167 fn the_http_client_is_a_bridge_peer() -> Result<(), TestError> {
168 let client = client()?;
169 assert_port(&client);
170 Ok(())
171 }
172
173 /// The port is object-safe, which is why `SendFuture` is boxed rather than
174 /// a native `async fn`. Without this the bridge could not hold a peer it
175 /// did not construct.
176 #[test]
177 fn the_port_is_object_safe() -> Result<(), TestError> {
178 fn use_dyn(peer: &dyn BridgePeer) -> SendFuture<'_, Result<(), BridgeError>> {
179 peer.close()
180 }
181
182 let client = client()?;
183 let erased: &dyn BridgePeer = &client;
184 // Bound, never polled: this asserts the *type* resolves through a
185 // trait object. Running it would mean dialling a dead port.
186 let _closer: SendFuture<'_, Result<(), BridgeError>> = use_dyn(erased);
187 Ok(())
188 }
189
190 /// A transport fault is unreachable; a refusal is not an error at all.
191 /// Both are checked through the mapping the adapter uses.
192 #[test]
193 fn transport_failures_map_to_unreachable() {
194 let error = crate::adapters::secondary::transport::http::HttpClientError::MissingMediaType;
195 let mapped = super::unreachable(&error);
196 assert!(
197 matches!(mapped, BridgeError::Unreachable { .. }),
198 "got {mapped:?}"
199 );
200 }
201
202 fn _json_is_used(_: &Value) -> bool {
203 let _ = json!({});
204 true
205 }
206}