diff --git a/contributors/people.toml b/contributors/people.toml
--- a/contributors/people.toml
+++ b/contributors/people.toml
@@ -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
diff --git a/crates/automation/src/tests.rs b/crates/automation/src/tests.rs
--- a/crates/automation/src/tests.rs
+++ b/crates/automation/src/tests.rs
@@ -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();
diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs
--- a/crates/automation/src/tools.rs
+++ b/crates/automation/src/tools.rs
@@ -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`)."}
                 }),
                 &[],
             )
diff --git a/docs/agents.md b/docs/agents.md
--- a/docs/agents.md
+++ b/docs/agents.md
@@ -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. |
diff --git a/docs/mcp.md b/docs/mcp.md
--- a/docs/mcp.md
+++ b/docs/mcp.md
@@ -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 |
