fix(mcp): declare items on array properties in tool schemas (#207)

MCP clients such as the VS Code extension reject a tool whose inputSchema has an array property without the items keyword ("tool parameters array type must have items"). Add items to the sections/children arrays of design_skeleton and design_content, and add a test enforcing every array property declares items.
This commit is contained in:
ganondev 2026-08-09 18:34:36 -04:00 committed by GitHub
parent c4f46d16c7
commit 42bda72623
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -93,8 +93,8 @@ pub const TOOL_SCHEMAS: &[&str] = &[
r##"{"name":"set_variable_color","description":"Set a Color-kind variable's value.","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"hex":{"type":"string","description":"#rgb / #rrggbb / #rrggbbaa"}},"required":["name","hex"]}}"##,
r#"{"name":"batch_design","description":"Insert or refine design content. Accepts nodes_json, I(parent,nodeJson) insert operations, multi-line I/U/C/R/G/M/D programs, or a sandboxed JavaScript program via script. Transactional: if any operation fails, NONE of the batch is applied (the document is unchanged) and errors[] lists every failing line — fix them and resend the whole batch. Keep each call to <=25 operations; split large screens into logical section batches.","inputSchema":{"type":"object","properties":{"filePath":{"type":"string","description":"Optional target .op file path; omit to use the server document"},"nodes_json":{"type":"string","description":"JSON array of simple leaf descriptors"},"operations":{"type":"string","description":"TS batch_design DSL, e.g. root=I(null,{...}), U(\"n1\",{\"x\":10}), D(\"n1\"), M(\"n1\",null), copy=C(root,null,{...}), img=G(\"image-slot-id\",\"search\",\"prompt\") requires an existing EMPTY target, while img=G(\"rail-id\",\"search\",\"prompt\",\"append\") is accepted only when the parent declares horizontal/vertical layout and should be followed by U(img,{\"width\":120,\"height\":90}); null/populated slot targets and layout-none/omitted append targets are rejected"},"script":{"type":"string","description":"JavaScript program run in a sandboxed QuickJS: build nodes by calling I(parent, obj) or K(kitId,parent,overrides), with loops and data arrays allowed. C/U/D/M/R/G are rejected here with an instruction to use operations; console is a no-op. Expands to the operations DSL. Mutually exclusive with nodes_json/operations. Limits: 256 KiB source, 4096 inserts, 2s, 64 MiB, 8 MiB recorded output."},"postProcess":{"type":"boolean"},"canvasWidth":{"type":"number"},"pageId":{"type":"string"}}}}"#,
r#"{"name":"get_design_prompt","description":"Get OpenPencil design-generation prompt knowledge. Pass section for a focused subset; omit it for all sections. style and design-md are derived from the live document's design.md when present.","inputSchema":{"type":"object","properties":{"section":{"type":"string","description":"Prompt section name, e.g. all, layout, style, design-md, elements, codegen-react"},"filePath":{"type":"string","description":"Optional target .op file path; omit to use the server document"}},"required":[]}}"#,
r#"{"name":"design_skeleton","description":"Layered design workflow phase 1: create a root frame plus section frames. Accepts TS-style rootFrame/sections plus optional canvasWidth/pageId; legacy nodes_json/operations payloads remain accepted for compatibility.","inputSchema":{"type":"object","properties":{"filePath":{"type":"string","description":"Optional target .op file path; omit to use the server document"},"rootFrame":{"type":"object","description":"Root frame definition: name, width, height, layout, gap, fill, padding"},"sections":{"type":"array","description":"Section frame definitions; each item needs name and may include height, layout, padding, gap, fill, role, justifyContent, alignItems"},"styleGuide":{"type":"object","description":"Optional style guide metadata"},"canvasWidth":{"type":"number","description":"Canvas width for section contentWidth estimates"},"pageId":{"type":"string","description":"Target page ID or index"},"nodes_json":{"type":"string","description":"Legacy JSON array of simple leaf descriptors"},"operations":{"type":"string","description":"Legacy TS batch_design DSL"}},"required":["rootFrame","sections"]}}"#,
r#"{"name":"design_content","description":"Layered design workflow phase 2: fill child nodes into a section frame created by design_skeleton. Accepts TS-style sectionId/children plus optional postProcess/canvasWidth/pageId; legacy nodes_json/operations payloads remain accepted for compatibility.","inputSchema":{"type":"object","properties":{"filePath":{"type":"string","description":"Optional target .op file path; omit to use the server document"},"sectionId":{"type":"string","description":"ID of the section frame from design_skeleton"},"children":{"type":"array","description":"Child node definitions to insert under the section"},"postProcess":{"type":"boolean","description":"Apply post-processing after insertion; default true in the TS MCP"},"canvasWidth":{"type":"number","description":"Canvas width for post-processing; default 1200"},"pageId":{"type":"string","description":"Target page ID or index"},"nodes_json":{"type":"string","description":"Legacy JSON array of simple leaf descriptors"},"operations":{"type":"string","description":"Legacy TS batch_design DSL with I(parent,nodeJson) inserts or single U/D/M refine op"}},"required":["sectionId","children"]}}"#,
r#"{"name":"design_skeleton","description":"Layered design workflow phase 1: create a root frame plus section frames. Accepts TS-style rootFrame/sections plus optional canvasWidth/pageId; legacy nodes_json/operations payloads remain accepted for compatibility.","inputSchema":{"type":"object","properties":{"filePath":{"type":"string","description":"Optional target .op file path; omit to use the server document"},"rootFrame":{"type":"object","description":"Root frame definition: name, width, height, layout, gap, fill, padding"},"sections":{"type":"array","items":{"type":"object"},"description":"Section frame definitions; each item needs name and may include height, layout, padding, gap, fill, role, justifyContent, alignItems"},"styleGuide":{"type":"object","description":"Optional style guide metadata"},"canvasWidth":{"type":"number","description":"Canvas width for section contentWidth estimates"},"pageId":{"type":"string","description":"Target page ID or index"},"nodes_json":{"type":"string","description":"Legacy JSON array of simple leaf descriptors"},"operations":{"type":"string","description":"Legacy TS batch_design DSL"}},"required":["rootFrame","sections"]}}"#,
r#"{"name":"design_content","description":"Layered design workflow phase 2: fill child nodes into a section frame created by design_skeleton. Accepts TS-style sectionId/children plus optional postProcess/canvasWidth/pageId; legacy nodes_json/operations payloads remain accepted for compatibility.","inputSchema":{"type":"object","properties":{"filePath":{"type":"string","description":"Optional target .op file path; omit to use the server document"},"sectionId":{"type":"string","description":"ID of the section frame from design_skeleton"},"children":{"type":"array","items":{"type":"object"},"description":"Child node definitions to insert under the section"},"postProcess":{"type":"boolean","description":"Apply post-processing after insertion; default true in the TS MCP"},"canvasWidth":{"type":"number","description":"Canvas width for post-processing; default 1200"},"pageId":{"type":"string","description":"Target page ID or index"},"nodes_json":{"type":"string","description":"Legacy JSON array of simple leaf descriptors"},"operations":{"type":"string","description":"Legacy TS batch_design DSL with I(parent,nodeJson) inserts or single U/D/M refine op"}},"required":["sectionId","children"]}}"#,
r#"{"name":"design_refine","description":"Layered design workflow phase 3: polish an existing design root. Accepts rootId plus optional canvasWidth/pageId and runs deterministic cleanup; legacy nodes_json/operations payloads remain accepted for compatibility.","inputSchema":{"type":"object","properties":{"filePath":{"type":"string","description":"Optional target .op file path; omit to use the server document"},"rootId":{"type":"string","description":"ID of the root frame to refine"},"canvasWidth":{"type":"number","description":"Canvas width for cleanup heuristics"},"pageId":{"type":"string","description":"Target page ID or index"},"nodes_json":{"type":"string","description":"Legacy JSON array of simple leaf descriptors"},"operations":{"type":"string","description":"Legacy TS batch_design DSL"}}}}"#,
r#"{"name":"set_variable_number","description":"Set a Number-kind variable's value (decimal, may be negative or fractional).","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"]}}"#,
r#"{"name":"set_variable_string","description":"Set a String-kind variable's value (free-form text).","inputSchema":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"]}}"#,
@ -139,6 +139,40 @@ pub const DEBUG_TOOL_SCHEMAS: &[&str] = &[
#[cfg(test)]
mod tests {
use super::TOOL_SCHEMAS;
#[cfg(feature = "mcp-debug-tools")]
use super::DEBUG_TOOL_SCHEMAS;
#[test]
fn all_array_properties_declare_items() {
// MCP clients (e.g. the VS Code extension) reject a tool whose
// inputSchema contains `"type":"array"` without an `items` keyword
// ("tool parameters array type must have items"). Enumerate every
// schema property and assert every array declares `items`.
let catalogs = [
TOOL_SCHEMAS,
#[cfg(feature = "mcp-debug-tools")]
DEBUG_TOOL_SCHEMAS,
];
for catalog in catalogs {
for raw in catalog {
let value: serde_json::Value =
serde_json::from_str(raw).expect("tool schema must be valid JSON");
let properties = value["inputSchema"]["properties"]
.as_object()
.unwrap_or_else(|| panic!("tool {} must declare inputSchema.properties", value["name"]));
for (key, prop) in properties {
if prop.get("type").and_then(|t| t.as_str()) == Some("array") {
assert!(
prop.get("items").is_some(),
"tool {} property `{key}` is an array but missing `items` — MCP clients \
reject schemas without it (tool parameters array type must have items)",
value["name"]
);
}
}
}
}
}
#[test]
fn codegen_schemas_describe_blocking_and_retryable_plan_retention() {