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 }