feat(mcp): add_page write tool — 29th tool, LLM-driven page CRUD
Append a fresh empty page + switch the active page to it. No
args. Mirrors TS `addPage()`. Applier returns false on
id-space exhaustion at `max_node_id() + 1`.
Wire shape:
args: {}
result: { "wrote": "true" }
command: `McpCommand::AddPage`
Companion to set_active_page (commit 1af880c2): LLMs can now
create + navigate pages without touching the UI. Future patches
can add rename_page / delete_page / duplicate_page to complete
the page CRUD (Document already has the mutators).
Desktop --mcp registry + tools/list schema + exact-count test
updated for 29 tools.
This commit is contained in:
parent
750434646d
commit
85ea875765
|
|
@ -26,9 +26,9 @@ use openpencil_shell_core::document::Document;
|
|||
use openpencil_shell_core::mcp::{
|
||||
batch_design_snapshot, copy_node_snapshot, delete_node_snapshot,
|
||||
design_content_snapshot, design_refine_snapshot, design_skeleton_snapshot,
|
||||
create_component_snapshot, delete_component_snapshot, document_info_snapshot,
|
||||
get_component_snapshot, instantiate_component_snapshot, list_components_snapshot,
|
||||
rename_component_snapshot, set_active_page_snapshot,
|
||||
add_page_snapshot, create_component_snapshot, delete_component_snapshot,
|
||||
document_info_snapshot, get_component_snapshot, instantiate_component_snapshot,
|
||||
list_components_snapshot, rename_component_snapshot, set_active_page_snapshot,
|
||||
get_active_theme_snapshot, get_node_snapshot, insert_node_snapshot,
|
||||
list_pages_snapshot, list_variables_snapshot, move_node_snapshot,
|
||||
replace_node_snapshot, run_stdio_with_applier, selection_snapshot,
|
||||
|
|
@ -163,6 +163,7 @@ fn rebuild_registry(doc: &Document) -> ToolRegistry {
|
|||
r.register(Box::new(delete_component_snapshot()));
|
||||
r.register(Box::new(rename_component_snapshot()));
|
||||
r.register(Box::new(set_active_page_snapshot()));
|
||||
r.register(Box::new(add_page_snapshot()));
|
||||
r
|
||||
}
|
||||
|
||||
|
|
@ -391,6 +392,7 @@ const TOOL_SCHEMAS: &[&str] = &[
|
|||
r#"{"name":"delete_component","description":"Remove a component from the registry by id. Live instances already on the page are NOT affected — they're independent clones.","inputSchema":{"type":"object","properties":{"component_id":{"type":"string","description":"positive u64 component id"}},"required":["component_id"]}}"#,
|
||||
r#"{"name":"rename_component","description":"Rename a registered component. Name must be non-empty / non-whitespace.","inputSchema":{"type":"object","properties":{"component_id":{"type":"string","description":"positive u64 component id"},"name":{"type":"string"}},"required":["component_id","name"]}}"#,
|
||||
r#"{"name":"set_active_page","description":"Switch which page is the active target for subsequent inserts / batch_design / design_* commands. index is 0-based.","inputSchema":{"type":"object","properties":{"index":{"type":"string","description":"0-based page index"}},"required":["index"]}}"#,
|
||||
r#"{"name":"add_page","description":"Append a fresh empty page and switch the active page to it. Returns false on id-space exhaustion.","inputSchema":{"type":"object","properties":{},"additionalProperties":false}}"#,
|
||||
r#"{"name":"set_active_axis_value","description":"Pin a theme axis to one of its allowed values.","inputSchema":{"type":"object","properties":{"axis":{"type":"string"},"value":{"type":"string"}},"required":["axis","value"]}}"#,
|
||||
r#"{"name":"insert_node","description":"Create a new leaf node on the active page.","inputSchema":{"type":"object","properties":{"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"}},"required":["kind","name","x","y","width","height"]}}"#,
|
||||
r#"{"name":"update_node","description":"Patch fields on an existing node. Pass any subset of x/y/width/height/name/fill_hex.","inputSchema":{"type":"object","properties":{"node_id":{"type":"string"},"x":{"type":"string"},"y":{"type":"string"},"width":{"type":"string"},"height":{"type":"string"},"name":{"type":"string"},"fill_hex":{"type":"string"}},"required":["node_id"]}}"#,
|
||||
|
|
@ -444,7 +446,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn tools_list_response_includes_all_twenty_eight_tools() {
|
||||
fn tools_list_response_includes_all_twenty_nine_tools() {
|
||||
let r = tools_list_response("3");
|
||||
// Exact-count assertion: any tool added without
|
||||
// updating this test will trip the count first. Codex
|
||||
|
|
@ -453,7 +455,7 @@ mod tests {
|
|||
// without being added to the list below.
|
||||
assert_eq!(
|
||||
TOOL_SCHEMAS.len(),
|
||||
28,
|
||||
29,
|
||||
"tools/list catalog count must match the registered tools — add the new tool to this test"
|
||||
);
|
||||
for name in [
|
||||
|
|
@ -470,6 +472,7 @@ mod tests {
|
|||
"delete_component",
|
||||
"rename_component",
|
||||
"set_active_page",
|
||||
"add_page",
|
||||
"set_variable_color",
|
||||
"set_active_axis_value",
|
||||
"insert_node",
|
||||
|
|
|
|||
|
|
@ -509,6 +509,7 @@ impl Document {
|
|||
crate::mcp::McpCommand::SetActivePage { index } => {
|
||||
self.set_active_page(*index as usize)
|
||||
}
|
||||
crate::mcp::McpCommand::AddPage => self.add_page().is_some(),
|
||||
crate::mcp::McpCommand::BatchInsert { items } => {
|
||||
// Validate EVERY descriptor before any mutation.
|
||||
// A single bad entry rejects the entire batch so
|
||||
|
|
|
|||
|
|
@ -198,7 +198,8 @@ impl VariableTable {
|
|||
| crate::mcp::McpCommand::CreateComponent { .. }
|
||||
| crate::mcp::McpCommand::DeleteComponent { .. }
|
||||
| crate::mcp::McpCommand::RenameComponent { .. }
|
||||
| crate::mcp::McpCommand::SetActivePage { .. } => {
|
||||
| crate::mcp::McpCommand::SetActivePage { .. }
|
||||
| crate::mcp::McpCommand::AddPage => {
|
||||
// Not VariableTable mutations — Pages-level commands
|
||||
// live on `Document::apply_mcp_command`. Return false
|
||||
// so callers with only a VariableTable handle know
|
||||
|
|
|
|||
|
|
@ -39,9 +39,10 @@ pub use write_tools::{
|
|||
SetActiveAxisValue, SetVariableColor, UpdateNode,
|
||||
};
|
||||
pub use component_tools::{
|
||||
create_component_snapshot, delete_component_snapshot, instantiate_component_snapshot,
|
||||
rename_component_snapshot, set_active_page_snapshot, CreateComponent, DeleteComponent,
|
||||
InstantiateComponent, RenameComponent, SetActivePage,
|
||||
add_page_snapshot, create_component_snapshot, delete_component_snapshot,
|
||||
instantiate_component_snapshot, rename_component_snapshot, set_active_page_snapshot,
|
||||
AddPage, CreateComponent, DeleteComponent, InstantiateComponent, RenameComponent,
|
||||
SetActivePage,
|
||||
};
|
||||
pub use batch_design::{
|
||||
batch_design_snapshot, design_content_snapshot, design_refine_snapshot,
|
||||
|
|
@ -293,6 +294,11 @@ pub enum McpCommand {
|
|||
SetActivePage {
|
||||
index: u32,
|
||||
},
|
||||
/// Append a fresh empty page to the document + switch active
|
||||
/// to it. Mirrors TS `addPage()`. Applier returns false on
|
||||
/// id-space exhaustion (same guard as the rest of the write
|
||||
/// tools).
|
||||
AddPage,
|
||||
}
|
||||
|
||||
/// Wire-friendly value payload for `McpCommand::SetVariableScalar`.
|
||||
|
|
|
|||
|
|
@ -219,3 +219,23 @@ pub fn set_active_page_snapshot() -> SetActivePage {
|
|||
SetActivePage
|
||||
}
|
||||
|
||||
/// First-party `add_page` tool — append a fresh empty page +
|
||||
/// switch the active page to it. No args. The applier returns
|
||||
/// false on id-space exhaustion at `max_node_id() + 1`.
|
||||
pub struct AddPage;
|
||||
|
||||
impl McpTool for AddPage {
|
||||
fn name(&self) -> &str {
|
||||
"add_page"
|
||||
}
|
||||
fn call(&self, _args: &BTreeMap<String, String>) -> ToolOutcome {
|
||||
let mut out = BTreeMap::new();
|
||||
out.insert("wrote".into(), "true".into());
|
||||
ToolOutcome::OkWithCommand(out, McpCommand::AddPage)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_page_snapshot() -> AddPage {
|
||||
AddPage
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue