graphic design tool
git clone https://git.lucas.co/cce-designer.git
refactor: retire the HTTP API; MCP is the only automation surface
The bespoke HTTP server (port 3000, GET /state + POST /action,
CCE_DESIGNER_HTTP_PORT) is removed; the MCP server on 3001 already
speaks the same action surface with discoverable, schema'd tools.
HttpAction is renamed McpAction and the GetState/PostAction events are
gone. The app was also its own HTTP client: the cce-files chooser
threads delivered the picked path by POSTing load/save to port 3000 —
they now send a fire-and-forget CustomEvent::RunAction over the engine
calloop sender stored in State::event_sender.
Co-Authored-By: Claude Fable 5 <[email protected]>
CLAUDE.md | 30 +++++------
src/api.rs | 146 +++--------------------------------------------------
src/app.rs | 60 ++++++----------------
src/application.rs | 8 +--
src/main.rs | 18 +++----
src/window.rs | 70 +++++++++++++------------
6 files changed, 85 insertions(+), 247 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 51c26a2..693fdd4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -39,23 +39,21 @@ kernels and need a working OpenCL runtime; they are not pure-CPU tests.
with the main window by autosaving/polling `default_project.json` mtime (see the
main loop in `src/main.rs`) — there is no socket between the two.
-### HTTP automation API + MCP server
-
-The main window runs an embedded HTTP server on `127.0.0.1:3000` (`src/api.rs`):
-`GET /state` returns the app state as JSON; `POST /action` takes an `HttpAction`
-JSON body (`add_node`, `set_param`, `menu_action`, `save`, `load`, …— see the enum
-in `src/app.rs`). This is the main way to drive/inspect the running app when
-debugging: `curl -X POST localhost:3000/action -d '{"action":"add_node","template_name":"Sphere","x":5,"y":3}'`.
-
-The same surface is exposed as an MCP server on `127.0.0.1:3001`
-(`CCE_DESIGNER_MCP_PORT` overrides; attach with
-`claude mcp add --transport http cce-designer http://127.0.0.1:3001/mcp`):
-one tool per `HttpAction` variant (tool name = the variant's serde tag,
-dispatched in `apply_mcp_call` in `src/window.rs`) plus `get_state`. The tool
-list lives in `mcp_tools()` in `src/api.rs`; the protocol layer is
+### MCP automation server
+
+The main window runs an embedded MCP server on `127.0.0.1:3001`
+(`CCE_DESIGNER_MCP_PORT` overrides so a second instance can run alongside;
+`src/api.rs`). This is the way to drive/inspect the running app: attach with
+`claude mcp add --transport http cce-designer http://127.0.0.1:3001/mcp`, or
+speak JSON-RPC directly with curl (`initialize` / `tools/list` / `tools/call`).
+There is one tool per `McpAction` variant (tool name = the variant's serde
+tag, dispatched in `apply_mcp_call` in `src/window.rs`) plus `get_state`. The
+tool list lives in `mcp_tools()` in `src/api.rs`; the protocol layer is
`cce_ui::mcp` (tools-only Streamable HTTP). Keep the enum, the tool list, and
-the schemas in sync — `test_mcp_tools_map_to_http_actions` enforces the
-mapping.
+the schemas in sync — `test_mcp_tools_map_to_actions` enforces the mapping.
+(The former bespoke HTTP API on port 3000 was retired in favor of this;
+app-internal threads like the cce-files choosers now return results via
+`CustomEvent::RunAction` instead of POSTing to it.)
## Architecture
diff --git a/src/api.rs b/src/api.rs
index d08c2a2..c574359 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -1,146 +1,16 @@
-use std::net::TcpListener;
-use std::io::{BufRead, BufReader, Read, Write};
+//! The embedded MCP automation server — the way to drive/inspect the
+//! running app (the former bespoke HTTP API was retired in its favor).
+
use cce_ui::mcp::McpTool;
use serde_json::json;
-use crate::{CustomEvent, HttpAction};
-
-pub fn start_http_server(server_sender: calloop::channel::Sender<CustomEvent>) {
- std::thread::spawn(move || {
- // CCE_DESIGNER_HTTP_PORT overrides the default so a second instance
- // (tests, debugging) can run alongside one already holding 3000.
- let port: u16 = std::env::var("CCE_DESIGNER_HTTP_PORT")
- .ok()
- .and_then(|p| p.parse().ok())
- .unwrap_or(3000);
- let listener = match TcpListener::bind(("127.0.0.1", port)) {
- Ok(l) => l,
- Err(e) => {
- eprintln!("Failed to bind HTTP server to port {port}: {:?}", e);
- return;
- }
- };
- println!("Embedded HTTP Server listening on http://127.0.0.1:{port}");
-
- for stream in listener.incoming() {
- let stream = match stream {
- Ok(s) => s,
- Err(_) => continue,
- };
-
- let server_sender = server_sender.clone();
- std::thread::spawn(move || {
- let mut write_stream = match stream.try_clone() {
- Ok(s) => s,
- Err(_) => return,
- };
- let mut reader = BufReader::new(stream);
- let mut request_line = String::new();
- if reader.read_line(&mut request_line).is_err() {
- return;
- }
-
- if request_line.starts_with("GET /state") {
- let (tx, rx) = std::sync::mpsc::channel();
- if server_sender.send(CustomEvent::GetState(tx)).is_ok() {
- let response_body = rx.recv().unwrap_or_else(|_| "null".to_string());
- let response = format!(
- "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
- response_body.len(),
- response_body
- );
- let _ = write_stream.write_all(response.as_bytes());
- }
- } else if request_line.starts_with("POST /action") {
- let mut content_length = 0;
- loop {
- let mut header_line = String::new();
- if reader.read_line(&mut header_line).is_err() || header_line == "\r\n" || header_line == "\n" || header_line.is_empty() {
- break;
- }
- let lower = header_line.to_lowercase();
- if lower.starts_with("content-length:") {
- if let Some(val) = lower.split(':').nth(1) {
- if let Ok(len) = val.trim().parse::<usize>() {
- content_length = len;
- }
- }
- }
- }
-
- let mut body = vec![0; content_length];
- if reader.read_exact(&mut body).is_ok() {
- let body_str = String::from_utf8_lossy(&body);
- if let Ok(action) = serde_json::from_str::<HttpAction>(&body_str) {
- let (tx, rx) = std::sync::mpsc::channel();
- if server_sender.send(CustomEvent::PostAction(action, tx)).is_ok() {
- let res = rx.recv().unwrap_or_else(|_| Err("internal error".to_string()));
- let response = match res {
- Ok(msg) => {
- let body = format!("{{\"status\":\"success\",\"message\":\"{}\"}}", msg);
- format!(
- "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
- body.len(),
- body
- )
- }
- Err(err) => {
- let body = format!("{{\"status\":\"error\",\"error\":\"{}\"}}", err);
- format!(
- "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
- body.len(),
- body
- )
- }
- };
- let _ = write_stream.write_all(response.as_bytes());
- } else {
- let body = "{\"status\":\"error\",\"error\":\"failed to send action to event loop\"}";
- let response = format!(
- "HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
- body.len(),
- body
- );
- let _ = write_stream.write_all(response.as_bytes());
- }
- } else {
- let body = "{\"status\":\"error\",\"error\":\"failed to parse action JSON\"}";
- let response = format!(
- "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
- body.len(),
- body
- );
- let _ = write_stream.write_all(response.as_bytes());
- }
- } else {
- let body = "{\"status\":\"error\",\"error\":\"failed to read complete body\"}";
- let response = format!(
- "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
- body.len(),
- body
- );
- let _ = write_stream.write_all(response.as_bytes());
- }
- } else {
- let body = "{\"error\":\"not found\"}";
- let response = format!(
- "HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
- body.len(),
- body
- );
- let _ = write_stream.write_all(response.as_bytes());
- }
- let _ = write_stream.flush();
- });
- }
- });
-}
+use crate::CustomEvent;
/// Start the embedded MCP server (cce-ui's tools-only Streamable HTTP
-/// implementation): the same automation surface as the HTTP API, spoken as
-/// MCP tools so agents can attach with
+/// implementation). Agents attach with
/// `claude mcp add --transport http cce-designer http://127.0.0.1:3001/mcp`.
pub fn start_mcp_server(server_sender: calloop::channel::Sender<CustomEvent>) {
- // CCE_DESIGNER_MCP_PORT overrides the default, same as the HTTP port.
+ // CCE_DESIGNER_MCP_PORT overrides the default so a second instance
+ // (tests, debugging) can run alongside one already holding 3001.
let port: u16 = std::env::var("CCE_DESIGNER_MCP_PORT")
.ok()
.and_then(|p| p.parse().ok())
@@ -148,7 +18,7 @@ pub fn start_mcp_server(server_sender: calloop::channel::Sender<CustomEvent>) {
cce_ui::mcp::start_mcp_server("cce-designer", port, mcp_tools(), server_sender, CustomEvent::McpCall);
}
-/// The designer's MCP tools: `get_state` plus one tool per `HttpAction`
+/// The designer's MCP tools: `get_state` plus one tool per `McpAction`
/// variant — the tool name is the variant's serde tag and the arguments are
/// its fields, so dispatch is deserialization (see `apply_mcp_call`).
pub(crate) fn mcp_tools() -> Vec<McpTool> {
diff --git a/src/app.rs b/src/app.rs
index cea9892..267a0eb 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -318,7 +318,7 @@ pub struct Project {
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "action", rename_all = "snake_case")]
-pub enum HttpAction {
+pub enum McpAction {
Up,
Enter { slot: usize },
SetParam { slot: usize, name: String, value: String },
@@ -342,12 +342,13 @@ pub enum HttpAction {
#[derive(Debug, Clone)]
pub enum CustomEvent {
- GetState(std::sync::mpsc::Sender<String>),
- PostAction(HttpAction, std::sync::mpsc::Sender<Result<String, String>>),
/// An MCP `tools/call` from the embedded MCP server (carries its own
- /// reply channel) — the tool name is an `HttpAction` tag, or `get_state`.
+ /// reply channel) — the tool name is an `McpAction` tag, or `get_state`.
McpCall(cce_ui::mcp::McpToolCall),
- /// App-requested exit (menu File > Exit, HTTP menu_action): the engine's
+ /// A fire-and-forget action from an app-internal thread (the cce-files
+ /// choosers deliver their picked path this way).
+ RunAction(McpAction),
+ /// App-requested exit (menu File > Exit, MCP menu_action): the engine's
/// update hook is the only place with exit access, so input handlers that
/// see `exit_requested` route it here.
Exit,
@@ -922,6 +923,10 @@ pub struct State {
pub shortcut_manager: ShortcutManager,
pub pending_action: Option<Action>,
pub exit_requested: bool,
+ /// Engine event-loop sender so app-spawned threads (the cce-files
+ /// choosers) can deliver results back as `CustomEvent`s; set once by
+ /// `Application::new`.
+ pub event_sender: Option<calloop::channel::Sender<CustomEvent>>,
pub slots: Box<WidgetSlots>,
pub positions: Vec<(f32, f32, f32, f32)>,
@@ -1417,7 +1422,7 @@ impl State {
/// One label-matched menu action: the button-param menu pane's items, drained
/// per triggered button by `sync_parameters_to_project`, and reachable directly
- /// through the HTTP API's `menu_action` (the index-matched `menu_click` cannot
+ /// through the `menu_action` tool (the index-matched `menu_click` cannot
/// reach these). Returns false for an unrecognized label.
pub fn execute_menu_action(&mut self, label: &str) -> bool {
match label {
@@ -1724,9 +1729,9 @@ impl State {
}
pub fn open_file_chooser(&self) {
+ let Some(sender) = self.event_sender.clone() else { return };
std::thread::spawn(move || {
use std::process::{Command, Stdio};
- use std::io::Write;
// Find executable path
let exe_path = if let Ok(cur_exe) = std::env::current_exe() {
@@ -1783,33 +1788,16 @@ impl State {
if output.status.success() {
let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !selected.is_empty() {
- let body = format!(
- "{{\"action\":\"load\",\"path\":\"{}\"}}",
- selected.replace('\\', "\\\\").replace('"', "\\\"")
- );
- let req = format!(
- "POST /action HTTP/1.1\r\n\
- Host: 127.0.0.1:3000\r\n\
- Content-Type: application/json\r\n\
- Content-Length: {}\r\n\
- Connection: close\r\n\r\n\
- {}",
- body.len(),
- body
- );
- if let Ok(mut stream) = std::net::TcpStream::connect("127.0.0.1:3000") {
- let _ = stream.write_all(req.as_bytes());
- let _ = stream.flush();
- }
+ let _ = sender.send(CustomEvent::RunAction(McpAction::Load { path: selected }));
}
}
});
}
pub fn save_file_chooser(&self) {
+ let Some(sender) = self.event_sender.clone() else { return };
std::thread::spawn(move || {
use std::process::{Command, Stdio};
- use std::io::Write;
// Find executable path
let exe_path = if let Ok(cur_exe) = std::env::current_exe() {
@@ -1866,24 +1854,7 @@ impl State {
if output.status.success() {
let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !selected.is_empty() {
- let body = format!(
- "{{\"action\":\"save\",\"path\":\"{}\"}}",
- selected.replace('\\', "\\\\").replace('"', "\\\"")
- );
- let req = format!(
- "POST /action HTTP/1.1\r\n\
- Host: 127.0.0.1:3000\r\n\
- Content-Type: application/json\r\n\
- Content-Length: {}\r\n\
- Connection: close\r\n\r\n\
- {}",
- body.len(),
- body
- );
- if let Ok(mut stream) = std::net::TcpStream::connect("127.0.0.1:3000") {
- let _ = stream.write_all(req.as_bytes());
- let _ = stream.flush();
- }
+ let _ = sender.send(CustomEvent::RunAction(McpAction::Save { path: selected }));
}
}
});
@@ -2491,6 +2462,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
shortcut_manager,
pending_action: None,
exit_requested: false,
+ event_sender: None,
slots,
positions,
splitter_layout,
diff --git a/src/application.rs b/src/application.rs
index ef7e995..892df41 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -15,7 +15,7 @@ use cce_ui::vk::VkRenderer;
use cce_ui::widget::{ElementState, KeyEvent, MouseButton, MouseScrollDelta};
use wayland_client::QueueHandle;
-use crate::api::{start_http_server, start_mcp_server};
+use crate::api::start_mcp_server;
use crate::app::{CustomEvent, PendingWindowDrag, State, TouchPhase, LEFT_MENUBAR_IDX};
use crate::window::{LocalPosition, WindowEvent};
@@ -146,9 +146,9 @@ impl Application for State {
sender: calloop::channel::Sender<CustomEvent>,
) -> Self {
let is_detached_network = std::env::args().any(|arg| arg == "--detached-network");
- let state = State::new(is_detached_network);
+ let mut state = State::new(is_detached_network);
+ state.event_sender = Some(sender.clone());
if !is_detached_network {
- start_http_server(sender.clone());
start_mcp_server(sender);
}
state
@@ -179,7 +179,7 @@ impl Application for State {
if self.apply_custom_event(msg) {
*needs_rebuild = true;
}
- // HTTP can reach File > Exit through menu_action.
+ // MCP can reach File > Exit through menu_action.
if self.exit_requested {
self.autosave_on_exit();
*exit = true;
diff --git a/src/main.rs b/src/main.rs
index c15d841..1656190 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,7 +4,7 @@ pub mod application;
// Root-level aliases some modules import via `crate::` paths.
#[allow(unused_imports)]
-use app::{CustomEvent, HttpAction, ModifiersState};
+use app::{CustomEvent, McpAction, ModifiersState};
pub mod viewport_3d;
pub mod api;
pub mod window;
@@ -20,7 +20,7 @@ mod test_prelude {
pub use std::path::Path;
pub use glam::{Mat4, Vec3};
pub use cce_ui::widget::{Key, NamedKey};
- pub use crate::app::{State, HttpAction, ModifiersState};
+ pub use crate::app::{State, McpAction, ModifiersState};
}
fn main() {
@@ -452,11 +452,11 @@ mod tests {
}
#[test]
- fn test_http_action_parsing() {
+ fn test_mcp_action_parsing() {
let json_str = "{\"action\": \"add_node\", \"template_name\": \"Sphere\", \"name\": \"MySphere\", \"x\": 5.0, \"y\": 3.0}";
- let action: HttpAction = serde_json::from_str(json_str).unwrap();
+ let action: McpAction = serde_json::from_str(json_str).unwrap();
match action {
- HttpAction::AddNode { template_name, name, x, y } => {
+ McpAction::AddNode { template_name, name, x, y } => {
assert_eq!(template_name, "Sphere");
assert_eq!(name, Some("MySphere".to_string()));
assert_eq!(x, 5.0);
@@ -742,9 +742,9 @@ mod tests {
}
#[test]
- fn test_mcp_tools_map_to_http_actions() {
+ fn test_mcp_tools_map_to_actions() {
// Every MCP tool except get_state must dispatch by injecting its name
- // as the HttpAction serde tag; filling each schema property with a
+ // as the McpAction serde tag; filling each schema property with a
// dummy of its declared type must yield a deserializable action, so
// this catches tool-name/field drift against the enum.
let tools = crate::api::mcp_tools();
@@ -770,8 +770,8 @@ mod tests {
}
}
args.insert("action".to_string(), serde_json::json!(tool.name));
- serde_json::from_value::<HttpAction>(serde_json::Value::Object(args))
- .unwrap_or_else(|e| panic!("tool '{}' does not map to an HttpAction: {e}", tool.name));
+ serde_json::from_value::<McpAction>(serde_json::Value::Object(args))
+ .unwrap_or_else(|e| panic!("tool '{}' does not map to an McpAction: {e}", tool.name));
}
}
}
diff --git a/src/window.rs b/src/window.rs
index 2caafec..197dd87 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -4,13 +4,13 @@
//! lives in cce-ui's window runner; the `Application` impl (application.rs)
//! translates the runner's hooks into [`WindowEvent`]s. What remains here is
//! app policy: the post-event side-effect pass (`process_window_event`) and
-//! HTTP-action application (`apply_custom_event`).
+//! MCP-action application (`apply_custom_event`).
use std::path::Path;
use cce_ui::widget::WidgetHost;
use crate::shortcut::Action;
-use crate::app::{State, CustomEvent, HttpAction, TouchPhase, LEFT_MENUBAR_IDX, RIGHT_MENUBAR_IDX, PARAM_MENUBAR_IDX, SPREADSHEET_MENUBAR_IDX, HEADER_IDX, PARAM_IDX, WIDGET_COUNT, get_next_visible_pane, Project, ProjectViewState, ParamDef, param_display};
+use crate::app::{State, CustomEvent, McpAction, TouchPhase, LEFT_MENUBAR_IDX, RIGHT_MENUBAR_IDX, PARAM_MENUBAR_IDX, SPREADSHEET_MENUBAR_IDX, HEADER_IDX, PARAM_IDX, WIDGET_COUNT, get_next_visible_pane, Project, ProjectViewState, ParamDef, param_display};
#[derive(Debug, Clone, Copy)]
pub struct LocalPosition {
@@ -568,25 +568,22 @@ impl State {
result
}
- /// Apply an HTTP/API event (the engine `update` hook). Returns true when
- /// a redraw is needed.
+ /// Apply an automation event (the engine `update` hook). Returns true
+ /// when a redraw is needed.
pub(crate) fn apply_custom_event(&mut self, event: CustomEvent) -> bool {
let mut needs_redraw = false;
{
let state = &mut *self;
match event {
- CustomEvent::GetState(tx) => {
- let json = serde_json::to_string_pretty(&state.project_snapshot()).unwrap_or_default();
- let _ = tx.send(json);
- }
- CustomEvent::PostAction(action, tx) => {
- let res = state.apply_http_action(action, &mut needs_redraw);
- let _ = tx.send(res);
- }
CustomEvent::McpCall(call) => {
let res = state.apply_mcp_call(&call, &mut needs_redraw);
let _ = call.reply.send(res);
}
+ CustomEvent::RunAction(action) => {
+ if let Err(e) = state.apply_action(action, &mut needs_redraw) {
+ eprintln!("Action failed: {e}");
+ }
+ }
// Exit is handled by the Application::update wrapper
// (autosave + engine exit) before this is reached.
CustomEvent::Exit => {}
@@ -616,7 +613,7 @@ impl State {
}
/// An MCP tool call: `get_state` returns the project snapshot; every
- /// other tool name is an `HttpAction` tag — injected into the arguments
+ /// other tool name is an `McpAction` tag — injected into the arguments
/// and run through the shared action path.
fn apply_mcp_call(
&mut self,
@@ -633,24 +630,25 @@ impl State {
serde_json::json!({})
};
req["action"] = serde_json::Value::String(call.name.clone());
- match serde_json::from_value::<HttpAction>(req) {
+ match serde_json::from_value::<McpAction>(req) {
Ok(action) => self
- .apply_http_action(action, needs_redraw)
+ .apply_action(action, needs_redraw)
.map(serde_json::Value::String),
Err(e) => Err(format!("invalid arguments for '{}': {e}", call.name)),
}
}
- /// Apply one automation action (HTTP `POST /action` or an MCP tool call).
- pub(crate) fn apply_http_action(
+ /// Apply one automation action (an MCP tool call, or an app-internal
+ /// fire-and-forget `RunAction`).
+ pub(crate) fn apply_action(
&mut self,
- action: HttpAction,
+ action: McpAction,
redraw: &mut bool,
) -> Result<String, String> {
let mut needs_redraw = false;
let state = self;
let res = match action {
- HttpAction::Up => {
+ McpAction::Up => {
if state.move_up() {
needs_redraw = true;
Ok("Moved up".to_string())
@@ -658,7 +656,7 @@ impl State {
Err("Already at root".to_string())
}
}
- HttpAction::Enter { slot } => {
+ McpAction::Enter { slot } => {
let dir = state.current_dir();
if slot < dir.children.len() && (dir.children[slot].node_type == "node" || dir.children[slot].node_type == "utility" || !dir.children[slot].children.is_empty()) {
state.current_path.push(slot);
@@ -669,14 +667,14 @@ impl State {
Err("Not a valid subnet".to_string())
}
}
- HttpAction::SetParam { slot, name, value } => {
+ McpAction::SetParam { slot, name, value } => {
let dir = state.current_dir_mut();
if let Some(child) = dir.children.get_mut(slot) {
if let Some(p) = child.params.iter_mut().find(|p| p.name == name) {
p.default = value;
// Same sequence as the interactive param-pane
// path, so settings params (viewport flags,
- // grid) actually take effect over HTTP.
+ // grid) actually take effect via automation.
state.apply_settings_from_menubar_subnets();
state.sync_grid_settings();
state.sync_nodes();
@@ -690,7 +688,7 @@ impl State {
Err("Slot index out of bounds".to_string())
}
}
- HttpAction::ResetCamera => {
+ McpAction::ResetCamera => {
if state.active_camera != "Default Camera" {
state.update_active_camera_rotation_reset();
} else {
@@ -702,7 +700,7 @@ impl State {
needs_redraw = true;
Ok("Camera reset".to_string())
}
- HttpAction::Load { path } => {
+ McpAction::Load { path } => {
if let Err(e) = state.load_from_file(Path::new(&path)) {
Err(format!("Load failed: {:?}", e))
} else {
@@ -710,7 +708,7 @@ impl State {
Ok("Project loaded".to_string())
}
}
- HttpAction::Save { path } => {
+ McpAction::Save { path } => {
if let Err(e) = state.save_to_file(Path::new(&path)) {
Err(format!("Save failed: {:?}", e))
} else {
@@ -721,7 +719,7 @@ impl State {
Ok("Project saved".to_string())
}
}
- HttpAction::ToggleGeometry { slot } => {
+ McpAction::ToggleGeometry { slot } => {
let active_nodes = state.current_dir().children.len();
if slot < active_nodes {
if state.current_dir().children[slot].node_type == "utility" {
@@ -738,7 +736,7 @@ impl State {
Err("Slot out of bounds".to_string())
}
}
- HttpAction::AddNode { template_name, name, x, y } => {
+ McpAction::AddNode { template_name, name, x, y } => {
let template_idx = state.node_templates.iter().position(|t| {
t.label.to_lowercase() == template_name.to_lowercase()
|| t.node.name.to_lowercase() == template_name.to_lowercase()
@@ -775,7 +773,7 @@ impl State {
Err(format!("Template '{}' not found", template_name))
}
}
- HttpAction::DeleteNode { slot } => {
+ McpAction::DeleteNode { slot } => {
if state.delete_node(slot) {
needs_redraw = true;
Ok("Node deleted".to_string())
@@ -783,7 +781,7 @@ impl State {
Err("Slot out of bounds".to_string())
}
}
- HttpAction::RenameNode { slot, new_name } => {
+ McpAction::RenameNode { slot, new_name } => {
let len = state.current_dir().children.len();
if slot < len {
state.current_dir_mut().children[slot].name = new_name;
@@ -794,7 +792,7 @@ impl State {
Err("Slot out of bounds".to_string())
}
}
- HttpAction::MoveNode { slot, x, y } => {
+ McpAction::MoveNode { slot, x, y } => {
let len = state.current_dir().children.len();
if slot < len {
let (nx, ny) = state.find_empty_cell(x, y, Some(slot));
@@ -809,7 +807,7 @@ impl State {
Err("Slot out of bounds".to_string())
}
}
- HttpAction::AddParam { slot, name, param_type, default } => {
+ McpAction::AddParam { slot, name, param_type, default } => {
let len = state.current_dir().children.len();
if slot < len {
let param = ParamDef {
@@ -830,7 +828,7 @@ impl State {
Err("Slot out of bounds".to_string())
}
}
- HttpAction::DeleteParam { slot, name } => {
+ McpAction::DeleteParam { slot, name } => {
let len = state.current_dir().children.len();
if slot < len {
let params = &mut state.current_dir_mut().children[slot].params;
@@ -846,7 +844,7 @@ impl State {
Err("Slot out of bounds".to_string())
}
}
- HttpAction::ToggleCircularPane => {
+ McpAction::ToggleCircularPane => {
state.circular_network_pane = !state.circular_network_pane;
let val = state.circular_network_pane;
state.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 2, val);
@@ -856,7 +854,7 @@ impl State {
needs_redraw = true;
Ok(format!("Circular pane: {}", state.circular_network_pane))
}
- HttpAction::MenuClick { widget_idx, menu_idx, item_idx } => {
+ McpAction::MenuClick { widget_idx, menu_idx, item_idx } => {
// Validate before touching menu_mut(): a non-menubar widget_idx
// panics its MenuBar downcast, and out-of-range menu/item indices
// used to reply "Menu clicked" while dispatching nowhere. NB the
@@ -891,7 +889,7 @@ impl State {
Err(e) => Err(e),
}
}
- HttpAction::MenuAction { label } => {
+ McpAction::MenuAction { label } => {
if state.execute_menu_action(&label) {
// The arms relayout themselves but render() draws the last
// uploaded buffer (same ritual as ToggleCircularPane).
@@ -901,7 +899,7 @@ impl State {
Err(format!("unknown menu action label: {}", label.replace(['"', '\\'], "'")))
}
}
- HttpAction::MenuClosed { widget_idx, menu_idx } => {
+ McpAction::MenuClosed { widget_idx, menu_idx } => {
if state.active_menu_cloud_idx == Some((widget_idx, menu_idx)) {
state.active_menu_cloud_pid = None;
state.active_menu_cloud_idx = None;