MCP: complete tool hints and check nested argument keys

Extend the existing MCP tools instead of adding a second set (#259):

- every tool states destructiveHint and idempotentHint next to
  readOnlyHint/openWorldHint; input schemas are closed
- the #776 unknown-key check walks the schema, so a misspelt key in a
  batch step, a pointer event or mods is -32602 with its path instead
  of being ignored
- script::parse refuses JSON step keys other than command/params
- a panic outside a command (rendering, inspection, the app connection)
  is a tool error and the server keeps running

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
storybored379andClaude Opus 5.5 committed 2026-10-21 21:26:50 -07:00
1 parent 438d7b6d79
commit ecb2fc1676
6 files changed
+171 -10

No files matched your search

+15 -1
View File
@@ -11,7 +11,7 @@
//! **Script text** ([`parse`]): a JSON array of steps, JSON lines (`{"command": …, "params": …}`),
//! or compact lines `command.id {json}` / `command.id={json}` / `command.id`; blank lines and
//! lines starting with `#` or `//` are ignored. A leading byte order mark and CRLF line endings are
//! accepted.
//! accepted. A JSON step takes only `command` and `params`; any other key is an error.
use serde_json::{Value, json};
@@ -59,6 +59,10 @@ pub fn parse(text: &str) -> Result<Vec<Step>, String> {
}
fn step_of(v: &Value) -> Result<Step, String> {
// A misspelt key (`parms`) would otherwise run the command with its defaults.
if let Some(key) = v.as_object().and_then(|o| o.keys().find(|k| !matches!(k.as_str(), "command" | "params"))) {
return Err(format!("unknown key `{key}` (a step takes `command` and `params`)"));
}
let command = v.get("command").and_then(Value::as_str).filter(|s| !s.is_empty()).ok_or("missing `command`")?.to_string();
let params = v.get("params").cloned().filter(|p| !p.is_null()).unwrap_or(json!({}));
if !params.is_object() {
@@ -195,6 +199,16 @@ mod tests {
assert!(parse("frame.create {bad").unwrap_err().contains("line 1"));
}
/// A misspelt step key ran the command with its defaults.
#[test]
fn a_step_with_an_unknown_key_is_refused() {
for text in [r#"[{"command": "file.new", "parms": {"width": 300}}]"#, r#"{"command": "file.new", "parms": {"width": 300}}"#] {
let e = parse(text).unwrap_err();
assert!(e.contains("unknown key `parms`"), "{e}");
}
assert!(parse(r#"[{"command": "file.new", "params": {"width": 300}}]"#).is_ok());
}
/// Windows editors and PowerShell 5.1 save "UTF-8 with BOM" with CRLF line endings; the BOM
/// became part of the first command id (#654).
#[test]