From 8d252c8b2eef4bb4469d2b1857281b5c964fb72f Mon Sep 17 00:00:00 2001 From: euisuh <55032583+euisuh@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:15:26 +0900 Subject: [PATCH] fix(mcp): M8 reject oversized batches before execution Reject oversized batches before any side effects instead of reporting success after truncating calls. Closes #460 Co-Authored-By: OpenAI Codex --- ROADMAP.md | 4 ++- crates/automation/src/mcp.rs | 10 +++++--- crates/automation/tests/agent_tasks.rs | 34 ++++++++++++++++++++++++++ docs/mcp.md | 2 +- docs/roadmap.md | 6 ++++- 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 73197650..c44cdcf6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: pre-alpha** · next: alpha, ~5% and ~110–180 h away (the alpha gate's three failing workflows) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (readiness by audience with hours to ~95 %) · **Target:** Avid Pro Tools Ultimate 2026.4.1 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (MCP batch limit validation) · **Target:** Avid Pro Tools Ultimate 2026.4.1 SoundCraft is a clean-room, pure-Rust digital audio workstation that aims at full parity with Avid Pro Tools, and then beyond it: faster, open, scriptable and agent-controllable. This page is @@ -122,6 +122,7 @@ mutated and truncated movies. Field reports from the first public week are track ## Progress log +- **2026-10-14:** MCP `batch` rejects more than 1000 calls before executing any command, instead of silently running only the first 1000 (#460). No readiness percentage change. - **2026-10-13:** `mix.instrument` takes the built-in instrument's parameters in `params` (validated, clamped) and refuses any other field, so a synth setting written beside `plugin` is an error instead of being accepted and ignored (#265) - **2026-10-13:** A bounce of a bus that an aux returns onto lines up with the timeline: the aux insert's lookahead is compensated like the main mix's (it left the bus late and cut the end off), and tracks routed to that bus wait for it too (#246) - **2026-10-13:** Delay compensation covers the inserts of the master faders: a bounce of the main mix with a Maximizer on the master lines up with the same mix without it (it came out 71 samples late) (#351) @@ -155,6 +156,7 @@ mutated and truncated movies. Field reports from the first public week are track | Date | Change | Summary | |---|---|---| +| 2026-10-14 | trivial | Progress log: oversized MCP batches rejected before execution (#460) | | 2026-10-13 | trivial | Progress log: mix.instrument params, unknown fields refused (#265) | | 2026-10-13 | trivial | Progress log: bus bounce compensates aux returns (#246) | | 2026-10-13 | trivial | Progress log: master insert latency compensated (#351) | diff --git a/crates/automation/src/mcp.rs b/crates/automation/src/mcp.rs index cacd9a03..886b25d7 100644 --- a/crates/automation/src/mcp.rs +++ b/crates/automation/src/mcp.rs @@ -6,6 +6,7 @@ use std::io::{BufRead, Write}; pub const PROTOCOL_VERSION: &str = "2025-06-18"; const SUPPORTED: &[&str] = &["2025-06-18", "2025-03-26", "2024-11-05"]; +const MAX_BATCH_CALLS: usize = 1000; const INSTRUCTIONS: &str = "SoundCraft is a digital audio workstation (Pro Tools-class). Everything a user can do is a \ command: call `list_commands` to discover ids and their params, then `execute` (or `batch`). Positions are samples \ @@ -37,8 +38,8 @@ pub fn tool_definitions() -> Vec { ), tool( "batch", - "Run several commands in order; stops at the first error unless keep_going.", - json!({"calls": {"type": "array", "items": {"type": "object", "properties": {"command": {"type": "string"}, "params": {"type": "object"}}}}, "keep_going": {"type": "boolean"}}), + "Run up to 1000 commands in order; stops at the first error unless keep_going. Larger batches are rejected before any command runs.", + json!({"calls": {"type": "array", "maxItems": MAX_BATCH_CALLS, "items": {"type": "object", "properties": {"command": {"type": "string"}, "params": {"type": "object"}}}}, "keep_going": {"type": "boolean"}}), &["calls"], ), tool( @@ -135,9 +136,12 @@ impl Server { let Some(calls) = args.get("calls").and_then(Value::as_array) else { return error_result("`calls` must be an array"); }; + if calls.len() > MAX_BATCH_CALLS { + return error_result(&format!("`calls` must contain at most {MAX_BATCH_CALLS} commands; no commands were executed")); + } let mut out = Vec::new(); let mut failed = false; - for c in calls.iter().take(1000) { + for c in calls { let Some(cmd) = c.get("command").and_then(Value::as_str).map(str::trim).filter(|s| !s.is_empty()) else { out.push(json!({"command": "", "ok": false, "error": "`command` required"})); failed = true; diff --git a/crates/automation/tests/agent_tasks.rs b/crates/automation/tests/agent_tasks.rs index 3636cfe3..b94178b1 100644 --- a/crates/automation/tests/agent_tasks.rs +++ b/crates/automation/tests/agent_tasks.rs @@ -31,6 +31,8 @@ fn handshake_and_tool_list() { assert_eq!(r["result"]["serverInfo"]["name"], "soundcraft"); let r = rpc(&mut s, 2, "tools/list", json!({})); assert!(r["result"]["tools"].as_array().unwrap().len() >= 10); + let batch = r["result"]["tools"].as_array().unwrap().iter().find(|tool| tool["name"] == "batch").unwrap(); + assert_eq!(batch["inputSchema"]["properties"]["calls"]["maxItems"], 1000); assert!(s.handle(&json!({"jsonrpc": "2.0", "method": "notifications/initialized"})).is_none()); let r = rpc(&mut s, 3, "nope", json!({})); assert_eq!(r["error"]["code"], -32601); @@ -171,3 +173,35 @@ fn batch_rejects_non_array_calls() { assert!(text.contains("`calls` must be an array"), "{text}"); assert_eq!(session(&mut s)["tracks"].as_array().unwrap().len(), 0); } + +#[test] +fn batch_rejects_oversize_calls_before_any_mutation() { + for keep_going in [false, true] { + let mut s = Server::new(Headless::new(Engine::default())); + let before = session(&mut s); + let mut calls = vec![json!({"command": "engine.history"}); 1001]; + calls[0] = json!({"command": "track.new", "params": {"name": "Must not be created"}}); + let r = rpc(&mut s, 1, "tools/call", json!({"name": "batch", "arguments": {"calls": calls, "keep_going": keep_going}})); + assert_eq!(session(&mut s), before, "oversize batch mutated the session (keep_going={keep_going})"); + assert!(s.backend.engine.journal.is_empty()); + assert!(!s.backend.engine.can_undo()); + assert_eq!(r["result"]["isError"], true); + let text = r["result"]["content"][0]["text"].as_str().unwrap(); + assert!(text.contains("1000"), "{text}"); + } +} + +#[test] +fn batch_accepts_empty_and_limit_sized_calls_in_order() { + for count in [0, 1000] { + let mut s = Server::new(Headless::new(Engine::default())); + let calls: Vec<_> = (0..count).map(|at| json!({"command": "session.format_time", "params": {"at": at, "format": "samples"}})).collect(); + let results = tool(&mut s, "batch", json!({"calls": calls})); + let results = results.as_array().unwrap(); + assert_eq!(results.len(), count); + for (at, result) in results.iter().enumerate() { + assert_eq!(result["ok"], true); + assert_eq!(result["result"]["text"], at.to_string()); + } + } +} diff --git a/docs/mcp.md b/docs/mcp.md index d31712bb..558852c1 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -90,7 +90,7 @@ files it was given rather than guarding against other software on the computer. |---|---| | `list_commands` | Discover every command (id, label, menu path, shortcut, params doc, enabled). | | `execute` | Run one command with JSON params. | -| `batch` | Run several commands in order (`keep_going` optional). Any failed call makes the tool result `isError: true`; with `keep_going` the remaining calls still run and every result is listed. | +| `batch` | Run up to 1000 commands in order (`keep_going` optional). Larger batches return `isError: true` before executing any command. Any failed call makes the tool result `isError: true`; with `keep_going` the remaining calls still run and every result is listed. | | `inspect_session` | Verify work: tracks, clips, mixer, plugins, sends, automation, markers, selection; each `sources[]` entry includes `loaded` (decoded audio in the pool). | | `new_session`, `open_session`, `save_session` | Session files (`.scraft`, audio in `Audio Files/`). `open_session` returns `missing`: the media files that could not be loaded (empty when all are found). `save_session` won't save over another existing file unless `replace` is true. | | `import_audio` | WAV, AIFF, FLAC, MP3, OGG, AAC/M4A, ALAC, CAF onto a new or existing track. | diff --git a/docs/roadmap.md b/docs/roadmap.md index d8ea3f8c..c24f4255 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # SoundCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (alpha gate added; stage re-normalized to pre-alpha) · **Target:** Avid Pro Tools Ultimate 2026.4.1 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (M8 batch limit validation) · **Target:** Avid Pro Tools Ultimate 2026.4.1 Forward-looking plan. Status and numbers: [`../ROADMAP.md`](../ROADMAP.md) and [`target-app-parity.md`](target-app-parity.md); the ranked work list: [`gaps.md`](gaps.md). @@ -54,6 +54,9 @@ Status: ✅ done · 🟡 in progress · ⬜ not started. | M16 | Built-in plugin depth, sidechain, ARA | ⬜ | 110–170 | | M17 | Immersive delivery (ADM BWF, object renderer, binaural) and video depth | ⬜ | 80–130 | +M8 follow-up (2026-10-14): MCP batches above 1000 calls are rejected before execution (#460). +The limit is advertised in the tool schema and description; stage and readiness estimates are unchanged. + ## To beta (~350–570 h, about 60–70 % parallelizable) Beta needs ~75 % ready for real work and no blocking gap in session interchange. @@ -86,5 +89,6 @@ There is no `cargo xtask scorecard` in this repo; the measured number is `cargo | Date | Change | Summary | |---|---|---| +| 2026-10-14 | trivial | M8: reject oversized MCP batches before execution (#460) | | 2026-10-10 | minor | Added the alpha gate (3 of 6 core workflows fail → pre-alpha) | | 2026-10-10 | major | Created from ROADMAP.md's milestones and alpha list; added M12–M17 and the beta plan with estimates |