1pub mod auth;
4pub mod compose;
5pub mod config;
6pub mod executor;
7pub mod protocol;
8pub mod retry;
9pub mod tool_map;
10pub mod types;
11
12pub use config::load_tool_map_config;
13pub use executor::WasixExecutor;
14pub use tool_map::ToolMap;
15pub use types::{McpError, ToolInput, ToolMapConfig, ToolOutput, ToolRef};
16
17use greentic_mcp_exec::{ExecConfig, ExecError, ExecRequest, RunnerError};
18use serde_json::{Value, json};
19use std::sync::Arc;
20use tokio::time::sleep;
21pub async fn invoke_with_map(
23 map: &ToolMap,
24 executor: &WasixExecutor,
25 name: &str,
26 input_json: Value,
27) -> Result<Value, McpError> {
28 let tool = map.get(name)?;
29 let input = ToolInput {
30 payload: input_json,
31 };
32 let output = executor.invoke(tool, &input).await?;
33 Ok(output.payload)
34}
35
36pub fn load_tool_map(path: &std::path::Path) -> Result<ToolMap, McpError> {
38 let config = load_tool_map_config(path)?;
39 ToolMap::from_config(&config)
40}
41
42pub mod test_tools;
43
44use std::time::Duration;
45
46type ExecFn = dyn Fn(ExecRequest, &ExecConfig) -> Result<Value, ExecError> + Send + Sync;
47
48pub async fn exec_with_retries(req: ExecRequest, cfg: &ExecConfig) -> Result<Value, ExecError> {
49 exec_with_retries_with(req, cfg, Arc::new(greentic_mcp_exec::exec)).await
50}
51
52pub async fn exec_with_retries_backend<F>(
53 req: ExecRequest,
54 cfg: &ExecConfig,
55 exec_fn: F,
56) -> Result<Value, ExecError>
57where
58 F: Fn(ExecRequest, &ExecConfig) -> Result<Value, ExecError> + Send + Sync + 'static,
59{
60 exec_with_retries_with(req, cfg, Arc::new(exec_fn)).await
61}
62
63async fn exec_with_retries_with(
64 mut req: ExecRequest,
65 cfg: &ExecConfig,
66 executor: Arc<ExecFn>,
67) -> Result<Value, ExecError> {
68 let max_attempts = cfg.runtime.max_attempts.max(1);
69
70 for attempt in 1..=max_attempts {
71 if let Some(tenant) = req.tenant.as_mut() {
72 tenant.attempt = attempt - 1;
73 }
74
75 let req_clone = req.clone();
76 let cfg_clone = cfg.clone();
77 let executor = executor.clone();
78 let attempt_result =
79 tokio::task::spawn_blocking(move || executor(req_clone, &cfg_clone)).await;
80
81 let exec_result = match attempt_result {
82 Ok(result) => result,
83 Err(err) => {
84 return Err(ExecError::runner(
85 req.component.clone(),
86 RunnerError::Internal(format!("blocking exec failed: {err:?}")),
87 ));
88 }
89 };
90
91 match exec_result {
92 Ok(value) => return Ok(value),
93 Err(err) => {
94 let should_retry = attempt < max_attempts && is_transient_error(&err);
95 if !should_retry {
96 return Err(err);
97 }
98 let backoff = cfg
99 .runtime
100 .base_backoff
101 .checked_mul(attempt)
102 .unwrap_or(cfg.runtime.base_backoff);
103 sleep(backoff).await;
104 }
105 }
106 }
107
108 unreachable!("retry loop should never exit without returning")
109}
110
111fn is_transient_error(err: &ExecError) -> bool {
112 match err {
113 ExecError::Runner { source, .. } => matches!(source, RunnerError::Timeout { .. }),
114 ExecError::Tool { code, .. } => code.starts_with("transient."),
115 _ => false,
116 }
117}
118
119pub enum TestBackend {
121 NativeEcho,
122 NativeFlaky,
123 NativeTimeout(Duration),
124}
125
126pub fn exec_test_backend(
127 backend: TestBackend,
128 input: Value,
129 cfg: &ExecConfig,
130) -> Result<Value, ExecError> {
131 use crate::test_tools::*;
132
133 match backend {
134 TestBackend::NativeEcho => {
135 echo(&input).map_err(|message| tool_error("echo", "tool-invoke", "echo", message))
136 }
137 TestBackend::NativeFlaky => flaky_echo(&input)
138 .map_err(|message| tool_error("echo-flaky", "tool-invoke", "transient.echo", message)),
139 TestBackend::NativeTimeout(sleep) => {
140 if sleep > cfg.runtime.per_call_timeout {
141 Err(ExecError::runner(
142 "echo-timeout",
143 RunnerError::Timeout {
144 elapsed: cfg.runtime.per_call_timeout,
145 },
146 ))
147 } else {
148 timeout_echo(&input, sleep).map_err(|message| {
149 tool_error("echo-timeout", "tool-invoke", "timeout", message)
150 })
151 }
152 }
153 }
154}
155
156fn tool_error(component: &str, action: &str, code: &str, message: String) -> ExecError {
157 ExecError::tool_error(component, action, code, json!({ "message": message }))
158}