diff --git a/src/godotiq/bridge/protocol.py b/src/godotiq/bridge/protocol.py
index d19bd39..fe178ea 100644
--- a/src/godotiq/bridge/protocol.py
+++ b/src/godotiq/bridge/protocol.py
@@ -15,6 +15,7 @@ TOOL_TIMEOUTS: dict[str, float] = {
     "stop": 5.0,
     "scene_tree": 5.0,
     "node_ops": 10.0,
+    "build_scene": 30.0,
 }
 
 SCREENSHOT_MAX_RETRIES: int = 3
diff --git a/src/godotiq/tools/bridge/__init__.py b/src/godotiq/tools/bridge/__init__.py
index d284240..a0dde86 100644
--- a/src/godotiq/tools/bridge/__init__.py
+++ b/src/godotiq/tools/bridge/__init__.py
@@ -24,6 +24,7 @@ from godotiq.tools.bridge.undo_history import undo_history as _undo_history
 from godotiq.tools.bridge.save_scene import save_scene as _save_scene
 from godotiq.tools.bridge.camera import camera as _camera
 from godotiq.tools.bridge.ui_map import ui_map as _ui_map
+from godotiq.tools.bridge.build_scene import build_scene as _build_scene
 from godotiq.tools.output_limit import apply_output_limit, _CHAR_LIMITS
 
 
