Skip to main content

purview_gpui/
install.rs

1//! Bridge setup: [`Bridge`] wires the `Send` channel, poster, build, background
2//! server, main-thread drain loop, and keep-alive, returning a ready
3//! [`AppHandle`].
4
5use std::{
6    io,
7    net::{SocketAddr, ToSocketAddrs},
8};
9
10use gpui::{App, Global};
11use purview::{AppHandle, BoundHttp, GuiBridge, RunningBridge};
12use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
13
14/// A closure posted from the MCP thread, to run on gpui's main thread.
15type Posted = Box<dyn FnOnce(&mut App) + Send>;
16
17/// Builder that installs a purview bridge onto a live gpui [`App`].
18///
19/// Call it inside `Application::run`'s `|cx: &mut App|` closure; installing
20/// returns a [`purview::AppHandle`] on which you declare windows and actions
21/// exactly as with the core crate. The channel, drain loop, and server-handle
22/// lifetime are all handled internally (the server stops when the `App` tears
23/// down).
24///
25/// Pick a transport with [`http`](Self::http) or
26/// [`install_stdio`](Self::install_stdio). The two shapes differ because HTTP
27/// has an address to report and a bind that can fail, and stdio has neither —
28/// the same split the core crate makes.
29#[derive(Default)]
30pub struct Bridge {
31    instructions: Option<String>,
32}
33
34impl Bridge {
35    /// Start a new bridge builder.
36    pub fn new() -> Self {
37        Bridge::default()
38    }
39
40    /// Append to the MCP `instructions` (mirrors
41    /// [`GuiBridgeBuilder::instructions_append`](purview::GuiBridgeBuilder::instructions_append)).
42    pub fn instructions_append(mut self, extra: impl Into<String>) -> Self {
43        let extra = extra.into();
44        self.instructions = Some(match self.instructions.take() {
45            Some(s) => format!("{s}\n\n{extra}"),
46            None => extra,
47        });
48        self
49    }
50
51    /// Switch to the Streamable HTTP builder — the door to a token and to the
52    /// served address.
53    ///
54    /// ```ignore
55    /// let papp = Bridge::new()
56    ///     .instructions_append("A single-window counter.")
57    ///     .http()
58    ///     .bind("127.0.0.1:8931")
59    ///     .expect("bind the MCP port")
60    ///     .install(cx);
61    /// ```
62    pub fn http(self) -> HttpBridge {
63        HttpBridge {
64            instructions: self.instructions,
65            token: None,
66        }
67    }
68
69    /// Serve MCP over stdio on a background thread and return the
70    /// [`AppHandle`].
71    ///
72    /// One call, unlike the HTTP path: there is no address to hand back, and
73    /// nothing here that can fail.
74    pub fn install_stdio(self, cx: &mut App) -> AppHandle<App> {
75        let (bridge, app, rx) = build(self.instructions);
76        install(cx, app, bridge.spawn_stdio(), None, rx)
77    }
78}
79
80/// The Streamable HTTP half of [`Bridge`], from [`Bridge::http`].
81///
82/// It exists so that HTTP-only settings do not sit on a builder a stdio app
83/// also uses: a token is meaningless without a header to carry it.
84pub struct HttpBridge {
85    instructions: Option<String>,
86    token: Option<String>,
87}
88
89impl HttpBridge {
90    /// Require every request to carry `Authorization: Bearer <token>` (mirrors
91    /// [`HttpBuilder::token`](purview::HttpBuilder::token)).
92    ///
93    /// Every process on the machine can reach a loopback port, browser pages
94    /// included; a token is what makes "on this machine" mean "authorized".
95    pub fn token(mut self, token: impl Into<String>) -> Self {
96        self.token = Some(token.into());
97        self
98    }
99
100    /// Build the bridge and bind the listener, without installing anything on
101    /// the `App` yet.
102    ///
103    /// Binding is its own step so that [`BoundBridge::local_addr`] is readable
104    /// before the server runs — pass port `0`, let the OS choose, and you still
105    /// know what to tell a client. It also puts the one fallible part of setup
106    /// here, which leaves [`BoundBridge::install`] unable to fail.
107    ///
108    /// # Errors
109    ///
110    /// [`io::Error`](std::io::Error): binding a socket is the only thing here
111    /// that touches the outside world. Usually the port is already in use.
112    pub fn bind(self, addr: impl ToSocketAddrs) -> io::Result<BoundBridge> {
113        let (bridge, app, rx) = build(self.instructions);
114        let mut http = bridge.http();
115        if let Some(token) = self.token {
116            http = http.token(token);
117        }
118        let bound = http.bind(addr)?;
119        Ok(BoundBridge {
120            local_addr: bound.local_addr(),
121            app,
122            bound,
123            rx,
124        })
125    }
126}
127
128/// A bridge that is built and bound but not yet installed on an `App`, from
129/// [`HttpBridge::bind`].
130///
131/// Holding this is what lets you learn the address, and declare your windows,
132/// before a single client can read anything.
133pub struct BoundBridge {
134    app: AppHandle<App>,
135    bound: BoundHttp<App>,
136    rx: UnboundedReceiver<Posted>,
137    local_addr: SocketAddr,
138}
139
140impl BoundBridge {
141    /// The address the server will listen on — the port the OS chose, when `0`
142    /// was asked for.
143    ///
144    /// This is what to render in the UI, put behind a "copy the connect
145    /// command" button, or write wherever a client will look for it. Unlike
146    /// [`PurviewAppExt::purview_addr`](crate::PurviewAppExt::purview_addr) it
147    /// is no `Option`: here the transport is known.
148    pub fn local_addr(&self) -> SocketAddr {
149        self.local_addr
150    }
151
152    /// The [`AppHandle`], before the server starts.
153    ///
154    /// Declaring windows through this and *then* calling [`install`] means the
155    /// projection is complete before the first client can read it — the same
156    /// order the core crate's `build()` / `serve()` pair gives you. `install`
157    /// returns the same handle, so ignore this unless you want that ordering.
158    ///
159    /// [`install`]: Self::install
160    pub fn app(&self) -> AppHandle<App> {
161        self.app.clone()
162    }
163
164    /// Serve on a background thread, tie its lifetime to the `App`, and return
165    /// the [`AppHandle`].
166    ///
167    /// Infallible: the bind already happened.
168    pub fn install(self, cx: &mut App) -> AppHandle<App> {
169        let BoundBridge {
170            app,
171            bound,
172            rx,
173            local_addr,
174        } = self;
175        install(cx, app, bound.spawn(), Some(local_addr), rx)
176    }
177}
178
179/// Build the bridge, its [`AppHandle`], and the channel the poster writes to.
180///
181/// The poster (running on the background MCP thread) sends each
182/// `FnOnce(&mut App)` over that `Send` channel; [`install`] drains it on the
183/// main thread. `AsyncApp` is `!Send`, so a `Send` channel is the necessary
184/// bridge.
185fn build(
186    instructions: Option<String>,
187) -> (GuiBridge<App>, AppHandle<App>, UnboundedReceiver<Posted>) {
188    let (tx, rx) = unbounded_channel::<Posted>();
189    let builder = GuiBridge::builder(move |task: Posted| {
190        let _ = tx.send(task);
191    });
192    let builder = match instructions {
193        Some(s) => builder.instructions_append(s),
194        None => builder,
195    };
196    let (bridge, app) = builder.build();
197    (bridge, app, rx)
198}
199
200/// Stash the running server on the `App` and start draining posted closures.
201fn install(
202    cx: &mut App,
203    app: AppHandle<App>,
204    running: RunningBridge,
205    addr: Option<SocketAddr>,
206    mut rx: UnboundedReceiver<Posted>,
207) -> AppHandle<App> {
208    debug_assert!(
209        cx.try_global::<Installed>().is_none(),
210        "purview-gpui: install a bridge only once per App"
211    );
212
213    // The handle lives in a gpui Global, so its lifetime is exactly the `App`'s
214    // (drop = graceful stop).
215    cx.set_global(Installed {
216        app: app.clone(),
217        addr,
218        _running: running,
219    });
220
221    // Drain posted closures on the main thread for the App's lifetime.
222    cx.spawn(async move |cx| {
223        while let Some(task) = rx.recv().await {
224            let _ = cx.update(|app| task(app));
225        }
226    })
227    .detach();
228
229    app
230}
231
232/// gpui global holding the running server (keep-alive tied to the `App`) and
233/// the [`AppHandle`] read back by [`PurviewAppExt`](crate::PurviewAppExt).
234pub(crate) struct Installed {
235    pub(crate) app: AppHandle<App>,
236    /// The served address, for an HTTP bridge; `None` for stdio.
237    pub(crate) addr: Option<SocketAddr>,
238    _running: RunningBridge,
239}
240
241impl Global for Installed {}