1use std::path::PathBuf;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8use std::time::Duration;
9
10use serde_json::Value;
11use yapi_types::sync::lock;
12
13use super::client::{ClientOptions, McpClient, RequestOptions, ResourceKind, Root, Tool};
14use super::config::{ServerEntry, ServerTransport};
15use super::http::{HttpAuth, HttpOptions, HttpTransport, ProviderToken};
16use super::jsonrpc::{METHOD_NOT_FOUND, McpError};
17use super::sign_in::{CredentialStore, McpAuth, SignInPrompt};
18use super::stdio::{StdioOptions, StdioTransport};
19use super::tools::is_app_resource;
20use super::transport::Transport;
21
22const DEFAULT_TIMEOUT_SECONDS: f64 = 60.0;
23const STDERR_TAIL_CHARS: usize = 2_000;
24const CONNECT_RETRY_DELAYS: [Duration; 2] =
26 [Duration::from_millis(250), Duration::from_millis(1_000)];
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum State {
31 Connecting,
33 Connected,
35 Disconnected,
37 NeedsAuth,
39 Failed,
41 Closed,
43}
44
45impl State {
46 pub fn as_str(self) -> &'static str {
48 match self {
49 State::Connecting => "connecting",
50 State::Connected => "connected",
51 State::Disconnected => "disconnected",
52 State::NeedsAuth => "needs-auth",
53 State::Failed => "failed",
54 State::Closed => "closed",
55 }
56 }
57}
58
59#[derive(Clone, Debug)]
61pub struct Snapshot {
62 pub state: State,
64 pub error: Option<String>,
66 pub tools: Vec<Tool>,
68 pub has_resources: bool,
70 pub resources: Vec<Value>,
72 pub instructions: Option<String>,
74}
75
76pub type ToolsListener = Arc<dyn Fn(&Arc<Connection>) + Send + Sync>;
78
79pub struct Connection {
81 entry: ServerEntry,
82 cwd: PathBuf,
83 agent_dir: PathBuf,
84 snapshot: Mutex<Snapshot>,
85 client: Mutex<Option<McpClient>>,
86 opening: tokio::sync::Mutex<()>,
87 closed: AtomicBool,
88 on_tools: ToolsListener,
89 readable_resources: Arc<dyn Fn(&str) -> bool + Send + Sync>,
90 auth: Option<HttpAuth>,
91 credentials: CredentialStore,
92 tokens_at_sign_in: Mutex<Option<String>>,
95}
96
97pub(super) async fn resolve_value(value: &str, description: &str) -> Result<String, McpError> {
99 if let Some(resolved) = yapi_ai::credentials::resolve(value, None, false).await {
100 return Ok(resolved);
101 }
102 if let Some(command) = value.strip_prefix('!') {
103 return Err(McpError::Other(format!(
104 "Failed to resolve {description} from shell command: {command}"
105 )));
106 }
107 let missing: Vec<String> = yapi_ai::credentials::env_var_names(value)
108 .into_iter()
109 .filter(|name| std::env::var(name).map_or(true, |value| value.is_empty()))
110 .collect();
111 Err(McpError::Other(match missing.as_slice() {
112 [name] => format!("Failed to resolve {description} from environment variable: {name}"),
113 [] => format!("Failed to resolve {description}"),
114 names => format!(
115 "Failed to resolve {description} from environment variables: {}",
116 names.join(", ")
117 ),
118 }))
119}
120
121impl Connection {
122 pub fn new(
128 entry: ServerEntry,
129 cwd: PathBuf,
130 agent_dir: PathBuf,
131 on_tools: ToolsListener,
132 readable_resources: Arc<dyn Fn(&str) -> bool + Send + Sync>,
133 provider_token: Option<ProviderToken>,
134 ) -> Arc<Connection> {
135 let credentials = CredentialStore::new(&agent_dir);
136 let auth = match &entry.config.transport {
137 transport @ ServerTransport::Http {
138 url,
139 auth_provider,
140 oauth,
141 ..
142 } => {
143 if transport.uses_oauth() {
144 Some(HttpAuth::OAuth(Arc::new(McpAuth::new(
145 &entry.name,
146 url,
147 &credentials,
148 oauth.clone().unwrap_or_default(),
149 ))))
150 } else {
151 auth_provider.clone().map(|provider| HttpAuth::Provider {
153 provider,
154 token: provider_token,
155 })
156 }
157 }
158 ServerTransport::Stdio { .. } => None,
159 };
160 Arc::new(Connection {
161 entry,
162 cwd,
163 agent_dir,
164 snapshot: Mutex::new(Snapshot {
165 state: State::Connecting,
166 error: None,
167 tools: Vec::new(),
168 has_resources: false,
169 resources: Vec::new(),
170 instructions: None,
171 }),
172 client: Mutex::new(None),
173 opening: tokio::sync::Mutex::new(()),
174 closed: AtomicBool::new(false),
175 on_tools,
176 readable_resources,
177 auth,
178 credentials,
179 tokens_at_sign_in: Mutex::new(None),
180 })
181 }
182
183 pub fn oauth_url(&self) -> Option<&str> {
185 match &self.entry.config.transport {
186 transport @ ServerTransport::Http { url, .. } if transport.uses_oauth() => Some(url),
187 _ => None,
188 }
189 }
190
191 fn oauth(&self) -> Option<&Arc<McpAuth>> {
192 match &self.auth {
193 Some(HttpAuth::OAuth(auth)) => Some(auth),
194 _ => None,
195 }
196 }
197
198 pub async fn sign_in(&self, prompt: &SignInPrompt) -> Result<(), yapi_ai::auth::AuthError> {
200 match self.oauth() {
201 Some(auth) => auth.sign_in(prompt).await,
202 None => Err(yapi_ai::auth::AuthError::Failed(format!(
203 "MCP server \"{}\" does not use OAuth.",
204 self.entry.name
205 ))),
206 }
207 }
208
209 pub async fn remove_credentials(&self) -> Result<bool, McpError> {
212 match self.oauth_url() {
213 Some(url) => self.credentials.remove(&self.entry.name, url).await,
214 None => Ok(false),
215 }
216 }
217
218 pub async fn sign_out(&self) {
220 let _opening = self.opening.lock().await;
221 let client = lock(&self.client).take();
222 if let Some(client) = client {
223 client.close().await;
224 }
225 if !self.closed.load(Ordering::SeqCst) {
226 self.mark_needs_auth().await;
227 }
228 }
229
230 pub async fn signed_in_elsewhere(&self) -> bool {
233 let Some(before) = lock(&self.tokens_at_sign_in).clone() else {
234 return false;
235 };
236 self.snapshot().state == State::NeedsAuth && self.stored_tokens().await != before
237 }
238
239 async fn stored_tokens(&self) -> String {
240 let tokens = match self.oauth_url() {
241 Some(url) => self
242 .credentials
243 .tokens(&self.entry.name, url)
244 .await
245 .ok()
246 .flatten(),
247 None => None,
248 };
249 yapi_types::json::stringify(&tokens.unwrap_or(Value::Null))
250 }
251
252 fn needs_sign_in(&self, error: &McpError) -> bool {
255 matches!(error, McpError::SignInRequired)
256 || (self.auth.is_some() && matches!(error, McpError::AuthRequired { .. }))
257 }
258
259 fn sign_in_required_message(&self) -> String {
260 let command = match &self.entry.config.transport {
261 ServerTransport::Http {
262 auth_provider: Some(provider),
263 ..
264 } => format!("/login {provider}"),
265 _ => "/mcp".to_owned(),
266 };
267 format!(
268 "MCP server \"{}\" requires sign-in. Run {command} to sign in.",
269 self.entry.name
270 )
271 }
272
273 async fn mark_needs_auth(&self) {
274 self.set_state(State::NeedsAuth, None);
275 if lock(&self.tokens_at_sign_in).is_none() {
276 let tokens = self.stored_tokens().await;
277 lock(&self.tokens_at_sign_in).get_or_insert(tokens);
278 }
279 }
280
281 pub fn name(&self) -> &str {
283 &self.entry.name
284 }
285
286 pub fn entry(&self) -> &ServerEntry {
288 &self.entry
289 }
290
291 pub fn timeout(&self) -> Duration {
293 Duration::try_from_secs_f64(self.entry.config.timeout.unwrap_or(DEFAULT_TIMEOUT_SECONDS))
294 .unwrap_or(Duration::MAX)
295 }
296
297 pub fn snapshot(&self) -> Snapshot {
299 lock(&self.snapshot).clone()
300 }
301
302 pub fn state(&self) -> State {
304 lock(&self.snapshot).state
305 }
306
307 pub fn readable_resources(&self) -> bool {
309 (self.readable_resources)(&self.entry.name)
310 }
311
312 fn connected_client(&self) -> Option<McpClient> {
313 lock(&self.client)
314 .as_ref()
315 .filter(|client| client.is_connected())
316 .cloned()
317 }
318
319 pub async fn client(self: &Arc<Self>) -> Result<McpClient, McpError> {
321 if self.closed.load(Ordering::SeqCst) {
322 return Err(McpError::Other(format!(
323 "MCP server \"{}\" is shut down",
324 self.entry.name
325 )));
326 }
327 if let Some(client) = self.connected_client() {
328 return Ok(client);
329 }
330 let _opening = self.opening.lock().await;
331 if let Some(client) = self.connected_client() {
332 return Ok(client);
333 }
334 self.open().await
335 }
336
337 async fn open(self: &Arc<Self>) -> Result<McpClient, McpError> {
338 self.set_state(State::Connecting, None);
339 let http = matches!(self.entry.config.transport, ServerTransport::Http { .. });
340 let mut attempt = 0;
341 loop {
342 match self.connect_once().await {
343 Ok(client) => return Ok(client),
344 Err(failure) => {
345 let (error, stderr) = *failure;
346 if self.needs_sign_in(&error) && !self.closed.load(Ordering::SeqCst) {
347 self.mark_needs_auth().await;
348 return Err(McpError::Other(self.sign_in_required_message()));
349 }
350 let delay = http.then(|| CONNECT_RETRY_DELAYS.get(attempt)).flatten();
351 attempt += 1;
352 match delay {
353 Some(delay)
354 if error.is_transient() && !self.closed.load(Ordering::SeqCst) =>
355 {
356 tokio::time::sleep(*delay).await;
357 }
358 _ => return Err(self.connect_failed(&error, stderr)),
359 }
360 }
361 }
362 }
363 }
364
365 async fn transport(&self) -> Result<Transport, McpError> {
366 let name = &self.entry.name;
367 match &self.entry.config.transport {
368 ServerTransport::Http { url, headers, .. } => {
369 let mut resolved = Vec::new();
370 for (key, value) in headers {
371 let description = format!("MCP server \"{name}\" header \"{key}\"");
372 resolved.push((key.clone(), resolve_value(value, &description).await?));
373 }
374 Ok(Transport::Http(HttpTransport::new(HttpOptions {
375 url: url.clone(),
376 headers: resolved,
377 auth: self.auth.clone(),
378 })))
379 }
380 ServerTransport::Stdio {
381 command,
382 args,
383 env,
384 cwd,
385 } => {
386 let mut resolved = Vec::new();
387 for (key, value) in env {
388 let description = format!("MCP server \"{name}\" env \"{key}\"");
389 resolved.push((key.clone(), resolve_value(value, &description).await?));
390 }
391 Ok(Transport::Stdio(Box::new(StdioTransport::new(
392 StdioOptions {
393 command: crate::tools::path::expand_home(command),
394 args: args
395 .iter()
396 .map(|arg| crate::tools::path::expand_home(arg))
397 .collect(),
398 cwd: self.cwd.join(crate::tools::path::expand_home(
399 cwd.as_deref().unwrap_or("."),
400 )),
401 env: resolved,
402 },
403 ))))
404 }
405 }
406 }
407
408 async fn connect_once(self: &Arc<Self>) -> Result<McpClient, Box<(McpError, Option<String>)>> {
410 let transport = self
411 .transport()
412 .await
413 .map_err(|error| Box::new((error, None)))?;
414 let options = ClientOptions {
415 name: "yapi".into(),
416 version: env!("CARGO_PKG_VERSION").into(),
417 request_timeout: self.timeout(),
418 roots: vec![Root {
419 uri: crate::tools::path::file_url(&self.cwd),
420 name: self
421 .cwd
422 .file_name()
423 .map(|name| name.to_string_lossy().into_owned()),
424 }],
425 };
426 let client = McpClient::connect_with_stderr(options, transport).await?;
427 let log = super::log::Log::new(self.agent_dir.join("mcp.log"));
428 let server = self.entry.name.clone();
429 client.on_notification("notifications/message", move |params| {
430 log.write(&server, params)
431 });
432 let weak = Arc::downgrade(self);
433 let refreshing = client.clone();
434 client.on_notification("notifications/tools/list_changed", move |_| {
435 if let Some(connection) = weak.upgrade() {
436 let client = refreshing.clone();
437 tokio::spawn(async move { connection.refresh_tools(client).await });
438 }
439 });
440 let weak = Arc::downgrade(self);
441 let refreshing = client.clone();
442 client.on_notification("notifications/resources/list_changed", move |_| {
443 if let Some(connection) = weak.upgrade() {
444 let client = refreshing.clone();
445 tokio::spawn(async move { connection.refresh_resources(client).await });
446 }
447 });
448 let weak = Arc::downgrade(self);
449 let watched = client.clone();
450 client.on_close(move || {
451 if let Some(connection) = weak.upgrade() {
452 connection.client_closed(&watched);
453 }
454 });
455 let capabilities = client
456 .server()
457 .map(|server| server.capabilities)
458 .unwrap_or_default();
459 let has_resources = capabilities.contains_key("resources");
460 let setup = async {
461 let tools = if capabilities.contains_key("tools") {
462 client.list_tools(RequestOptions::default()).await?
463 } else {
464 Vec::new()
465 };
466 let resources = if has_resources {
467 fetch_resources(&client).await
468 } else {
469 Vec::new()
470 };
471 Ok::<_, McpError>((tools, resources))
472 };
473 let (tools, resources) = match setup.await {
474 Ok(found) => found,
475 Err(error) => {
476 let stderr = client.stderr();
477 client.close().await;
478 return Err(Box::new((error, stderr)));
479 }
480 };
481 if self.closed.load(Ordering::SeqCst) || !client.is_connected() {
482 let stderr = client.stderr();
483 client.close().await;
484 let reason = if self.closed.load(Ordering::SeqCst) {
485 "shut down while connecting"
486 } else {
487 "connection closed during setup"
488 };
489 return Err(Box::new((McpError::Other(reason.into()), stderr)));
490 }
491 let instructions = client
492 .server()
493 .and_then(|server| server.instructions)
494 .map(|text| text.trim().to_owned())
495 .filter(|text| !text.is_empty());
496 *lock(&self.client) = Some(client.clone());
497 {
498 let mut snapshot = lock(&self.snapshot);
499 snapshot.tools = tools;
500 snapshot.has_resources = has_resources;
501 snapshot.resources = resources;
502 snapshot.instructions = instructions;
503 snapshot.state = State::Connected;
504 snapshot.error = None;
505 }
506 *lock(&self.tokens_at_sign_in) = None;
507 (self.on_tools)(self);
508 Ok(client)
509 }
510
511 fn connect_failed(&self, error: &McpError, stderr: Option<String>) -> McpError {
512 let closed = self.closed.load(Ordering::SeqCst);
513 let tail = stderr
514 .map(|text| {
515 let text = text.trim();
516 let start = text
517 .char_indices()
518 .rev()
519 .nth(STDERR_TAIL_CHARS - 1)
520 .map_or(0, |(index, _)| index);
521 text[start..].to_owned()
522 })
523 .filter(|text| !text.is_empty());
524 let message = match tail {
525 Some(tail) => format!("{error}\n{tail}"),
526 None => error.to_string(),
527 };
528 self.set_state(
529 if closed { State::Closed } else { State::Failed },
530 Some(message.clone()),
531 );
532 McpError::Other(format!(
533 "MCP server \"{}\" failed to connect: {message}",
534 self.entry.name
535 ))
536 }
537
538 fn set_state(&self, state: State, error: Option<String>) {
539 let mut snapshot = lock(&self.snapshot);
540 snapshot.state = state;
541 snapshot.error = error;
542 if state != State::NeedsAuth {
543 *lock(&self.tokens_at_sign_in) = None;
544 }
545 }
546
547 fn client_closed(&self, client: &McpClient) {
549 let mut current = lock(&self.client);
550 if !current.as_ref().is_some_and(|current| current.same(client))
551 || self.closed.load(Ordering::SeqCst)
552 {
553 return;
554 }
555 *current = None;
556 drop(current);
557 let stderr = client
558 .stderr()
559 .map(|text| text.trim().to_owned())
560 .filter(|text| !text.is_empty());
561 let error = match stderr {
562 Some(stderr) => format!("Connection closed\n{stderr}"),
563 None => "Connection closed".to_owned(),
564 };
565 self.set_state(State::Disconnected, Some(error));
566 }
567
568 async fn refresh_tools(self: Arc<Self>, client: McpClient) {
569 match client.list_tools(RequestOptions::default()).await {
570 Ok(tools) => {
571 if self.closed.load(Ordering::SeqCst) || !client.is_connected() {
572 return;
573 }
574 lock(&self.snapshot).tools = tools;
575 (self.on_tools)(&self);
576 }
577 Err(error) => {
578 lock(&self.snapshot).error = Some(format!("Failed to refresh tools: {error}"));
579 }
580 }
581 }
582
583 async fn refresh_resources(self: Arc<Self>, client: McpClient) {
584 let resources = fetch_resources(&client).await;
585 if self.closed.load(Ordering::SeqCst) || !client.is_connected() {
586 return;
587 }
588 lock(&self.snapshot).resources = resources;
589 (self.on_tools)(&self);
590 }
591
592 async fn with_client<T, F>(
596 self: &Arc<Self>,
597 read_only: bool,
598 run: impl Fn(McpClient) -> F,
599 ) -> Result<T, McpError>
600 where
601 F: std::future::Future<Output = Result<T, McpError>>,
602 {
603 let mut attempt = 1;
604 loop {
605 let client = self.client().await?;
606 match run(client.clone()).await {
607 Ok(value) => return Ok(value),
608 Err(error)
609 if read_only
610 && attempt == 1
611 && error.status().is_some()
612 && error.is_transient() =>
613 {
614 tokio::time::sleep(CONNECT_RETRY_DELAYS[0]).await;
615 }
616 Err(McpError::SessionExpired) if attempt == 1 => {
620 let mut current = lock(&self.client);
621 if current
622 .as_ref()
623 .is_some_and(|current| current.same(&client))
624 {
625 *current = None;
626 }
627 }
628 Err(error) if self.needs_sign_in(&error) => {
629 {
630 let mut current = lock(&self.client);
631 if current
632 .as_ref()
633 .is_some_and(|current| current.same(&client))
634 {
635 *current = None;
636 }
637 }
638 client.close().await;
639 self.mark_needs_auth().await;
640 return Err(McpError::Other(self.sign_in_required_message()));
641 }
642 Err(error) => return Err(error),
643 }
644 attempt += 1;
645 }
646 }
647
648 pub async fn call_tool(
650 self: &Arc<Self>,
651 name: &str,
652 args: Value,
653 options: RequestOptions,
654 ) -> Result<Value, McpError> {
655 self.with_client(false, |client| {
656 let (args, options) = (args.clone(), options.clone());
657 async move { client.call_tool(name, args, options).await }
658 })
659 .await
660 }
661
662 pub async fn read_resource(
664 self: &Arc<Self>,
665 uri: &str,
666 options: RequestOptions,
667 ) -> Result<Value, McpError> {
668 self.with_client(true, |client| {
669 let options = options.clone();
670 async move { client.read_resource(uri, options).await }
671 })
672 .await
673 }
674
675 pub async fn resources_page(
678 self: &Arc<Self>,
679 kind: ResourceKind,
680 cursor: Option<String>,
681 options: RequestOptions,
682 ) -> Result<(Vec<Value>, Option<String>), McpError> {
683 self.with_client(true, |client| {
684 let (cursor, options) = (cursor.clone(), options.clone());
685 async move {
686 let page = client.list_resources_page(kind, cursor, options).await;
687 without_templates(kind, page, (Vec::new(), None))
688 }
689 })
690 .await
691 }
692
693 pub async fn all_resources(
696 self: &Arc<Self>,
697 kind: ResourceKind,
698 options: RequestOptions,
699 ) -> Result<Vec<Value>, McpError> {
700 self.with_client(true, |client| {
701 let options = options.clone();
702 async move {
703 without_templates(kind, client.list_resources(kind, options).await, Vec::new())
704 }
705 })
706 .await
707 }
708
709 pub async fn reconnect(self: &Arc<Self>) -> Result<(), McpError> {
711 let _opening = self.opening.lock().await;
712 let client = lock(&self.client).take();
713 if let Some(client) = client {
714 client.close().await;
715 }
716 self.open().await.map(|_| ())
717 }
718
719 pub async fn close(&self) {
722 self.closed.store(true, Ordering::SeqCst);
723 self.set_state(State::Closed, None);
724 let client = lock(&self.client).take();
725 if let Some(client) = client {
726 client.close().await;
727 }
728 if let Some(auth) = self.oauth() {
729 auth.settled().await;
730 }
731 }
732}
733
734fn without_templates<T>(
736 kind: ResourceKind,
737 result: Result<T, McpError>,
738 empty: T,
739) -> Result<T, McpError> {
740 match result {
741 Err(McpError::Rpc { code, .. })
742 if kind == ResourceKind::Templates && code == METHOD_NOT_FOUND =>
743 {
744 Ok(empty)
745 }
746 other => other,
747 }
748}
749
750async fn fetch_resources(client: &McpClient) -> Vec<Value> {
753 client
754 .list_resources(ResourceKind::Resources, RequestOptions::default())
755 .await
756 .unwrap_or_default()
757 .into_iter()
758 .filter(|resource| !is_app_resource(resource))
759 .collect()
760}