diff --git a/src/godotiq/tools/animation/__init__.py b/src/godotiq/tools/animation/__init__.py
index bd1c834..411b8e0 100644
--- a/src/godotiq/tools/animation/__init__.py
+++ b/src/godotiq/tools/animation/__init__.py
@@ -10,6 +10,8 @@ from __future__ import annotations
 import re
 from collections import defaultdict
 
+from mcp.server.fastmcp import Context
+
 from godotiq.parsers.tscn_parser import TscnScene
 from godotiq.session import GodotIQSession
 
@@ -455,3 +457,236 @@ def _build_animation_summary(
         "code_only": code_only,
         "unused": unused,
     }
+
+
+# ---------------------------------------------------------------------------
+# 3.0  _build_animation_info
+# ---------------------------------------------------------------------------
+
+_MAX_ANIMATIONS_PER_PLAYER = 50
+
+
+def _build_animation_info(
+    session: GodotIQSession,
+    scene_path: str,
+    node: str | None = None,
+    detail: str = "normal",
+) -> dict:
+    """Build animation info response for a scene.
+
+    Args:
+        session: A loaded GodotIQSession.
+        scene_path: res:// path to the .tscn file.
+        node: Optional AnimationPlayer node name to filter on.
+        detail: "brief", "normal", or "full".
+
+    Returns:
+        Dict with animation_players, animation_trees, code_animations, summary.
+    """
+    scene = session.get_scene(scene_path)
+    if scene is None:
+        return {"error": "Scene not found", "scene": scene_path}
+
+    summary = _build_animation_summary(scene_path, scene, session)
+    detail = detail.lower()
+
+    # Build player info with animations resolved through libraries
+    libraries = summary["libraries"]
+    anim_lookup: dict[str, dict] = {a["sub_resource_id"]: a for a in summary["animations"]}
+
+    player_dicts: list[dict] = []
+    for player in summary["players"]:
+        # Resolve animations through libraries
+        animations: list[dict] = []
+        for _lib_name, lib_sub_id in player["library_refs"].items():
+            lib_map = libraries.get(lib_sub_id, {})
+            for anim_name, anim_sub_id in lib_map.items():
+                anim_data = anim_lookup.get(anim_sub_id)
+                if anim_data:
+                    animations.append(anim_data)
+
+        truncated = len(animations) > _MAX_ANIMATIONS_PER_PLAYER
+        if truncated:
+            animations = animations[:_MAX_ANIMATIONS_PER_PLAYER]
+
+        # Apply detail level filtering to animations
+        filtered_anims = _filter_animations_by_detail(animations, detail)
+
+        player_dict: dict = {
+            "name": player["name"],
+            "path": player["path"],
+            "animations": filtered_anims,
+            "animation_count": len(filtered_anims),
+        }
+        if truncated:
+            player_dict["truncated"] = True
+
+        player_dicts.append(player_dict)
+
+    # Node filtering
+    if node is not None:
+        matched = _match_player_by_node(player_dicts, node)
+        if matched is None:
+            available = [p["name"] for p in player_dicts]
+            return {
+                "error": f"No AnimationPlayer matching '{node}'",
+                "alternatives": available,
+                "scene": scene_path,
+            }
+        player_dicts = [matched]
+
+    # Build tree dicts
+    tree_dicts: list[dict] = []
+    for tree in summary["trees"]:
+        tree_dicts.append({
+            "node_name": tree["node_name"],
+            "node_path": tree["node_path"],
+            "anim_player_path": tree["anim_player_path"],
+            "states": tree["states"],
+            "transitions": tree["transitions"],
+            "animation_refs": tree["animation_refs"],
+        })
+
+    # Code animations (merged from all scripts)
+    code_anim_dict: dict | None = None
+    if summary["code_animations"]:
+        merged = _merge_code_animations(summary["code_animations"])
+        code_anim_dict = merged
+
+    # Count unique animation names
+    all_names: set[str] = set()
+    for player in player_dicts:
+        for anim in player["animations"]:
+            all_names.add(anim["name"])
+    for co in summary["code_only"]:
+        all_names.add(co)
+
+    result: dict = {
+        "scene": scene_path,
+        "animation_players": player_dicts,
+        "animation_trees": tree_dicts,
+        "summary": {
+            "total_animations": len(all_names),
+            "total_players": len(player_dicts),
+            "total_trees": len(tree_dicts),
+            "inline_count": len(summary["animations"]),
+            "code_only_count": len(summary["code_only"]),
+            "sources": _determine_sources(summary),
+        },
+    }
+    if code_anim_dict is not None:
+        result["code_animations"] = code_anim_dict
+
+    return result
+
+
+def _filter_animations_by_detail(animations: list[dict], detail: str) -> list[dict]:
+    """Filter animation dicts based on detail level.
+
+    brief: name only.
+    normal: everything except track_paths.
+    full: everything.
+    """
+    if detail == "brief":
+        return [{"name": a["name"]} for a in animations]
+
+    if detail == "full":
+        return [dict(a) for a in animations]
+
+    # normal — exclude track_paths
+    result = []
+    for a in animations:
+        d = {k: v for k, v in a.items() if k != "track_paths"}
+        result.append(d)
+    return result
+
+
+def _match_player_by_node(players: list[dict], node: str) -> dict | None:
+    """Fuzzy-match a player by node name.
+
+    Priority: exact name > case-insensitive > substring.
+    """
+    # Exact match
+    for p in players:
+        if p["name"] == node:
+            return p
+    # Case-insensitive
+    lower = node.lower()
+    for p in players:
+        if p["name"].lower() == lower:
+            return p
+    # Substring
+    for p in players:
+        if lower in p["name"].lower():
+            return p
+    return None
+
+
+def _merge_code_animations(code_anims: list[dict]) -> dict:
+    """Merge multiple code animation results from different scripts."""
+    merged: dict = {
+        "play_calls": [],
+        "travel_calls": [],
+        "anim_maps": [],
+        "looping_lists": [],
+        "has_anim_player": False,
+        "has_anim_tree": False,
+        "source": "code",
+    }
+    for ca in code_anims:
+        merged["play_calls"].extend(ca.get("play_calls", []))
+        merged["travel_calls"].extend(ca.get("travel_calls", []))
+        merged["anim_maps"].extend(ca.get("anim_maps", []))
+        merged["looping_lists"].extend(ca.get("looping_lists", []))
+        if ca.get("has_anim_player"):
+            merged["has_anim_player"] = True
+        if ca.get("has_anim_tree"):
+            merged["has_anim_tree"] = True
+    return merged
+
+
+def _determine_sources(summary: dict) -> list[str]:
+    """Determine which animation sources are present."""
+    sources: list[str] = []
+    if summary["animations"]:
+        sources.append("tscn_inline")
+    if summary["code_animations"]:
+        sources.append("code")
+    if summary["trees"]:
+        sources.append("animation_tree")
+    return sources
+
+
+# ---------------------------------------------------------------------------
+# MCP Tool: godotiq_animation_info
+# ---------------------------------------------------------------------------
+
+from godotiq.server import mcp  # noqa: E402
+
+
+@mcp.tool(
+    name="godotiq_animation_info",
+    annotations={
+        "readOnlyHint": True,
+        "destructiveHint": False,
+        "idempotentHint": True,
+        "openWorldHint": False,
+    },
+)
+async def godotiq_animation_info(
+    scene: str,
+    node: str | None = None,
+    detail: str = "normal",
+    ctx: Context = None,
+) -> dict:
+    """Complete animation understanding for a scene or specific node.
+
+    Args:
+        scene: Path to .tscn file (res:// or relative).
+        node: Optional AnimationPlayer node name to filter on.
+        detail: Level of detail - "brief", "normal", or "full".
+    """
+    session = ctx.request_context.lifespan_context["session"]
+    if session is None:
+        return {"error": "No Godot project loaded"}
+    return _build_animation_info(session, scene, node, detail)
