Skip to main content

greentic_mcp/
lib.rs

1//! Host-side ToolMap management and WASIX/WASI execution bridge for Greentic MCP tools.
2
3pub 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;
21/// Invoke a tool by name using a [`ToolMap`] and [`WasixExecutor`].
22pub 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
36/// Convenience helper for loading a tool map from disk and building a [`ToolMap`].
37pub 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
119/// Test-only helpers that run native “tools” without Wasm.
120pub 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}