1use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use brep_render::engine_state::EngineState;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
14#[serde(rename_all = "snake_case")]
15pub enum Phase {
16 Input,
18 Mutate,
20 Read,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
25pub struct Annotations {
26 pub read_only: bool,
27 pub destructive: bool,
28 pub idempotent: bool,
29 pub waits: bool,
31}
32
33impl Annotations {
34 pub const READ: Annotations = Annotations { read_only: true, destructive: false, idempotent: true, waits: false };
35 pub const INPUT: Annotations = Annotations { read_only: false, destructive: false, idempotent: false, waits: false };
36 pub const MUTATE: Annotations = Annotations { read_only: false, destructive: false, idempotent: false, waits: true };
37 pub const MUTATE_NOWAIT: Annotations = Annotations { read_only: false, destructive: false, idempotent: true, waits: false };
38 pub const DESTRUCTIVE: Annotations = Annotations { read_only: false, destructive: true, idempotent: false, waits: true };
39}
40
41pub struct Ctx<'a> {
43 pub app: &'a mut crate::app::BrepApp,
44 pub egui: &'a egui::Context,
45}
46
47pub enum Outcome {
49 Done(Value),
50 Blob { json: Value, bytes: Vec<u8> },
52 AwaitScreenshot { token: u64, region: crate::automation::cmd_capture::Region, document: Option<String> },
56 Poll(Poller),
62}
63
64pub type Poller = Box<dyn FnMut(&mut Ctx<'_>) -> Option<Result<Outcome, String>> + Send>;
66
67pub type InputHandler = fn(&mut crate::automation::pointer::Pointer, Value, &mut Vec<egui::Event>) -> Result<Value, String>;
68pub type AppHandler = fn(&mut Ctx<'_>, Value) -> Result<Outcome, String>;
69
70pub enum Handler {
71 Input(InputHandler),
72 App(AppHandler),
73}
74
75pub struct CommandSpec {
76 pub name: &'static str,
77 pub group: &'static str,
78 pub doc: &'static str,
79 pub phase: Phase,
80 pub annotations: Annotations,
81 pub args_schema: fn() -> Value,
82 pub result_schema: fn() -> Value,
83 pub handler: Handler,
84}
85
86macro_rules! app_command {
94 ($group:literal, $name:literal, $doc:expr, $args:ty, $handler:path, $phase:ident, $annotation:ident, $output:expr) => {
95 $crate::automation::command::CommandSpec {
96 name: $name,
97 group: $group,
98 doc: $doc,
99 phase: $crate::automation::command::Phase::$phase,
100 annotations: $crate::automation::command::Annotations::$annotation,
101 args_schema: $crate::automation::command::schema_of::<$args>,
102 result_schema: $output,
103 handler: $crate::automation::command::Handler::App($handler),
104 }
105 };
106 ($group:literal, $name:literal, $doc:expr, $args:ty, $handler:path, $phase:ident, $annotation:ident) => {
107 $crate::automation::command::app_command!($group, $name, $doc, $args, $handler, $phase, $annotation, $crate::automation::command::empty_result_schema)
108 };
109}
110pub(crate) use app_command;
111
112pub fn engine_call<A, R, E>(ctx: &mut Ctx<'_>, args: Value, f: impl FnOnce(&mut EngineState, A) -> Result<R, E>) -> Result<Outcome, String>
116where
117 A: serde::de::DeserializeOwned,
118 String: From<E>,
119{
120 let a: A = parse_args(args)?;
121 f(ctx.app.docs.engine_mut(), a)?;
122 Ok(Outcome::Done(serde_json::json!({})))
123}
124
125pub fn read_engine(ctx: &mut Ctx<'_>, args: Value, f: impl FnOnce(&EngineState) -> Value) -> Result<Outcome, String> {
128 let _: NoArgs = parse_args(args)?;
129 Ok(Outcome::Done(f(ctx.app.docs.engine())))
130}
131
132pub fn schema_of<T: schemars::JsonSchema>() -> Value {
140 let mut schema = serde_json::to_value(schemars::schema_for!(T)).unwrap_or(Value::Null);
141 normalize_bool_subschemas(&mut schema);
142 schema
143}
144
145pub fn empty_result_schema() -> Value {
149 serde_json::json!({
150 "type": "object",
151 "properties": {},
152 "additionalProperties": false,
153 "description": "No native application payload. The MCP adapter adds frame, optional notices and settlement information."
154 })
155}
156
157pub(super) fn normalize_bool_subschemas(node: &mut Value) {
162 match node {
163 Value::Object(map) => {
164 if let Some(Value::Object(props)) = map.get_mut("properties") {
165 for value in props.values_mut() {
166 if let Some(b) = value.as_bool() {
167 *value = if b { serde_json::json!({}) } else { serde_json::json!({"not": {}}) };
168 }
169 }
170 }
171 if let Some(items) = map.get_mut("items") {
172 if let Some(b) = items.as_bool() {
173 *items = if b { serde_json::json!({}) } else { serde_json::json!({"not": {}}) };
174 }
175 }
176 for value in map.values_mut() {
177 normalize_bool_subschemas(value);
178 }
179 }
180 Value::Array(arr) => {
181 for value in arr.iter_mut() {
182 normalize_bool_subschemas(value);
183 }
184 }
185 _ => {}
186 }
187}
188
189pub fn parse_args<T: serde::de::DeserializeOwned>(args: Value) -> Result<T, String> {
190 let args = if args.is_null() { Value::Object(Default::default()) } else { args };
192 serde_json::from_value(args).map_err(|e| format!("arguments: {e}"))
193}
194
195#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
197#[serde(deny_unknown_fields)]
198pub struct NoArgs {}
199
200#[derive(Debug, Default, Serialize, schemars::JsonSchema)]
202pub struct Empty {}
203
204pub fn registry() -> Vec<&'static CommandSpec> {
209 use crate::automation::*;
210 let sets: &[&[CommandSpec]] = &[
211 cmd_frame::COMMANDS,
212 cmd_files::COMMANDS,
213 cmd_perf::COMMANDS,
214 cmd_input::COMMANDS,
215 cmd_capture::COMMANDS,
216 cmd_state::COMMANDS,
217 cmd_document::COMMANDS,
218 cmd_classes::COMMANDS,
219 cmd_history::COMMANDS,
220 cmd_scene::COMMANDS,
221 cmd_camera::COMMANDS,
222 cmd_settings::COMMANDS,
223 cmd_shell::COMMANDS,
224 cmd_plugins::COMMANDS,
225 cmd_metadata::COMMANDS,
226 cmd_assembly::COMMANDS,
227 cmd_pmi::COMMANDS,
228 cmd_sheets::COMMANDS,
229 cmd_wire_harness::COMMANDS,
230 cmd_cam::COMMANDS,
231 cmd_exploded::COMMANDS,
232 cmd_instructions::COMMANDS,
233 cmd_instructions_motion::COMMANDS,
234 cmd_simulation::COMMANDS,
235 cmd_simulation_play::COMMANDS,
236 cmd_cam_sim::COMMANDS,
237 cmd_ecad::COMMANDS,
238 #[cfg(not(target_arch = "wasm32"))]
239 cmd_plm::COMMANDS,
240 #[cfg(not(target_arch = "wasm32"))]
241 cmd_kicad::COMMANDS,
242 ];
243 sets.iter().flat_map(|s| s.iter()).collect()
244}
245
246pub fn lookup(name: &str) -> Option<&'static CommandSpec> {
247 registry().into_iter().find(|c| c.name == name)
248}
249
250pub fn describe() -> Value {
253 Value::Array(
254 registry()
255 .into_iter()
256 .map(|c| {
257 serde_json::json!({
258 "name": c.name,
259 "group": c.group,
260 "doc": c.doc,
261 "phase": c.phase,
262 "annotations": c.annotations,
263 "argsSchema": (c.args_schema)(),
264 "resultSchema": (c.result_schema)(),
265 })
266 })
267 .collect(),
268 )
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct Envelope {
275 pub id: u64,
276 pub cmd: String,
277 #[serde(default)]
278 pub args: Value,
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
282#[serde(rename_all = "snake_case")]
283pub enum NoticeKind {
284 Panic,
285 LogError,
286 RunnerRefusal,
287 Toast,
288 Console,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
292pub struct Notice {
293 pub kind: NoticeKind,
294 pub frame: u64,
295 pub text: String,
296}
297
298#[derive(Debug, Clone, Default, Serialize, Deserialize)]
299pub struct Reply {
300 pub id: u64,
301 pub frame: u64,
302 pub ok: bool,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub result: Option<Value>,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub error: Option<String>,
307 #[serde(default, skip_serializing_if = "Vec::is_empty")]
308 pub notices: Vec<Notice>,
309 #[serde(skip)]
312 pub blob: Option<Vec<u8>>,
313}
314
315impl Reply {
316 pub fn ok(id: u64, frame: u64, result: Value, notices: Vec<Notice>) -> Self {
317 Self { id, frame, ok: true, result: Some(result), error: None, notices, blob: None }
318 }
319 pub fn err(id: u64, frame: u64, error: impl Into<String>, notices: Vec<Notice>) -> Self {
320 Self { id, frame, ok: false, result: None, error: Some(error.into()), notices, blob: None }
321 }
322
323 pub fn into_host(mut self) -> brep_mcp_core::host::Reply {
329 use brep_mcp_core::host::Reply as HostReply;
330 let blob = self.blob.take();
331 let (id, frame) = (self.id, self.frame);
332 match serde_json::to_value(&self).map_err(|e| e.to_string()).and_then(|v| HostReply::from_app(v, blob)) {
333 Ok(reply) => reply,
334 Err(e) => HostReply::err(id, frame, e),
335 }
336 }
337}
338