graphic design tool
git clone https://git.lucas.co/cce-designer.git
src/api.rs (12.4K)
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, plus the playbar, the network grid and the status line's text (`status`).",
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 (radius, base_resolution); its label (Base Resolution) is accepted too" },
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_bypass",
95 "Bypass the node at the given slot, or stop bypassing it. A bypassed node stays in the graph and does nothing: what reads it gets the node's own Input.",
96 json!({
97 "type": "object",
98 "properties": { "slot": slot("Child index in the current network level") },
99 "required": ["slot"],
100 }),
101 ),
102 tool(
103 "toggle_geometry",
104 "Toggle geometry visibility for the node at the given slot.",
105 json!({
106 "type": "object",
107 "properties": { "slot": slot("Child index in the current network level") },
108 "required": ["slot"],
109 }),
110 ),
111 tool(
112 "add_node",
113 "Add a node from a template (e.g. \"Sphere\") at grid position (x, y) in the current network level. The root holds Geometry nodes, cameras and pages; every operator goes inside a Geometry node, and a template that does not belong at the level is refused.",
114 json!({
115 "type": "object",
116 "properties": {
117 "template_name": { "type": "string", "description": "Template label or type, case-insensitive" },
118 "name": { "type": "string", "description": "Optional node name; auto-numbered when omitted" },
119 "x": { "type": "number", "description": "Grid column" },
120 "y": { "type": "number", "description": "Grid row" },
121 },
122 "required": ["template_name", "x", "y"],
123 }),
124 ),
125 tool(
126 "delete_node",
127 "Delete the node at the given slot in the current network level.",
128 json!({
129 "type": "object",
130 "properties": { "slot": slot("Child index in the current network level") },
131 "required": ["slot"],
132 }),
133 ),
134 tool(
135 "rename_node",
136 "Rename the node at the given slot.",
137 json!({
138 "type": "object",
139 "properties": {
140 "slot": slot("Child index in the current network level"),
141 "new_name": { "type": "string" },
142 },
143 "required": ["slot", "new_name"],
144 }),
145 ),
146 tool(
147 "move_node",
148 "Move the node at the given slot to grid position (x, y).",
149 json!({
150 "type": "object",
151 "properties": {
152 "slot": slot("Child index in the current network level"),
153 "x": { "type": "number", "description": "Grid column" },
154 "y": { "type": "number", "description": "Grid row" },
155 },
156 "required": ["slot", "x", "y"],
157 }),
158 ),
159 tool(
160 "add_param",
161 "Add a parameter to the node at the given slot.",
162 json!({
163 "type": "object",
164 "properties": {
165 "slot": slot("Child index in the current network level"),
166 "name": { "type": "string", "description": "The parameter's name, what a ch() path spells: lowercase letters, digits and underscores (base_resolution)" },
167 "label": { "type": "string", "description": "What the params pane shows for it (Base Resolution); the name when absent" },
168 "param_type": { "type": "string", "description": "One of text, float, slider, spinbox, float3, choice, toggle, button, code, node, attribute, group — optionally with detail after a colon (slider:-2:2, choice:A,B,C)" },
169 "default": { "type": "string", "description": "Default value, as a string" },
170 },
171 "required": ["slot", "name", "param_type", "default"],
172 }),
173 ),
174 tool(
175 "delete_param",
176 "Delete a parameter from the node at the given slot.",
177 json!({
178 "type": "object",
179 "properties": {
180 "slot": slot("Child index in the current network level"),
181 "name": { "type": "string", "description": "Parameter name (radius, base_resolution); its label (Base Resolution) is accepted too" },
182 },
183 "required": ["slot", "name"],
184 }),
185 ),
186 tool("toggle_circular_pane", "Toggle the circular network pane.", no_args()),
187 tool(
188 "set_pane_collapsed",
189 "Collapse a pane to its title stub, or expand it back.",
190 json!({
191 "type": "object",
192 "properties": {
193 "pane": { "type": "string", "description": "network | parameters | spreadsheet | playbar" },
194 "collapsed": { "type": "boolean", "description": "true to collapse, false to expand" },
195 },
196 "required": ["pane", "collapsed"],
197 }),
198 ),
199 tool(
200 "set_pane_detached",
201 "Move a pane into its own window, or take it back.",
202 json!({
203 "type": "object",
204 "properties": {
205 "pane": { "type": "string", "description": "network | parameters | spreadsheet | playbar" },
206 "detached": { "type": "boolean", "description": "true to detach, false to reattach" },
207 },
208 "required": ["pane", "detached"],
209 }),
210 ),
211 tool(
212 "set_frame",
213 "Move the playhead. Simnets solve up to this frame.",
214 json!({
215 "type": "object",
216 "properties": {
217 "frame": { "type": "number", "description": "Timeline frame" },
218 },
219 "required": ["frame"],
220 }),
221 ),
222 tool(
223 "curve_set_points",
224 "Replace a curve node's control points (world-space [x, y, z] triples). The Catmull-Rom strip re-evaluates immediately.",
225 json!({
226 "type": "object",
227 "properties": {
228 "slot": { "type": "integer", "description": "Node index in the current directory; must be a curve node" },
229 "points": {
230 "type": "array",
231 "items": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 },
232 "description": "Control points as [x, y, z] triples; replaces the whole list",
233 },
234 },
235 "required": ["slot", "points"],
236 }),
237 ),
238 tool(
239 "menu_click",
240 "Click a menubar item by indices. Only the viewport menubar's Camera menu (menu 0) and the parameters menubar's Preset and Reset menus (0, 1) are dispatched this way; everything else is a command, see run_command.",
241 json!({
242 "type": "object",
243 "properties": {
244 "widget_idx": { "type": "integer", "description": "Widget slot of the menubar" },
245 "menu_idx": { "type": "integer", "description": "Menu index within the menubar" },
246 "item_idx": { "type": "integer", "description": "Item index within the menu" },
247 },
248 "required": ["widget_idx", "menu_idx", "item_idx"],
249 }),
250 ),
251 tool(
252 "run_command",
253 "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.",
254 json!({
255 "type": "object",
256 "properties": { "id": { "type": "string", "description": "Command id, snake_case, as input.kdl binds it" } },
257 "required": ["id"],
258 }),
259 ),
260 tool(
261 "menu_action",
262 "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.",
263 json!({
264 "type": "object",
265 "properties": { "label": { "type": "string", "description": "Menu item label" } },
266 "required": ["label"],
267 }),
268 ),
269 ]
270 }