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