mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 20:47:18 -06:00
Merge pull request #2344 from becauseno7/mcp-compact-listings
MCP: list_commands and list_effects take compact: true (#2020)
This commit is contained in:
@@ -98,3 +98,8 @@ sync_github_name = false
|
||||
|
||||
[people.ki2pixel]
|
||||
display_name = "ki2pixel"
|
||||
|
||||
[people.becauseno7]
|
||||
real_name = "Shuvra Basak"
|
||||
display_name = "Shuvra Basak"
|
||||
sync_github_name = false
|
||||
|
||||
@@ -491,6 +491,8 @@ fn bridge_forwards_to_control_channel() {
|
||||
|
||||
call_json(&mut s, "execute_command", json!({"command": "comp.new", "params": {"name": "B", "width": 64, "height": 64}}));
|
||||
assert_eq!(call_json(&mut s, "get_comp", json!({}))["name"], "B");
|
||||
let cmds = call_json(&mut s, "list_commands", json!({"filter": "layer.newSolid", "compact": true}));
|
||||
assert!(cmds.as_array().unwrap().contains(&json!({"id": "layer.newSolid", "label": "Solid..."})), "{cmds}");
|
||||
|
||||
let (c, err) = call(&mut s, "render_frame", json!({}));
|
||||
assert!(!err, "{c:?}");
|
||||
@@ -738,6 +740,40 @@ fn every_engine_command_is_reachable_over_mcp() {
|
||||
}
|
||||
}
|
||||
|
||||
/// `compact: true` shrinks the listings for MCP clients' tool-result limits (#2020): commands with
|
||||
/// only `id` and `label`, effects with only `id`, `name` and `category`.
|
||||
#[test]
|
||||
fn compact_listings_keep_only_ids_and_names() {
|
||||
let mut s = server();
|
||||
let cmds = call_json(&mut s, "list_commands", json!({"filter": "layer.new", "compact": true}));
|
||||
assert!(cmds.as_array().unwrap().contains(&json!({"id": "layer.newSolid", "label": "Solid..."})), "{cmds}");
|
||||
let fx = call_json(&mut s, "list_effects", json!({"filter": "gaussian", "compact": true}));
|
||||
assert!(fx.as_array().unwrap().contains(&json!({"id": "ec.blur.gaussian", "name": "Gaussian Blur", "category": "Blur & Sharpen"})), "{fx}");
|
||||
// Each compact listing is the full one with the same arguments, entry for entry; `compact`
|
||||
// wins over `schemas`.
|
||||
let check = |s: &mut McpServer, tool: &str, args: Value, keys: &[&str]| {
|
||||
let full = call_json(s, tool, args.clone());
|
||||
let want: Value = full.as_array().unwrap().iter().map(|e| Value::Object(keys.iter().map(|&k| (k.to_string(), e[k].clone())).collect())).collect();
|
||||
let mut compact = args.clone();
|
||||
compact["compact"] = json!(true);
|
||||
assert_eq!(call_json(s, tool, compact), want, "{tool} {args}");
|
||||
};
|
||||
for args in [json!({}), json!({"filter": "layer.new"}), json!({"enabled_only": true}), json!({"schemas": true})] {
|
||||
check(&mut s, "list_commands", args.clone(), &["id", "label"]);
|
||||
check(&mut s, "command_list", args, &["id", "label"]);
|
||||
}
|
||||
check(&mut s, "list_effects", json!({}), &["id", "name", "category"]);
|
||||
check(&mut s, "list_effects", json!({"filter": "gaussian"}), &["id", "name", "category"]);
|
||||
// `compact: false` is the default listing, byte for byte; `compact: true` is a fraction of it.
|
||||
let text = |s: &mut McpServer, tool: &str, args: Value| call(s, tool, args).0[0]["text"].as_str().unwrap().to_string();
|
||||
for (tool, ratio) in [("list_commands", 2), ("list_effects", 10)] {
|
||||
let full = text(&mut s, tool, json!({}));
|
||||
assert_eq!(text(&mut s, tool, json!({"compact": false})), full, "{tool}");
|
||||
let compact = text(&mut s, tool, json!({"compact": true})).len();
|
||||
assert!(compact * ratio < full.len(), "{tool}: {compact} of {} bytes", full.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_level_tools_add_effect_and_report_state() {
|
||||
let mut s = server();
|
||||
|
||||
@@ -175,16 +175,21 @@ fn params_of(v: Option<&Value>) -> Result<Value> {
|
||||
|
||||
// ------------------------------------------------------------------ engine tools
|
||||
|
||||
/// `list_commands` tool: the engine command list, minus empty fields; with `compact`, only `id` and `label`.
|
||||
fn list_commands(b: &mut Backend, a: &Value) -> Result<Reply> {
|
||||
let schemas = get(a, "schemas").and_then(Value::as_bool).unwrap_or(false).then_some(json!(true));
|
||||
let compact = get(a, "compact").and_then(Value::as_bool).unwrap_or(false);
|
||||
let schemas = (!compact && get(a, "schemas").and_then(Value::as_bool).unwrap_or(false)).then_some(json!(true));
|
||||
let r = b.exec("command.list", obj(&[("filter", get(a, "filter")), ("enabledOnly", get(a, "enabled_only")), ("schemas", schemas.as_ref())]))?;
|
||||
// Drop empty fields to keep the listing compact.
|
||||
let list: Vec<Value> = r
|
||||
.as_array()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|mut c| {
|
||||
if compact {
|
||||
return json!({"id": c["id"], "label": c["label"]});
|
||||
}
|
||||
// Drop empty fields.
|
||||
if let Some(o) = c.as_object_mut() {
|
||||
o.retain(|_, v| !(v.is_null() || v.as_array().is_some_and(Vec::is_empty) || v.as_str() == Some("{}")));
|
||||
}
|
||||
@@ -205,8 +210,13 @@ fn describe_command(b: &mut Backend, a: &Value) -> Result<Reply> {
|
||||
json_reply(b.exec("command.describe", json!({"command": id}))?)
|
||||
}
|
||||
|
||||
/// `list_effects` tool: the effect registry; with `compact`, only `id`, `name` and `category`.
|
||||
fn list_effects(b: &mut Backend, a: &Value) -> Result<Reply> {
|
||||
json_reply(b.exec("effect.list", obj(&[("filter", get(a, "filter"))]))?)
|
||||
let mut r = b.exec("effect.list", obj(&[("filter", get(a, "filter"))]))?;
|
||||
if get(a, "compact").and_then(Value::as_bool).unwrap_or(false) {
|
||||
r = r.as_array().into_iter().flatten().map(|e| json!({"id": e["id"], "name": e["name"], "category": e["category"]})).collect();
|
||||
}
|
||||
json_reply(r)
|
||||
}
|
||||
|
||||
fn list_fonts(b: &mut Backend, a: &Value) -> Result<Reply> {
|
||||
@@ -542,14 +552,15 @@ fn control(b: &mut Backend, a: &Value) -> Result<Reply> {
|
||||
static TOOLS: &[ToolDef] = &[
|
||||
ToolDef {
|
||||
name: "list_commands",
|
||||
description: "List engine commands: id, label, menu path, shortcut, `params` doc (e.g. `{name?, width?, height?, frameRate?, duration? (s)}`) and whether it can run now (`why` if not). Every menu item, shortcut and panel gesture maps to one of these ids; run them with execute_command. Narrow with `filter` (e.g. `layer.new`, `keys`, `effect`).",
|
||||
description: "List engine commands: id, label, menu path, shortcut, `params` doc (e.g. `{name?, width?, height?, frameRate?, duration? (s)}`) and whether it can run now (`why` if not). Every menu item, shortcut and panel gesture maps to one of these ids; run them with execute_command. Narrow with `filter` (e.g. `layer.new`, `keys`, `effect`); `compact: true` lists only ids and labels.",
|
||||
bridge_only: false,
|
||||
schema: || {
|
||||
schema(
|
||||
json!({
|
||||
"filter": {"type": "string", "description": "Only commands whose id or label contains this text (case-insensitive)."},
|
||||
"enabled_only": {"type": "boolean", "description": "Only commands that can run right now."},
|
||||
"schemas": {"type": "boolean", "description": "Also return each command's parameter JSON Schema (`schema`)."}
|
||||
"schemas": {"type": "boolean", "description": "Also return each command's parameter JSON Schema (`schema`)."},
|
||||
"compact": {"type": "boolean", "description": "Return only each command's `id` and `label` (overrides `schemas`)."}
|
||||
}),
|
||||
&[],
|
||||
)
|
||||
@@ -696,9 +707,17 @@ static TOOLS: &[ToolDef] = &[
|
||||
},
|
||||
ToolDef {
|
||||
name: "list_effects",
|
||||
description: "The effect registry: id, After Effects name, category, GPU and 32-bpc support. Narrow with `filter` (id, name or category text). Apply with add_effect.",
|
||||
description: "The effect registry: id, After Effects name, category, GPU and 32-bpc support. Narrow with `filter` (id, name or category text); `compact: true` lists only ids, names and categories. Apply with add_effect.",
|
||||
bridge_only: false,
|
||||
schema: || schema(json!({"filter": {"type": "string", "description": "Only effects whose id, name or category contains this text."}}), &[]),
|
||||
schema: || {
|
||||
schema(
|
||||
json!({
|
||||
"filter": {"type": "string", "description": "Only effects whose id, name or category contains this text."},
|
||||
"compact": {"type": "boolean", "description": "Return only each effect's `id`, `name` and `category`."}
|
||||
}),
|
||||
&[],
|
||||
)
|
||||
},
|
||||
run: list_effects,
|
||||
},
|
||||
ToolDef {
|
||||
@@ -911,14 +930,15 @@ static TOOLS: &[ToolDef] = &[
|
||||
},
|
||||
ToolDef {
|
||||
name: "command_list",
|
||||
description: "List engine commands: id, label, menu path, shortcut, `params` doc (e.g. `{name?, width?, height?, frameRate?, duration? (s)}`) and whether it can run now (`why` if not). Every menu item, shortcut and panel gesture maps to one of these ids; run them with execute_command. Narrow with `filter` (e.g. `layer.new`, `keys`, `effect`).",
|
||||
description: "List engine commands: id, label, menu path, shortcut, `params` doc (e.g. `{name?, width?, height?, frameRate?, duration? (s)}`) and whether it can run now (`why` if not). Every menu item, shortcut and panel gesture maps to one of these ids; run them with execute_command. Narrow with `filter` (e.g. `layer.new`, `keys`, `effect`); `compact: true` lists only ids and labels.",
|
||||
bridge_only: false,
|
||||
schema: || {
|
||||
schema(
|
||||
json!({
|
||||
"filter": {"type": "string", "description": "Only commands whose id or label contains this text (case-insensitive)."},
|
||||
"enabled_only": {"type": "boolean", "description": "Only commands that can run right now."},
|
||||
"schemas": {"type": "boolean", "description": "Also return each command's parameter JSON Schema (`schema`)."}
|
||||
"schemas": {"type": "boolean", "description": "Also return each command's parameter JSON Schema (`schema`)."},
|
||||
"compact": {"type": "boolean", "description": "Return only each command's `id` and `label` (overrides `schemas`)."}
|
||||
}),
|
||||
&[],
|
||||
)
|
||||
|
||||
+2
-2
@@ -101,7 +101,7 @@ See [MCP conventions](mcp.md) for core tools, resources, strict arguments, and r
|
||||
|
||||
| Tool | What it does |
|
||||
|---|---|
|
||||
| `list_commands {filter?, enabled_only?, schemas?}` | Discover command ids, their param docs (and, with `schemas`, a JSON Schema each), and whether each can run now. |
|
||||
| `list_commands {filter?, enabled_only?, schemas?, compact?}` | Discover command ids, their param docs (and, with `schemas`, a JSON Schema each), and whether each can run now; `compact` lists only ids and labels (and overrides `schemas`). |
|
||||
| `describe_command {command \| id}` | One command in full: label, menu, shortcut, params doc, JSON `schema`, enabled / `why`. |
|
||||
| `execute_command {command \| id, params?}` | Run any command (undoable). Unknown parameter keys are rejected with the accepted list. |
|
||||
| `get_state` | Editor state (active comp, time, selections, tool) plus `app`: version, command and effect counts, export formats, parity summary. |
|
||||
@@ -111,7 +111,7 @@ See [MCP conventions](mcp.md) for core tools, resources, strict arguments, and r
|
||||
| `get_property {layer, path, comp?, time?}` | A leaf property's value at a time, keyframes and expression. Group paths (for example `geometryOptions`) contain children: inspect them with `get_layer`, then query a leaf such as `geometryOptions/extrusionDepth`. |
|
||||
| `set_property {layer, path, value?, time?, timeBase?, expression?, comp?}` | Sets a static value. With `time` it sets a keyframe (`timeBase: "comp"` for comp seconds); with `expression` it sets an expression. |
|
||||
| `add_keyframe {layer, path, time+value \| keys:[...], timeBase?, interpolation?, comp?}` | Adds keys (layer seconds, or comp seconds with `timeBase: "comp"`), then optionally applies linear/bezier/hold/easyEase. |
|
||||
| `list_effects {filter?}` | Effect ids, names, categories, GPU / 32-bpc support and parameters. |
|
||||
| `list_effects {filter?, compact?}` | Effect ids, names, categories, GPU / 32-bpc support and parameters; `compact` lists only ids, names and categories. |
|
||||
| `list_fonts {query?, variable?, rescan?}` | Font families text layers can use, bundled and installed, with their styles, origin, whether each is a variable font and own-language name; `variable` lists only variable fonts; `rescan` picks up fonts installed since launch. |
|
||||
| `add_effect {layer, effect, values?, comp?}` | Apply an effect and set its parameters in one call and one undo step (a failing value leaves nothing applied); returns the instance path (`effects/#n`) and its parameter paths. |
|
||||
| `render_frame {comp?, time?, max_side?, path?, inline?, transparent?}` | Returns a PNG image of a frame; `transparent: true` keeps the alpha (as an RGB + Alpha render writes it) instead of compositing over the comp background. |
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ The server provides the same core conventions as FilmCraft #28:
|
||||
|
||||
| Tool | Arguments | Result |
|
||||
| --- | --- | --- |
|
||||
| `command_list` | `filter?`, `enabled_only?`, `schemas?` | Command catalog |
|
||||
| `command_list` | `filter?`, `enabled_only?`, `schemas?`, `compact?` | Command catalog |
|
||||
| `command_run` | `id`, `params?` | Engine command result |
|
||||
| `command_batch` | `steps: [{id, params?}]`, `stop_on_error?` | `completed`, `failed`, per-step `results` |
|
||||
| `doc_inspect` | none | Project overview and active composition |
|
||||
|
||||
Reference in New Issue
Block a user