BREP_mcp_core 0.3.0

The BREP MCP server core: the Model Context Protocol surface generated from the CAD app's own registries, its sessions, script runner and transports (stdio and streamable HTTP). Embedded in the app behind `brep-app --mcp`; hosted headlessly by BREP_mcp.
Documentation
//! The `test-mcp` script format (spec Appendix E): a session description plus
//! a list of tool calls, each with optional expectations on the tool's JSON
//! result and optional image capture / comparison.
//!
//! The script vocabulary is the tool list itself — a step names a tool and
//! passes its arguments verbatim — so the format never needs to know which
//! tools exist. Only the `expect` mini-language is defined here.
use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Script {
    pub name: String,
    /// What the script checks, for the reader; ignored by the runner.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub note: Option<String>,
    #[serde(default = "default_backend")]
    pub backend: String,
    #[serde(default = "default_size")]
    pub size: [f32; 2],
    #[serde(default = "default_ppp")]
    pub ppp: f32,
    /// Start on the seed model instead of an empty document.
    #[serde(default)]
    pub seed: bool,
    /// A `.BREP.json` to open before the first step (repo-relative or absolute).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub document: Option<String>,
    #[serde(default = "default_timeout")]
    pub timeout_ms: u64,
    pub steps: Vec<Step>,
}

fn default_backend() -> String {
    "headless".into()
}
fn default_size() -> [f32; 2] {
    [1400.0, 960.0]
}
fn default_ppp() -> f32 {
    1.0
}
fn default_timeout() -> u64 {
    120_000
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Step {
    pub tool: String,
    #[serde(default)]
    pub args: Value,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub expect: Vec<Expect>,
    /// Save the step's image (if the tool produced one) under this name.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub save: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub compare: Option<Compare>,
    /// A note for the reader; ignored by the runner.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub note: Option<String>,
    /// The step is expected to FAIL: the tool must return an error, and the
    /// error text must match this glob (`*` = anything).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub expect_error: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Compare {
    /// Path under `BREP_mcp/tests/baselines/`.
    pub baseline: String,
    #[serde(default = "default_max_diff")]
    pub max_diff_ratio: f64,
}

fn default_max_diff() -> f64 {
    0.002
}

/// One expectation: a JSON pointer into the tool result plus exactly one
/// operator. `near` takes a relative tolerance `tol` (default 1e-6).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Expect {
    pub path: String,
    #[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "present")]
    pub eq: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "present")]
    pub ne: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub gt: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub gte: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lt: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lte: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub near: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tol: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub contains: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub matches: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub exists: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub len: Option<usize>,
}

/// Deserialize an optional field so that a WRITTEN `null` is `Some(Value::Null)`
/// and only an ABSENT key is `None`.
///
/// `Option<Value>` normally collapses the two, which makes `{"eq": null}` read
/// as "no operator given" — so the one way to assert that a field IS null (an
/// inactive PMI view, a solid with no colour override) parsed as a script
/// error. The distinction is exactly what this expectation format needs.
fn present<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    Value::deserialize(deserializer).map(Some)
}

/// Exit codes of `brep-mcp test`.
pub mod exit {
    pub const PASS: i32 = 0;
    pub const EXPECTATION_FAILED: i32 = 1;
    pub const TOOL_ERROR: i32 = 2;
    pub const HOST_ERROR: i32 = 3;
    pub const TIMEOUT: i32 = 4;
}

impl Script {
    pub fn parse(text: &str) -> Result<Self, String> {
        let s: Script = serde_json::from_str(text).map_err(|e| format!("script parse: {e}"))?;
        if s.steps.is_empty() {
            return Err("script has no steps".into());
        }
        for (i, step) in s.steps.iter().enumerate() {
            for (j, e) in step.expect.iter().enumerate() {
                if e.operator_count() != 1 {
                    return Err(format!(
                        "step {i} (`{}`) expect {j}: exactly one operator is required",
                        step.tool
                    ));
                }
                if !e.path.starts_with('/') && !e.path.is_empty() {
                    return Err(format!(
                        "step {i} expect {j}: `path` must be a JSON pointer starting with `/` (got `{}`)",
                        e.path
                    ));
                }
            }
        }
        Ok(s)
    }

    /// The JSON Schema of the format (`brep://script/format`), derived from
    /// these types.
    pub fn json_schema() -> Value {
        serde_json::to_value(schemars::schema_for!(Script)).unwrap_or(Value::Null)
    }
}

