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