From 93ac65ab43d3442b60a4fcbbe30aa10c460c2bca Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Sat, 6 Jun 2026 23:16:26 +0800 Subject: [PATCH] fix(mcp): exclude debug tools from release builds --- crates/op-host-desktop/Cargo.toml | 4 ++ crates/op-host-desktop/src/mcp_serve.rs | 42 ++++++++++++------- .../op-host-desktop/src/mcp_serve/schemas.rs | 1 + crates/op-host-desktop/src/mcp_serve/tests.rs | 23 +++++++++- crates/op-mcp/Cargo.toml | 6 ++- crates/op-mcp/src/lib.rs | 6 +++ 6 files changed, 64 insertions(+), 18 deletions(-) diff --git a/crates/op-host-desktop/Cargo.toml b/crates/op-host-desktop/Cargo.toml index 7bcbab16e..948025374 100644 --- a/crates/op-host-desktop/Cargo.toml +++ b/crates/op-host-desktop/Cargo.toml @@ -13,6 +13,10 @@ description = "OpenPencil desktop host — winit + skia-safe binary that drives name = "openpencil-desktop" path = "src/main.rs" +[features] +default = [] +mcp-debug-tools = ["op-mcp/debug-tools"] + # File-association metadata for `cargo-bundle` (`cargo bundle`). This # declares OpenPencil as the OS-level handler for `.op` / `.pen` # documents: macOS writes `CFBundleDocumentTypes` into the `.app` diff --git a/crates/op-host-desktop/src/mcp_serve.rs b/crates/op-host-desktop/src/mcp_serve.rs index f86fae388..65b2896c8 100644 --- a/crates/op-host-desktop/src/mcp_serve.rs +++ b/crates/op-host-desktop/src/mcp_serve.rs @@ -21,22 +21,21 @@ use op_mcp::{ codegen_clean_snapshot, codegen_plan_snapshot, codegen_submit_chunk_snapshot, copy_node_snapshot, copy_selected_snapshot, count_nodes_snapshot, create_component_snapshot, create_variable_snapshot, cut_selected_snapshot, cycle_active_axis_value_snapshot, - debug_logs_tail_snapshot, debug_screenshot_snapshot, debug_tools_enabled, - debug_validation_report_snapshot, delete_component_snapshot, delete_node_snapshot, - delete_page_snapshot, delete_selected_snapshot, delete_variable_snapshot, - design_content_snapshot, design_refine_snapshot, design_skeleton_snapshot, - document_info_snapshot, duplicate_page_snapshot, duplicate_selected_snapshot, - export_design_md_snapshot, find_empty_space_snapshot, find_node_by_name_snapshot, - get_active_theme_snapshot, get_canvas_bounds_snapshot, get_component_snapshot, - get_design_md_snapshot, get_design_prompt_snapshot, get_history_depth_snapshot, - get_node_children_snapshot, get_node_parent_snapshot, get_node_snapshot, - get_selection_set_snapshot, get_style_guide_snapshot, get_style_guide_tags_snapshot, - get_variables_snapshot, get_viewport_snapshot, group_selected_snapshot, import_svg_snapshot, - insert_node_snapshot, instantiate_component_snapshot, list_components_snapshot, - list_node_kinds_snapshot, list_pages_snapshot, list_theme_presets_snapshot, - list_variables_snapshot, load_theme_preset_snapshot, move_node_snapshot, - nudge_selected_snapshot, open_document_snapshot, paste_clipboard_snapshot, read_nodes_snapshot, - redo_snapshot, remove_node_effect_snapshot, remove_page_snapshot, rename_component_snapshot, + debug_tools_enabled, delete_component_snapshot, delete_node_snapshot, delete_page_snapshot, + delete_selected_snapshot, delete_variable_snapshot, design_content_snapshot, + design_refine_snapshot, design_skeleton_snapshot, document_info_snapshot, + duplicate_page_snapshot, duplicate_selected_snapshot, export_design_md_snapshot, + find_empty_space_snapshot, find_node_by_name_snapshot, get_active_theme_snapshot, + get_canvas_bounds_snapshot, get_component_snapshot, get_design_md_snapshot, + get_design_prompt_snapshot, get_history_depth_snapshot, get_node_children_snapshot, + get_node_parent_snapshot, get_node_snapshot, get_selection_set_snapshot, + get_style_guide_snapshot, get_style_guide_tags_snapshot, get_variables_snapshot, + get_viewport_snapshot, group_selected_snapshot, import_svg_snapshot, insert_node_snapshot, + instantiate_component_snapshot, list_components_snapshot, list_node_kinds_snapshot, + list_pages_snapshot, list_theme_presets_snapshot, list_variables_snapshot, + load_theme_preset_snapshot, move_node_snapshot, nudge_selected_snapshot, + open_document_snapshot, paste_clipboard_snapshot, read_nodes_snapshot, redo_snapshot, + remove_node_effect_snapshot, remove_page_snapshot, rename_component_snapshot, rename_page_snapshot, rename_variable_snapshot, reorder_page_snapshot, reorder_selected_snapshot, replace_all_matching_properties_snapshot, replace_node_snapshot, run_stdio_with_applier, save_document_snapshot, save_theme_preset_snapshot, @@ -53,6 +52,10 @@ use op_mcp::{ toggle_node_selection_snapshot, undo_snapshot, ungroup_selected_snapshot, update_node_snapshot, McpTool, ToolRegistry, }; +#[cfg(feature = "mcp-debug-tools")] +use op_mcp::{ + debug_logs_tail_snapshot, debug_screenshot_snapshot, debug_validation_report_snapshot, +}; pub(crate) mod file_path; @@ -512,6 +515,7 @@ fn rebuild_registry(doc: &EditorState, requested_tool: Option<&str>) -> ToolRegi register_tool!("get_history_depth", get_history_depth_snapshot(doc)); register_tool!("get_viewport", get_viewport_snapshot(doc)); register_tool!("get_selection_set", get_selection_set_snapshot(doc)); + #[cfg(feature = "mcp-debug-tools")] if debug_tools_enabled() { register_tool!( "debug_validation_report", @@ -775,6 +779,9 @@ pub(crate) use doc_sync::*; fn tools_list_response(id_raw: &str, state: &EditorState, debug_enabled: bool) -> String { let mut entries: Vec = TOOL_SCHEMAS.iter().map(|s| (*s).to_string()).collect(); entries.extend(op_mcp::element_tools::element_tool_schemas(state)); + #[cfg(not(feature = "mcp-debug-tools"))] + let _ = debug_enabled; + #[cfg(feature = "mcp-debug-tools")] if debug_enabled { entries.extend(DEBUG_TOOL_SCHEMAS.iter().map(|s| (*s).to_string())); } @@ -785,6 +792,9 @@ fn tools_list_response(id_raw: &str, state: &EditorState, debug_enabled: bool) - } mod schemas; +#[cfg(not(feature = "mcp-debug-tools"))] +pub(crate) use schemas::TOOL_SCHEMAS; +#[cfg(feature = "mcp-debug-tools")] pub(crate) use schemas::{DEBUG_TOOL_SCHEMAS, TOOL_SCHEMAS}; #[cfg(test)] diff --git a/crates/op-host-desktop/src/mcp_serve/schemas.rs b/crates/op-host-desktop/src/mcp_serve/schemas.rs index 4d7fb9e2b..7bbab55d1 100644 --- a/crates/op-host-desktop/src/mcp_serve/schemas.rs +++ b/crates/op-host-desktop/src/mcp_serve/schemas.rs @@ -112,6 +112,7 @@ pub(crate) const TOOL_SCHEMAS: &[&str] = &[ r#"{"name":"replace_node","description":"Swap an existing node at the same parent slot with a freshly-built leaf. Accepts Rust flat fields or TS-style nodeId + data object. Set drop_children=true to discard a container's subtree.","inputSchema":{"type":"object","properties":{"filePath":{"type":"string","description":"Optional target .op file path; omit to use the server document"},"node_id":{"type":"string"},"nodeId":{"type":"string"},"kind":{"type":"string","enum":["frame","group","rect","ellipse","polygon","line","text","path"]},"name":{"type":"string"},"x":{"type":"string"},"y":{"type":"string"},"width":{"type":"string"},"height":{"type":"string"},"fill_hex":{"type":"string"},"data":{"type":"object","description":"TS-style PenNode data; rich fields are preserved as a subtree"},"postProcess":{"type":"boolean","description":"Accepted for TS compatibility; Rust replace path ignores post-processing"},"canvasWidth":{"type":"number","description":"Accepted for TS compatibility; Rust replace path ignores post-processing width"},"drop_children":{"type":"string","enum":["true","false"]},"dropChildren":{"type":"string","enum":["true","false"]},"pageId":{"type":"string","description":"optional target page id or legacy page index; omitted = active page"}}}}"#, ]; +#[cfg(feature = "mcp-debug-tools")] pub(crate) const DEBUG_TOOL_SCHEMAS: &[&str] = &[ r#"{"name":"debug_validation_report","description":"Run the op-design-lint detectors over the active page and return the design-issue list. Read-only, no parameters. Result: count + categories (`;`-separated `category|count`) + issues (JSON-serialized Issue array). Gated behind the OPENPENCIL_DEBUG_TOOLS=1 env flag.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}}"#, r#"{"name":"debug_logs_tail","description":"Read the tail of ~/.openpencil/logs/server-YYYY-MM-DD.log with API keys and Authorization headers redacted. Gated behind OPENPENCIL_DEBUG_TOOLS=1.","inputSchema":{"type":"object","properties":{"tailLines":{"type":"number","description":"Maximum lines to return (default 100, max 500)."},"sinceMs":{"type":"number","description":"Unix ms timestamp; only return lines newer than this."},"grep":{"type":"string","description":"Regex to filter lines by content after redaction."}}}}"#, diff --git a/crates/op-host-desktop/src/mcp_serve/tests.rs b/crates/op-host-desktop/src/mcp_serve/tests.rs index 8a7750eb1..a71da8c6a 100644 --- a/crates/op-host-desktop/src/mcp_serve/tests.rs +++ b/crates/op-host-desktop/src/mcp_serve/tests.rs @@ -28,6 +28,20 @@ fn tools_list_response_includes_all_registered_tools() { !r.contains("debug_validation_report"), "production tools/list must not advertise the debug tool: {r}" ); + #[cfg(not(feature = "mcp-debug-tools"))] + { + let r_forced_debug = tools_list_response("3", &state, true); + for name in [ + "debug_validation_report", + "debug_logs_tail", + "debug_screenshot", + ] { + assert!( + !r_forced_debug.contains(name), + "formal release catalog must exclude {name} even if debug listing is requested: {r_forced_debug}" + ); + } + } // UIKit element tools are appended dynamically — one per // built-in starter-kit component (6) — and ride alongside // the static schemas in the tools/list response. @@ -172,8 +186,10 @@ fn tools_list_response_includes_all_registered_tools() { assert!(r.contains(name), "tools/list must include {name}: {r}"); } - // Gate open (debug_enabled = true) — the debug tools join the catalog. + // Gate open (debug_enabled = true) — internal debug builds can opt in + // to the debug tools catalog. let r_debug = tools_list_response("3", &state, true); + #[cfg(feature = "mcp-debug-tools")] for name in [ "debug_validation_report", "debug_logs_tail", @@ -184,6 +200,11 @@ fn tools_list_response_includes_all_registered_tools() { "debug tools/list must advertise {name}: {r_debug}" ); } + #[cfg(not(feature = "mcp-debug-tools"))] + assert!( + !r_debug.contains("debug_validation_report"), + "default release feature set must not include debug tools: {r_debug}" + ); } #[test] diff --git a/crates/op-mcp/Cargo.toml b/crates/op-mcp/Cargo.toml index f373d1f6a..77e6e81d2 100644 --- a/crates/op-mcp/Cargo.toml +++ b/crates/op-mcp/Cargo.toml @@ -10,10 +10,14 @@ description = "OpenPencil MCP server — JSON-RPC tool registry over op-editor-c name = "op_mcp" path = "src/lib.rs" +[features] +default = [] +debug-tools = ["dep:op-design-lint"] + [dependencies] jian-ops-schema = { path = "../../vendor/jian/crates/jian-ops-schema" } op-ai-skills = { path = "../op-ai-skills" } -op-design-lint = { path = "../op-design-lint" } +op-design-lint = { path = "../op-design-lint", optional = true } op-editor-core = { path = "../op-editor-core" } op-editor-ui = { path = "../op-editor-ui" } op-pen-loader = { path = "../op-pen-loader" } diff --git a/crates/op-mcp/src/lib.rs b/crates/op-mcp/src/lib.rs index 7cda32556..d19802618 100644 --- a/crates/op-mcp/src/lib.rs +++ b/crates/op-mcp/src/lib.rs @@ -43,6 +43,7 @@ pub mod component_tools; mod component_tools_tests; #[cfg(test)] mod copy_node_tests; +#[cfg(feature = "debug-tools")] pub mod debug_tools; pub mod design_md_tools; #[cfg(test)] @@ -180,10 +181,15 @@ pub use component_tools::{ rename_component_snapshot, set_node_collapsed_snapshot, CreateComponent, DeleteComponent, InstantiateComponent, RenameComponent, SetNodeCollapsed, }; +#[cfg(feature = "debug-tools")] pub use debug_tools::{ debug_logs_tail_snapshot, debug_screenshot_snapshot, debug_tools_enabled, debug_validation_report_snapshot, DebugLogsTail, DebugScreenshot, DebugValidationReport, }; +#[cfg(not(feature = "debug-tools"))] +pub fn debug_tools_enabled() -> bool { + false +} pub use design_md_tools::{ export_design_md_snapshot, get_design_md_snapshot, set_design_md_snapshot, ExportDesignMd, GetDesignMd, SetDesignMd,