impl Expect {
    fn operator_count(&self) -> usize {
        [
            self.eq.is_some(),
            self.ne.is_some(),
            self.gt.is_some(),
            self.gte.is_some(),
            self.lt.is_some(),
            self.lte.is_some(),
            self.near.is_some(),
            self.contains.is_some(),
            self.matches.is_some(),
            self.exists.is_some(),
            self.len.is_some(),
        ]
        .iter()
        .filter(|b| **b)
        .count()
    }

    /// Evaluate against a tool result. `Ok(())` when satisfied; `Err(why)`
    /// otherwise, with the actual value in the message.
    pub fn check(&self, result: &Value) -> Result<(), String> {
        let actual = result.pointer(&self.path);
        if let Some(exists) = self.exists {
            return if actual.is_some() == exists {
                Ok(())
            } else {
                Err(format!("{}: exists == {} expected {exists}", self.path, actual.is_some()))
            };
        }
        let Some(actual) = actual else {
            return Err(format!("{}: no value at that path", self.path));
        };
        let num = |v: &Value| v.as_f64();
        let shown = {
            let t = actual.to_string();
            if t.chars().count() > 160 {
                format!("{}", t.chars().take(160).collect::<String>())
            } else {
                t
            }
        };
        let fail = |what: &str| Err(format!("{}: {what}; actual {shown}", self.path));
        if let Some(e) = &self.eq {
            return if actual == e { Ok(()) } else { fail(&format!("expected {e}")) };
        }
        if let Some(e) = &self.ne {
            return if actual != e { Ok(()) } else { fail(&format!("expected not {e}")) };
        }
        if let Some(b) = self.gt {
            return match num(actual) { Some(a) if a > b => Ok(()), _ => fail(&format!("expected > {b}")) };
        }
        if let Some(b) = self.gte {
            return match num(actual) { Some(a) if a >= b => Ok(()), _ => fail(&format!("expected >= {b}")) };
        }
        if let Some(b) = self.lt {
            return match num(actual) { Some(a) if a < b => Ok(()), _ => fail(&format!("expected < {b}")) };
        }
        if let Some(b) = self.lte {
            return match num(actual) { Some(a) if a <= b => Ok(()), _ => fail(&format!("expected <= {b}")) };
        }
        if let Some(b) = self.near {
            let tol = self.tol.unwrap_or(1e-6);
            return match num(actual) {
                Some(a) if (a - b).abs() <= tol * b.abs().max(1.0) => Ok(()),
                _ => fail(&format!("expected within {tol} (relative) of {b}")),
            };
        }
        if let Some(needle) = &self.contains {
            let ok = match (actual, needle) {
                (Value::String(s), Value::String(n)) => s.contains(n.as_str()),
                (Value::Array(a), n) => a.contains(n),
                (Value::Object(o), Value::String(n)) => o.contains_key(n),
                _ => false,
            };
            return if ok { Ok(()) } else { fail(&format!("expected to contain {needle}")) };
        }
        if let Some(pattern) = &self.matches {
            let Some(s) = actual.as_str() else { return fail("expected a string to match") };
            return if glob_match(pattern, s) { Ok(()) } else { fail(&format!("expected to match `{pattern}`")) };
        }
        if let Some(n) = self.len {
            let l = match actual {
                Value::Array(a) => Some(a.len()),
                Value::String(s) => Some(s.chars().count()),
                Value::Object(o) => Some(o.len()),
                _ => None,
            };
            return match l { Some(l) if l == n => Ok(()), _ => fail(&format!("expected length {n}")) };
        }
        Err(format!("{}: no operator", self.path))
    }
}

/// `matches` uses a small glob (`*` any run, `?` one char) rather than a regex
/// crate: enough for ids like `E*` and messages like `*cannot determine*`.
pub fn glob_match(pattern: &str, text: &str) -> bool {
    fn rec(p: &[char], t: &[char]) -> bool {
        match (p.first(), t.first()) {
            (None, None) => true,
            (Some('*'), _) => rec(&p[1..], t) || (!t.is_empty() && rec(p, &t[1..])),
            (Some('?'), Some(_)) => rec(&p[1..], &t[1..]),
            (Some(a), Some(b)) if a == b => rec(&p[1..], &t[1..]),
            _ => false,
        }
    }
    let p: Vec<char> = pattern.chars().collect();
    let t: Vec<char> = text.chars().collect();
    rec(&p, &t)
}

// BREP private tests: 83aa0801f811b4a8