git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

src/api.rs (11.1K)

  1 //! The embedded MCP automation server — the way to drive/inspect the
  2 //! running app (the former bespoke HTTP API was retired in its favor).
  3 
  4 use cce_ui::mcp::McpTool;
  5 use serde_json::json;
  6 use crate::CustomEvent;
  7 
  8 /// Start the embedded MCP server (cce-ui's tools-only Streamable HTTP
  9 /// implementation). Agents attach with
 10 /// `claude mcp add --transport http cce-designer http://127.0.0.1:3001/mcp`.
 11 pub fn start_mcp_server(server_sender: calloop::channel::Sender<CustomEvent>) {
 12     // CCE_DESIGNER_MCP_PORT overrides the default so a second instance
 13     // (tests, debugging) can run alongside one already holding 3001.
 14     let port: u16 = std::env::var("CCE_DESIGNER_MCP_PORT")
 15         .ok()
 16         .and_then(|p| p.parse().ok())
 17         .unwrap_or(3001);
 18     cce_ui::mcp::start_mcp_server("cce-designer", port, mcp_tools(), server_sender, CustomEvent::McpCall);
 19 }
 20 
 21 /// The designer's MCP tools: `get_state` plus one tool per `McpAction`
 22 /// variant — the tool name is the variant's serde tag and the arguments are
 23 /// its fields, so dispatch is deserialization (see `apply_mcp_call`).
 24 pub(crate) fn mcp_tools() -> Vec<McpTool> {
 25     let tool = |name: &str, description: &str, schema: serde_json::Value| McpTool {
 26         name: name.to_string(),
 27         description: description.to_string(),
 28         input_schema: schema,
 29     };
 30     let no_args = || json!({ "type": "object", "properties": {} });
 31     let slot = |desc: &str| json!({ "type": "integer", "description": desc });
 32     vec![
 33         tool(
 34             "get_state",
 35             "Get the current project state (node tree with params, cameras, pan, current path, selection) as JSON.",
 36             no_args(),
 37         ),
 38         tool(
 39             "up",
 40             "Navigate up one level in the node network (out of the current subnet).",
 41             no_args(),
 42         ),
 43         tool(
 44             "enter",
 45             "Enter the subnet/node at the given slot index in the current network level.",
 46             json!({
 47                 "type": "object",
 48                 "properties": { "slot": slot("Child index in the current network level") },
 49                 "required": ["slot"],
 50             }),
 51         ),
 52         tool(
 53             "select",
 54             "Select the node at the given slot in the current network level (like clicking it); its parameters populate the parameter pane.",
 55             json!({
 56                 "type": "object",
 57                 "properties": { "slot": slot("Child index in the current network level") },
 58                 "required": ["slot"],
 59             }),
 60         ),
 61         tool(
 62             "set_param",
 63             "Set a parameter on the node at the given slot. All values are strings (e.g. \"1.5\", \"0.2,0.4,1\"). A value that reads as an expression — ch(\"../sphere1/Radius\") * 2, $F / 24 — becomes one (Houdini paths: relative to the node, .. its parent, / the root, a bare name its own parameter).",
 64             json!({
 65                 "type": "object",
 66                 "properties": {
 67                     "slot": slot("Child index in the current network level"),
 68                     "name": { "type": "string", "description": "Parameter name" },
 69                     "value": { "type": "string", "description": "New value, as a string" },
 70                 },
 71                 "required": ["slot", "name", "value"],
 72             }),
 73         ),
 74         tool("reset_camera", "Reset the 3D viewport camera rotation and zoom.", no_args()),
 75         tool(
 76             "load",
 77             "Load a project (a project directory containing state.json, or a single state .json file).",
 78             json!({
 79                 "type": "object",
 80                 "properties": { "path": { "type": "string", "description": "Filesystem path" } },
 81                 "required": ["path"],
 82             }),
 83         ),
 84         tool(
 85             "save",
 86             "Save the current project to the given path.",
 87             json!({
 88                 "type": "object",
 89                 "properties": { "path": { "type": "string", "description": "Filesystem path" } },
 90                 "required": ["path"],
 91             }),
 92         ),
 93         tool(
 94             "toggle_geometry",
 95             "Toggle geometry visibility for the node at the given slot.",
 96             json!({
 97                 "type": "object",
 98                 "properties": { "slot": slot("Child index in the current network level") },
 99                 "required": ["slot"],
100             }),
101         ),
102         tool(
103             "add_node",
104             "Add a node from a template (e.g. \"Sphere\") at grid position (x, y) in the current network level.",
105             json!({
106                 "type": "object",
107                 "properties": {
108                     "template_name": { "type": "string", "description": "Template label or type, case-insensitive" },
109                     "name": { "type": "string", "description": "Optional node name; auto-numbered when omitted" },
110                     "x": { "type": "number", "description": "Grid column" },
111                     "y": { "type": "number", "description": "Grid row" },
112                 },
113                 "required": ["template_name", "x", "y"],
114             }),
115         ),
116         tool(
117             "delete_node",
118             "Delete the node at the given slot in the current network level.",
119             json!({
120                 "type": "object",
121                 "properties": { "slot": slot("Child index in the current network level") },
122                 "required": ["slot"],
123             }),
124         ),
125         tool(
126             "rename_node",
127             "Rename the node at the given slot.",
128             json!({
129                 "type": "object",
130                 "properties": {
131                     "slot": slot("Child index in the current network level"),
132                     "new_name": { "type": "string" },
133                 },
134                 "required": ["slot", "new_name"],
135             }),
136         ),
137         tool(
138             "move_node",
139             "Move the node at the given slot to grid position (x, y).",
140             json!({
141                 "type": "object",
142                 "properties": {
143                     "slot": slot("Child index in the current network level"),
144                     "x": { "type": "number", "description": "Grid column" },
145                     "y": { "type": "number", "description": "Grid row" },
146                 },
147                 "required": ["slot", "x", "y"],
148             }),
149         ),
150         tool(
151             "add_param",
152             "Add a parameter to the node at the given slot.",
153             json!({
154                 "type": "object",
155                 "properties": {
156                     "slot": slot("Child index in the current network level"),
157                     "name": { "type": "string" },
158                     "param_type": { "type": "string", "description": "e.g. float, int, slider, float3, spinbox, choice" },
159                     "default": { "type": "string", "description": "Default value, as a string" },
160                 },
161                 "required": ["slot", "name", "param_type", "default"],
162             }),
163         ),
164         tool(
165             "delete_param",
166             "Delete a parameter from the node at the given slot.",
167             json!({
168                 "type": "object",
169                 "properties": {
170                     "slot": slot("Child index in the current network level"),
171                     "name": { "type": "string", "description": "Parameter name" },
172                 },
173                 "required": ["slot", "name"],
174             }),
175         ),
176         tool("toggle_circular_pane", "Toggle the circular network pane.", no_args()),
177         tool(
178             "set_pane_collapsed",
179             "Collapse a pane to its title stub, or expand it back.",
180             json!({
181                 "type": "object",
182                 "properties": {
183                     "pane": { "type": "string", "description": "network | parameters | spreadsheet | playbar" },
184                     "collapsed": { "type": "boolean", "description": "true to collapse, false to expand" },
185                 },
186                 "required": ["pane", "collapsed"],
187             }),
188         ),
189         tool(
190             "set_pane_detached",
191             "Move a pane into its own window, or take it back.",
192             json!({
193                 "type": "object",
194                 "properties": {
195                     "pane": { "type": "string", "description": "network | parameters | spreadsheet | playbar" },
196                     "detached": { "type": "boolean", "description": "true to detach, false to reattach" },
197                 },
198                 "required": ["pane", "detached"],
199             }),
200         ),
201         tool(
202             "set_frame",
203             "Move the playhead. Simnets solve up to this frame.",
204             json!({
205                 "type": "object",
206                 "properties": {
207                     "frame": { "type": "number", "description": "Timeline frame" },
208                 },
209                 "required": ["frame"],
210             }),
211         ),
212         tool(
213             "curve_set_points",
214             "Replace a curve node's control points (world-space [x, y, z] triples). The Catmull-Rom strip re-evaluates immediately.",
215             json!({
216                 "type": "object",
217                 "properties": {
218                     "slot": { "type": "integer", "description": "Node index in the current directory; must be a curve node" },
219                     "points": {
220                         "type": "array",
221                         "items": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 },
222                         "description": "Control points as [x, y, z] triples; replaces the whole list",
223                     },
224                 },
225                 "required": ["slot", "points"],
226             }),
227         ),
228         tool(
229             "menu_click",
230             "Click a menubar item by indices (widget_idx must be a menubar widget slot).",
231             json!({
232                 "type": "object",
233                 "properties": {
234                     "widget_idx": { "type": "integer", "description": "Widget slot of the menubar" },
235                     "menu_idx": { "type": "integer", "description": "Menu index within the menubar" },
236                     "item_idx": { "type": "integer", "description": "Item index within the menu" },
237                 },
238                 "required": ["widget_idx", "menu_idx", "item_idx"],
239             }),
240         ),
241         tool(
242             "run_command",
243             "Run a command by its registry id (e.g. \"toggle_dialog\", \"toggle_grid\", \"save_document\") — every command the dialog lists, including the ones no menu label reaches.",
244             json!({
245                 "type": "object",
246                 "properties": { "id": { "type": "string", "description": "Command id, snake_case, as input.kdl binds it" } },
247                 "required": ["id"],
248             }),
249         ),
250         tool(
251             "menu_action",
252             "Execute a menu action by its label (e.g. \"Show Spreadsheet Pane\", \"Save\") — reaches label-matched menu-pane items that menu_click's index dispatch cannot.",
253             json!({
254                 "type": "object",
255                 "properties": { "label": { "type": "string", "description": "Menu item label" } },
256                 "required": ["label"],
257             }),
258         ),
259     ]
260 }