1#![forbid(unsafe_code)]
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::future::Future;
5use std::path::PathBuf;
6use std::pin::Pin;
7use std::process::ExitStatus;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::Arc;
10use std::time::Duration;
11
12use futures_core::Stream;
13
14mod agent_kind;
15
16use crate::mcp::{
17 normalize_mcp_add_request, normalize_mcp_get_request, normalize_mcp_remove_request,
18 AgentWrapperMcpAddRequest, AgentWrapperMcpCommandOutput, AgentWrapperMcpGetRequest,
19 AgentWrapperMcpListRequest, AgentWrapperMcpRemoveRequest, CAPABILITY_MCP_ADD_V1,
20 CAPABILITY_MCP_GET_V1, CAPABILITY_MCP_LIST_V1, CAPABILITY_MCP_REMOVE_V1,
21};
22use agent_kind::validate_agent_kind;
23
24#[cfg(any(
25 feature = "codex",
26 feature = "claude_code",
27 feature = "aider",
28 feature = "gemini_cli",
29 feature = "opencode"
30))]
31mod bounds;
32
33#[cfg(any(
34 feature = "codex",
35 feature = "claude_code",
36 feature = "aider",
37 feature = "gemini_cli",
38 feature = "opencode"
39))]
40mod run_handle_gate;
41
42#[cfg(any(
43 feature = "codex",
44 feature = "claude_code",
45 feature = "aider",
46 feature = "gemini_cli",
47 feature = "opencode"
48))]
49mod backend_harness;
50
51pub mod backends;
52pub mod mcp;
53
54const CAPABILITY_CONTROL_CANCEL_V1: &str = "agent_api.control.cancel.v1";
55#[allow(dead_code)]
56#[cfg(any(
57 feature = "codex",
58 feature = "claude_code",
59 feature = "aider",
60 feature = "gemini_cli",
61 feature = "opencode",
62 test
63))]
64pub(crate) const EXT_AGENT_API_CONFIG_MODEL_V1: &str = "agent_api.config.model.v1";
65
66#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
67pub struct AgentWrapperKind(String);
68
69impl AgentWrapperKind {
70 pub fn new(value: impl Into<String>) -> Result<Self, AgentWrapperError> {
74 let value = value.into();
75 validate_agent_kind(&value)?;
76 Ok(Self(value))
77 }
78
79 pub fn as_str(&self) -> &str {
81 &self.0
82 }
83}
84
85#[derive(Clone, Debug, Default, Eq, PartialEq)]
86pub struct AgentWrapperCapabilities {
87 pub ids: BTreeSet<String>,
89}
90
91impl AgentWrapperCapabilities {
92 pub fn contains(&self, capability_id: &str) -> bool {
93 self.ids.contains(capability_id)
94 }
95}
96
97#[derive(Clone, Debug, Eq, PartialEq)]
98pub enum AgentWrapperEventKind {
99 TextOutput,
100 ToolCall,
101 ToolResult,
102 Status,
103 Error,
104 Unknown,
105}
106
107#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct AgentWrapperEvent {
109 pub agent_kind: AgentWrapperKind,
110 pub kind: AgentWrapperEventKind,
111 pub channel: Option<String>,
112 pub text: Option<String>,
114 pub message: Option<String>,
116 pub data: Option<serde_json::Value>,
117}
118
119#[derive(Clone, Debug, Default)]
120pub struct AgentWrapperRunRequest {
121 pub prompt: String,
122 pub working_dir: Option<PathBuf>,
123 pub timeout: Option<Duration>,
124 pub env: BTreeMap<String, String>,
125 pub extensions: BTreeMap<String, serde_json::Value>,
127}
128
129pub type DynAgentWrapperEventStream = Pin<Box<dyn Stream<Item = AgentWrapperEvent> + Send>>;
130pub type DynAgentWrapperCompletion =
131 Pin<Box<dyn Future<Output = Result<AgentWrapperCompletion, AgentWrapperError>> + Send>>;
132
133pub struct AgentWrapperRunHandle {
134 pub events: DynAgentWrapperEventStream,
135 pub completion: DynAgentWrapperCompletion,
136}
137
138impl std::fmt::Debug for AgentWrapperRunHandle {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.debug_struct("AgentWrapperRunHandle")
141 .field("events", &"<stream>")
142 .field("completion", &"<future>")
143 .finish()
144 }
145}
146
147struct AgentWrapperCancelInner {
148 called: AtomicBool,
149 cancel: Box<dyn Fn() + Send + Sync + 'static>,
150}
151
152#[derive(Clone)]
153pub struct AgentWrapperCancelHandle {
154 inner: Arc<AgentWrapperCancelInner>,
155}
156
157impl std::fmt::Debug for AgentWrapperCancelHandle {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 f.debug_struct("AgentWrapperCancelHandle")
160 .finish_non_exhaustive()
161 }
162}
163
164impl AgentWrapperCancelHandle {
165 #[allow(dead_code)]
166 pub(crate) fn new(cancel: impl Fn() + Send + Sync + 'static) -> Self {
167 Self {
168 inner: Arc::new(AgentWrapperCancelInner {
169 called: AtomicBool::new(false),
170 cancel: Box::new(cancel),
171 }),
172 }
173 }
174
175 pub fn cancel(&self) {
185 if self.inner.called.swap(true, Ordering::SeqCst) {
186 return;
187 }
188
189 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
190 (self.inner.cancel)();
191 }));
192 }
193}
194
195#[derive(Debug)]
196pub struct AgentWrapperRunControl {
197 pub handle: AgentWrapperRunHandle,
198 pub cancel: AgentWrapperCancelHandle,
199}
200
201#[derive(Clone, Debug)]
202pub struct AgentWrapperCompletion {
203 pub status: ExitStatus,
204 pub final_text: Option<String>,
206 pub data: Option<serde_json::Value>,
211}
212
213#[derive(Clone, Debug)]
214pub struct AgentWrapperRunResult {
215 pub completion: AgentWrapperCompletion,
216}
217
218#[derive(Debug, thiserror::Error)]
219pub enum AgentWrapperError {
220 #[error("unknown backend: {agent_kind}")]
221 UnknownBackend { agent_kind: String },
222 #[error("unsupported capability for {agent_kind}: {capability}")]
223 UnsupportedCapability {
224 agent_kind: String,
225 capability: String,
226 },
227 #[error("invalid agent kind: {message}")]
228 InvalidAgentKind { message: String },
229 #[error("invalid request: {message}")]
230 InvalidRequest { message: String },
231 #[error("backend error: {message}")]
232 Backend { message: String },
233}
234
235pub trait AgentWrapperBackend: Send + Sync {
236 fn kind(&self) -> AgentWrapperKind;
237 fn capabilities(&self) -> AgentWrapperCapabilities;
238
239 fn run(
243 &self,
244 request: AgentWrapperRunRequest,
245 ) -> Pin<Box<dyn Future<Output = Result<AgentWrapperRunHandle, AgentWrapperError>> + Send + '_>>;
246
247 fn run_control(
253 &self,
254 _request: AgentWrapperRunRequest,
255 ) -> Pin<Box<dyn Future<Output = Result<AgentWrapperRunControl, AgentWrapperError>> + Send + '_>>
256 {
257 let agent_kind = self.kind().as_str().to_string();
258 Box::pin(async move {
259 Err(AgentWrapperError::UnsupportedCapability {
260 agent_kind,
261 capability: CAPABILITY_CONTROL_CANCEL_V1.to_string(),
262 })
263 })
264 }
265
266 fn mcp_list(
272 &self,
273 _request: AgentWrapperMcpListRequest,
274 ) -> Pin<
275 Box<
276 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
277 + Send
278 + '_,
279 >,
280 > {
281 let agent_kind = self.kind().as_str().to_string();
282 Box::pin(async move {
283 Err(AgentWrapperError::UnsupportedCapability {
284 agent_kind,
285 capability: CAPABILITY_MCP_LIST_V1.to_string(),
286 })
287 })
288 }
289
290 fn mcp_get(
296 &self,
297 _request: AgentWrapperMcpGetRequest,
298 ) -> Pin<
299 Box<
300 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
301 + Send
302 + '_,
303 >,
304 > {
305 let agent_kind = self.kind().as_str().to_string();
306 Box::pin(async move {
307 Err(AgentWrapperError::UnsupportedCapability {
308 agent_kind,
309 capability: CAPABILITY_MCP_GET_V1.to_string(),
310 })
311 })
312 }
313
314 fn mcp_add(
320 &self,
321 _request: AgentWrapperMcpAddRequest,
322 ) -> Pin<
323 Box<
324 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
325 + Send
326 + '_,
327 >,
328 > {
329 let agent_kind = self.kind().as_str().to_string();
330 Box::pin(async move {
331 Err(AgentWrapperError::UnsupportedCapability {
332 agent_kind,
333 capability: CAPABILITY_MCP_ADD_V1.to_string(),
334 })
335 })
336 }
337
338 fn mcp_remove(
344 &self,
345 _request: AgentWrapperMcpRemoveRequest,
346 ) -> Pin<
347 Box<
348 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
349 + Send
350 + '_,
351 >,
352 > {
353 let agent_kind = self.kind().as_str().to_string();
354 Box::pin(async move {
355 Err(AgentWrapperError::UnsupportedCapability {
356 agent_kind,
357 capability: CAPABILITY_MCP_REMOVE_V1.to_string(),
358 })
359 })
360 }
361}
362
363#[derive(Clone, Default)]
364pub struct AgentWrapperGateway {
365 backends: BTreeMap<AgentWrapperKind, Arc<dyn AgentWrapperBackend>>,
366}
367
368impl AgentWrapperGateway {
369 pub fn new() -> Self {
370 Self::default()
371 }
372
373 pub fn register(
377 &mut self,
378 backend: Arc<dyn AgentWrapperBackend>,
379 ) -> Result<(), AgentWrapperError> {
380 let kind = backend.kind();
381 if self.backends.contains_key(&kind) {
382 return Err(AgentWrapperError::InvalidRequest {
383 message: format!("backend already registered: {}", kind.as_str()),
384 });
385 }
386 self.backends.insert(kind, backend);
387 Ok(())
388 }
389
390 pub fn backend(&self, agent_kind: &AgentWrapperKind) -> Option<Arc<dyn AgentWrapperBackend>> {
392 self.backends.get(agent_kind).cloned()
393 }
394
395 pub fn run(
399 &self,
400 agent_kind: &AgentWrapperKind,
401 request: AgentWrapperRunRequest,
402 ) -> Pin<Box<dyn Future<Output = Result<AgentWrapperRunHandle, AgentWrapperError>> + Send + '_>>
403 {
404 let backend = self.backends.get(agent_kind).cloned();
405 let agent_kind = agent_kind.as_str().to_string();
406 Box::pin(async move {
407 let backend = backend.ok_or(AgentWrapperError::UnknownBackend { agent_kind })?;
408 backend.run(request).await
409 })
410 }
411
412 pub fn run_control(
426 &self,
427 agent_kind: &AgentWrapperKind,
428 request: AgentWrapperRunRequest,
429 ) -> Pin<Box<dyn Future<Output = Result<AgentWrapperRunControl, AgentWrapperError>> + Send + '_>>
430 {
431 let backend = self.backends.get(agent_kind).cloned();
432 let agent_kind = agent_kind.as_str().to_string();
433 Box::pin(async move {
434 let backend = backend.ok_or(AgentWrapperError::UnknownBackend {
435 agent_kind: agent_kind.clone(),
436 })?;
437
438 if !backend
439 .capabilities()
440 .contains(CAPABILITY_CONTROL_CANCEL_V1)
441 {
442 return Err(AgentWrapperError::UnsupportedCapability {
443 agent_kind,
444 capability: CAPABILITY_CONTROL_CANCEL_V1.to_string(),
445 });
446 }
447
448 backend.run_control(request).await
449 })
450 }
451
452 pub fn mcp_list(
463 &self,
464 agent_kind: &AgentWrapperKind,
465 request: AgentWrapperMcpListRequest,
466 ) -> Pin<
467 Box<
468 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
469 + Send
470 + '_,
471 >,
472 > {
473 let backend = self.backends.get(agent_kind).cloned();
474 let agent_kind = agent_kind.as_str().to_string();
475 Box::pin(async move {
476 let backend = backend.ok_or(AgentWrapperError::UnknownBackend {
477 agent_kind: agent_kind.clone(),
478 })?;
479
480 if !backend.capabilities().contains(CAPABILITY_MCP_LIST_V1) {
481 return Err(AgentWrapperError::UnsupportedCapability {
482 agent_kind,
483 capability: CAPABILITY_MCP_LIST_V1.to_string(),
484 });
485 }
486
487 backend.mcp_list(request).await
488 })
489 }
490
491 pub fn mcp_get(
502 &self,
503 agent_kind: &AgentWrapperKind,
504 request: AgentWrapperMcpGetRequest,
505 ) -> Pin<
506 Box<
507 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
508 + Send
509 + '_,
510 >,
511 > {
512 let backend = self.backends.get(agent_kind).cloned();
513 let agent_kind = agent_kind.as_str().to_string();
514 Box::pin(async move {
515 let backend = backend.ok_or(AgentWrapperError::UnknownBackend {
516 agent_kind: agent_kind.clone(),
517 })?;
518
519 if !backend.capabilities().contains(CAPABILITY_MCP_GET_V1) {
520 return Err(AgentWrapperError::UnsupportedCapability {
521 agent_kind,
522 capability: CAPABILITY_MCP_GET_V1.to_string(),
523 });
524 }
525
526 let request = normalize_mcp_get_request(request)?;
527 backend.mcp_get(request).await
528 })
529 }
530
531 pub fn mcp_add(
542 &self,
543 agent_kind: &AgentWrapperKind,
544 request: AgentWrapperMcpAddRequest,
545 ) -> Pin<
546 Box<
547 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
548 + Send
549 + '_,
550 >,
551 > {
552 let backend = self.backends.get(agent_kind).cloned();
553 let agent_kind = agent_kind.as_str().to_string();
554 Box::pin(async move {
555 let backend = backend.ok_or(AgentWrapperError::UnknownBackend {
556 agent_kind: agent_kind.clone(),
557 })?;
558
559 if !backend.capabilities().contains(CAPABILITY_MCP_ADD_V1) {
560 return Err(AgentWrapperError::UnsupportedCapability {
561 agent_kind,
562 capability: CAPABILITY_MCP_ADD_V1.to_string(),
563 });
564 }
565
566 let request = normalize_mcp_add_request(request)?;
567 backend.mcp_add(request).await
568 })
569 }
570
571 pub fn mcp_remove(
582 &self,
583 agent_kind: &AgentWrapperKind,
584 request: AgentWrapperMcpRemoveRequest,
585 ) -> Pin<
586 Box<
587 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
588 + Send
589 + '_,
590 >,
591 > {
592 let backend = self.backends.get(agent_kind).cloned();
593 let agent_kind = agent_kind.as_str().to_string();
594 Box::pin(async move {
595 let backend = backend.ok_or(AgentWrapperError::UnknownBackend {
596 agent_kind: agent_kind.clone(),
597 })?;
598
599 if !backend.capabilities().contains(CAPABILITY_MCP_REMOVE_V1) {
600 return Err(AgentWrapperError::UnsupportedCapability {
601 agent_kind,
602 capability: CAPABILITY_MCP_REMOVE_V1.to_string(),
603 });
604 }
605
606 let request = normalize_mcp_remove_request(request)?;
607 backend.mcp_remove(request).await
608 })
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use std::collections::BTreeSet;
615 use std::future::Future;
616 use std::pin::Pin;
617 use std::sync::atomic::{AtomicUsize, Ordering};
618 use std::sync::{Arc, Mutex};
619 use std::task::{Context, Poll, Wake, Waker};
620
621 use super::*;
622 use crate::mcp::{
623 AgentWrapperMcpAddRequest, AgentWrapperMcpAddTransport, AgentWrapperMcpCommandContext,
624 AgentWrapperMcpGetRequest, CAPABILITY_MCP_ADD_V1, CAPABILITY_MCP_GET_V1,
625 };
626
627 #[test]
628 fn cancel_handle_is_idempotent() {
629 let calls = Arc::new(AtomicUsize::new(0));
630 let calls_for_cancel = Arc::clone(&calls);
631 let cancel = AgentWrapperCancelHandle::new(move || {
632 calls_for_cancel.fetch_add(1, Ordering::SeqCst);
633 });
634
635 cancel.cancel();
636 cancel.cancel();
637 cancel.cancel();
638
639 assert_eq!(calls.load(Ordering::SeqCst), 1);
640 }
641
642 #[test]
643 fn mcp_add_returns_unknown_backend_before_validation() {
644 let gateway = AgentWrapperGateway::new();
645 let agent_kind = AgentWrapperKind::new("codex").expect("valid kind");
646 let secret = "SECRET_BACKEND_VALUE";
647
648 let err = block_on_immediate(gateway.mcp_add(
649 &agent_kind,
650 AgentWrapperMcpAddRequest {
651 name: format!(" {secret} "),
652 transport: AgentWrapperMcpAddTransport::Url {
653 url: format!("relative/{secret}"),
654 bearer_token_env_var: None,
655 },
656 context: AgentWrapperMcpCommandContext::default(),
657 },
658 ))
659 .expect_err("unknown backend should fail first");
660
661 match err {
662 AgentWrapperError::UnknownBackend { agent_kind } => {
663 assert_eq!(agent_kind, "codex");
664 }
665 other => panic!("expected UnknownBackend, got {other:?}"),
666 }
667 }
668
669 #[test]
670 fn mcp_add_returns_unsupported_capability_before_validation_and_without_hook_call() {
671 let backend = Arc::new(TestBackend::new(BTreeSet::new()));
672 let mut gateway = AgentWrapperGateway::new();
673 gateway.register(backend.clone()).expect("register backend");
674 let agent_kind = backend.kind();
675 let secret = "SECRET_UNSUPPORTED_VALUE";
676
677 let err = block_on_immediate(gateway.mcp_add(
678 &agent_kind,
679 AgentWrapperMcpAddRequest {
680 name: format!(" {secret} "),
681 transport: AgentWrapperMcpAddTransport::Url {
682 url: format!("relative/{secret}"),
683 bearer_token_env_var: None,
684 },
685 context: AgentWrapperMcpCommandContext::default(),
686 },
687 ))
688 .expect_err("unsupported capability should fail before validation");
689
690 match err {
691 AgentWrapperError::UnsupportedCapability {
692 agent_kind,
693 capability,
694 } => {
695 assert_eq!(agent_kind, "test_backend");
696 assert_eq!(capability, CAPABILITY_MCP_ADD_V1);
697 }
698 other => panic!("expected UnsupportedCapability, got {other:?}"),
699 }
700
701 assert_eq!(backend.mcp_add_calls.load(Ordering::SeqCst), 0);
702 }
703
704 #[test]
705 fn mcp_add_returns_invalid_request_before_hook_when_capability_is_advertised() {
706 let backend = Arc::new(TestBackend::new(BTreeSet::from([
707 CAPABILITY_MCP_ADD_V1.to_string()
708 ])));
709 let mut gateway = AgentWrapperGateway::new();
710 gateway.register(backend.clone()).expect("register backend");
711 let agent_kind = backend.kind();
712 let secret = "SECRET_INVALID_VALUE";
713
714 let err = block_on_immediate(gateway.mcp_add(
715 &agent_kind,
716 AgentWrapperMcpAddRequest {
717 name: " example ".to_string(),
718 transport: AgentWrapperMcpAddTransport::Url {
719 url: format!("https:{secret}.example.com"),
720 bearer_token_env_var: None,
721 },
722 context: AgentWrapperMcpCommandContext::default(),
723 },
724 ))
725 .expect_err("invalid request should fail before hook");
726
727 match err {
728 AgentWrapperError::InvalidRequest { message } => {
729 assert_eq!(message, "mcp add url must be an absolute http or https URL");
730 assert!(!message.contains(secret));
731 }
732 other => panic!("expected InvalidRequest, got {other:?}"),
733 }
734
735 assert_eq!(backend.mcp_add_calls.load(Ordering::SeqCst), 0);
736 }
737
738 #[test]
739 fn mcp_get_passes_normalized_request_to_hook() {
740 let backend = Arc::new(TestBackend::new(BTreeSet::from([
741 CAPABILITY_MCP_GET_V1.to_string()
742 ])));
743 let mut gateway = AgentWrapperGateway::new();
744 gateway.register(backend.clone()).expect("register backend");
745 let agent_kind = backend.kind();
746
747 let output = block_on_immediate(gateway.mcp_get(
748 &agent_kind,
749 AgentWrapperMcpGetRequest {
750 name: " normalized-name ".to_string(),
751 context: AgentWrapperMcpCommandContext::default(),
752 },
753 ))
754 .expect("normalized request should reach hook");
755
756 assert_eq!(output.stdout, "ok");
757 assert_eq!(backend.mcp_get_calls.load(Ordering::SeqCst), 1);
758 assert_eq!(
759 backend
760 .last_get_name
761 .lock()
762 .expect("get name mutex")
763 .as_deref(),
764 Some("normalized-name")
765 );
766 }
767
768 struct NoopWake;
769
770 impl Wake for NoopWake {
771 fn wake(self: Arc<Self>) {}
772 }
773
774 fn block_on_immediate<F>(future: F) -> F::Output
775 where
776 F: Future,
777 {
778 let waker = Waker::from(Arc::new(NoopWake));
779 let mut future = Box::pin(future);
780 let mut context = Context::from_waker(&waker);
781
782 loop {
783 match future.as_mut().poll(&mut context) {
784 Poll::Ready(output) => return output,
785 Poll::Pending => std::thread::yield_now(),
786 }
787 }
788 }
789
790 fn success_exit_status() -> std::process::ExitStatus {
791 #[cfg(unix)]
792 {
793 use std::os::unix::process::ExitStatusExt;
794 std::process::ExitStatus::from_raw(0)
795 }
796 #[cfg(windows)]
797 {
798 use std::os::windows::process::ExitStatusExt;
799 std::process::ExitStatus::from_raw(0)
800 }
801 }
802
803 fn success_output() -> AgentWrapperMcpCommandOutput {
804 AgentWrapperMcpCommandOutput {
805 status: success_exit_status(),
806 stdout: "ok".to_string(),
807 stderr: String::new(),
808 stdout_truncated: false,
809 stderr_truncated: false,
810 }
811 }
812
813 struct TestBackend {
814 capabilities: AgentWrapperCapabilities,
815 mcp_add_calls: AtomicUsize,
816 mcp_get_calls: AtomicUsize,
817 last_get_name: Mutex<Option<String>>,
818 }
819
820 impl TestBackend {
821 fn new(capabilities: BTreeSet<String>) -> Self {
822 Self {
823 capabilities: AgentWrapperCapabilities { ids: capabilities },
824 mcp_add_calls: AtomicUsize::new(0),
825 mcp_get_calls: AtomicUsize::new(0),
826 last_get_name: Mutex::new(None),
827 }
828 }
829 }
830
831 impl AgentWrapperBackend for TestBackend {
832 fn kind(&self) -> AgentWrapperKind {
833 AgentWrapperKind("test_backend".to_string())
834 }
835
836 fn capabilities(&self) -> AgentWrapperCapabilities {
837 self.capabilities.clone()
838 }
839
840 fn run(
841 &self,
842 _request: AgentWrapperRunRequest,
843 ) -> Pin<
844 Box<dyn Future<Output = Result<AgentWrapperRunHandle, AgentWrapperError>> + Send + '_>,
845 > {
846 Box::pin(async {
847 Err(AgentWrapperError::Backend {
848 message: "run not used in tests".to_string(),
849 })
850 })
851 }
852
853 fn mcp_get(
854 &self,
855 request: AgentWrapperMcpGetRequest,
856 ) -> Pin<
857 Box<
858 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
859 + Send
860 + '_,
861 >,
862 > {
863 self.mcp_get_calls.fetch_add(1, Ordering::SeqCst);
864 *self.last_get_name.lock().expect("last get name mutex") = Some(request.name);
865 Box::pin(async move { Ok(success_output()) })
866 }
867
868 fn mcp_add(
869 &self,
870 _request: AgentWrapperMcpAddRequest,
871 ) -> Pin<
872 Box<
873 dyn Future<Output = Result<AgentWrapperMcpCommandOutput, AgentWrapperError>>
874 + Send
875 + '_,
876 >,
877 > {
878 self.mcp_add_calls.fetch_add(1, Ordering::SeqCst);
879 Box::pin(async move { Ok(success_output()) })
880 }
881 }
882}