Skip to main content

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}