mcp_exec/
describe.rs

1use anyhow::{Context, Result};
2use serde_json::Value;
3
4use crate::{ExecConfig, ExecError, ExecRequest, exec};
5
6#[cfg(feature = "describe-v1")]
7const DESCRIBE_INTERFACE: &str = "greentic:component/describe-v1@1.0.0";
8#[cfg(feature = "describe-v1")]
9const DESCRIBE_EXPORT: &str = "greentic:component/describe-v1@1.0.0#describe-json";
10
11#[derive(Debug)]
12pub enum Maybe<T> {
13    Data(T),
14    Unsupported,
15}
16
17#[derive(Debug)]
18pub struct ToolDescribe {
19    pub describe_v1: Option<Value>,
20    pub capabilities: Maybe<Vec<String>>,
21    pub secrets: Maybe<Value>,
22    pub config_schema: Maybe<Value>,
23}
24
25pub fn describe_tool(name: &str, cfg: &ExecConfig) -> Result<ToolDescribe> {
26    #[cfg(feature = "describe-v1")]
27    {
28        if let Some(document) = try_describe_v1(name, cfg)? {
29            return Ok(ToolDescribe {
30                describe_v1: Some(document),
31                capabilities: Maybe::Unsupported,
32                secrets: Maybe::Unsupported,
33                config_schema: Maybe::Unsupported,
34            });
35        }
36    }
37
38    fn try_action(name: &str, action: &str, cfg: &ExecConfig) -> Result<Maybe<Value>> {
39        let req = ExecRequest {
40            component: name.to_string(),
41            action: action.to_string(),
42            args: Value::Object(Default::default()),
43            tenant: None,
44        };
45
46        match exec(req, cfg) {
47            Ok(v) => Ok(Maybe::Data(v)),
48            Err(ExecError::NotFound { .. }) => Ok(Maybe::Unsupported),
49            Err(ExecError::Tool { code, payload, .. }) if code == "iface-error.not-found" => {
50                let _ = payload;
51                Ok(Maybe::Unsupported)
52            }
53            Err(e) => Err(e.into()),
54        }
55    }
56
57    let capabilities_value = try_action(name, "capabilities", cfg)?;
58    let secrets = try_action(name, "list_secrets", cfg)?;
59    let config_schema = try_action(name, "config_schema", cfg)?;
60
61    let capabilities = match capabilities_value {
62        Maybe::Data(value) => {
63            if let Some(arr) = value.as_array() {
64                let list = arr
65                    .iter()
66                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
67                    .collect::<Vec<_>>();
68                Maybe::Data(list)
69            } else {
70                Maybe::Data(Vec::new())
71            }
72        }
73        Maybe::Unsupported => Maybe::Unsupported,
74    };
75
76    Ok(ToolDescribe {
77        describe_v1: None,
78        capabilities,
79        secrets,
80        config_schema,
81    })
82}
83
84#[cfg(feature = "describe-v1")]
85fn try_describe_v1(name: &str, cfg: &ExecConfig) -> Result<Option<Value>> {
86    use wasmtime::component::{Component, Linker};
87    use wasmtime::{Config, Engine, Store};
88
89    let resolved =
90        crate::resolve::resolve(name, &cfg.store).map_err(|err| ExecError::resolve(name, err))?;
91    let verified = crate::verify::verify(name, resolved, &cfg.security)
92        .map_err(|err| ExecError::verification(name, err))?;
93
94    let mut config = Config::new();
95    config.wasm_component_model(true);
96    config.async_support(false);
97    config.epoch_interruption(true);
98
99    let engine = Engine::new(&config)?;
100    let component = match Component::from_binary(&engine, verified.resolved.bytes.as_ref()) {
101        Ok(component) => component,
102        Err(_) => return Ok(None),
103    };
104    let linker = Linker::new(&engine);
105    let mut store = Store::new(&engine, ());
106
107    let instance = match linker.instantiate(&mut store, &component) {
108        Ok(instance) => instance,
109        Err(_) => return Ok(None),
110    };
111    if instance
112        .get_export(&mut store, None, DESCRIBE_INTERFACE)
113        .is_none()
114    {
115        return Ok(None);
116    }
117
118    let func = match instance.get_typed_func::<(), (String,)>(&mut store, DESCRIBE_EXPORT) {
119        Ok(func) => func,
120        Err(err) => {
121            let msg = err.to_string();
122            if msg.contains("unknown export") {
123                return Ok(None);
124            }
125            return Err(err);
126        }
127    };
128
129    let (raw,) = func.call(&mut store, ())?;
130    let value: Value =
131        serde_json::from_str(&raw).with_context(|| "describe-json returned invalid JSON")?;
132    Ok(Some(value))
133}