1use crate::cache::Cache;
2use crate::config::{Config, ServerConfig};
3use crate::model::ToolInfo;
4use crate::oauth::{OAuth, TokenStore};
5use anyhow::{anyhow, bail};
6use rmcp::ServiceExt;
7use rmcp::model::CallToolResult;
8use std::collections::BTreeMap;
9use std::sync::Arc;
10use tokio::sync::{Mutex, OnceCell, RwLock};
11
12struct Entry {
13 cfg: ServerConfig,
14 client: RwLock<OnceCell<Arc<rmcp::service::RunningService<rmcp::service::RoleClient, ()>>>>,
16 failed: Mutex<Option<String>>,
17 tools: Mutex<Vec<ToolInfo>>,
18 instructions: Mutex<Option<String>>,
19 oauth: Option<Arc<OAuth>>,
20 token_store: Option<TokenStore>,
21}
22
23pub struct Upstreams {
24 entries: BTreeMap<String, Arc<Entry>>,
25 cache: Mutex<Cache>,
26 config_hash: u64,
27}
28
29const DEFAULT_CONNECT_TIMEOUT_SECS: u64 = 10;
30
31async fn connect(
32 e: &Entry,
33) -> anyhow::Result<rmcp::service::RunningService<rmcp::service::RoleClient, ()>> {
34 let cfg = &e.cfg;
35 let fut = async {
36 if let Some(cmd) = &cfg.command {
37 let mut c = tokio::process::Command::new(cmd);
38 c.args(&cfg.args).envs(&cfg.env);
39 let (transport, stderr) = rmcp::transport::TokioChildProcess::builder(c)
40 .stderr(std::process::Stdio::piped())
41 .spawn()?;
42 if let Some(mut err) = stderr {
43 use tokio::io::AsyncBufReadExt;
44 tokio::spawn(async move {
45 let mut lines = tokio::io::BufReader::new(&mut err).lines();
46 while let Ok(Some(line)) = lines.next_line().await {
47 tracing::debug!(target: "upstream-stderr", "{line}");
48 }
49 });
50 }
51 Ok(().serve(transport).await?)
52 } else if let Some(url) = &cfg.url {
53 let mut config = rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig::with_uri(url.clone());
54 for (k, v) in &cfg.headers {
55 config.custom_headers.insert(
56 http::HeaderName::from_bytes(k.as_bytes())?,
57 http::HeaderValue::from_str(v)?,
58 );
59 }
60 if let Some(oauth) = &e.oauth {
61 let store = e.token_store.as_ref().unwrap();
62 match oauth.access_token(cfg, store).await? {
63 Some(token) => {
64 config.custom_headers.insert(
65 http::header::AUTHORIZATION,
66 http::HeaderValue::from_str(&format!("Bearer {token}"))?,
67 );
68 }
69 None => {
70 let auth_url = oauth.begin_flow(cfg, store).await?;
71 bail!(
72 "server requires OAuth authorization. Open in a browser: {auth_url} — then retry. Headless? Call authorize_server with the final redirect URL as pasted_url."
73 );
74 }
75 }
76 }
77 let transport = rmcp::transport::StreamableHttpClientTransport::from_config(config);
78 Ok(().serve(transport).await?)
79 } else {
80 bail!("server has neither command nor url")
81 }
82 };
83 let timeout =
84 std::time::Duration::from_secs(cfg.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT_SECS));
85 match tokio::time::timeout(timeout, fut).await {
86 Ok(r) => r,
87 Err(_) => Err(anyhow!("connect timed out after {}s", timeout.as_secs())),
88 }
89}
90
91impl Upstreams {
92 pub fn new(cfg: Config, cache: Cache, config_hash: u64) -> Upstreams {
93 let token_lock = Arc::new(Mutex::new(()));
94 let entries = cfg
95 .mcp_servers
96 .iter()
97 .map(|(name, sc)| {
98 let tools = cache.servers.get(name).cloned().unwrap_or_default();
99 let instr = cache.instructions.get(name).cloned();
100 (
101 name.clone(),
102 Arc::new(Entry {
103 cfg: sc.clone(),
104 client: RwLock::new(OnceCell::new()),
105 failed: Mutex::new(None),
106 tools: Mutex::new(tools),
107 instructions: Mutex::new(instr),
108 oauth: sc.oauth.then(OAuth::new),
109 token_store: sc.oauth.then(|| TokenStore::new(name, token_lock.clone())),
110 }),
111 )
112 })
113 .collect();
114 Upstreams {
115 entries,
116 cache: Mutex::new(cache),
117 config_hash,
118 }
119 }
120
121 pub fn server_names(&self) -> Vec<String> {
122 self.entries.keys().cloned().collect()
123 }
124
125 pub fn status(&self, name: &str) -> &'static str {
126 match self.entries.get(name) {
127 None => "unknown",
128 Some(e)
129 if e.client
130 .try_read()
131 .map(|c| c.initialized())
132 .unwrap_or(false) =>
133 {
134 "connected"
135 }
136 Some(e) => match e.failed.try_lock() {
137 Ok(g) if g.is_some() => "unavailable",
138 _ => "cold",
139 },
140 }
141 }
142
143 pub fn instructions(&self, name: &str) -> Option<String> {
144 self.entries
145 .get(name)?
146 .instructions
147 .try_lock()
148 .ok()?
149 .clone()
150 }
151
152 fn entry(&self, name: &str) -> anyhow::Result<&Arc<Entry>> {
153 self.entries.get(name).ok_or_else(|| {
154 anyhow!(
155 "unknown server {name:?}; available: {}",
156 self.server_names().join(", ")
157 )
158 })
159 }
160
161 async fn ensure(
162 &self,
163 name: &str,
164 ) -> anyhow::Result<Arc<rmcp::service::RunningService<rmcp::service::RoleClient, ()>>> {
165 let e = self.entry(name)?;
166 let cell = e.client.read().await;
167 let r = cell
168 .get_or_try_init(|| async {
169 match connect(e).await {
170 Ok(c) => {
171 *e.failed.lock().await = None;
172 Ok(Arc::new(c))
173 }
174 Err(err) => {
175 *e.failed.lock().await = Some(err.to_string());
176 Err(err)
177 }
178 }
179 })
180 .await?
181 .clone();
182 drop(cell);
183 if e.tools.lock().await.is_empty() {
185 self.refresh_inner(name, &r).await.ok();
186 }
187 Ok(r)
188 }
189
190 pub async fn refresh(&self, name: &str) -> anyhow::Result<()> {
191 let client = self.ensure(name).await?;
192 self.refresh_inner(name, &client).await
193 }
194
195 async fn refresh_inner(
196 &self,
197 name: &str,
198 client: &rmcp::service::RunningService<rmcp::service::RoleClient, ()>,
199 ) -> anyhow::Result<()> {
200 let e = self.entry(name)?;
201 let listed = client.list_all_tools().await?;
202 let mut out = Vec::new();
203 for t in listed {
204 if !e.cfg.is_allowed(&t.name) {
205 continue;
206 }
207 out.push(ToolInfo {
208 name: t.name.to_string(),
209 description: t.description.map(|d| d.to_string()),
210 schema: serde_json::to_value(&t.input_schema)?,
211 annotations: t.annotations.map(serde_json::to_value).transpose()?,
212 });
213 }
214 *e.tools.lock().await = out.clone();
215 if let Some(info) = client.peer_info()
216 && let Some(instr) = info.instructions.clone()
217 {
218 *e.instructions.lock().await = Some(instr.clone());
219 self.cache
220 .lock()
221 .await
222 .instructions
223 .insert(name.to_string(), instr);
224 }
225 self.cache
226 .lock()
227 .await
228 .servers
229 .insert(name.to_string(), out);
230 self.save_cache().await;
232 Ok(())
233 }
234
235 pub fn cached_tools(&self, name: &str) -> Vec<ToolInfo> {
237 self.entries
238 .get(name)
239 .and_then(|e| e.tools.try_lock().ok().map(|g| g.clone()))
240 .unwrap_or_default()
241 }
242
243 pub async fn tools(&self, name: &str) -> anyhow::Result<Vec<ToolInfo>> {
244 let e = self.entry(name)?;
245 let cached = e.tools.lock().await.clone();
246 if !cached.is_empty() {
247 return Ok(cached);
248 }
249 self.refresh(name).await?;
250 Ok(e.tools.lock().await.clone())
251 }
252
253 pub async fn call(
254 &self,
255 name: &str,
256 tool: &str,
257 args: Option<serde_json::Map<String, serde_json::Value>>,
258 ) -> anyhow::Result<CallToolResult> {
259 let e = self.entry(name)?;
260 if !e.cfg.is_allowed(tool) {
261 bail!("tool {tool:?} on server {name:?} is blocked by config");
262 }
263 let client = self.ensure(name).await?;
264 let mut params = rmcp::model::CallToolRequestParams::new(tool.to_string());
265 if let Some(a) = args {
266 params = params.with_arguments(a);
267 }
268 match client.call_tool(params.clone()).await {
269 Ok(r) => Ok(r),
270 Err(err) => {
271 tracing::debug!(%err, "call failed, reconnecting and retrying once");
274 e.client.write().await.take();
275 let client = self.ensure(name).await?;
276 self.refresh_inner(name, &client).await.ok();
277 Ok(client.call_tool(params).await?)
278 }
279 }
280 }
281
282 pub async fn oauth_begin(&self, name: &str) -> anyhow::Result<Option<String>> {
285 let e = self.entry(name)?;
286 let (Some(oauth), Some(store)) = (&e.oauth, &e.token_store) else {
287 bail!("server {name:?} does not have \"oauth\": true in the config");
288 };
289 match oauth.access_token(&e.cfg, store).await? {
290 Some(_) => Ok(None),
291 None => Ok(Some(oauth.begin_flow(&e.cfg, store).await?)),
292 }
293 }
294
295 pub async fn oauth_complete(&self, name: &str, pasted: &str) -> anyhow::Result<()> {
297 let e = self.entry(name)?;
298 let Some(oauth) = &e.oauth else {
299 bail!("server {name:?} does not have \"oauth\": true in the config");
300 };
301 oauth.complete_with_url(pasted).await
302 }
303
304 pub async fn save_cache(&self) {
305 if let Err(e) = self.cache.lock().await.save(self.config_hash) {
306 tracing::warn!(%e, "failed to save index cache");
307 }
308 }
309}