Skip to content

Tool Schema Builder — Rust source

Build function-calling and MCP tool schemas that pass strict mode on the first try, and lint pasted ones against the strict-mode contract — additionalProperties, required-sync, defaults, enums — with one-click autofix for every mechanical violation. Runs entirely in your browser.

This is the Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! Tool Schema Builder — strict-mode validation of function-calling / MCP
//! tool definitions (OpenAI strict mode / MCP inputSchema contract).
//! CosmoDev polyglot showcase port from src/lib/tool-schema.ts. Rust 2021 + serde_json.

use serde_json::{json, Map, Value};

const SUPPORTED_TYPES: [&str; 6] = ["string", "number", "integer", "boolean", "object", "array"];

struct Issue { rule: &'static str, path: String }

macro_rules! issue {
    ($issues:expr, $rule:expr, $path:expr) => { $issues.push(Issue { rule: $rule, path: $path.into() }) };
}

fn as_obj(v: Option<&Value>) -> Option<&Map<String, Value>> { match v { Some(Value::Object(m)) => Some(m), _ => None } }

fn str_field<'a>(m: &'a Map<String, Value>, k: &str) -> Option<&'a str> { m.get(k).and_then(Value::as_str) }

// The recursive strict-mode walk — every object nests the same rules.
fn check_object(path: &str, obj: &Map<String, Value>, issues: &mut Vec<Issue>) {
    if obj.get("additionalProperties").and_then(Value::as_bool) != Some(false) {
        issue!(issues, "no-additional-properties", path);
    }
    if let Some(props) = as_obj(obj.get("properties")) {
        let required: Vec<&str> = obj.get("required").and_then(Value::as_array)
            .map(|a| a.iter().filter_map(Value::as_str).collect()).unwrap_or_default();
        let missing: Vec<&str> = props.keys()
            .filter(|k| !required.contains(&k.as_str())).map(String::as_str).collect();
        if !missing.is_empty() {
            issue!(issues, "all-required", format!("{path}: required missing {}", missing.join(", ")));
        }
        for (key, raw) in props {
            let Some(prop) = as_obj(Some(raw)) else { continue };
            let p = format!("{path}.properties.{key}");
            if prop.contains_key("default") { issue!(issues, "no-defaults", p.clone()); }
            if !str_field(prop, "description").is_some_and(|d| !d.trim().is_empty()) {
                issue!(issues, "description-present", p.clone());
            }
            let ty = str_field(prop, "type");
            if !ty.is_some_and(|t| SUPPORTED_TYPES.contains(&t)) {
                issue!(issues, "typed-properties", format!("{p}: must be one of {}", SUPPORTED_TYPES.join(" | ")));
            }
            if let Some(Value::Array(en)) = prop.get("enum") {
                let kind = |v: &Value| match v {
                    Value::String(_) => "string", Value::Number(_) => "number",
                    Value::Bool(_) => "boolean", _ => "other",
                };
                let mut mixed = false;
                if let Some(first) = en.first().map(kind) { mixed = en.iter().any(|v| kind(v) != first); }
                if en.is_empty() || mixed || en.iter().any(|v| kind(v) == "other") {
                    issue!(issues, "enum-values", p.clone());
                }
            }
            if ty == Some("array") && as_obj(prop.get("items")).is_none() { issue!(issues, "array-items", p.clone()); }
            if ty == Some("object") && as_obj(prop.get("properties")).is_some() { check_object(&p, prop, issues); }
        }
    }
}

// Validate a parsed tool definition ({name, description, input_schema}).
fn validate_schema(root: &Value) -> Vec<Issue> {
    let Some(m) = as_obj(Some(root)) else {
        return vec![Issue { rule: "json-parseable", path: "$: input must be a JSON object".into() }];
    };
    let mut issues = Vec::new();
    let name = str_field(m, "name").unwrap_or_default();
    let name_ok = (1..=64).contains(&name.len()) && name.chars()
        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-');
    if !name_ok { issue!(issues, "non-empty-name", "name: must be 1-64 chars of [a-z0-9_-]"); }
    if !str_field(m, "description").is_some_and(|d| !d.trim().is_empty()) {
        issue!(issues, "description-present", "description: the tool needs a description");
    }
    match as_obj(m.get("input_schema")) {
        Some(s) if str_field(s, "type") == Some("object") => check_object("input_schema", s, &mut issues),
        _ => issue!(issues, "json-parseable", "input_schema: must be an object with type: \"object\""),
    }
    issues
}

fn main() {
    // Demo: validate a small broken definition and print the issues.
    let broken = json!({"name": "Get_Weather", "input_schema": {"type": "object", "properties": {
        "city": {"type": "string", "default": "Paris"},
        "unit": {"type": "string", "description": "celsius or fahrenheit", "enum": ["c", 2]},
        "tags": {"type": "array"}}, "required": ["city"]}});
    for i in validate_schema(&broken) { println!("{}  {}", i.rule, i.path); }
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →