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 resolve;
9mod runner;
10mod store;
11mod verify;
12
13pub use config::{ExecConfig, RuntimePolicy, VerifyPolicy};
14pub use error::ExecError;
15pub use store::{ToolInfo, ToolStore};
16
17use greentic_types::TenantCtx;
18use serde_json::Value;
19
20use crate::error::RunnerError;
21use crate::runner::Runner;
22
23#[derive(Clone, Debug)]
24pub struct ExecRequest {
25    pub component: String,
26    pub action: String,
27    pub args: Value,
28    pub tenant: Option<TenantCtx>,
29}
30
31/// Execute a single action exported by an MCP component.
32///
33/// Resolution, verification, and runtime enforcement are performed in sequence,
34/// with detailed errors surfaced through [`ExecError`].
35pub fn exec(req: ExecRequest, cfg: &ExecConfig) -> Result<Value, ExecError> {
36    let resolved = resolve::resolve(&req.component, &cfg.store)
37        .map_err(|err| ExecError::resolve(&req.component, err))?;
38
39    let verified = verify::verify(&req.component, resolved, &cfg.security)
40        .map_err(|err| ExecError::verification(&req.component, err))?;
41
42    let runner = runner::DefaultRunner::new(&cfg.runtime)
43        .map_err(|err| ExecError::runner(&req.component, err))?;
44
45    let result = runner.run(
46        &req,
47        &verified,
48        runner::ExecutionContext {
49            runtime: &cfg.runtime,
50            http_enabled: cfg.http_enabled,
51            tenant: req.tenant.as_ref(),
52        },
53    );
54
55    let value = match result {
56        Ok(v) => v,
57        Err(RunnerError::ActionNotFound { .. }) => {
58            return Err(ExecError::not_found(
59                req.component.clone(),
60                req.action.clone(),
61            ));
62        }
63        Err(err) => return Err(ExecError::runner(&req.component, err)),
64    };
65
66    if let Some(error_value) = value.get("error").cloned()
67        && let Some(code) = error_value
68            .get("code")
69            .and_then(Value::as_str)
70            .map(|s| s.to_string())
71    {
72        if code == "iface-error.not-found" {
73            return Err(ExecError::not_found(req.component, req.action));
74        } else {
75            return Err(ExecError::tool_error(
76                req.component,
77                req.action,
78                code,
79                value,
80            ));
81        }
82    }
83
84    Ok(value)
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::config::{RuntimePolicy, VerifyPolicy};
91    use crate::error::RunnerError;
92    use crate::store::ToolStore;
93    use serde_json::json;
94    use std::collections::HashMap;
95    use std::path::PathBuf;
96
97    use crate::verify::VerifiedArtifact;
98
99    #[derive(Default)]
100    struct MockRunner;
101
102    impl runner::Runner for MockRunner {
103        fn run(
104            &self,
105            request: &ExecRequest,
106            artifact: &VerifiedArtifact,
107            _ctx: runner::ExecutionContext<'_>,
108        ) -> Result<Value, RunnerError> {
109            let mut payload = request.args.clone();
110            if let Value::Object(map) = &mut payload {
111                map.insert(
112                    "component_digest".to_string(),
113                    Value::String(artifact.resolved.digest.clone()),
114                );
115            }
116            Ok(payload)
117        }
118    }
119
120    #[test]
121    fn local_resolve_and_verify_success() {
122        let tempdir = tempfile::tempdir().expect("tempdir");
123        let wasm_path = tempdir.path().join("echo.component.wasm");
124        std::fs::write(&wasm_path, b"fake wasm contents").expect("write");
125
126        let digest = crate::resolve::resolve(
127            "echo.component",
128            &ToolStore::LocalDir(PathBuf::from(tempdir.path())),
129        )
130        .expect("resolve")
131        .digest;
132
133        let mut required = HashMap::new();
134        required.insert("echo.component".to_string(), digest.clone());
135
136        let cfg = ExecConfig {
137            store: ToolStore::LocalDir(PathBuf::from(tempdir.path())),
138            security: VerifyPolicy {
139                allow_unverified: false,
140                required_digests: required,
141                trusted_signers: Vec::new(),
142            },
143            runtime: RuntimePolicy::default(),
144            http_enabled: false,
145        };
146
147        let req = ExecRequest {
148            component: "echo.component".into(),
149            action: "noop".into(),
150            args: json!({"message": "hello"}),
151            tenant: None,
152        };
153
154        // Inject our mock runner to exercise pipeline without executing wasm.
155        let resolved =
156            crate::resolve::resolve(&req.component, &cfg.store).expect("resolve second time");
157        let verified =
158            crate::verify::verify(&req.component, resolved, &cfg.security).expect("verify");
159        let result = MockRunner
160            .run(
161                &req,
162                &verified,
163                runner::ExecutionContext {
164                    runtime: &cfg.runtime,
165                    http_enabled: cfg.http_enabled,
166                    tenant: req.tenant.as_ref(),
167                },
168            )
169            .expect("run");
170
171        assert_eq!(
172            result.get("component_digest").and_then(Value::as_str),
173            Some(digest.as_str())
174        );
175    }
176}