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