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        },
52    );
53
54    let value = match result {
55        Ok(v) => v,
56        Err(RunnerError::ActionNotFound { .. }) => {
57            return Err(ExecError::not_found(
58                req.component.clone(),
59                req.action.clone(),
60            ));
61        }
62        Err(err) => return Err(ExecError::runner(&req.component, err)),
63    };
64
65    if let Some(code) = value
66        .get("error")
67        .and_then(|error| error.get("code"))
68        .and_then(Value::as_str)
69        .map(str::to_owned)
70    {
71        if code == "iface-error.not-found" {
72            return Err(ExecError::not_found(req.component, req.action));
73        } else {
74            return Err(ExecError::tool_error(
75                req.component,
76                req.action,
77                code,
78                value,
79            ));
80        }
81    }
82
83    Ok(value)
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::config::{RuntimePolicy, VerifyPolicy};
90    use crate::error::RunnerError;
91    use crate::store::ToolStore;
92    use serde_json::json;
93    use std::collections::HashMap;
94    use std::path::PathBuf;
95
96    use crate::verify::VerifiedArtifact;
97
98    #[derive(Default)]
99    struct MockRunner;
100
101    impl runner::Runner for MockRunner {
102        fn run(
103            &self,
104            request: &ExecRequest,
105            artifact: &VerifiedArtifact,
106            _ctx: runner::ExecutionContext<'_>,
107        ) -> Result<Value, RunnerError> {
108            let mut payload = request.args.clone();
109            if let Value::Object(map) = &mut payload {
110                map.insert(
111                    "component_digest".to_string(),
112                    Value::String(artifact.resolved.digest.clone()),
113                );
114            }
115            Ok(payload)
116        }
117    }
118
119    #[test]
120    fn local_resolve_and_verify_success() {
121        let tempdir = tempfile::tempdir().expect("tempdir");
122        let wasm_path = tempdir.path().join("echo.component.wasm");
123        std::fs::write(&wasm_path, b"fake wasm contents").expect("write");
124
125        let digest = crate::resolve::resolve(
126            "echo.component",
127            &ToolStore::LocalDir(PathBuf::from(tempdir.path())),
128        )
129        .expect("resolve")
130        .digest;
131
132        let mut required = HashMap::new();
133        required.insert("echo.component".to_string(), digest.clone());
134
135        let cfg = ExecConfig {
136            store: ToolStore::LocalDir(PathBuf::from(tempdir.path())),
137            security: VerifyPolicy {
138                allow_unverified: false,
139                required_digests: required,
140                trusted_signers: Vec::new(),
141            },
142            runtime: RuntimePolicy::default(),
143            http_enabled: false,
144        };
145
146        let req = ExecRequest {
147            component: "echo.component".into(),
148            action: "noop".into(),
149            args: json!({"message": "hello"}),
150            tenant: None,
151        };
152
153        // Inject our mock runner to exercise pipeline without executing wasm.
154        let resolved =
155            crate::resolve::resolve(&req.component, &cfg.store).expect("resolve second time");
156        let verified =
157            crate::verify::verify(&req.component, resolved, &cfg.security).expect("verify");
158        let result = MockRunner
159            .run(
160                &req,
161                &verified,
162                runner::ExecutionContext {
163                    runtime: &cfg.runtime,
164                    http_enabled: cfg.http_enabled,
165                },
166            )
167            .expect("run");
168
169        assert_eq!(
170            result.get("component_digest").and_then(Value::as_str),
171            Some(digest.as_str())
172        );
173    }
174}