1#![allow(deprecated)]
2
3use std::{
4 collections::{BTreeMap, HashMap, HashSet},
5 path::{Path, PathBuf},
6 sync::{
7 Arc, Mutex, Weak,
8 atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
9 },
10 time::Duration,
11};
12
13#[cfg(unix)]
14use std::{
15 fs::{self, OpenOptions},
16 io::{Read as _, Write as _},
17};
18
19use async_trait::async_trait;
20use base64::Engine as _;
21use cookie_agent_config::{LoadedMcpServer, McpOAuthSettings, McpServerConfig};
22#[cfg(windows)]
23use cookie_agent_models::secure_store::{DEFAULT_LOCK_BUDGET, SecureDirectory};
24#[cfg(unix)]
25use cookie_agent_models::secure_store::{DEFAULT_LOCK_BUDGET, lock_within_file, unlock};
26use cookie_agent_protocol::{
27 ApprovalBoundary, ApprovalCapability, ApprovalResourceSource, PermissionAction,
28 PersistedToolResult as ToolResult, PreparedApprovalResource, PreparedBindingLifetime,
29 PreparedCapabilityOperation, PreparedOperationIdentity, PreparedResourceDigest,
30 PreparedResourceIdentity, SafeDisplayText, Sha256Digest, ToolAttachment, ToolCallId,
31 ToolEmittedContent, ToolEmittedMessage, ToolEmittedMessageRole,
32};
33use futures_util::StreamExt as _;
34use process_wrap::tokio::CommandWrap;
35#[cfg(windows)]
36use process_wrap::tokio::JobObject;
37#[cfg(unix)]
38use process_wrap::tokio::ProcessGroup;
39use rmcp::transport::auth::{AuthorizationCallback, OAuthClientConfig, OAuthTokenResponse};
40use rmcp::{
41 ClientHandler, ClientLifecycleMode, ClientServiceExt, Peer,
42 model::{
43 CallToolRequestParams, ContentBlock, CreateMessageRequestParams, CreateMessageResult,
44 ElicitRequestParams, ElicitResult, ErrorCode, ErrorData, ListRootsResult,
45 ProgressNotificationParam, ProtocolVersion, ResourceContents, Tool,
46 },
47 service::{
48 NotificationContext, RequestContext, RoleClient, RunningService, RxJsonRpcMessage,
49 TxJsonRpcMessage,
50 },
51 transport::{
52 AuthClient, AuthorizationManager, AuthorizationRequest, AuthorizationSession,
53 CredentialStore, OAuthHttpClient, OAuthHttpClientFuture, OAuthHttpRedirectPolicy,
54 StoredCredentials, StreamableHttpClientTransport, TokioChildProcess, Transport,
55 },
56};
57use serde::{Deserialize, Serialize};
58use serde_json::{Map, Value};
59use sha2::{Digest as _, Sha256};
60use tokio::sync::Notify;
61use tokio::{
62 io::{AsyncReadExt as _, AsyncWriteExt as _},
63 net::{TcpListener, TcpStream},
64 sync::Mutex as AsyncMutex,
65};
66use tokio_util::sync::CancellationToken;
67
68use crate::{
69 PreparedExecutor, PreparedTool, SessionToolContext, ToolCall, ToolConcurrency, ToolError,
70 ToolExecutionContext, ToolPreparationContext, ToolProgress, ToolProvider, ToolSpec,
71};
72
73const DEFAULT_TIMEOUT_MS: u64 = 30_000;
74const TOOL_LIST_DEBOUNCE: Duration = Duration::from_millis(750);
75#[cfg(not(test))]
76const OAUTH_CALLBACK_TIMEOUT: Duration = Duration::from_secs(5 * 60);
77#[cfg(test)]
78const OAUTH_CALLBACK_TIMEOUT: Duration = Duration::from_millis(200);
79#[cfg(any(unix, test))]
80const OAUTH_STORE_FILE: &str = "mcp-oauth.json";
81const OAUTH_STORE_LOCK_FILE: &str = "mcp-oauth.lock";
82const OAUTH_CALLBACK_MAX_BYTES: usize = 16 * 1024;
83const OAUTH_STORE_MAX_BYTES: u64 = 1024 * 1024;
84const REDACTED_AUTHORIZATION_CODE: &str = "cookie-agent-redacted-authorization-code";
85
86#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
87#[serde(rename_all = "snake_case")]
88pub enum McpServerState {
89 Disabled,
90 Disconnected,
91 Connecting,
92 Connected,
93 NeedsAuth,
94 Failed,
95}
96
97#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
98pub struct McpServerStatus {
99 pub server: String,
100 pub state: McpServerState,
101 pub message: Option<String>,
102 pub tools: Vec<String>,
103 pub auth_in_progress: bool,
104}
105
106#[derive(Clone)]
107struct CachedTool {
108 raw_name: String,
109 spec: ToolSpec,
110}
111
112type ClientService = RunningService<RoleClient, McpClientHandler>;
113
114struct ServerRuntime {
115 name: String,
116 sanitized_name: String,
117 loaded: LoadedMcpServer,
118 status: Mutex<McpServerStatus>,
119 tools: Mutex<Vec<CachedTool>>,
120 list_generation: AtomicU64,
121 publication: Mutex<()>,
122 tool_refresh: Mutex<ToolRefreshDebounce>,
123 connect_lock: AsyncMutex<()>,
124 service: AsyncMutex<Option<ClientService>>,
125 superseded: CancellationToken,
126 registry: Weak<RegistryInner>,
127 progress: AsyncMutex<HashMap<ToolCallId, crate::ProgressSink>>,
128 auth_challenge: Mutex<Option<String>>,
129 oauth_response_error: Arc<Mutex<Option<String>>>,
130 auth_flow: Mutex<Option<OAuthFlowState>>,
131 auth_generation: AtomicU64,
132 auth_lock: AsyncMutex<()>,
133}
134
135struct ToolRefreshDebounce {
136 peer: Option<Peer<RoleClient>>,
137 cancellation: CancellationToken,
138 notify: Arc<Notify>,
139 worker: Option<tokio::task::JoinHandle<()>>,
140 #[cfg(test)]
141 armed: Arc<AtomicU64>,
142}
143
144impl Default for ToolRefreshDebounce {
145 fn default() -> Self {
146 Self {
147 peer: None,
148 cancellation: CancellationToken::new(),
149 notify: Arc::new(Notify::new()),
150 worker: None,
151 #[cfg(test)]
152 armed: Arc::new(AtomicU64::new(0)),
153 }
154 }
155}
156
157impl ToolRefreshDebounce {
158 fn cancel(&mut self) {
159 self.peer = None;
160 self.cancellation.cancel();
161 if let Some(worker) = self.worker.take() {
162 worker.abort();
163 }
164 }
165}
166
167impl Drop for ServerRuntime {
168 fn drop(&mut self) {
169 self.tool_refresh
170 .get_mut()
171 .unwrap_or_else(|poisoned| poisoned.into_inner())
172 .cancel();
173 }
174}
175
176struct RegistryInner {
177 servers: Mutex<BTreeMap<String, Arc<ServerRuntime>>>,
178 reserved_names: Mutex<HashSet<String>>,
179 claimed_names: Mutex<HashMap<String, String>>,
180 plugin_collision_handler: Mutex<Option<PluginCollisionHandler>>,
181 oauth_credentials: OAuthCredentialFile,
182 shutdown: CancellationToken,
183 connection_tasks: Mutex<Vec<tokio::task::JoinHandle<()>>>,
184 active_connects: AtomicUsize,
185 connects_idle: Notify,
186 eager_pending: AtomicUsize,
187 eager_ready: Notify,
188}
189
190type PluginCollisionHandler = Arc<dyn Fn(&str, &str, Option<&str>) + Send + Sync>;
191
192struct OAuthFlowState {
193 generation: u64,
194 authorization_url: String,
195 cancellation: CancellationToken,
196}
197
198#[derive(Clone)]
199struct OAuthCredentialFile {
200 inner: Arc<OAuthCredentialFileInner>,
201}
202
203struct OAuthCredentialFileInner {
204 path: PathBuf,
205 transaction: Mutex<()>,
206}
207
208#[cfg(unix)]
209struct OAuthStoreLock {
210 file: fs::File,
211}
212
213#[derive(Clone)]
214struct ServerCredentialStore {
215 file: OAuthCredentialFile,
216 key: String,
217 binding: OAuthCredentialBinding,
218}
219
220#[derive(Clone, Deserialize, Serialize)]
221#[serde(deny_unknown_fields)]
222struct PersistedOAuthCredential {
223 binding: OAuthCredentialBinding,
224 credentials: StrictStoredCredentials,
225}
226
227#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
228#[serde(deny_unknown_fields)]
229struct OAuthCredentialBinding {
230 resource_url: String,
231 configured_client_id: Option<String>,
232 client_metadata_url: Option<String>,
233 client_secret_sha256: Option<String>,
234 scopes: Vec<String>,
235}
236
237#[derive(Clone, Deserialize, Serialize)]
238#[serde(deny_unknown_fields)]
239struct StrictStoredCredentials {
240 client_id: String,
241 token_response: Option<OAuthTokenResponse>,
242 granted_scopes: Vec<String>,
243 token_received_at: Option<u64>,
244 issuer: Option<String>,
245}
246
247struct McpOAuthHttpClient {
248 follow_redirects: reqwest::Client,
249 stop_redirects: reqwest::Client,
250 exchange: OAuthExchangeState,
251 store: ServerCredentialStore,
252}
253
254#[derive(Clone, Default)]
255struct OAuthExchangeState {
256 code: Arc<Mutex<Option<zeroize::Zeroizing<String>>>>,
257 response_error: Arc<Mutex<Option<String>>>,
258}
259
260#[derive(Debug)]
261struct OAuthHttpFailure;
262
263impl std::fmt::Display for OAuthHttpFailure {
264 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 formatter.write_str("OAuth HTTP operation failed")
266 }
267}
268
269impl std::error::Error for OAuthHttpFailure {}
270
271#[derive(Clone)]
272pub struct McpRegistry {
273 inner: Arc<RegistryInner>,
274}
275
276impl std::fmt::Debug for McpRegistry {
277 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 formatter
279 .debug_struct("McpRegistry")
280 .finish_non_exhaustive()
281 }
282}
283
284#[derive(Clone)]
285struct McpClientHandler {
286 server: Weak<ServerRuntime>,
287}
288
289struct ActiveConnect(Arc<RegistryInner>);
290
291impl ActiveConnect {
292 fn new(registry: Arc<RegistryInner>) -> Self {
293 registry.active_connects.fetch_add(1, Ordering::AcqRel);
294 Self(registry)
295 }
296}
297
298impl Drop for ActiveConnect {
299 fn drop(&mut self) {
300 if self.0.active_connects.fetch_sub(1, Ordering::AcqRel) == 1 {
301 self.0.connects_idle.notify_waiters();
302 }
303 }
304}
305
306struct EagerConnect(Arc<RegistryInner>);
307
308impl Drop for EagerConnect {
309 fn drop(&mut self) {
310 if self.0.eager_pending.fetch_sub(1, Ordering::AcqRel) == 1 {
311 self.0.eager_ready.notify_waiters();
312 }
313 }
314}
315
316#[derive(Clone)]
317struct ChildCleanup(Arc<ChildCleanupInner>);
318
319struct ChildCleanupInner {
320 done: AtomicBool,
321 notify: Notify,
322}
323
324impl ChildCleanup {
325 fn new() -> Self {
326 Self(Arc::new(ChildCleanupInner {
327 done: AtomicBool::new(false),
328 notify: Notify::new(),
329 }))
330 }
331
332 fn finish(&self) {
333 self.0.done.store(true, Ordering::Release);
334 self.0.notify.notify_waiters();
335 }
336
337 async fn wait(&self) {
338 while !self.0.done.load(Ordering::Acquire) {
339 let notified = self.0.notify.notified();
340 if self.0.done.load(Ordering::Acquire) {
341 break;
342 }
343 notified.await;
344 }
345 }
346}
347
348struct ReapingChildTransport {
349 inner: Option<TokioChildProcess>,
350 cleanup: ChildCleanup,
351}
352
353impl ReapingChildTransport {
354 fn new(inner: TokioChildProcess, cleanup: ChildCleanup) -> Self {
355 Self {
356 inner: Some(inner),
357 cleanup,
358 }
359 }
360}
361
362impl Transport<RoleClient> for ReapingChildTransport {
363 type Error = std::io::Error;
364
365 fn send(
366 &mut self,
367 item: TxJsonRpcMessage<RoleClient>,
368 ) -> impl Future<Output = Result<(), Self::Error>> + Send + 'static {
369 self.inner
370 .as_mut()
371 .expect("open child transport")
372 .send(item)
373 }
374
375 fn receive(&mut self) -> impl Future<Output = Option<RxJsonRpcMessage<RoleClient>>> + Send {
376 self.inner.as_mut().expect("open child transport").receive()
377 }
378
379 fn close(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send {
380 let inner = self.inner.take();
381 let cleanup = self.cleanup.clone();
382 async move {
383 let result = match inner {
384 Some(mut inner) => inner.graceful_shutdown().await,
385 None => Ok(()),
386 };
387 cleanup.finish();
388 result
389 }
390 }
391}
392
393impl Drop for ReapingChildTransport {
394 fn drop(&mut self) {
395 let Some(mut inner) = self.inner.take() else {
396 return;
397 };
398 let cleanup = self.cleanup.clone();
399 tokio::spawn(async move {
400 let _ = inner.graceful_shutdown().await;
401 cleanup.finish();
402 });
403 }
404}
405
406impl ClientHandler for McpClientHandler {
407 fn create_message(
408 &self,
409 _params: CreateMessageRequestParams,
410 _context: RequestContext<RoleClient>,
411 ) -> impl Future<Output = Result<CreateMessageResult, ErrorData>> + Send + '_ {
412 std::future::ready(Err(unsupported_server_request("sampling/createMessage")))
413 }
414
415 fn list_roots(
416 &self,
417 _context: RequestContext<RoleClient>,
418 ) -> impl Future<Output = Result<ListRootsResult, ErrorData>> + Send + '_ {
419 std::future::ready(Err(unsupported_server_request("roots/list")))
420 }
421
422 fn create_elicitation(
423 &self,
424 _request: ElicitRequestParams,
425 _context: RequestContext<RoleClient>,
426 ) -> impl Future<Output = Result<ElicitResult, ErrorData>> + Send + '_ {
427 std::future::ready(Err(unsupported_server_request("elicitation/create")))
428 }
429
430 async fn on_progress(
431 &self,
432 params: ProgressNotificationParam,
433 _context: NotificationContext<RoleClient>,
434 ) {
435 let Some(server) = self.server.upgrade() else {
436 return;
437 };
438 let message = params.message.unwrap_or_else(|| match params.total {
439 Some(total) => format!("MCP progress: {} / {total}", params.progress),
440 None => format!("MCP progress: {}", params.progress),
441 });
442 let sinks = server
443 .progress
444 .lock()
445 .await
446 .iter()
447 .map(|(id, sink)| (*id, sink.clone()))
448 .collect::<Vec<_>>();
449 for (tool_call_id, sink) in sinks {
450 let _ = sink
451 .send(ToolProgress {
452 output: Vec::new(),
453 tool_call_id,
454 message: message.clone(),
455 display: Some(safe_title(&message).to_string()),
456 })
457 .await;
458 }
459 }
460
461 async fn on_tool_list_changed(&self, context: NotificationContext<RoleClient>) {
462 let Some(server) = self.server.upgrade() else {
463 return;
464 };
465 server.schedule_tool_refresh(context.peer);
466 }
467}
468
469fn unsupported_server_request(method: &str) -> ErrorData {
470 ErrorData::new(
471 ErrorCode::METHOD_NOT_FOUND,
472 format!("unsupported MCP server request `{method}`"),
473 None,
474 )
475}
476
477fn oauth_enabled_config(config: &McpServerConfig) -> bool {
478 config.url.is_some()
479 && config.oauth.enabled()
480 && !config
481 .headers
482 .keys()
483 .any(|name| name.eq_ignore_ascii_case("authorization"))
484}
485
486fn auth_challenge(error: &(dyn std::error::Error + 'static)) -> Option<String> {
487 use rmcp::transport::streamable_http_client::{AuthRequiredError, InsufficientScopeError};
488
489 let mut source = Some(error);
490 while let Some(current) = source {
491 if let Some(required) = current.downcast_ref::<AuthRequiredError>() {
492 return Some(required.www_authenticate_header.clone());
493 }
494 if let Some(scope) = current.downcast_ref::<InsufficientScopeError>() {
495 return Some(scope.www_authenticate_header.clone());
496 }
497 source = current.source();
498 }
499 None
500}
501
502fn oauth_requires_auth(error: &(dyn std::error::Error + 'static)) -> bool {
503 if auth_challenge(error).is_some() {
504 return true;
505 }
506 let mut source = Some(error);
507 while let Some(current) = source {
508 if let Some(auth) = current.downcast_ref::<rmcp::transport::AuthError>()
509 && matches!(
510 auth,
511 rmcp::transport::AuthError::AuthorizationRequired
512 | rmcp::transport::AuthError::TokenExpired
513 | rmcp::transport::AuthError::TokenRefreshFailed(_)
514 | rmcp::transport::AuthError::TokenRefreshRejected(_)
515 )
516 {
517 return true;
518 }
519 source = current.source();
520 }
521 false
522}
523
524fn initialize_error_requires_auth(error: &rmcp::service::ClientInitializeError) -> bool {
525 error.is_authorization_required()
526 || match error {
527 rmcp::service::ClientInitializeError::TransportError { error, .. } => {
528 oauth_requires_auth(error.error.as_ref())
529 }
530 _ => false,
531 }
532}
533
534fn service_error_requires_auth(error: &rmcp::service::ServiceError) -> bool {
535 match error {
536 rmcp::service::ServiceError::TransportSend(error) => {
537 oauth_requires_auth(error.error.as_ref())
538 }
539 _ => oauth_requires_auth(error),
540 }
541}
542
543impl OAuthCredentialFile {
544 fn open(path: PathBuf) -> Result<Self, ToolError> {
545 #[cfg(windows)]
546 {
547 load_oauth_store_windows(&path).map_err(|()| oauth_store_startup_error(&path))?;
548 Ok(Self {
549 inner: Arc::new(OAuthCredentialFileInner {
550 path,
551 transaction: Mutex::new(()),
552 }),
553 })
554 }
555 #[cfg(unix)]
556 {
557 load_oauth_store(&path).map_err(|()| oauth_store_startup_error(&path))?;
558 Ok(Self {
559 inner: Arc::new(OAuthCredentialFileInner {
560 path,
561 transaction: Mutex::new(()),
562 }),
563 })
564 }
565 }
566
567 fn scoped(&self, server: &str, binding: OAuthCredentialBinding) -> ServerCredentialStore {
568 ServerCredentialStore {
569 file: self.clone(),
570 key: oauth_credential_key(server, &binding.resource_url),
571 binding,
572 }
573 }
574
575 fn remove(&self, server: &str, binding: &OAuthCredentialBinding) -> Result<(), ToolError> {
576 let key = oauth_credential_key(server, &binding.resource_url);
577 self.update(|credentials| {
578 credentials.remove(&key);
579 })
580 .map_err(|()| oauth_store_runtime_error(&self.inner.path))
581 }
582
583 fn update(
584 &self,
585 update: impl FnOnce(&mut BTreeMap<String, PersistedOAuthCredential>),
586 ) -> Result<(), ()> {
587 #[cfg(windows)]
588 {
589 let _transaction = self
590 .inner
591 .transaction
592 .lock()
593 .unwrap_or_else(|poisoned| poisoned.into_inner());
594 let (directory, name) = oauth_store_directory_windows(&self.inner.path)?;
595 let lock = directory
596 .lock_within(OAUTH_STORE_LOCK_FILE, DEFAULT_LOCK_BUDGET)
597 .map_err(|_| ())?;
598 let mut candidate = load_oauth_store_from_lock_windows(&lock, &name)?;
599 update(&mut candidate);
600 let bytes = serde_json::to_vec(&candidate).map_err(|_| ())?;
601 lock.atomic_replace(&name, &bytes).map_err(|_| ())
602 }
603 #[cfg(unix)]
604 {
605 let _transaction = self
606 .inner
607 .transaction
608 .lock()
609 .unwrap_or_else(|poisoned| poisoned.into_inner());
610 let _file_lock = OAuthStoreLock::acquire(&self.inner.path)?;
611 let mut candidate = load_oauth_store(&self.inner.path)?;
612 update(&mut candidate);
613 persist_oauth_store(&self.inner.path, &candidate)?;
614 Ok(())
615 }
616 }
617
618 fn get(&self, key: &str) -> Result<Option<PersistedOAuthCredential>, ()> {
626 #[cfg(windows)]
627 {
628 Ok(load_oauth_store_windows(&self.inner.path)?
629 .get(key)
630 .cloned())
631 }
632 #[cfg(unix)]
633 {
634 Ok(load_oauth_store(&self.inner.path)?.get(key).cloned())
635 }
636 }
637
638 fn get_for_claim(&self, key: &str) -> Result<Option<PersistedOAuthCredential>, ()> {
645 #[cfg(windows)]
646 {
647 let _transaction = self
648 .inner
649 .transaction
650 .lock()
651 .unwrap_or_else(|poisoned| poisoned.into_inner());
652 let (directory, name) = oauth_store_directory_windows(&self.inner.path)?;
653 let lock = directory
654 .lock_within(OAUTH_STORE_LOCK_FILE, DEFAULT_LOCK_BUDGET)
655 .map_err(|_| ())?;
656 Ok(load_oauth_store_from_lock_windows(&lock, &name)?
657 .get(key)
658 .cloned())
659 }
660 #[cfg(unix)]
661 {
662 let _transaction = self
663 .inner
664 .transaction
665 .lock()
666 .unwrap_or_else(|poisoned| poisoned.into_inner());
667 let _file_lock = OAuthStoreLock::acquire(&self.inner.path)?;
668 Ok(load_oauth_store(&self.inner.path)?.get(key).cloned())
669 }
670 }
671}
672
673#[async_trait]
674impl CredentialStore for ServerCredentialStore {
675 async fn load(&self) -> Result<Option<StoredCredentials>, rmcp::transport::AuthError> {
676 let stored = self
677 .file
678 .get(&self.key)
679 .map_err(|()| oauth_store_auth_error())?;
680 let Some(stored) = stored else {
681 return Ok(None);
682 };
683 if stored.binding != self.binding {
684 self.clear().await?;
685 return Ok(None);
686 }
687 Ok(Some(stored.credentials.into_rmcp()))
688 }
689
690 async fn save(&self, credentials: StoredCredentials) -> Result<(), rmcp::transport::AuthError> {
691 let stored = PersistedOAuthCredential {
692 binding: self.binding.clone(),
693 credentials: StrictStoredCredentials::from_rmcp(credentials),
694 };
695 self.file
696 .update(|all| {
697 if let Some(existing) = all.get(&self.key)
698 && credential_superseded(&existing.credentials, &stored.credentials)
699 {
700 return;
705 }
706 all.insert(self.key.clone(), stored);
707 })
708 .map_err(|()| oauth_store_auth_error())
709 }
710
711 async fn clear(&self) -> Result<(), rmcp::transport::AuthError> {
712 self.file
713 .update(|all| {
714 all.remove(&self.key);
715 })
716 .map_err(|()| oauth_store_auth_error())
717 }
718}
719
720impl ServerCredentialStore {
721 fn claim_credential(&self) -> Option<PersistedOAuthCredential> {
726 self.file.get_for_claim(&self.key).ok().flatten()
727 }
728}
729
730fn credential_superseded(
738 existing: &StrictStoredCredentials,
739 candidate: &StrictStoredCredentials,
740) -> bool {
741 if candidate.token_response.is_none() {
742 return false;
743 }
744 let strictly_newer = existing.token_received_at > candidate.token_received_at;
745 let same_timestamp = existing.token_received_at == candidate.token_received_at;
746 strictly_newer || (same_timestamp && existing.refresh_token() != candidate.refresh_token())
747}
748
749fn refresh_token_of(response: &OAuthTokenResponse) -> Option<String> {
750 serde_json::to_value(response)
751 .ok()?
752 .get("refresh_token")?
753 .as_str()
754 .map(str::to_owned)
755}
756
757impl OAuthCredentialBinding {
758 fn from_config(config: &McpServerConfig) -> Option<Self> {
759 let resource_url = canonical_oauth_resource_url(config.url.as_deref()?);
760 let settings = config.oauth.settings();
761 let client_secret_sha256 = settings.client_secret.as_ref().map(|secret| {
762 let digest = Sha256::digest(secret.as_bytes());
763 digest
764 .iter()
765 .map(|byte| format!("{byte:02x}"))
766 .collect::<String>()
767 });
768 Some(Self {
769 resource_url,
770 configured_client_id: settings.client_id,
771 client_metadata_url: settings.client_metadata_url,
772 client_secret_sha256,
773 scopes: settings.scopes,
774 })
775 }
776}
777
778fn canonical_oauth_resource_url(resource_url: &str) -> String {
779 let Ok(mut url) = url::Url::parse(resource_url) else {
780 return resource_url.to_owned();
782 };
783 if matches!(
784 (url.scheme(), url.port()),
785 ("http", Some(80)) | ("https", Some(443))
786 ) {
787 let _ = url.set_port(None);
788 }
789 url.into()
790}
791
792fn oauth_credential_key(server: &str, canonical_resource_url: &str) -> String {
793 let digest = Sha256::digest(canonical_resource_url.as_bytes());
794 let hash = digest
795 .iter()
796 .map(|byte| format!("{byte:02x}"))
797 .collect::<String>();
798 format!("{server}:{hash}")
799}
800
801impl StrictStoredCredentials {
802 fn from_rmcp(credentials: StoredCredentials) -> Self {
803 Self {
804 client_id: credentials.client_id,
805 token_response: credentials.token_response,
806 granted_scopes: credentials.granted_scopes,
807 token_received_at: credentials.token_received_at,
808 issuer: credentials.issuer,
809 }
810 }
811
812 fn into_rmcp(self) -> StoredCredentials {
813 StoredCredentials::new(
814 self.client_id,
815 self.token_response,
816 self.granted_scopes,
817 self.token_received_at,
818 )
819 .with_issuer(self.issuer)
820 }
821
822 fn refresh_token(&self) -> Option<String> {
823 self.token_response.as_ref().and_then(refresh_token_of)
824 }
825}
826
827impl McpOAuthHttpClient {
828 fn new(exchange: OAuthExchangeState, store: ServerCredentialStore) -> Result<Self, ToolError> {
829 let base = || {
830 reqwest::Client::builder()
831 .timeout(Duration::from_secs(30))
832 .pool_max_idle_per_host(0)
833 };
834 let follow_redirects = base()
835 .build()
836 .map_err(|_| ToolError::execution("MCP OAuth HTTP client setup failed"))?;
837 let stop_redirects = base()
838 .redirect(reqwest::redirect::Policy::none())
839 .build()
840 .map_err(|_| ToolError::execution("MCP OAuth HTTP client setup failed"))?;
841 Ok(Self {
842 follow_redirects,
843 stop_redirects,
844 exchange,
845 store,
846 })
847 }
848
849 fn adopted_success(&self, requested: Option<&str>) -> Option<http::Response<Vec<u8>>> {
857 let requested = requested?;
858 let disk = self.store.claim_credential()?;
859 if disk.binding != self.store.binding {
860 return None;
861 }
862 let token_response = disk.credentials.token_response.as_ref()?;
863 let disk_refresh = refresh_token_of(token_response)?;
864 if disk_refresh == requested {
865 return None;
866 }
867 let body = serde_json::to_vec(token_response).ok()?;
868 http::Response::builder()
869 .status(http::StatusCode::OK)
870 .header(http::header::CONTENT_TYPE, "application/json")
871 .body(body)
872 .ok()
873 }
874}
875
876fn refresh_grant_token(body: &[u8]) -> Option<String> {
883 let mut grant_type = None;
884 let mut refresh_token = None;
885 for (name, value) in url::form_urlencoded::parse(body) {
886 match name.as_ref() {
887 "grant_type" => grant_type = Some(value.into_owned()),
888 "refresh_token" => refresh_token = Some(value.into_owned()),
889 _ => {}
890 }
891 }
892 if grant_type.as_deref() != Some("refresh_token") {
893 return None;
894 }
895 refresh_token
896}
897
898fn response_is_invalid_grant(response: &http::Response<Vec<u8>>) -> bool {
900 if response.status() != http::StatusCode::BAD_REQUEST {
901 return false;
902 }
903 if let Ok(value) = serde_json::from_slice::<serde_json::Value>(response.body())
904 && value.get("error").and_then(serde_json::Value::as_str) == Some("invalid_grant")
905 {
906 return true;
907 }
908 response
909 .body()
910 .windows(b"invalid_grant".len())
911 .any(|window| window == b"invalid_grant")
912}
913
914impl OAuthExchangeState {
915 fn diagnostic(&self, error: &(dyn std::error::Error + 'static)) -> String {
916 self.response_error
917 .lock()
918 .unwrap_or_else(|p| p.into_inner())
919 .clone()
920 .unwrap_or_else(|| cookie_agent_protocol::diagnostics::error_chain(error))
921 }
922
923 fn install(&self, code: String) {
924 *self
925 .code
926 .lock()
927 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(zeroize::Zeroizing::new(code));
928 }
929
930 fn restore_in_request(&self, request: &mut http::Request<Vec<u8>>) {
931 let parameters = url::form_urlencoded::parse(request.body())
932 .into_owned()
933 .collect::<Vec<_>>();
934 if !parameters
935 .iter()
936 .any(|(name, value)| name == "code" && value == REDACTED_AUTHORIZATION_CODE)
937 {
938 return;
939 }
940 let Some(code) = self
941 .code
942 .lock()
943 .unwrap_or_else(|poisoned| poisoned.into_inner())
944 .take()
945 else {
946 return;
947 };
948 let mut encoded = url::form_urlencoded::Serializer::new(String::new());
949 for (name, value) in parameters {
950 encoded.append_pair(
951 &name,
952 if name == "code" && value == REDACTED_AUTHORIZATION_CODE {
953 code.as_str()
954 } else {
955 &value
956 },
957 );
958 }
959 *request.body_mut() = encoded.finish().into_bytes();
960 }
961}
962
963impl OAuthHttpClient for McpOAuthHttpClient {
964 fn execute(&self, operation: rmcp::transport::OAuthHttpRequest) -> OAuthHttpClientFuture<'_> {
965 Box::pin(async move {
966 let client = match operation.redirect_policy {
967 OAuthHttpRedirectPolicy::Follow => &self.follow_redirects,
968 OAuthHttpRedirectPolicy::Stop => &self.stop_redirects,
969 _ => &self.stop_redirects,
970 };
971 let timeout = operation.timeout;
972 let mut oauth_request = operation.request;
973 self.exchange.restore_in_request(&mut oauth_request);
974 let refresh_token = refresh_grant_token(oauth_request.body());
977 if let Some(response) = self.adopted_success(refresh_token.as_deref()) {
978 return Ok(response);
979 }
980 let mut request = reqwest::Request::try_from(oauth_request)
981 .map_err(|_| Box::new(OAuthHttpFailure) as rmcp::transport::OAuthHttpClientError)?;
982 *request.timeout_mut() = timeout;
983 let response = client
984 .execute(request)
985 .await
986 .map_err(|_| Box::new(OAuthHttpFailure) as rmcp::transport::OAuthHttpClientError)?;
987 let status = response.status();
988 let version = response.version();
989 let headers = response.headers().clone();
990 let mut body = Vec::new();
991 let mut stream = response.bytes_stream();
992 while let Some(chunk) = stream.next().await {
993 let chunk = chunk.map_err(|_| {
994 Box::new(OAuthHttpFailure) as rmcp::transport::OAuthHttpClientError
995 })?;
996 if chunk.len() > OAUTH_STORE_MAX_BYTES as usize - body.len() {
997 return Err(Box::new(OAuthHttpFailure) as rmcp::transport::OAuthHttpClientError);
998 }
999 body.extend_from_slice(&chunk);
1000 }
1001 *self
1004 .exchange
1005 .response_error
1006 .lock()
1007 .unwrap_or_else(|p| p.into_inner()) = (!status.is_success()).then(|| {
1008 cookie_agent_protocol::diagnostics::detail(&format!(
1009 "OAuth HTTP {status}\nResponse body:\n{}",
1010 String::from_utf8_lossy(&body)
1011 ))
1012 .to_string()
1013 });
1014 let mut response = http::Response::builder().status(status).version(version);
1015 for (name, value) in &headers {
1016 response = response.header(name, value);
1017 }
1018 let response = response
1019 .body(body)
1020 .map_err(|_| Box::new(OAuthHttpFailure) as rmcp::transport::OAuthHttpClientError)?;
1021 if refresh_token.is_some()
1026 && response_is_invalid_grant(&response)
1027 && let Some(adopted) = self.adopted_success(refresh_token.as_deref())
1028 {
1029 return Ok(adopted);
1030 }
1031 Ok(response)
1032 })
1033 }
1034}
1035
1036fn parse_oauth_callback(url: &str) -> Result<AuthorizationCallback, String> {
1037 AuthorizationCallback::from_redirect_url(url).map_err(|error| {
1038 let mut message = error.to_string();
1039 if let Ok(url) = url::Url::parse(url) {
1040 for (key, value) in url
1041 .query_pairs()
1042 .filter(|(key, _)| matches!(key.as_ref(), "error" | "error_description"))
1043 {
1044 message.push_str(&format!("\n{key}: {value}"));
1045 }
1046 }
1047 cookie_agent_protocol::diagnostics::detail(&message).to_string()
1048 })
1049}
1050
1051fn oauth_store_startup_error(path: &Path) -> ToolError {
1052 ToolError::execution(format!(
1053 "invalid MCP OAuth credential store `{}`; fix its contents or remove the file to reset user MCP OAuth credentials",
1054 path.display()
1055 ))
1056}
1057
1058#[cfg(windows)]
1059fn oauth_store_directory_windows(path: &Path) -> Result<(SecureDirectory, String), ()> {
1060 let parent = path.parent().ok_or(())?;
1061 let name = path
1062 .file_name()
1063 .and_then(|name| name.to_str())
1064 .ok_or(())?
1065 .to_owned();
1066 let directory = SecureDirectory::open(parent).map_err(|_| ())?;
1067 Ok((directory, name))
1068}
1069
1070#[cfg(windows)]
1071fn load_oauth_store_windows(path: &Path) -> Result<BTreeMap<String, PersistedOAuthCredential>, ()> {
1072 let (directory, name) = oauth_store_directory_windows(path)?;
1076 let Some(bytes) = directory
1077 .read(&name, OAUTH_STORE_MAX_BYTES)
1078 .map_err(|_| ())?
1079 else {
1080 return Ok(BTreeMap::new());
1081 };
1082 serde_json::from_slice(&bytes).map_err(|_| ())
1083}
1084
1085#[cfg(windows)]
1086fn load_oauth_store_from_lock_windows(
1087 lock: &cookie_agent_models::secure_store::SecureDirectoryLock<'_>,
1088 name: &str,
1089) -> Result<BTreeMap<String, PersistedOAuthCredential>, ()> {
1090 let Some(bytes) = lock.read(name, OAUTH_STORE_MAX_BYTES).map_err(|_| ())? else {
1091 return Ok(BTreeMap::new());
1092 };
1093 serde_json::from_slice(&bytes).map_err(|_| ())
1094}
1095
1096fn oauth_store_runtime_error(path: &Path) -> ToolError {
1097 ToolError::execution(format!(
1098 "MCP OAuth credential storage failed at `{}`",
1099 path.display()
1100 ))
1101}
1102
1103fn oauth_store_auth_error() -> rmcp::transport::AuthError {
1104 rmcp::transport::AuthError::InternalError("OAuth credential storage failed".into())
1105}
1106
1107#[cfg(unix)]
1108impl OAuthStoreLock {
1109 fn acquire(store_path: &Path) -> Result<Self, ()> {
1110 let parent = store_path.parent().ok_or(())?;
1111 if !parent.exists() {
1112 use std::os::unix::fs::DirBuilderExt as _;
1113
1114 let mut builder = fs::DirBuilder::new();
1115 builder.recursive(true).mode(0o700);
1116 builder.create(parent).map_err(|_| ())?;
1117 }
1118 let path = parent.join(OAUTH_STORE_LOCK_FILE);
1119 let mut options = OpenOptions::new();
1120 options.read(true).write(true).create(true);
1121 #[cfg(unix)]
1122 {
1123 use std::os::unix::fs::OpenOptionsExt as _;
1124 options.mode(0o600).custom_flags(libc::O_CLOEXEC);
1125 }
1126 let file = options.open(&path).map_err(|_| ())?;
1127 if !lock_within_file(&file, DEFAULT_LOCK_BUDGET).map_err(|_| ())? {
1128 return Err(());
1129 }
1130 Ok(Self { file })
1131 }
1132}
1133
1134#[cfg(unix)]
1135impl Drop for OAuthStoreLock {
1136 fn drop(&mut self) {
1137 let _ = unlock(&self.file);
1138 }
1139}
1140
1141#[cfg(unix)]
1142fn load_oauth_store(path: &Path) -> Result<BTreeMap<String, PersistedOAuthCredential>, ()> {
1143 let Some(mut file) = open_oauth_store(path)? else {
1144 return Ok(BTreeMap::new());
1145 };
1146 let mut bytes = Vec::new();
1147 (&mut file)
1148 .take(OAUTH_STORE_MAX_BYTES + 1)
1149 .read_to_end(&mut bytes)
1150 .map_err(|_| ())?;
1151 if bytes.len() as u64 > OAUTH_STORE_MAX_BYTES {
1152 return Err(());
1153 }
1154 serde_json::from_slice(&bytes).map_err(|_| ())
1155}
1156
1157#[cfg(unix)]
1158fn open_oauth_store(path: &Path) -> Result<Option<fs::File>, ()> {
1159 match OpenOptions::new().read(true).open(path) {
1160 Ok(file) => Ok(Some(file)),
1161 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
1162 Err(_) => Err(()),
1163 }
1164}
1165
1166#[cfg(unix)]
1167fn persist_oauth_store(
1168 path: &Path,
1169 credentials: &BTreeMap<String, PersistedOAuthCredential>,
1170) -> Result<(), ()> {
1171 let parent = path.parent().ok_or(())?;
1172 if !parent.exists() {
1173 use std::os::unix::fs::DirBuilderExt as _;
1174
1175 let mut builder = fs::DirBuilder::new();
1176 builder.recursive(true).mode(0o700);
1177 builder.create(parent).map_err(|_| ())?;
1178 }
1179 let bytes = serde_json::to_vec(credentials).map_err(|_| ())?;
1180 let temporary = parent.join(format!(".{OAUTH_STORE_FILE}.{}.tmp", uuid::Uuid::now_v7()));
1181 let mut options = OpenOptions::new();
1182 options.write(true).create_new(true);
1183 {
1184 use std::os::unix::fs::OpenOptionsExt as _;
1185 options
1186 .mode(0o600)
1187 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
1188 }
1189 let mut file = options.open(&temporary).map_err(|_| ())?;
1190 {
1191 use std::os::unix::fs::PermissionsExt as _;
1192 file.set_permissions(fs::Permissions::from_mode(0o600))
1193 .map_err(|_| ())?;
1194 }
1195 let result = (|| {
1196 file.write_all(&bytes).map_err(|_| ())?;
1197 file.sync_all().map_err(|_| ())?;
1198 drop(file);
1199 fs::rename(&temporary, path).map_err(|_| ())?;
1200 sync_oauth_directory(parent).map_err(|_| ())?;
1201 Ok(())
1202 })();
1203 if result.is_err() {
1204 let _ = fs::remove_file(temporary);
1205 }
1206 result
1207}
1208
1209#[cfg(unix)]
1210fn sync_oauth_directory(path: &Path) -> std::io::Result<()> {
1211 fs::File::open(path)?.sync_all()
1212}
1213
1214fn apply_oauth_settings(
1215 mut request: AuthorizationRequest,
1216 settings: &McpOAuthSettings,
1217) -> AuthorizationRequest {
1218 if !settings.scopes.is_empty() {
1219 request = request.with_scopes(settings.scopes.clone());
1220 }
1221 if let Some(client_id) = &settings.client_id {
1222 request = request.with_preregistered_client(client_id);
1223 }
1224 if let Some(client_secret) = &settings.client_secret {
1225 request = request.with_client_secret(client_secret);
1226 }
1227 if let Some(client_metadata_url) = &settings.client_metadata_url {
1228 request = request.with_client_metadata_url(client_metadata_url);
1229 }
1230 request
1231}
1232
1233async fn receive_oauth_callback(listener: TcpListener) -> Result<(TcpStream, String), ()> {
1234 loop {
1235 let (mut stream, _) = listener.accept().await.map_err(|_| ())?;
1236 let mut request = Vec::new();
1237 loop {
1238 let mut buffer = [0_u8; 1024];
1239 let read = stream.read(&mut buffer).await.map_err(|_| ())?;
1240 if read == 0 {
1241 break;
1242 }
1243 request.extend_from_slice(&buffer[..read]);
1244 if request.windows(4).any(|window| window == b"\r\n\r\n") {
1245 break;
1246 }
1247 if request.len() > OAUTH_CALLBACK_MAX_BYTES {
1248 break;
1249 }
1250 }
1251 if request.len() > OAUTH_CALLBACK_MAX_BYTES {
1252 let _ = write_oauth_browser_response(&mut stream, false).await;
1253 continue;
1254 }
1255 let Ok(request) = std::str::from_utf8(&request) else {
1256 let _ = write_oauth_browser_response(&mut stream, false).await;
1257 continue;
1258 };
1259 let Some(target) = request
1260 .lines()
1261 .next()
1262 .and_then(|line| line.strip_prefix("GET "))
1263 .and_then(|line| line.split_once(' '))
1264 .map(|(target, _)| target)
1265 else {
1266 let _ = write_oauth_browser_response(&mut stream, false).await;
1267 continue;
1268 };
1269 let Ok(url) = url::Url::parse(&format!("http://127.0.0.1{target}")) else {
1270 let _ = write_oauth_browser_response(&mut stream, false).await;
1271 continue;
1272 };
1273 if url.path() != "/callback" {
1274 let _ = write_oauth_browser_response(&mut stream, false).await;
1275 continue;
1276 }
1277 return Ok((stream, url.to_string()));
1278 }
1279}
1280
1281async fn write_oauth_browser_response(
1282 stream: &mut TcpStream,
1283 success: bool,
1284) -> std::io::Result<()> {
1285 let (status, body) = if success {
1286 (
1287 "200 OK",
1288 "Authorization complete. You can close this window.",
1289 )
1290 } else {
1291 (
1292 "400 Bad Request",
1293 "Authorization failed. Return to Cookie Agent and try again.",
1294 )
1295 };
1296 let response = format!(
1297 "HTTP/1.1 {status}\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1298 body.len()
1299 );
1300 stream.write_all(response.as_bytes()).await?;
1301 stream.shutdown().await
1302}
1303
1304impl McpRegistry {
1305 pub(crate) fn set_plugin_collision_handler(&self, handler: PluginCollisionHandler) {
1306 *self
1307 .inner
1308 .plugin_collision_handler
1309 .lock()
1310 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(handler);
1311 }
1312
1313 pub(crate) fn claim_plugin_tools(
1314 &self,
1315 plugin: &str,
1316 names: &[String],
1317 ) -> Result<(), ToolError> {
1318 let reserved = self
1319 .inner
1320 .reserved_names
1321 .lock()
1322 .unwrap_or_else(|poisoned| poisoned.into_inner());
1323 let mut claimed = self
1324 .inner
1325 .claimed_names
1326 .lock()
1327 .unwrap_or_else(|poisoned| poisoned.into_inner());
1328 let owner = format!("plugin:{plugin}");
1329 let mut local = HashSet::new();
1330 let mut displaced = Vec::new();
1331 for name in names {
1332 if reserved.contains(name) || !local.insert(name) {
1333 return Err(ToolError::execution(format!(
1334 "plugin `{plugin}` declared colliding tool name `{name}`"
1335 )));
1336 }
1337 if let Some(current) = claimed.get(name) {
1338 if let Some(previous) = current.strip_prefix("plugin:") {
1339 if previous != plugin {
1340 displaced.push((previous.to_owned(), name.clone()));
1341 }
1342 } else {
1343 return Err(ToolError::execution(format!(
1344 "plugin `{plugin}` declared colliding tool name `{name}`"
1345 )));
1346 }
1347 }
1348 }
1349 for name in names {
1350 claimed.insert(name.clone(), owner.clone());
1351 }
1352 drop(claimed);
1353 drop(reserved);
1354 let collision_handler = self
1355 .inner
1356 .plugin_collision_handler
1357 .lock()
1358 .unwrap_or_else(|poisoned| poisoned.into_inner())
1359 .clone();
1360 if let Some(handler) = collision_handler {
1361 for (previous, tool) in displaced {
1362 handler(&previous, &tool, Some(plugin));
1363 }
1364 }
1365 Ok(())
1366 }
1367
1368 pub(crate) fn plugin_owns_tool(&self, plugin: &str, tool: &str) -> bool {
1369 self.inner
1370 .claimed_names
1371 .lock()
1372 .unwrap_or_else(|poisoned| poisoned.into_inner())
1373 .get(tool)
1374 .is_some_and(|owner| owner == &format!("plugin:{plugin}"))
1375 }
1376
1377 pub(crate) fn release_plugin_tools(&self, plugin: &str) {
1378 let owner = format!("plugin:{plugin}");
1379 self.inner
1380 .claimed_names
1381 .lock()
1382 .unwrap_or_else(|poisoned| poisoned.into_inner())
1383 .retain(|_, current| current != &owner);
1384 }
1385
1386 pub(crate) fn new(
1387 servers: BTreeMap<String, LoadedMcpServer>,
1388 oauth_path: std::path::PathBuf,
1389 ) -> Result<Self, ToolError> {
1390 let oauth_credentials = OAuthCredentialFile::open(oauth_path)?;
1391 let inner = Arc::new(RegistryInner {
1392 servers: Mutex::new(BTreeMap::new()),
1393 reserved_names: Mutex::new(HashSet::from([
1394 "read".into(),
1395 "write".into(),
1396 "edit".into(),
1397 "bash".into(),
1398 "delegate_subagent".into(),
1399 "get_subagent_result".into(),
1400 "cancel_subagent".into(),
1401 "skill".into(),
1402 ])),
1403 claimed_names: Mutex::new(HashMap::new()),
1404 plugin_collision_handler: Mutex::new(None),
1405 oauth_credentials,
1406 shutdown: CancellationToken::new(),
1407 connection_tasks: Mutex::new(Vec::new()),
1408 active_connects: AtomicUsize::new(0),
1409 connects_idle: Notify::new(),
1410 eager_pending: AtomicUsize::new(0),
1411 eager_ready: Notify::new(),
1412 });
1413 let mut sanitized_servers = HashMap::new();
1414 let mut runtimes = BTreeMap::new();
1415 for (name, loaded) in servers {
1416 let sanitized_name = sanitize_name(&name);
1417 if let Some(existing) = sanitized_servers.insert(sanitized_name.clone(), name.clone()) {
1418 return Err(ToolError::execution(format!(
1419 "MCP server `{name}` collides with server `{existing}` after name sanitization"
1420 )));
1421 }
1422 let state = if !loaded.config.enabled {
1423 McpServerState::Disabled
1424 } else {
1425 McpServerState::Disconnected
1426 };
1427 let runtime = Arc::new(ServerRuntime {
1428 name: name.clone(),
1429 sanitized_name,
1430 loaded,
1431 status: Mutex::new(McpServerStatus {
1432 server: name.clone(),
1433 state,
1434 message: None,
1435 tools: Vec::new(),
1436 auth_in_progress: false,
1437 }),
1438 tools: Mutex::new(Vec::new()),
1439 list_generation: AtomicU64::new(0),
1440 publication: Mutex::new(()),
1441 tool_refresh: Mutex::new(ToolRefreshDebounce::default()),
1442 connect_lock: AsyncMutex::new(()),
1443 service: AsyncMutex::new(None),
1444 superseded: CancellationToken::new(),
1445 registry: Arc::downgrade(&inner),
1446 progress: AsyncMutex::new(HashMap::new()),
1447 auth_challenge: Mutex::new(None),
1448 oauth_response_error: Arc::default(),
1449 auth_flow: Mutex::new(None),
1450 auth_generation: AtomicU64::new(0),
1451 auth_lock: AsyncMutex::new(()),
1452 });
1453 runtimes.insert(name, runtime);
1454 }
1455 *inner.servers.lock().unwrap_or_else(|p| p.into_inner()) = runtimes;
1456 Ok(Self { inner })
1457 }
1458
1459 pub(crate) fn reserve_provider(&self, provider: &dyn ToolProvider) -> Result<(), ToolError> {
1460 let Ok(tools) = provider.tools_for_session(&SessionToolContext::new(
1461 cookie_agent_protocol::SessionId::new_v7(),
1462 )) else {
1463 return Ok(());
1465 };
1466 let mut reserved = self
1467 .inner
1468 .reserved_names
1469 .lock()
1470 .unwrap_or_else(|p| p.into_inner());
1471 let claimed = self
1472 .inner
1473 .claimed_names
1474 .lock()
1475 .unwrap_or_else(|p| p.into_inner());
1476 for tool in tools {
1477 if let Some(server) = claimed.get(&tool.name) {
1478 return Err(ToolError::execution(format!(
1479 "MCP server `{server}` generated tool name `{}` which collides with a registered tool",
1480 tool.name
1481 )));
1482 }
1483 reserved.insert(tool.name);
1484 }
1485 Ok(())
1486 }
1487
1488 pub(crate) fn start_eager(&self, runtime: &tokio::runtime::Handle) {
1489 let servers = self
1490 .servers()
1491 .into_iter()
1492 .filter(|server| {
1493 server.loaded.config.enabled
1494 && !server.loaded.config.lazy
1495 && server.current_state() == McpServerState::Disconnected
1496 })
1497 .collect::<Vec<_>>();
1498 self.inner
1499 .eager_pending
1500 .store(servers.len(), Ordering::Release);
1501 for server in servers {
1502 let registry = Arc::clone(&self.inner);
1503 let task_registry = Arc::clone(®istry);
1504 let eager = EagerConnect(Arc::clone(&task_registry));
1505 let task = runtime.spawn(async move {
1506 let _eager = eager;
1507 if let Err(error) = server.connect().await
1508 && !task_registry.shutdown.is_cancelled()
1509 {
1510 server.fail(error.to_string());
1511 }
1512 });
1513 registry
1514 .connection_tasks
1515 .lock()
1516 .unwrap_or_else(|p| p.into_inner())
1517 .push(task);
1518 }
1519 }
1520
1521 pub(crate) async fn await_eager_ready(&self) {
1522 while self.inner.eager_pending.load(Ordering::Acquire) != 0 {
1523 let notified = self.inner.eager_ready.notified();
1524 if self.inner.eager_pending.load(Ordering::Acquire) == 0 {
1525 break;
1526 }
1527 notified.await;
1528 }
1529 }
1530
1531 pub async fn shutdown(&self) {
1532 self.inner.shutdown.cancel();
1533 for server in self.servers() {
1534 server.cancel_tool_refresh();
1535 }
1536 let tasks = self
1537 .inner
1538 .connection_tasks
1539 .lock()
1540 .map(|mut tasks| tasks.drain(..).collect::<Vec<_>>())
1541 .unwrap_or_default();
1542 for mut task in tasks {
1543 if tokio::time::timeout(Duration::from_secs(5), &mut task)
1544 .await
1545 .is_err()
1546 {
1547 task.abort();
1548 let _ = task.await;
1549 }
1550 }
1551 for server in self.servers() {
1552 let mut service = server.service.lock().await;
1553 if let Some(service) = service.as_mut() {
1554 let _ = service.close_with_timeout(Duration::from_secs(4)).await;
1555 }
1556 *service = None;
1557 }
1558 while self.inner.active_connects.load(Ordering::Acquire) != 0 {
1559 let notified = self.inner.connects_idle.notified();
1560 if self.inner.active_connects.load(Ordering::Acquire) == 0 {
1561 break;
1562 }
1563 notified.await;
1564 }
1565 }
1566
1567 #[must_use]
1568 pub fn statuses(&self) -> Vec<McpServerStatus> {
1569 self.servers()
1570 .into_iter()
1571 .map(|server| {
1572 server
1573 .status
1574 .lock()
1575 .unwrap_or_else(|p| p.into_inner())
1576 .clone()
1577 })
1578 .collect()
1579 }
1580
1581 pub(crate) async fn upsert_server(
1582 &self,
1583 name: String,
1584 loaded: LoadedMcpServer,
1585 ) -> Result<(), ToolError> {
1586 let sanitized_name = sanitize_name(&name);
1587 let previous = {
1588 let servers = self
1589 .inner
1590 .servers
1591 .lock()
1592 .unwrap_or_else(|poisoned| poisoned.into_inner());
1593 if let Some(existing) = servers
1594 .values()
1595 .find(|server| server.name != name && server.sanitized_name == sanitized_name)
1596 {
1597 return Err(ToolError::execution(format!(
1598 "MCP server `{name}` collides with server `{}` after name sanitization",
1599 existing.name
1600 )));
1601 }
1602 servers.get(&name).cloned()
1603 };
1604 let previous_binding = previous
1605 .as_ref()
1606 .and_then(|server| OAuthCredentialBinding::from_config(&server.loaded.config));
1607 let new_binding = OAuthCredentialBinding::from_config(&loaded.config);
1608 let binding_changed = previous.is_some() && previous_binding != new_binding;
1609 let _previous_auth = if let Some(previous) = &previous {
1610 let auth = previous.auth_lock.lock().await;
1611 if binding_changed && let Some(binding) = &previous_binding {
1612 self.inner.oauth_credentials.remove(&name, binding)?;
1613 }
1614 previous.supersede();
1615 Some(auth)
1616 } else {
1617 None
1618 };
1619 let needs_auth_after_edit = binding_changed && oauth_enabled_config(&loaded.config);
1620 let state = if !loaded.config.enabled {
1621 McpServerState::Disabled
1622 } else if needs_auth_after_edit {
1623 McpServerState::NeedsAuth
1624 } else {
1625 McpServerState::Disconnected
1626 };
1627 let message = (state == McpServerState::NeedsAuth).then(|| {
1628 "OAuth credentials invalidated by MCP server configuration change; authenticate to connect".into()
1629 });
1630 let runtime = Arc::new(ServerRuntime {
1631 name: name.clone(),
1632 sanitized_name,
1633 loaded,
1634 status: Mutex::new(McpServerStatus {
1635 server: name.clone(),
1636 state,
1637 message,
1638 tools: Vec::new(),
1639 auth_in_progress: false,
1640 }),
1641 tools: Mutex::new(Vec::new()),
1642 list_generation: AtomicU64::new(0),
1643 publication: Mutex::new(()),
1644 tool_refresh: Mutex::new(ToolRefreshDebounce::default()),
1645 connect_lock: AsyncMutex::new(()),
1646 service: AsyncMutex::new(None),
1647 superseded: CancellationToken::new(),
1648 registry: Arc::downgrade(&self.inner),
1649 progress: AsyncMutex::new(HashMap::new()),
1650 auth_challenge: Mutex::new(None),
1651 oauth_response_error: Arc::default(),
1652 auth_flow: Mutex::new(None),
1653 auth_generation: AtomicU64::new(0),
1654 auth_lock: AsyncMutex::new(()),
1655 });
1656 {
1657 let mut servers = self
1658 .inner
1659 .servers
1660 .lock()
1661 .unwrap_or_else(|poisoned| poisoned.into_inner());
1662 servers.insert(name, Arc::clone(&runtime));
1663 }
1664 drop(_previous_auth);
1665 if let Some(previous) = previous
1666 && let Some(mut service) = previous.service.lock().await.take()
1667 {
1668 let _ = service.close_with_timeout(Duration::from_secs(4)).await;
1669 }
1670 if runtime.loaded.config.enabled
1671 && !runtime.loaded.config.lazy
1672 && runtime.current_state() == McpServerState::Disconnected
1673 {
1674 let handle = tokio::runtime::Handle::try_current()
1675 .map_err(|_| ToolError::execution("MCP runtime is unavailable"))?;
1676 self.spawn_connection(runtime, &handle, None)?;
1677 }
1678 Ok(())
1679 }
1680
1681 pub(crate) async fn remove_server(&self, name: &str) -> Result<(), ToolError> {
1682 let server = self.server(name)?;
1683 let _auth = server.auth_lock.lock().await;
1684 if let Some(binding) = OAuthCredentialBinding::from_config(&server.loaded.config) {
1685 self.inner.oauth_credentials.remove(name, &binding)?;
1686 }
1687 server.supersede();
1688 let removed = {
1689 let mut servers = self
1690 .inner
1691 .servers
1692 .lock()
1693 .unwrap_or_else(|poisoned| poisoned.into_inner());
1694 let removed = servers
1695 .get(name)
1696 .cloned()
1697 .ok_or_else(|| ToolError::execution(format!("unknown MCP server `{name}`")))?;
1698 servers.remove(name);
1699 removed
1700 };
1701 if let Some(mut service) = removed.service.lock().await.take() {
1702 let _ = service.close_with_timeout(Duration::from_secs(4)).await;
1703 }
1704 if let Some(flow) = removed
1705 .auth_flow
1706 .lock()
1707 .unwrap_or_else(|poisoned| poisoned.into_inner())
1708 .take()
1709 {
1710 flow.cancellation.cancel();
1711 }
1712 Ok(())
1713 }
1714
1715 pub(crate) async fn reconnect_server(&self, name: &str) -> Result<(), ToolError> {
1716 let server = self.server(name)?;
1717 if !server.loaded.config.enabled {
1718 return Err(ToolError::execution(format!(
1719 "MCP server `{name}` is disabled"
1720 )));
1721 }
1722 server.cancel_tool_refresh();
1723 server.clear_claims();
1724 server
1725 .tools
1726 .lock()
1727 .unwrap_or_else(|poisoned| poisoned.into_inner())
1728 .clear();
1729 if let Some(mut service) = server.service.lock().await.take() {
1730 let _ = service.close_with_timeout(Duration::from_secs(4)).await;
1731 }
1732 server.set_status(McpServerState::Disconnected, None);
1733 match server.connect().await {
1734 Ok(()) => Ok(()),
1735 Err(error) => {
1736 server.fail(error.to_string());
1737 Err(error)
1738 }
1739 }
1740 }
1741
1742 pub(crate) async fn begin_auth(&self, name: &str) -> Result<String, ToolError> {
1743 let server = self.server(name)?;
1744 server.begin_auth().await
1745 }
1746
1747 pub(crate) fn cancel_auth(&self, name: &str) -> Result<(), ToolError> {
1748 let server = self.server(name)?;
1749 let flow = server
1750 .auth_flow
1751 .lock()
1752 .unwrap_or_else(|poisoned| poisoned.into_inner())
1753 .take()
1754 .ok_or_else(|| {
1755 ToolError::execution(format!(
1756 "MCP server `{name}` has no OAuth authorization in progress"
1757 ))
1758 })?;
1759 flow.cancellation.cancel();
1760 server.set_auth_in_progress(false);
1761 server.set_status(
1762 McpServerState::NeedsAuth,
1763 Some("OAuth authorization cancelled; authenticate to connect".into()),
1764 );
1765 Ok(())
1766 }
1767
1768 fn spawn_connection(
1769 &self,
1770 server: Arc<ServerRuntime>,
1771 runtime: &tokio::runtime::Handle,
1772 readiness: Option<EagerConnect>,
1773 ) -> Result<(), ToolError> {
1774 let registry = Arc::clone(&self.inner);
1775 let mut tasks = registry
1776 .connection_tasks
1777 .lock()
1778 .unwrap_or_else(|p| p.into_inner());
1779 if registry.shutdown.is_cancelled() {
1780 return Err(ToolError::execution("MCP registry is shutting down"));
1781 }
1782 let task_registry = Arc::clone(®istry);
1783 let task = runtime.spawn(async move {
1784 let _readiness = readiness;
1785 if let Err(error) = server.connect().await
1786 && !task_registry.shutdown.is_cancelled()
1787 {
1788 server.fail(error.to_string());
1789 }
1790 });
1791 tasks.push(task);
1792 Ok(())
1793 }
1794
1795 fn servers(&self) -> Vec<Arc<ServerRuntime>> {
1796 self.inner
1797 .servers
1798 .lock()
1799 .unwrap_or_else(|p| p.into_inner())
1800 .values()
1801 .cloned()
1802 .collect()
1803 }
1804
1805 fn server(&self, name: &str) -> Result<Arc<ServerRuntime>, ToolError> {
1806 self.inner
1807 .servers
1808 .lock()
1809 .unwrap_or_else(|p| p.into_inner())
1810 .get(name)
1811 .cloned()
1812 .ok_or_else(|| ToolError::execution(format!("unknown MCP server `{name}`")))
1813 }
1814
1815 async fn resolve_tool(
1816 &self,
1817 tool_name: &str,
1818 ) -> Result<(Arc<ServerRuntime>, String), ToolError> {
1819 for server in self.servers() {
1820 if let Some(raw) = server.raw_tool_name(tool_name) {
1821 return Ok((server, raw));
1822 }
1823 }
1824 for server in self.servers() {
1825 if server.loaded.config.lazy
1826 && tool_name.starts_with(&format!("{}_", server.sanitized_name))
1827 {
1828 server.connect().await?;
1829 if let Some(raw) = server.raw_tool_name(tool_name) {
1830 return Ok((server, raw));
1831 }
1832 }
1833 }
1834 Err(ToolError::execution(format!(
1835 "unknown MCP tool `{tool_name}`"
1836 )))
1837 }
1838}
1839
1840impl ServerRuntime {
1841 fn timeout(&self) -> Duration {
1842 Duration::from_millis(self.loaded.config.timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS))
1843 }
1844
1845 fn current_state(&self) -> McpServerState {
1846 self.status
1847 .lock()
1848 .unwrap_or_else(|p| p.into_inner())
1849 .state
1850 .clone()
1851 }
1852
1853 fn set_status(&self, state: McpServerState, message: Option<String>) {
1854 let mut status = self.status.lock().unwrap_or_else(|p| p.into_inner());
1855 status.state = state;
1856 status.message = message;
1857 status.tools = self
1858 .tools
1859 .lock()
1860 .unwrap_or_else(|p| p.into_inner())
1861 .iter()
1862 .map(|tool| tool.spec.name.clone())
1863 .collect();
1864 }
1865
1866 fn set_auth_in_progress(&self, active: bool) {
1867 self.status
1868 .lock()
1869 .unwrap_or_else(|poisoned| poisoned.into_inner())
1870 .auth_in_progress = active;
1871 }
1872
1873 fn fail(&self, message: String) {
1874 let message = cookie_agent_protocol::diagnostics::sanitize(&message, 4096);
1875 if self.superseded.is_cancelled() {
1876 return;
1877 }
1878 if self.current_state() == McpServerState::NeedsAuth {
1879 return;
1880 }
1881 self.clear_claims();
1882 self.tools.lock().unwrap_or_else(|p| p.into_inner()).clear();
1883 self.set_status(McpServerState::Failed, Some(message));
1884 }
1885
1886 fn mark_needs_auth(&self, challenge: Option<String>, reason: &str) {
1887 if self.superseded.is_cancelled() {
1888 return;
1889 }
1890 *self
1891 .auth_challenge
1892 .lock()
1893 .unwrap_or_else(|poisoned| poisoned.into_inner()) = challenge;
1894 self.clear_claims();
1895 self.tools
1896 .lock()
1897 .unwrap_or_else(|poisoned| poisoned.into_inner())
1898 .clear();
1899 let response_error = self
1900 .oauth_response_error
1901 .lock()
1902 .unwrap_or_else(|p| p.into_inner())
1903 .take();
1904 let reason = response_error.as_deref().unwrap_or(reason);
1905 self.set_status(
1906 McpServerState::NeedsAuth,
1907 Some(
1908 cookie_agent_protocol::diagnostics::detail(&format!(
1909 "OAuth authorization required; authenticate to connect\n{reason}"
1910 ))
1911 .to_string(),
1912 ),
1913 );
1914 }
1915
1916 fn supersede(&self) {
1917 self.superseded.cancel();
1918 self.cancel_tool_refresh_task();
1919 if let Some(flow) = self
1920 .auth_flow
1921 .lock()
1922 .unwrap_or_else(|poisoned| poisoned.into_inner())
1923 .take()
1924 {
1925 flow.cancellation.cancel();
1926 }
1927 self.set_auth_in_progress(false);
1928 self.next_list_generation();
1929 self.clear_claims();
1930 }
1931
1932 fn clear_claims(&self) {
1933 if let Some(registry) = self.registry.upgrade() {
1934 registry
1935 .claimed_names
1936 .lock()
1937 .unwrap_or_else(|p| p.into_inner())
1938 .retain(|_, owner| owner != &self.name);
1939 }
1940 }
1941
1942 fn raw_tool_name(&self, generated: &str) -> Option<String> {
1943 self.tools
1944 .lock()
1945 .unwrap_or_else(|p| p.into_inner())
1946 .iter()
1947 .find(|tool| tool.spec.name == generated)
1948 .map(|tool| tool.raw_name.clone())
1949 }
1950
1951 fn oauth_enabled(&self) -> bool {
1952 oauth_enabled_config(&self.loaded.config)
1953 }
1954
1955 fn oauth_binding(&self) -> Result<OAuthCredentialBinding, ToolError> {
1956 OAuthCredentialBinding::from_config(&self.loaded.config)
1957 .ok_or_else(|| ToolError::execution("OAuth is available only for remote MCP servers"))
1958 }
1959
1960 fn oauth_store(&self) -> Result<ServerCredentialStore, ToolError> {
1961 self.registry
1962 .upgrade()
1963 .map(|registry| {
1964 self.oauth_binding()
1965 .map(|binding| registry.oauth_credentials.scoped(&self.name, binding))
1966 })
1967 .transpose()?
1968 .ok_or_else(|| ToolError::execution("MCP registry is unavailable"))
1969 }
1970
1971 async fn authorization_manager(
1972 &self,
1973 ) -> Result<(AuthorizationManager, OAuthExchangeState), ToolError> {
1974 let url = self.loaded.config.url.as_ref().ok_or_else(|| {
1975 ToolError::execution("OAuth is available only for remote MCP servers")
1976 })?;
1977 *self
1978 .oauth_response_error
1979 .lock()
1980 .unwrap_or_else(|p| p.into_inner()) = None;
1981 let authorization_code = OAuthExchangeState {
1982 response_error: self.oauth_response_error.clone(),
1983 ..Default::default()
1984 };
1985 let store = self.oauth_store()?;
1989 let http_client = McpOAuthHttpClient::new(authorization_code.clone(), store.clone())?;
1990 let mut manager =
1991 AuthorizationManager::new_with_oauth_http_client(url, Arc::new(http_client))
1992 .await
1993 .map_err(|error| ToolError::execution(authorization_code.diagnostic(&error)))?;
1994 manager.set_credential_store(store);
1995 Ok((manager, authorization_code))
1996 }
1997
1998 async fn begin_auth(self: &Arc<Self>) -> Result<String, ToolError> {
1999 if !self.oauth_enabled() {
2000 return Err(ToolError::execution(format!(
2001 "MCP server `{}` does not use OAuth (stdio, oauth=false, or a static Authorization header takes precedence)",
2002 self.name
2003 )));
2004 }
2005 if self.current_state() != McpServerState::NeedsAuth {
2006 return Err(ToolError::execution(format!(
2007 "MCP server `{}` does not currently require OAuth authorization",
2008 self.name
2009 )));
2010 }
2011 if let Some(flow) = self
2012 .auth_flow
2013 .lock()
2014 .unwrap_or_else(|poisoned| poisoned.into_inner())
2015 .as_ref()
2016 {
2017 return Ok(flow.authorization_url.clone());
2018 }
2019
2020 let listener = TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0))
2021 .await
2022 .map_err(|_| ToolError::execution("MCP OAuth callback listener failed"))?;
2023 let port = listener
2024 .local_addr()
2025 .map_err(|_| ToolError::execution("MCP OAuth callback listener failed"))?
2026 .port();
2027 let redirect_uri = format!("http://127.0.0.1:{port}/callback");
2028 let store = self.oauth_store()?;
2029 store
2030 .clear()
2031 .await
2032 .map_err(|_| ToolError::execution("MCP OAuth credential reset failed"))?;
2033 let (mut manager, authorization_code) = self.authorization_manager().await?;
2034 let settings = self.loaded.config.oauth.settings();
2035 let mut request = AuthorizationRequest::new(&redirect_uri)
2036 .with_client_name("Cookie Agent")
2037 .with_application_type("native");
2038 let challenge = self
2039 .auth_challenge
2040 .lock()
2041 .unwrap_or_else(|poisoned| poisoned.into_inner())
2042 .clone();
2043 if let Some(challenge) = &challenge {
2044 request = request.with_challenge(challenge);
2045 }
2046 let metadata = manager
2047 .resolve_metadata_from_challenge(challenge.as_deref())
2048 .await
2049 .map_err(|error| ToolError::execution(authorization_code.diagnostic(&error)))?;
2050 manager.set_metadata(metadata.metadata);
2051 request = apply_oauth_settings(request, &settings);
2052 let session = AuthorizationSession::new(manager, request)
2053 .await
2054 .map_err(|(_, error)| ToolError::execution(authorization_code.diagnostic(&error)))?;
2055 let authorization_url = session.get_authorization_url().to_owned();
2056 let cancellation = CancellationToken::new();
2057 let generation = self.auth_generation.fetch_add(1, Ordering::AcqRel) + 1;
2058 {
2059 let mut flow = self
2060 .auth_flow
2061 .lock()
2062 .unwrap_or_else(|poisoned| poisoned.into_inner());
2063 if let Some(existing) = flow.as_ref() {
2064 return Ok(existing.authorization_url.clone());
2065 }
2066 *flow = Some(OAuthFlowState {
2067 generation,
2068 authorization_url: authorization_url.clone(),
2069 cancellation: cancellation.clone(),
2070 });
2071 }
2072 self.set_auth_in_progress(true);
2073 self.set_status(
2074 McpServerState::NeedsAuth,
2075 Some("waiting for OAuth browser callback".into()),
2076 );
2077
2078 let registry = self
2079 .registry
2080 .upgrade()
2081 .ok_or_else(|| ToolError::execution("MCP registry is unavailable"))?;
2082 let server = Arc::clone(self);
2083 let task_registry = Arc::clone(®istry);
2084 let task = tokio::spawn(async move {
2085 let callback = tokio::select! {
2086 result = tokio::time::timeout(OAUTH_CALLBACK_TIMEOUT, receive_oauth_callback(listener)) => {
2087 match result {
2088 Ok(result) => result,
2089 Err(_) => {
2090 if server.finish_auth_flow(generation) {
2091 server.set_status(
2092 McpServerState::NeedsAuth,
2093 Some("OAuth authorization timed out; authenticate to try again".into()),
2094 );
2095 }
2096 return;
2097 }
2098 }
2099 }
2100 () = cancellation.cancelled() => return,
2101 () = task_registry.shutdown.cancelled() => return,
2102 () = server.superseded.cancelled() => return,
2103 };
2104 let Ok((mut browser, callback_url)) = callback else {
2105 if server.finish_auth_flow(generation) {
2106 server.set_status(
2107 McpServerState::NeedsAuth,
2108 Some("invalid OAuth callback; authenticate to try again".into()),
2109 );
2110 }
2111 return;
2112 };
2113 let auth_guard = tokio::select! {
2114 lock = server.auth_lock.lock() => lock,
2115 () = cancellation.cancelled() => return,
2116 () = task_registry.shutdown.cancelled() => return,
2117 () = server.superseded.cancelled() => return,
2118 };
2119 let exchanged = match parse_oauth_callback(&callback_url) {
2120 Ok(callback) => {
2121 authorization_code.install(callback.code);
2122 tokio::select! {
2123 result = session.handle_callback_with_issuer(
2124 REDACTED_AUTHORIZATION_CODE,
2125 &callback.csrf_token,
2126 callback.issuer.as_deref(),
2127 ) => result.map_err(|error| authorization_code.diagnostic(&error)),
2128 () = cancellation.cancelled() => return,
2129 () = task_registry.shutdown.cancelled() => return,
2130 () = server.superseded.cancelled() => return,
2131 }
2132 }
2133 Err(error) => Err(error),
2134 };
2135 drop(auth_guard);
2136 let _ = write_oauth_browser_response(&mut browser, exchanged.is_ok()).await;
2137 if !server.finish_auth_flow(generation) {
2138 return;
2139 }
2140 if let Err(message) = exchanged {
2141 server.set_status(
2142 McpServerState::NeedsAuth,
2143 Some(
2144 cookie_agent_protocol::diagnostics::detail(&format!(
2145 "OAuth authorization failed; authenticate to try again\n{message}"
2146 ))
2147 .to_string(),
2148 ),
2149 );
2150 return;
2151 }
2152 *server
2153 .auth_challenge
2154 .lock()
2155 .unwrap_or_else(|poisoned| poisoned.into_inner()) = None;
2156 server.cancel_tool_refresh();
2157 if let Some(mut service) = server.service.lock().await.take() {
2158 let _ = service.close_with_timeout(Duration::from_secs(4)).await;
2159 }
2160 server.set_status(McpServerState::Disconnected, None);
2161 if let Err(error) = server.connect().await {
2162 server.fail(error.to_string());
2163 }
2164 });
2165 registry
2166 .connection_tasks
2167 .lock()
2168 .unwrap_or_else(|poisoned| poisoned.into_inner())
2169 .push(task);
2170 Ok(authorization_url)
2171 }
2172
2173 fn finish_auth_flow(&self, generation: u64) -> bool {
2174 let mut flow = self
2175 .auth_flow
2176 .lock()
2177 .unwrap_or_else(|poisoned| poisoned.into_inner());
2178 let finished = if flow
2179 .as_ref()
2180 .is_some_and(|flow| flow.generation == generation)
2181 {
2182 *flow = None;
2183 true
2184 } else {
2185 false
2186 };
2187 drop(flow);
2188 if finished {
2189 self.set_auth_in_progress(false);
2190 }
2191 finished
2192 }
2193
2194 async fn connect(self: &Arc<Self>) -> Result<(), ToolError> {
2195 let registry = self
2196 .registry
2197 .upgrade()
2198 .ok_or_else(|| ToolError::execution("MCP registry is unavailable"))?;
2199 if registry.shutdown.is_cancelled() {
2200 return Err(ToolError::execution("MCP registry is shutting down"));
2201 }
2202 if self.superseded.is_cancelled() {
2203 return Err(self.superseded_error());
2204 }
2205 let _active = ActiveConnect::new(Arc::clone(®istry));
2206 let _connect = tokio::select! {
2207 lock = self.connect_lock.lock() => lock,
2208 () = registry.shutdown.cancelled() => {
2209 return Err(ToolError::execution("MCP registry is shutting down"));
2210 }
2211 () = self.superseded.cancelled() => {
2212 return Err(self.superseded_error());
2213 }
2214 };
2215 if registry.shutdown.is_cancelled() {
2216 return Err(ToolError::execution("MCP registry is shutting down"));
2217 }
2218 if self.superseded.is_cancelled() {
2219 return Err(self.superseded_error());
2220 }
2221 match self.current_state() {
2222 McpServerState::Connected => return Ok(()),
2223 McpServerState::Disabled => {
2224 return Err(ToolError::execution(format!(
2225 "MCP server `{}` is disabled",
2226 self.name
2227 )));
2228 }
2229 McpServerState::NeedsAuth => {
2230 return Err(ToolError::execution(format!(
2231 "MCP server `{}` requires OAuth authorization",
2232 self.name
2233 )));
2234 }
2235 McpServerState::Disconnected | McpServerState::Connecting | McpServerState::Failed => {}
2236 }
2237 self.reset_tool_refresh();
2238 self.set_status(McpServerState::Connecting, None);
2239 let handler = McpClientHandler {
2240 server: Arc::downgrade(self),
2241 };
2242 let lifecycle = ClientLifecycleMode::Auto {
2243 preferred_versions: vec![ProtocolVersion::V_2026_07_28],
2244 legacy_version: Some(ProtocolVersion::V_2025_11_25),
2245 };
2246 let timeout = self.timeout();
2247 let mut service = if let Some(command) = &self.loaded.config.command {
2248 let mut process = tokio::process::Command::new(command);
2249 process
2250 .args(&self.loaded.config.args)
2251 .envs(&self.loaded.config.env);
2252 if let Some(cwd) = &self.loaded.config.cwd {
2253 process.current_dir(cwd);
2254 }
2255 let mut wrapped = CommandWrap::from(process);
2256 #[cfg(unix)]
2257 wrapped.wrap(ProcessGroup::leader());
2258 #[cfg(windows)]
2259 wrapped.wrap(JobObject);
2260 let transport = TokioChildProcess::new(wrapped).map_err(|error| {
2261 ToolError::execution(format!(
2262 "MCP server `{}` failed to spawn command `{command}`: {error}",
2263 self.name,
2264 ))
2265 })?;
2266 let cleanup = ChildCleanup::new();
2267 let transport = ReapingChildTransport::new(transport, cleanup.clone());
2268 let outcome = tokio::select! {
2269 result = tokio::time::timeout(timeout, handler.serve_with_lifecycle(transport, lifecycle)) => Some(result),
2270 () = registry.shutdown.cancelled() => None,
2271 () = self.superseded.cancelled() => None,
2272 };
2273 match outcome {
2274 Some(Ok(Ok(service))) => service,
2275 Some(Ok(Err(error))) => {
2276 cleanup.wait().await;
2277 return Err(ToolError::execution(format!(
2278 "MCP server `{}` connect failed: {error}",
2279 self.name
2280 )));
2281 }
2282 Some(Err(_)) => {
2283 cleanup.wait().await;
2284 return Err(ToolError::execution(format!(
2285 "MCP server `{}` connect timed out",
2286 self.name
2287 )));
2288 }
2289 None => {
2290 cleanup.wait().await;
2291 return if self.superseded.is_cancelled() {
2292 Err(self.superseded_error())
2293 } else {
2294 Err(ToolError::execution("MCP registry is shutting down"))
2295 };
2296 }
2297 }
2298 } else {
2299 let url = self
2300 .loaded
2301 .config
2302 .url
2303 .as_ref()
2304 .expect("validated MCP transport");
2305 let mut headers = HashMap::new();
2306 for (name, value) in &self.loaded.config.headers {
2307 headers.insert(
2308 http::HeaderName::try_from(name)
2309 .map_err(|error| ToolError::execution(error.to_string()))?,
2310 http::HeaderValue::try_from(value)
2311 .map_err(|error| ToolError::execution(error.to_string()))?,
2312 );
2313 }
2314 let config = rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig::with_uri(url.clone())
2315 .custom_headers(headers);
2316 let result = if self.oauth_enabled() {
2317 let (mut manager, _authorization_code) = self.authorization_manager().await?;
2318 let restored = manager
2319 .initialize_from_store()
2320 .await
2321 .map_err(|_| ToolError::execution("MCP OAuth credential loading failed"))?;
2322 let settings = self.loaded.config.oauth.settings();
2323 if restored && let Some(client_id) = settings.client_id {
2324 let mut client = OAuthClientConfig::new(client_id, "http://127.0.0.1")
2325 .with_scopes(settings.scopes);
2326 if let Some(client_secret) = settings.client_secret {
2327 client = client.with_client_secret(client_secret);
2328 }
2329 manager.configure_client(client).map_err(|_| {
2330 ToolError::execution("MCP OAuth stored-client setup failed")
2331 })?;
2332 }
2333 let client = reqwest::Client::builder()
2334 .pool_max_idle_per_host(0)
2335 .redirect(reqwest::redirect::Policy::none())
2336 .build()
2337 .map_err(|_| ToolError::execution("MCP HTTP client setup failed"))?;
2338 let transport = StreamableHttpClientTransport::with_client(
2339 AuthClient::new(client, manager),
2340 config,
2341 );
2342 tokio::select! {
2343 result = tokio::time::timeout(timeout, handler.serve_with_lifecycle(transport, lifecycle)) => result,
2344 () = registry.shutdown.cancelled() => {
2345 return Err(ToolError::execution("MCP registry is shutting down"));
2346 }
2347 () = self.superseded.cancelled() => {
2348 return Err(self.superseded_error());
2349 }
2350 }
2351 } else {
2352 let transport = StreamableHttpClientTransport::from_config(config);
2353 tokio::select! {
2354 result = tokio::time::timeout(timeout, handler.serve_with_lifecycle(transport, lifecycle)) => result,
2355 () = registry.shutdown.cancelled() => {
2356 return Err(ToolError::execution("MCP registry is shutting down"));
2357 }
2358 () = self.superseded.cancelled() => {
2359 return Err(self.superseded_error());
2360 }
2361 }
2362 };
2363 match result {
2364 Err(_) => {
2365 return Err(ToolError::execution(format!(
2366 "MCP server `{}` connect timed out",
2367 self.name
2368 )));
2369 }
2370 Ok(Err(error))
2371 if self.oauth_enabled() && initialize_error_requires_auth(&error) =>
2372 {
2373 self.mark_needs_auth(
2374 error.auth_challenge().map(str::to_owned),
2375 &cookie_agent_protocol::diagnostics::error_chain(&error),
2376 );
2377 return Err(ToolError::execution(format!(
2378 "MCP server `{}` requires OAuth authorization",
2379 self.name
2380 )));
2381 }
2382 Ok(Err(error)) => {
2383 return Err(ToolError::execution(format!(
2384 "MCP server `{}` connect failed: {error}",
2385 self.name
2386 )));
2387 }
2388 Ok(Ok(service)) => service,
2389 }
2390 };
2391 let generation = self.next_list_generation();
2392 let tools = tokio::select! {
2393 result = tokio::time::timeout(timeout, service.peer().list_all_tools()) => result,
2394 () = registry.shutdown.cancelled() => {
2395 let _ = service.close_with_timeout(Duration::from_secs(4)).await;
2396 return Err(ToolError::execution("MCP registry is shutting down"));
2397 }
2398 () = self.superseded.cancelled() => {
2399 let _ = service.close_with_timeout(Duration::from_secs(4)).await;
2400 return Err(self.superseded_error());
2401 }
2402 }
2403 .map_err(|_| {
2404 ToolError::execution(format!("MCP server `{}` tools/list timed out", self.name))
2405 })?;
2406 let tools = match tools {
2407 Ok(tools) => tools,
2408 Err(error) if self.oauth_enabled() && service_error_requires_auth(&error) => {
2409 self.mark_needs_auth(
2410 auth_challenge(&error),
2411 &cookie_agent_protocol::diagnostics::error_chain(&error),
2412 );
2413 let _ = service.close_with_timeout(Duration::from_secs(4)).await;
2414 return Err(ToolError::execution(format!(
2415 "MCP server `{}` requires OAuth authorization",
2416 self.name
2417 )));
2418 }
2419 Err(error) => {
2420 return Err(ToolError::execution(format!(
2421 "MCP server `{}` tools/list failed: {error}",
2422 self.name
2423 )));
2424 }
2425 };
2426 self.publish_tools(generation, tools);
2427 if self.current_state() == McpServerState::Failed {
2428 return Err(ToolError::execution(
2429 self.status
2430 .lock()
2431 .unwrap_or_else(|p| p.into_inner())
2432 .message
2433 .clone()
2434 .unwrap_or_default(),
2435 ));
2436 }
2437 let mut installed = self.service.lock().await;
2438 if registry.shutdown.is_cancelled() || self.superseded.is_cancelled() {
2439 drop(installed);
2440 let _ = service.close_with_timeout(Duration::from_secs(4)).await;
2441 return if self.superseded.is_cancelled() {
2442 Err(self.superseded_error())
2443 } else {
2444 Err(ToolError::execution("MCP registry is shutting down"))
2445 };
2446 }
2447 *installed = Some(service);
2448 drop(installed);
2449 self.set_status(McpServerState::Connected, None);
2450 Ok(())
2451 }
2452
2453 fn next_list_generation(&self) -> u64 {
2454 let _publication = self.publication.lock().unwrap_or_else(|p| p.into_inner());
2455 self.list_generation.fetch_add(1, Ordering::AcqRel) + 1
2456 }
2457
2458 fn schedule_tool_refresh(self: &Arc<Self>, peer: Peer<RoleClient>) {
2459 let Some(registry) = self.registry.upgrade() else {
2460 return;
2461 };
2462 let mut refresh = self
2463 .tool_refresh
2464 .lock()
2465 .unwrap_or_else(|poisoned| poisoned.into_inner());
2466 if refresh.cancellation.is_cancelled() {
2467 return;
2468 }
2469 refresh.peer = Some(peer);
2470 if refresh.worker.is_none() {
2471 let server = Arc::downgrade(self);
2472 let notify = Arc::clone(&refresh.notify);
2473 let cancellation = refresh.cancellation.clone();
2474 let shutdown = registry.shutdown.clone();
2475 let superseded = self.superseded.clone();
2476 #[cfg(test)]
2477 let armed = Arc::clone(&refresh.armed);
2478 refresh.worker = Some(tokio::spawn(async move {
2479 loop {
2480 tokio::select! {
2481 () = notify.notified() => {}
2482 () = cancellation.cancelled() => return,
2483 () = shutdown.cancelled() => return,
2484 () = superseded.cancelled() => return,
2485 }
2486 loop {
2487 #[cfg(test)]
2488 armed.fetch_add(1, Ordering::Release);
2489 tokio::select! {
2490 () = tokio::time::sleep(TOOL_LIST_DEBOUNCE) => break,
2491 () = notify.notified() => {}
2492 () = cancellation.cancelled() => return,
2493 () = shutdown.cancelled() => return,
2494 () = superseded.cancelled() => return,
2495 }
2496 }
2497
2498 let Some(server) = server.upgrade() else {
2499 return;
2500 };
2501 if cancellation.is_cancelled() {
2502 return;
2503 }
2504 let peer = {
2505 let refresh = server
2506 .tool_refresh
2507 .lock()
2508 .unwrap_or_else(|poisoned| poisoned.into_inner());
2509 if !Arc::ptr_eq(&refresh.notify, ¬ify) {
2510 return;
2511 }
2512 refresh.peer.clone()
2513 };
2514 if let Some(peer) = peer
2515 && !peer.is_transport_closed()
2516 {
2517 server.refresh_tools(peer, &cancellation, &shutdown).await;
2518 }
2519 }
2520 }));
2521 }
2522 refresh.notify.notify_one();
2523 }
2524
2525 async fn refresh_tools(
2526 &self,
2527 peer: Peer<RoleClient>,
2528 cancellation: &CancellationToken,
2529 shutdown: &CancellationToken,
2530 ) {
2531 let generation = self.next_list_generation();
2532 let timeout = self.timeout();
2533 let result = tokio::select! {
2534 result = tokio::time::timeout(timeout, peer.list_all_tools()) => result,
2535 () = cancellation.cancelled() => return,
2536 () = shutdown.cancelled() => return,
2537 () = self.superseded.cancelled() => return,
2538 };
2539 if cancellation.is_cancelled() || shutdown.is_cancelled() {
2540 return;
2541 }
2542 match result {
2543 Ok(Ok(tools)) => self.publish_tools(generation, tools),
2544 Ok(Err(error)) if self.oauth_enabled() && service_error_requires_auth(&error) => {
2545 self.mark_needs_auth(
2546 auth_challenge(&error),
2547 &cookie_agent_protocol::diagnostics::error_chain(&error),
2548 );
2549 }
2550 Ok(Err(error)) => self
2551 .publish_refresh_failure(generation, format!("tools/list refresh failed: {error}")),
2552 Err(_) => {
2553 self.publish_refresh_failure(generation, "tools/list refresh timed out".into());
2554 }
2555 }
2556 }
2557
2558 fn reset_tool_refresh(&self) {
2559 let mut refresh = self
2560 .tool_refresh
2561 .lock()
2562 .unwrap_or_else(|poisoned| poisoned.into_inner());
2563 refresh.cancel();
2564 *refresh = ToolRefreshDebounce::default();
2565 }
2566
2567 fn cancel_tool_refresh_task(&self) {
2568 let mut refresh = self
2569 .tool_refresh
2570 .lock()
2571 .unwrap_or_else(|poisoned| poisoned.into_inner());
2572 refresh.cancel();
2573 }
2574
2575 fn cancel_tool_refresh(&self) {
2576 self.cancel_tool_refresh_task();
2577 self.next_list_generation();
2578 }
2579
2580 fn superseded_error(&self) -> ToolError {
2581 ToolError::execution(format!("MCP server `{}` was superseded", self.name))
2582 }
2583
2584 fn publish_tools(&self, generation: u64, tools: Vec<Tool>) {
2585 self.publish_tools_before_commit(generation, tools, || {});
2586 }
2587
2588 fn publish_refresh_failure(&self, generation: u64, message: String) {
2589 self.publish_refresh_failure_before_commit(generation, message, || {});
2590 }
2591
2592 fn publish_refresh_failure_before_commit(
2593 &self,
2594 generation: u64,
2595 message: String,
2596 before_commit: impl FnOnce(),
2597 ) {
2598 before_commit();
2599 let _publication = self.publication.lock().unwrap_or_else(|p| p.into_inner());
2600 if self.list_generation.load(Ordering::Acquire) == generation {
2601 self.fail(message);
2602 }
2603 }
2604
2605 fn publish_tools_before_commit(
2606 &self,
2607 generation: u64,
2608 tools: Vec<Tool>,
2609 before_commit: impl FnOnce(),
2610 ) {
2611 let converted = tools
2612 .into_iter()
2613 .map(|tool| convert_tool(&self.name, &self.sanitized_name, tool))
2614 .collect::<Result<Vec<_>, _>>();
2615 before_commit();
2616 let publication = self.publication.lock().unwrap_or_else(|p| p.into_inner());
2617 if self.superseded.is_cancelled()
2618 || self.list_generation.load(Ordering::Acquire) != generation
2619 {
2620 return;
2621 }
2622 let converted = match converted {
2623 Ok(tools) => tools,
2624 Err(error) => {
2625 self.fail(error.to_string());
2626 return;
2627 }
2628 };
2629 let unchanged = {
2630 let current = self
2631 .tools
2632 .lock()
2633 .unwrap_or_else(|poisoned| poisoned.into_inner());
2634 current.len() == converted.len()
2635 && current.iter().zip(&converted).all(|(current, refreshed)| {
2636 current.raw_name == refreshed.raw_name
2637 && current.spec.name == refreshed.spec.name
2638 && current.spec.description == refreshed.spec.description
2639 && current.spec.parameters == refreshed.spec.parameters
2640 })
2641 };
2642 if unchanged {
2643 return;
2644 }
2645 let Some(registry) = self.registry.upgrade() else {
2646 return;
2647 };
2648 let reserved = registry
2649 .reserved_names
2650 .lock()
2651 .unwrap_or_else(|p| p.into_inner());
2652 let mut claimed = registry
2653 .claimed_names
2654 .lock()
2655 .unwrap_or_else(|p| p.into_inner());
2656 claimed.retain(|_, owner| owner != &self.name);
2657 let mut local = HashSet::new();
2658 let mut preempted_plugins = Vec::new();
2659 for tool in &converted {
2660 let claimed_by_plugin = claimed
2661 .get(&tool.spec.name)
2662 .and_then(|owner| owner.strip_prefix("plugin:"));
2663 if reserved.contains(&tool.spec.name) || !local.insert(tool.spec.name.clone()) {
2664 drop(claimed);
2665 drop(reserved);
2666 self.fail(format!(
2667 "MCP server `{}` generated colliding tool name `{}`",
2668 self.name, tool.spec.name
2669 ));
2670 return;
2671 }
2672 if let Some(plugin) = claimed_by_plugin {
2673 preempted_plugins.push((plugin.to_owned(), tool.spec.name.clone()));
2674 } else if claimed.contains_key(&tool.spec.name) {
2675 drop(claimed);
2676 drop(reserved);
2677 self.fail(format!(
2678 "MCP server `{}` generated colliding tool name `{}`",
2679 self.name, tool.spec.name
2680 ));
2681 return;
2682 }
2683 }
2684 for tool in &converted {
2685 claimed.insert(tool.spec.name.clone(), self.name.clone());
2686 }
2687 drop(claimed);
2688 drop(reserved);
2689 let collision_handler = registry
2690 .plugin_collision_handler
2691 .lock()
2692 .unwrap_or_else(|poisoned| poisoned.into_inner())
2693 .clone();
2694 if let Some(handler) = collision_handler {
2695 for (plugin, tool) in preempted_plugins {
2696 handler(&plugin, &tool, None);
2697 }
2698 }
2699 *self.tools.lock().unwrap_or_else(|p| p.into_inner()) = converted;
2700 drop(publication);
2701 if self.current_state() == McpServerState::Connected {
2702 self.set_status(McpServerState::Connected, None);
2703 }
2704 }
2705}
2706
2707#[async_trait]
2708impl ToolProvider for McpRegistry {
2709 fn provider_id(&self) -> &'static str {
2710 "mcp"
2711 }
2712
2713 fn tools_for_session(&self, _ctx: &SessionToolContext) -> Result<Vec<ToolSpec>, ToolError> {
2714 Ok(self
2715 .servers()
2716 .into_iter()
2717 .flat_map(|server| {
2718 server
2719 .tools
2720 .lock()
2721 .unwrap_or_else(|p| p.into_inner())
2722 .iter()
2723 .map(|tool| tool.spec.clone())
2724 .collect::<Vec<_>>()
2725 })
2726 .collect())
2727 }
2728
2729 fn get_permission_name(_tool_name: &str) -> Result<&'static str, ToolError> {
2730 Ok("mcp")
2731 }
2732
2733 fn permission_for_unlisted_tool(
2734 &self,
2735 tool_name: &str,
2736 ) -> Result<Option<&'static str>, ToolError> {
2737 Ok(self
2738 .servers()
2739 .into_iter()
2740 .any(|server| tool_name.starts_with(&format!("{}_", server.sanitized_name)))
2741 .then_some("mcp"))
2742 }
2743
2744 fn get_permission_resource(
2745 &self,
2746 tool_name: &str,
2747 _arguments: &Value,
2748 ) -> Result<(&'static str, Option<String>), ToolError> {
2749 let owned = self
2750 .servers()
2751 .into_iter()
2752 .any(|server| tool_name.starts_with(&format!("{}_", server.sanitized_name)));
2753 if !owned {
2754 return Err(ToolError::execution(format!(
2755 "unknown MCP tool `{tool_name}`"
2756 )));
2757 }
2758 Ok(("mcp", Some(tool_name.to_owned())))
2759 }
2760
2761 fn get_display_argument(&self, name: &str, _arguments: &Value) -> Result<String, ToolError> {
2762 Ok(name.to_owned())
2763 }
2764
2765 async fn prepare(
2766 &self,
2767 ctx: ToolPreparationContext,
2768 call: ToolCall,
2769 ) -> Result<PreparedTool, ToolError> {
2770 let (server, raw_name) = self.resolve_tool(&call.name).await?;
2771 let arguments = call
2772 .arguments
2773 .as_object()
2774 .cloned()
2775 .ok_or_else(|| ToolError::execution("MCP tool arguments must be an object"))?;
2776 let normalized = Value::Object(arguments.clone());
2777 let argument_bytes = serde_json::to_vec(&normalized)
2778 .map_err(|error| ToolError::execution(error.to_string()))?;
2779 let label = call.name.clone();
2780 let resource = PreparedApprovalResource {
2781 capability: PermissionAction::Mcp,
2782 canonical: PreparedResourceIdentity::new(format!(
2783 "mcp-tool:{}",
2784 Sha256Digest::of_bytes(label.as_bytes())
2785 ))
2786 .map_err(|error| ToolError::execution(error.to_string()))?,
2787 binding_digest: PreparedResourceDigest::from_canonical_binding_bytes(label.as_bytes()),
2788 binding_lifetime: PreparedBindingLifetime::RestartStable,
2789 boundary: ApprovalBoundary::Exact,
2790 source: ApprovalResourceSource::PrimaryOperation,
2791 };
2792 let operation = PreparedOperationIdentity::new(
2793 Sha256Digest::of_bytes(&argument_bytes),
2794 vec![ApprovalCapability {
2795 action: PermissionAction::Mcp,
2796 operation: PreparedCapabilityOperation::new(format!("{}:call", call.name))
2797 .map_err(|error| ToolError::execution(error.to_string()))?,
2798 }],
2799 vec![resource],
2800 Sha256Digest::of_bytes(ctx.workspace_root.to_string_lossy().as_bytes()),
2801 )
2802 .map_err(|error| ToolError::execution(error.to_string()))?;
2803 PreparedTool::new(
2804 operation,
2805 normalized,
2806 None,
2807 Box::new(McpExecutor {
2808 server,
2809 generated_name: call.name,
2810 raw_name,
2811 arguments,
2812 call_id: call.id,
2813 }),
2814 )
2815 }
2816}
2817
2818struct McpExecutor {
2819 server: Arc<ServerRuntime>,
2820 generated_name: String,
2821 raw_name: String,
2822 arguments: Map<String, Value>,
2823 call_id: ToolCallId,
2824}
2825
2826#[async_trait]
2827impl PreparedExecutor for McpExecutor {
2828 async fn revalidate(&self) -> Result<(), ToolError> {
2829 if self.server.raw_tool_name(&self.generated_name).as_deref() != Some(&self.raw_name) {
2830 return Err(ToolError::operation_changed(
2831 "MCP tool list changed after approval",
2832 ));
2833 }
2834 Ok(())
2835 }
2836
2837 async fn execute(
2838 self: Box<Self>,
2839 context: ToolExecutionContext,
2840 ) -> Result<crate::ToolCompletion, ToolError> {
2841 let result: Result<ToolResult, ToolError> = async move {
2842 self.revalidate().await?;
2843 let params = CallToolRequestParams::new(self.raw_name.clone())
2844 .with_arguments(self.arguments.clone());
2845 let timeout = self.server.timeout();
2846 let service = self.server.service.lock().await;
2847 let Some(service) = service.as_ref() else {
2848 return Err(ToolError::execution("MCP server is not connected"));
2849 };
2850 self.server
2851 .progress
2852 .lock()
2853 .await
2854 .insert(self.call_id, context.progress.clone());
2855 let call = service.call_tool(params);
2856 tokio::pin!(call);
2857 let result = tokio::select! {
2858 result = &mut call => match result {
2859 Err(error)
2860 if self.server.oauth_enabled() && service_error_requires_auth(&error) =>
2861 {
2862 self.server.mark_needs_auth(auth_challenge(&error), &cookie_agent_protocol::diagnostics::error_chain(&error));
2863 Err(ToolError::execution(format!(
2864 "MCP server `{}` requires OAuth authorization",
2865 self.server.name
2866 )))
2867 }
2868 Err(error) => Err(ToolError::execution(error.to_string())),
2869 Ok(result) => Ok(result),
2870 },
2871 _ = context.cancellation.cancelled() => Err(ToolError::execution("MCP tool call cancelled")),
2872 _ = tokio::time::sleep(timeout) => Err(ToolError::execution("MCP tool call timed out")),
2873 };
2874 self.server.progress.lock().await.remove(&self.call_id);
2875 map_tool_result(&context, &self.generated_name, result?)
2876}.await;
2877 result.map(|result| {
2878 let failed = result.metadata["mcp"]["is_error"] == true;
2879 let mut completion = crate::ToolCompletion::single(result);
2880 completion.failed = failed;
2881 completion
2882 })
2883 }
2884}
2885
2886fn convert_tool(server: &str, sanitized_server: &str, tool: Tool) -> Result<CachedTool, ToolError> {
2887 let generated = format!("{}_{}", sanitized_server, sanitize_name(tool.name.as_ref()));
2888 let description = format!(
2889 "[{server}] {} (untrusted MCP output)",
2890 tool.description.as_deref().unwrap_or("MCP tool")
2891 );
2892 let mut schema = tool.input_schema.as_ref().clone();
2893 schema.insert("type".into(), Value::String("object".into()));
2894 schema.insert("additionalProperties".into(), Value::Bool(false));
2895 Ok(CachedTool {
2896 raw_name: tool.name.into_owned(),
2897 spec: ToolSpec {
2898 output: Default::default(),
2899 concurrency: ToolConcurrency::Parallel,
2900 result_truncation: Default::default(),
2902 name: generated,
2903 permission_name: "mcp".into(),
2904 description,
2905 parameters: Value::Object(schema),
2906 },
2907 })
2908}
2909
2910fn map_tool_result(
2911 context: &ToolExecutionContext,
2912 tool_name: &str,
2913 result: rmcp::model::CallToolResult,
2914) -> Result<ToolResult, ToolError> {
2915 let mut output = Vec::new();
2916 let mut attachments = Vec::new();
2917 let mut emitted_attachments = Vec::new();
2918 for block in result.content {
2919 match block {
2920 ContentBlock::Text(text) => output.push(text.text),
2921 ContentBlock::Image(image) => {
2922 if !mcp_attachment_budget_remaining(&attachments, &emitted_attachments) {
2923 output.push(
2924 "[MCP image attachment omitted: combined attachment limit reached]".into(),
2925 );
2926 continue;
2927 }
2928 match retain_base64_attachment(context, image.mime_type, &image.data) {
2929 Ok(retained) => {
2930 output.push(
2931 if route_mcp_attachment(
2932 retained,
2933 &mut attachments,
2934 &mut emitted_attachments,
2935 ) {
2936 "[MCP image attachment]".into()
2937 } else {
2938 "[MCP image attachment omitted: combined attachment limit reached]"
2939 .into()
2940 },
2941 );
2942 }
2943 Err(McpAttachmentError::Invalid(error)) => {
2944 output.push(format!("[Invalid MCP image content: {error}]"));
2945 }
2946 Err(McpAttachmentError::Gated(error)) => {
2947 return Err(ToolError::Failed(error));
2948 }
2949 }
2950 }
2951 ContentBlock::Audio(audio) => {
2952 if !mcp_attachment_budget_remaining(&attachments, &emitted_attachments) {
2953 output.push(
2954 "[MCP audio attachment omitted: combined attachment limit reached]".into(),
2955 );
2956 continue;
2957 }
2958 match retain_base64_attachment(context, audio.mime_type, &audio.data) {
2959 Ok(retained) => {
2960 output.push(
2961 if route_mcp_attachment(
2962 retained,
2963 &mut attachments,
2964 &mut emitted_attachments,
2965 ) {
2966 "[MCP audio attachment]".into()
2967 } else {
2968 "[MCP audio attachment omitted: combined attachment limit reached]"
2969 .into()
2970 },
2971 );
2972 }
2973 Err(McpAttachmentError::Invalid(error)) => {
2974 output.push(format!("[Invalid MCP audio content: {error}]"));
2975 }
2976 Err(McpAttachmentError::Gated(error)) => {
2977 return Err(ToolError::Failed(error));
2978 }
2979 }
2980 }
2981 ContentBlock::Resource(resource) => match resource.resource {
2982 ResourceContents::TextResourceContents { uri, text, .. } => {
2983 output.push(format!("[{uri}]\n{text}"));
2984 }
2985 ResourceContents::BlobResourceContents {
2986 uri,
2987 mime_type,
2988 blob,
2989 ..
2990 } => {
2991 if !mcp_attachment_budget_remaining(&attachments, &emitted_attachments) {
2992 output.push(format!(
2993 "[MCP embedded resource unavailable: {uri}: combined attachment limit reached]"
2994 ));
2995 continue;
2996 }
2997 match retain_base64_attachment(
2998 context,
2999 mime_type.unwrap_or_else(|| "application/octet-stream".into()),
3000 &blob,
3001 ) {
3002 Ok(retained) => {
3003 let routed = route_mcp_attachment(
3004 retained,
3005 &mut attachments,
3006 &mut emitted_attachments,
3007 );
3008 output.push(if routed {
3009 format!("[MCP embedded resource attachment: {uri}]")
3010 } else {
3011 format!(
3012 "[MCP embedded resource unavailable: {uri}: combined attachment limit reached]"
3013 )
3014 });
3015 }
3016 Err(McpAttachmentError::Invalid(error)) => output.push(format!(
3017 "[MCP embedded resource unavailable: {uri}: {error}]"
3018 )),
3019 Err(McpAttachmentError::Gated(error)) => {
3020 return Err(ToolError::Failed(error));
3021 }
3022 }
3023 }
3024 _ => output.push("[Unsupported MCP embedded resource]".into()),
3025 },
3026 ContentBlock::ResourceLink(link) => {
3027 output.push(format!("[MCP resource link: {} ({})]", link.name, link.uri))
3028 }
3029 _ => output.push("[Unsupported MCP content block]".into()),
3030 }
3031 }
3032 let text_output = output.join("\n");
3033 let structured_content = result.structured_content.filter(|structured| {
3034 !output
3035 .iter()
3036 .chain(std::iter::once(&text_output))
3037 .any(|text| serde_json::from_str::<Value>(text).ok().as_ref() == Some(structured))
3038 });
3039 let mut metadata = serde_json::json!({
3040 "mcp": {
3041 "is_error": result.is_error.unwrap_or(false),
3042 }
3043 });
3044 if let Some(structured) = structured_content {
3045 metadata["mcp"]["structured_content"] = structured;
3046 }
3047 let additional_messages = if emitted_attachments.is_empty() {
3048 Vec::new()
3049 } else {
3050 vec![
3051 ToolEmittedMessage::new(
3052 ToolEmittedMessageRole::User,
3053 emitted_attachments
3054 .into_iter()
3055 .map(ToolEmittedContent::File)
3056 .collect(),
3057 )
3058 .map_err(|error| ToolError::execution(error.to_string()))?,
3059 ]
3060 };
3061 Ok(ToolResult {
3062 display: None,
3063 retained_output: None,
3064 title: safe_title(tool_name),
3065 output: text_output,
3066 metadata,
3067 truncation: None,
3068 attachments,
3069 additional_messages,
3070 })
3071}
3072
3073#[derive(Debug)]
3074enum McpAttachmentError {
3075 Invalid(String),
3077 Gated(String),
3079}
3080
3081#[derive(Debug)]
3082struct RetainedMcpAttachment {
3083 attachment: ToolAttachment,
3084 delivery: crate::media::AttachmentGate,
3085}
3086
3087fn route_mcp_attachment(
3088 retained: RetainedMcpAttachment,
3089 attachments: &mut Vec<ToolAttachment>,
3090 emitted_attachments: &mut Vec<ToolAttachment>,
3091) -> bool {
3092 if !mcp_attachment_budget_remaining(attachments, emitted_attachments) {
3093 return false;
3094 }
3095 match retained.delivery {
3096 crate::media::AttachmentGate::AttachToolResult => attachments.push(retained.attachment),
3097 crate::media::AttachmentGate::DeliverViaUserTurn => {
3098 emitted_attachments.push(retained.attachment);
3099 }
3100 crate::media::AttachmentGate::RejectUnsupportedModel
3101 | crate::media::AttachmentGate::RejectUnsupportedFamily
3102 | crate::media::AttachmentGate::RejectTooLarge { .. } => {
3103 unreachable!("rejected MCP attachment gates return an error")
3104 }
3105 }
3106 true
3107}
3108
3109fn mcp_attachment_budget_remaining(
3110 attachments: &[ToolAttachment],
3111 emitted_attachments: &[ToolAttachment],
3112) -> bool {
3113 attachments.len().saturating_add(emitted_attachments.len()) < ToolResult::MAX_ATTACHMENTS
3114}
3115
3116fn retain_base64_attachment(
3117 context: &ToolExecutionContext,
3118 mime_type: String,
3119 data: &str,
3120) -> Result<RetainedMcpAttachment, McpAttachmentError> {
3121 let bytes = base64::engine::general_purpose::STANDARD
3122 .decode(data)
3123 .map_err(|error| McpAttachmentError::Invalid(error.to_string()))?;
3124 let mut delivery = crate::media::AttachmentGate::AttachToolResult;
3125 let sniffed = crate::media::approved_media_type(std::path::Path::new(""), &bytes)
3126 .map_err(|error| McpAttachmentError::Invalid(error.to_string()))?;
3127 let retained_mime = if mime_type.is_empty() || mime_type == "application/octet-stream" {
3128 sniffed.unwrap_or(mime_type.as_str())
3129 } else {
3130 mime_type.as_str()
3131 };
3132 let attachment = if let Some(mime) = sniffed {
3133 if crate::media::canonical_video_mime_type(mime)
3134 != crate::media::canonical_video_mime_type(retained_mime)
3135 {
3136 return Err(McpAttachmentError::Invalid(format!(
3137 "attachment MIME mismatch: declared {retained_mime}, validated {mime}"
3138 )));
3139 }
3140 let gate = crate::media::gate_attachment(
3141 context.turn_context.adapter_family,
3142 &context.turn_context.capabilities,
3143 mime,
3144 &bytes,
3145 );
3146 if let Some(error) = crate::media::attachment_gate_error(
3147 gate,
3148 mime,
3149 &context.turn_context.model,
3150 context.turn_context.adapter,
3151 ) {
3152 return Err(McpAttachmentError::Gated(error));
3153 }
3154 delivery = gate;
3155 context.retain_validated_attachment(retained_mime, None, &bytes)
3156 } else {
3157 context.retain_attachment(retained_mime, None, &bytes)
3158 }
3159 .map_err(|error| McpAttachmentError::Invalid(error.to_string()))?;
3160 Ok(RetainedMcpAttachment {
3161 attachment,
3162 delivery,
3163 })
3164}
3165
3166fn sanitize_name(value: &str) -> String {
3167 value
3168 .chars()
3169 .map(|character| {
3170 if character.is_ascii_alphanumeric() || matches!(character, '_' | '-') {
3171 character
3172 } else {
3173 '_'
3174 }
3175 })
3176 .collect()
3177}
3178
3179fn safe_title(value: &str) -> SafeDisplayText {
3180 let mut title = String::new();
3181 for character in value.chars() {
3182 let character = if character.is_control() {
3183 ' '
3184 } else {
3185 character
3186 };
3187 if title.len() + character.len_utf8() > SafeDisplayText::MAX_BYTES {
3188 break;
3189 }
3190 title.push(character);
3191 }
3192 SafeDisplayText::new(title).expect("sanitized MCP title")
3193}
3194
3195#[cfg(test)]
3196mod tests;
3197
3198#[cfg(test)]
3199#[cfg(unix)]
3200#[path = "mcp/oauth_tests.rs"]
3201mod oauth_tests;
3202
3203#[cfg(all(test, windows))]
3204mod windows_oauth_tests {
3205 use super::OAuthCredentialFile;
3206
3207 #[test]
3208 fn oauth_store_is_acl_protected_on_windows() {
3209 let temporary = tempfile::tempdir().expect("temporary root");
3210 let path = temporary.path().join("oauth").join("mcp-oauth.json");
3211 let store = OAuthCredentialFile::open(path.clone()).expect("OAuth store");
3212 store.update(|_| {}).expect("persist empty store");
3213 cookie_agent_models::secure_store::verify_windows_private_creation(&path)
3214 .expect("store ACL");
3215 cookie_agent_models::secure_store::verify_windows_private_creation(
3216 &path.parent().unwrap().join("mcp-oauth.lock"),
3217 )
3218 .expect("lock ACL");
3219 }
3220}