Skip to main content

greentic_mcp_exec/
lib.rs

1//! Executor library for loading and running `wasix:mcp` compatible Wasm components.
2//! Users supply an [`ExecConfig`] describing how to resolve artifacts and what
3//! runtime constraints to enforce, then call [`exec`] with a structured request.
4
5mod config;
6pub mod describe;
7mod error;
8mod path_safety;
9mod resolve;
10pub mod router;
11pub mod runner;
12mod store;
13#[cfg(test)]
14mod test_support;
15mod verify;
16
17pub use config::{DynSecretsStore, ExecConfig, RuntimePolicy, SecretsStore, VerifyPolicy};
18pub use error::{ExecError, RunnerError};
19pub use store::{ToolInfo, ToolStore};
20
21use greentic_types::TenantCtx;
22use serde_json::{Value, json};
23
24use crate::runner::Runner;
25
26/// One tool advertised by a `wasix:mcp/router` component.
27#[derive(Clone, Debug)]
28pub struct ToolDef {
29    pub name: String,
30    pub description: String,
31    pub input_schema: Value,
32}
33
34#[derive(Clone, Debug)]
35pub struct ExecRequest {
36    pub component: String,
37    pub action: String,
38    pub args: Value,
39    pub tenant: Option<TenantCtx>,
40}
41
42/// Execute a single action exported by an MCP component.
43///
44/// Resolution, verification, and runtime enforcement are performed in sequence,
45/// with detailed errors surfaced through [`ExecError`].
46pub fn exec(req: ExecRequest, cfg: &ExecConfig) -> Result<Value, ExecError> {
47    let resolved = resolve::resolve(&req.component, &cfg.store)
48        .map_err(|err| ExecError::resolve(&req.component, err))?;
49
50    let verified = verify::verify(&req.component, resolved, &cfg.security)
51        .map_err(|err| ExecError::verification(&req.component, err))?;
52
53    let runner = runner::DefaultRunner::new(&cfg.runtime)
54        .map_err(|err| ExecError::runner(&req.component, err))?;
55
56    let result = runner.run(
57        &req,
58        &verified,
59        runner::ExecutionContext {
60            runtime: &cfg.runtime,
61            http_enabled: cfg.http_enabled,
62            secrets_store: cfg.secrets_store.clone(),
63        },
64    );
65
66    let value = match result {
67        Ok(v) => v,
68        Err(RunnerError::ActionNotFound { .. }) => {
69            return Err(ExecError::not_found(
70                req.component.clone(),
71                req.action.clone(),
72            ));
73        }
74        Err(RunnerError::ToolTransient { component, message }) => {
75            return Err(ExecError::tool_error(
76                component,
77                req.action.clone(),
78                "transient",
79                json!({ "message": message }),
80            ));
81        }
82        Err(RunnerError::Internal(message)) => {
83            return Err(ExecError::runner(
84                &req.component,
85                RunnerError::Internal(message),
86            ));
87        }
88        Err(err) => return Err(ExecError::runner(&req.component, err)),
89    };
90
91    if let Some(code) = value
92        .get("error")
93        .and_then(|error| error.get("code"))
94        .and_then(Value::as_str)
95        .map(str::to_owned)
96    {
97        if code == "iface-error.not-found" {
98            return Err(ExecError::not_found(req.component, req.action));
99        } else {
100            return Err(ExecError::tool_error(
101                req.component,
102                req.action,
103                code,
104                value,
105            ));
106        }
107    }
108
109    Ok(value)
110}
111
112/// List the tools a local `wasix:mcp` component exports (the `tools/list`
113/// equivalent), resolving and verifying via the same pipeline as [`exec`].
114///
115/// Synchronous (blocking Wasmtime) — callers on an async runtime must wrap this
116/// in `spawn_blocking`.
117pub fn list_tools(component: &str, cfg: &ExecConfig) -> Result<Vec<ToolDef>, ExecError> {
118    let resolved = resolve::resolve(component, &cfg.store)
119        .map_err(|err| ExecError::resolve(component, err))?;
120    let verified = verify::verify(component, resolved, &cfg.security)
121        .map_err(|err| ExecError::verification(component, err))?;
122
123    let runner = runner::DefaultRunner::new(&cfg.runtime)
124        .map_err(|err| ExecError::runner(component, err))?;
125
126    let tools = runner
127        .list_tools_router(
128            &verified,
129            runner::ExecutionContext {
130                runtime: &cfg.runtime,
131                http_enabled: cfg.http_enabled,
132                secrets_store: cfg.secrets_store.clone(),
133            },
134        )
135        .map_err(|err| ExecError::runner(component, err))?
136        .unwrap_or_default();
137
138    Ok(tools
139        .into_iter()
140        .map(|t| ToolDef {
141            name: t.name,
142            description: t.description,
143            input_schema: serde_json::from_str(&t.input_schema)
144                .unwrap_or_else(|_| serde_json::json!({})),
145        })
146        .collect())
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::config::{RuntimePolicy, VerifyPolicy};
153    use crate::error::RunnerError;
154    use crate::store::ToolStore;
155    use serde_json::json;
156    use std::collections::HashMap;
157    use std::path::PathBuf;
158
159    use crate::verify::VerifiedArtifact;
160
161    #[derive(Default)]
162    struct MockRunner;
163
164    impl runner::Runner for MockRunner {
165        fn run(
166            &self,
167            request: &ExecRequest,
168            artifact: &VerifiedArtifact,
169            _ctx: runner::ExecutionContext<'_>,
170        ) -> Result<Value, RunnerError> {
171            let mut payload = request.args.clone();
172            if let Value::Object(map) = &mut payload {
173                map.insert(
174                    "component_digest".to_string(),
175                    Value::String(artifact.resolved.digest.clone()),
176                );
177            }
178            Ok(payload)
179        }
180    }
181
182    #[test]
183    fn local_resolve_and_verify_success() {
184        let tempdir = tempfile::tempdir().expect("tempdir");
185        let wasm_path = tempdir.path().join("echo.component.wasm");
186        std::fs::write(&wasm_path, b"fake wasm contents").expect("write");
187
188        let digest = crate::resolve::resolve(
189            "echo.component",
190            &ToolStore::LocalDir(PathBuf::from(tempdir.path())),
191        )
192        .expect("resolve")
193        .digest;
194
195        let mut required = HashMap::new();
196        required.insert("echo.component".to_string(), digest.clone());
197
198        let cfg = ExecConfig {
199            store: ToolStore::LocalDir(PathBuf::from(tempdir.path())),
200            security: VerifyPolicy {
201                allow_unverified: false,
202                required_digests: required,
203                trusted_signers: Vec::new(),
204            },
205            runtime: RuntimePolicy::default(),
206            http_enabled: false,
207            secrets_store: None,
208        };
209
210        let req = ExecRequest {
211            component: "echo.component".into(),
212            action: "noop".into(),
213            args: json!({"message": "hello"}),
214            tenant: None,
215        };
216
217        // Inject our mock runner to exercise pipeline without executing wasm.
218        let resolved =
219            crate::resolve::resolve(&req.component, &cfg.store).expect("resolve second time");
220        let verified =
221            crate::verify::verify(&req.component, resolved, &cfg.security).expect("verify");
222        let result = MockRunner
223            .run(
224                &req,
225                &verified,
226                runner::ExecutionContext {
227                    runtime: &cfg.runtime,
228                    http_enabled: cfg.http_enabled,
229                    secrets_store: cfg.secrets_store.clone(),
230                },
231            )
232            .expect("run");
233
234        assert_eq!(
235            result.get("component_digest").and_then(Value::as_str),
236            Some(digest.as_str())
237        );
238    }
239}