Skip to main content

subako_function_sdk/
package.rs

1//! A package as a value: its functions, the manifest they describe, and the
2//! `main` that serves them.
3
4use std::io::Write as _;
5use std::process::ExitCode;
6
7use schemars::JsonSchema;
8use schemars::generate::{Contract, SchemaSettings};
9use serde::Serialize;
10use serde::de::DeserializeOwned;
11
12use crate::{Input, Output, Reply, Server, ServerInfo, ToolError};
13
14/// The argument that prints the manifest instead of calling a function.
15pub const MANIFEST_FLAG: &str = "--subako-manifest";
16
17/// One function a package serves as a tool.
18pub struct Function {
19    name: String,
20    description: String,
21    input_schema: fn(Contract) -> serde_json::Value,
22    output_schema: fn(Contract) -> serde_json::Value,
23    call: Box<dyn Fn(Input) -> Result<Output, ToolError>>,
24}
25
26impl Function {
27    /// `f` as the tool `name`. The schemas come from `I` and `O`.
28    pub fn new<I, O, E>(
29        name: impl Into<String>,
30        description: impl Into<String>,
31        f: impl Fn(I) -> Result<O, E> + 'static,
32    ) -> Self
33    where
34        I: DeserializeOwned + JsonSchema,
35        O: Reply,
36        E: Into<ToolError>,
37    {
38        Function {
39            name: name.into(),
40            description: description.into(),
41            input_schema: schema::<I>,
42            output_schema: O::schema,
43            call: Box::new(move |input| f(input.parse()?).map_err(Into::into)?.into_output()),
44        }
45    }
46
47    pub fn name(&self) -> &str {
48        &self.name
49    }
50}
51
52/// The function ABI this SDK's guests speak: MCP 2026-07-28 over stdio.
53// CODESYNC(function-abi)
54pub const ABI: u32 = 1;
55
56/// What `subako-package.json` holds: the ABI the module speaks, and the
57/// package's name and description. Its tools are what `tools/list` answers.
58#[derive(Debug, Clone, PartialEq, Serialize)]
59pub struct Manifest {
60    pub abi: u32,
61    pub name: String,
62    pub description: String,
63}
64
65/// A package: its name, its description, and the functions it serves.
66pub struct Package {
67    name: String,
68    version: String,
69    description: String,
70    functions: Vec<Function>,
71}
72
73impl Package {
74    pub fn new(
75        name: impl Into<String>,
76        description: impl Into<String>,
77        functions: Vec<Function>,
78    ) -> Self {
79        Package {
80            name: name.into(),
81            version: "0.0.0".to_string(),
82            description: description.into(),
83            functions,
84        }
85    }
86
87    /// The version each result's `serverInfo` reports; `package!` sets the
88    /// crate's own.
89    pub fn with_version(self, version: impl Into<String>) -> Self {
90        Package {
91            version: version.into(),
92            ..self
93        }
94    }
95
96    pub fn functions(&self) -> &[Function] {
97        &self.functions
98    }
99
100    pub fn manifest(&self) -> Manifest {
101        Manifest {
102            abi: ABI,
103            name: self.name.clone(),
104            description: self.description.clone(),
105        }
106    }
107
108    /// Calls the function named `tool`.
109    pub fn call(&self, tool: &str, input: Input) -> Result<Output, ToolError> {
110        match self.functions.iter().find(|f| f.name == tool) {
111            Some(function) => (function.call)(input),
112            None => Err(ToolError::unknown_tool(tool)),
113        }
114    }
115
116    /// The whole process as a pure function of its arguments and stdin: the
117    /// manifest under [`MANIFEST_FLAG`], otherwise a call.
118    pub fn execute(&self, args: &[String], stdin: &[u8]) -> (Vec<u8>, u8) {
119        match args.get(1).map(String::as_str) {
120            Some(MANIFEST_FLAG) => (self.manifest_json(), 0),
121            _ => crate::execute(stdin, self),
122        }
123    }
124
125    /// The program's `main`. The manifest is printed without reading stdin,
126    /// so `module --subako-manifest` needs no input.
127    // COVERAGE(untestable): reads the real process's arguments and stdio; `execute` is its testable half.
128    pub fn run(&self) -> ExitCode {
129        let args: Vec<String> = std::env::args().collect();
130        match args.get(1).map(String::as_str) {
131            Some(MANIFEST_FLAG) => {
132                let mut stdout = std::io::stdout().lock();
133                match stdout
134                    .write_all(&self.manifest_json())
135                    .and_then(|()| stdout.flush())
136                {
137                    Ok(()) => ExitCode::SUCCESS,
138                    Err(_) => ExitCode::from(2),
139                }
140            }
141            _ => crate::run(self),
142        }
143    }
144
145    fn manifest_json(&self) -> Vec<u8> {
146        // A manifest is plain data, so it always serializes.
147        let mut json = serde_json::to_vec_pretty(&self.manifest()).unwrap_or_default();
148        json.push(b'\n');
149        json
150    }
151}
152
153impl Server for Package {
154    fn info(&self) -> ServerInfo<'_> {
155        ServerInfo {
156            name: &self.name,
157            version: &self.version,
158        }
159    }
160
161    fn tools(&self) -> Vec<serde_json::Value> {
162        self.functions
163            .iter()
164            .map(|f| {
165                serde_json::json!({
166                    "name": f.name,
167                    "description": f.description,
168                    "inputSchema": (f.input_schema)(Contract::Deserialize),
169                    "outputSchema": (f.output_schema)(Contract::Serialize),
170                })
171            })
172            .collect()
173    }
174
175    fn call(&self, tool: &str, input: Input) -> Result<Output, ToolError> {
176        Package::call(self, tool, input)
177    }
178}
179
180/// `T`'s JSON Schema as `contract` reads or writes it, with every subschema
181/// inline so a model reads one self-contained object.
182pub(crate) fn schema<T: JsonSchema>(contract: Contract) -> serde_json::Value {
183    let mut schema = SchemaSettings::draft2020_12()
184        .with(|s| {
185            s.inline_subschemas = true;
186            s.meta_schema = None;
187            s.contract = contract;
188        })
189        .into_generator()
190        .into_root_schema_for::<T>();
191    schema.remove("title");
192    schema.to_value()
193}