@@ -718,3 +719,57 @@ async def godotiq_ui_map(
         return {"error": f"[{e.code}] {e.error_message}"}
     except Exception as e:
         return {"error": f"Unexpected error: {e}"}
+
+
+@mcp.tool(
+    name="godotiq_build_scene",
+    annotations={
+        "readOnlyHint": False,
+        "destructiveHint": True,
+        "idempotentHint": False,
+        "openWorldHint": True,
+    },
+)
+async def godotiq_build_scene(
+    target_scene: str = "",
+    parent: str = "",
+    detail: str = "brief",
+    grid: dict | None = None,
+    line: dict | None = None,
+    scatter: dict | None = None,
+    nodes: list[dict] | None = None,
+    ctx: Context = None,
+) -> dict:
+    """Create multiple nodes in a scene using high-level patterns. One call = many nodes with Ctrl+Z undo. Use INSTEAD of repeated node_ops add_child calls.
+
+    Modes (exactly one required):
+        grid: Place nodes in a rows x cols grid. Keys: rows (int), cols (int), spacing (float, default 1.0), scene (res:// path), type (node type).
+        line: Place nodes along a polyline path. Keys: points (list of [x,y,z]), spacing (float, default 1.0), scene, type.
+        scatter: Place nodes at explicit positions. Keys: items (list of {scene, position, rotation, scale}).
+        nodes: Explicit node list. Each dict: {type, name, position, rotation, scale, properties, scene}.
+
+    Args:
+        target_scene: .tscn path for context (auto-detects if empty).
+        parent: Parent node path (e.g. "Level/Props"). Empty = scene root.
+        detail: Output verbosity -- "brief" (default), "normal", "full".
+        grid: Grid pattern config.
+        line: Line pattern config.
+        scatter: Scatter placement config.
+        nodes: Explicit node definitions.
+    """
+    try:
+        result = await _build_scene(
+            grid=grid, line=line, scatter=scatter, nodes=nodes,
+            parent=parent, target_scene=target_scene,
+        )
+        if "error" in result:
+            return result
+        return apply_output_limit(result, detail, list_keys=["errors"])
+    except BridgeConnectionError as e:
+        return {"error": str(e), "hint": "Enable the GodotIQ addon in Godot editor and ensure it is running."}
+    except BridgeTimeoutError as e:
+        return {"error": str(e), "hint": "Build may have timed out. Check editor for partial results."}
+    except BridgeError as e:
+        return {"error": f"[{e.code}] {e.error_message}"}
+    except Exception as e:
+        return {"error": f"Unexpected error: {e}"}
diff --git a/src/godotiq/tools/bridge/build_scene.py b/src/godotiq/tools/bridge/build_scene.py
new file mode 100644
index 0000000..df10238
--- /dev/null
+++ b/src/godotiq/tools/bridge/build_scene.py
@@ -0,0 +1,136 @@
+"""Build scene tool -- batch node creation with high-level patterns."""
+
+from __future__ import annotations
+
+import math
+
+from godotiq.bridge.session import get_bridge
+
+MAX_NODES = 256
+
+
+def validate_build_params(
+    grid: dict | None,
+    line: dict | None,
+    scatter: dict | None,
+    nodes: list[dict] | None,
+) -> tuple[dict | None, dict | None]:
+    """Validate build_scene parameters.
+
+    Returns (params_dict, None) on success, or (None, error_dict) on failure.
+    """
+    modes = [(k, v) for k, v in [("grid", grid), ("line", line), ("scatter", scatter), ("nodes", nodes)] if v is not None]
+
+    if len(modes) == 0:
+        return None, {"error": "Exactly one mode required: grid, line, scatter, or nodes. None provided."}
+    if len(modes) > 1:
+        names = ", ".join(k for k, _ in modes)
+        return None, {"error": f"Exactly one mode required, but got multiple: {names}."}
+
+    mode_name, mode_value = modes[0]
+
+    if mode_name == "grid":
+        return _validate_grid(mode_value)
+    elif mode_name == "line":
+        return _validate_line(mode_value)
+    elif mode_name == "scatter":
+        return _validate_scatter(mode_value)
+    else:
+        return _validate_nodes(mode_value)
+
+
+def _validate_grid(grid: dict) -> tuple[dict | None, dict | None]:
+    """Validate grid mode parameters."""
+    if "rows" not in grid:
+        return None, {"error": "Grid mode requires 'rows' key."}
+    if "cols" not in grid:
+        return None, {"error": "Grid mode requires 'cols' key."}
+    rows = grid["rows"]
+    cols = grid["cols"]
+    if not isinstance(rows, int) or rows <= 0:
+        return None, {"error": f"Grid 'rows' must be a positive integer, got {rows}."}
+    if not isinstance(cols, int) or cols <= 0:
+        return None, {"error": f"Grid 'cols' must be a positive integer, got {cols}."}
+    count = rows * cols
+    if count > MAX_NODES:
+        return None, {"error": f"Grid would create {count} nodes, exceeding the {MAX_NODES} limit."}
+    return {"grid": grid}, None
+
+
+def _validate_line(line: dict) -> tuple[dict | None, dict | None]:
+    """Validate line mode parameters."""
+    points = line.get("points")
+    if not points or not isinstance(points, list):
+        return None, {"error": "Line mode requires 'points' array with at least 2 points."}
+    if len(points) < 2:
+        return None, {"error": "Line mode requires at least 2 points."}
+    # Estimate node count from total distance / spacing
+    spacing = line.get("spacing", 1.0)
+    if spacing <= 0:
+        return None, {"error": "Line 'spacing' must be positive."}
+    total_dist = 0.0
+    for i in range(len(points) - 1):
+        a, b = points[i], points[i + 1]
+        dx = b[0] - a[0]
+        dy = b[1] - a[1] if len(a) > 1 and len(b) > 1 else 0
+        dz = b[2] - a[2] if len(a) > 2 and len(b) > 2 else 0
+        total_dist += math.sqrt(dx * dx + dy * dy + dz * dz)
+    estimated_count = int(total_dist / spacing) + 1
+    if estimated_count > MAX_NODES:
+        return None, {"error": f"Line would create ~{estimated_count} nodes, exceeding the {MAX_NODES} limit."}
+    return {"line": line}, None
+
+
+def _validate_scatter(scatter: dict) -> tuple[dict | None, dict | None]:
+    """Validate scatter mode parameters."""
+    items = scatter.get("items")
+    if items is None:
+        return None, {"error": "Scatter mode requires 'items' key."}
+    if not isinstance(items, list) or len(items) == 0:
+        return None, {"error": "Scatter 'items' must be a non-empty list."}
+    if len(items) > MAX_NODES:
+        return None, {"error": f"Scatter has {len(items)} items, exceeding the {MAX_NODES} limit."}
+    return {"scatter": scatter}, None
+
+
+def _validate_nodes(nodes: list[dict]) -> tuple[dict | None, dict | None]:
+    """Validate nodes mode parameters."""
+    if not nodes:
+        return None, {"error": "Nodes list must be non-empty."}
+    if len(nodes) > MAX_NODES:
+        return None, {"error": f"Nodes list has {len(nodes)} items, exceeding the {MAX_NODES} limit."}
+    return {"nodes": nodes}, None
+
+
+async def build_scene(
+    grid: dict | None = None,
+    line: dict | None = None,
+    scatter: dict | None = None,
+    nodes: list[dict] | None = None,
+    parent: str = "",
+    target_scene: str = "",
+) -> dict:
+    """Send a build_scene request to the Godot addon.
+
+    Validates parameters first, then forwards to the bridge.
+
+    Args:
+        grid: Grid pattern config (rows, cols, spacing, scene).
+        line: Line pattern config (points, spacing, scene).
+        scatter: Scatter config (items list with scene + position).
+        nodes: Explicit node list (type, name, position, etc.).
+        parent: Parent node path in the scene.
+        target_scene: Target .tscn path for context.
+
+    Returns:
+        Bridge response dict, or error dict if validation fails.
+    """
+    params, error = validate_build_params(grid, line, scatter, nodes)
+    if error is not None:
+        return error
+
+    bridge = await get_bridge()
+    return await bridge.request(
+        "build_scene",
+        params={**params, "parent": parent, "target_scene": target_scene},
+    )
diff --git a/tests/test_tools/test_build_scene.py b/tests/test_tools/test_build_scene.py
new file mode 100644
index 0000000..60e3150
--- /dev/null
+++ b/tests/test_tools/test_build_scene.py
@@ -0,0 +1,179 @@
+"""Tests for build_scene validation logic."""
+
+from __future__ import annotations
+
+from godotiq.tools.bridge.build_scene import validate_build_params
+
+
+class TestModeExclusivity:
+    """Exactly one of grid/line/scatter/nodes must be provided."""
+
+    def test_no_mode_returns_error(self) -> None:
+        """Passing zero modes returns an error dict."""
+        params, error = validate_build_params(grid=None, line=None, scatter=None, nodes=None)
+        assert params is None
+        assert error is not None
+        assert "error" in error
+
+    def test_multiple_modes_returns_error(self) -> None:
+        """Passing grid + scatter simultaneously returns an error."""
+        params, error = validate_build_params(
+            grid={"rows": 2, "cols": 2},
+            line=None,
+            scatter={"items": [{"scene": "res://a.tscn"}]},
+            nodes=None,
+        )
+        assert params is None
+        assert error is not None
+        assert "error" in error
+
+
+class TestGridValidation:
+    """Grid mode parameter validation."""
+
+    def test_valid_grid(self) -> None:
+        """Grid with rows=3, cols=3, spacing=2.0 passes; returned dict has 'grid' key."""
+        params, error = validate_build_params(
+            grid={"rows": 3, "cols": 3, "spacing": 2.0},
+            line=None, scatter=None, nodes=None,
+        )
+        assert error is None
+        assert params is not None
+        assert "grid" in params
+
+    def test_single_node_grid(self) -> None:
+        """Grid 1x1 passes validation (count=1)."""
+        params, error = validate_build_params(
+            grid={"rows": 1, "cols": 1},
+            line=None, scatter=None, nodes=None,
+        )
+        assert error is None
+        assert params is not None
+
+    def test_grid_missing_rows(self) -> None:
+        """Grid without 'rows' key returns error."""
+        params, error = validate_build_params(
+            grid={"cols": 3},
+            line=None, scatter=None, nodes=None,
+        )
+        assert params is None
+        assert "error" in error
+
+    def test_grid_zero_cols(self) -> None:
+        """Grid with cols=0 returns error."""
+        params, error = validate_build_params(
+            grid={"rows": 3, "cols": 0},
+            line=None, scatter=None, nodes=None,
+        )
+        assert params is None
+        assert "error" in error
+
+    def test_grid_exceeds_node_limit(self) -> None:
+        """Grid with rows=17, cols=16 (272 nodes) returns error mentioning 256 limit."""
+        params, error = validate_build_params(
+            grid={"rows": 17, "cols": 16},
+            line=None, scatter=None, nodes=None,
+        )
+        assert params is None
+        assert "256" in error["error"]
+
+
+class TestLineValidation:
+    """Line mode parameter validation."""
+
+    def test_valid_line(self) -> None:
+        """Line with 2 points passes validation."""
+        params, error = validate_build_params(
+            grid=None,
+            line={"points": [[0, 0, 0], [10, 0, 0]], "spacing": 2.0},
+            scatter=None, nodes=None,
+        )
+        assert error is None
+        assert params is not None
+        assert "line" in params
+
+    def test_line_insufficient_points(self) -> None:
+        """Line with only 1 point returns error (needs >= 2)."""
+        params, error = validate_build_params(
+            grid=None,
+            line={"points": [[0, 0, 0]]},
+            scatter=None, nodes=None,
+        )
+        assert params is None
+        assert "error" in error
+
+
+class TestScatterValidation:
+    """Scatter mode parameter validation."""
+
+    def test_valid_scatter(self) -> None:
+        """Scatter with 3 items passes; returned dict has 'scatter' key."""
+        params, error = validate_build_params(
+            grid=None, line=None,
+            scatter={"items": [
+                {"scene": "res://a.tscn", "position": [0, 0, 0]},
+                {"scene": "res://b.tscn", "position": [1, 0, 0]},
+                {"scene": "res://c.tscn", "position": [2, 0, 0]},
+            ]},
+            nodes=None,
+        )
+        assert error is None
+        assert params is not None
+        assert "scatter" in params
+
+    def test_scatter_missing_items(self) -> None:
+        """Scatter without 'items' key returns error."""
+        params, error = validate_build_params(
+            grid=None, line=None,
+            scatter={"count": 5},
+            nodes=None,
+        )
+        assert params is None
+        assert "error" in error
+
+    def test_scatter_empty_items(self) -> None:
+        """Scatter with empty items list returns error."""
+        params, error = validate_build_params(
+            grid=None, line=None,
+            scatter={"items": []},
+            nodes=None,
+        )
+        assert params is None
+        assert "error" in error
+
+    def test_scatter_exceeds_node_limit(self) -> None:
+        """Scatter with 257 items returns error mentioning 256."""
+        params, error = validate_build_params(
+            grid=None, line=None,
+            scatter={"items": [{"scene": f"res://{i}.tscn"} for i in range(257)]},
+            nodes=None,
+        )
+        assert params is None
+        assert "256" in error["error"]
+
+
+class TestNodesValidation:
+    """Nodes mode parameter validation."""
+
+    def test_valid_nodes(self) -> None:
+        """Nodes list with 4 items passes validation."""
+        params, error = validate_build_params(
+            grid=None, line=None, scatter=None,
+            nodes=[
+                {"type": "MeshInstance3D", "name": "A"},
+                {"type": "MeshInstance3D", "name": "B"},
+                {"type": "MeshInstance3D", "name": "C"},
+                {"type": "MeshInstance3D", "name": "D"},
+            ],
+        )
+        assert error is None
+        assert params is not None
+        assert "nodes" in params
+
+    def test_nodes_empty_list(self) -> None:
+        """Nodes as empty list returns error."""
+        params, error = validate_build_params(
+            grid=None, line=None, scatter=None, nodes=[],
+        )
+        assert params is None
+        assert "error" in error
