Skip to main content
Glama

RQL / Query

rql-query
Read-onlyIdempotent

Run read-only RQL queries against the live Godot editor to filter, sort, and project scene, file, project, or console data into compact rows, reducing exploratory calls.

Instructions

RQL (Reflection Query Language, reference: https://ai-game.dev/docs/rql). Run a read-only RQL query against the live Godot editor and return compact rows. One precise query beats many exploratory calls. Changes need rql-mutate.

RQL: source | stage | stage ... Sources: nodes (every node of the edited scene, depth-first, root included), root (the edited scene root), selection, scenes (open scenes), files (every res:// file the editor knows; on request size (bytes), importedSize, deps, missingDeps, usedBy (scenes / resources that load it) and importSettings["option"]), folders (path fileCount folderCount empty), classes (Godot classes, ClassDB), console (captured editor log entries), gameLog (what the game run from the editor printed: Sequence LogType Message StackTrace), editor (editor state, undoHistory / redoHistory, and operations), project (project settings: settings["section/key"], name, mainScene; input map, autoload and global group calls), settings (every project setting: name section value type changed), inputActions (name events deadzone builtIn), autoloads (name path singleton order), groups (name nodes count global), layers (kind index name: the project's layer names), tools (every MCP tool of this editor, hidden ones included: name title visible description; a hidden tool is not offered to this client: to use one, ask the user to show it in the AI Game Developer dock). Resources: godot.resources("StandardMaterial3D") or godot.resources("Texture2D", "res://art"), godot.resource("res://materials/red.tres"). ref("Node3D#1234") resolves a $ref; godot.find("Player") or godot.find("Level/Player") a node by unique name or path below the scene root; godot.group("enemies") the nodes of a group; godot.callable("Player", "_on_hit") a Callable. Stages: where filter (a boolean condition: where Mesh != null, where has(Mesh), where children.count() > 0) select a b.c alias=(expr) project columns (wrap computed expressions in parentheses) sort [desc], ... stable sort first N | skip N | count get replace each item by a member (lists are flattened) recurse children [max-depth=N] [include-self=false] walk the scene tree (input items included by default) distinct [expr] | sum expr | min expr | max expr | avg expr group-by expr rows key|count; group-by a, b (or kind=class) gives one column per key, then count; group-by class | select key, count, avgY = items.avg(Position.Y) Expressions: == != < <= > >= and or not + - * / %; contains, starts-with, ends-with (case-insensitive); matches "regex"; x in [a, b]; x is null; x is not null; $ is Node3D (includes subclasses); has(member). Conditional: cond ? a : b, e.g. select name kind=($ is MeshInstance3D ? "mesh" : "other"). Text: replace(s, from, to) (case-insensitive), replace-regex(s, pattern, repl) ($1 = group 1), substring(s, start[, length]), split(s, sep), trim(s), pad(v, width[, fill]) — also as methods: name.replace("Old", "New"), name.substring(0, 4). Literals: numbers, "strings", true/false/null, lists [a, b], objects {x: 1, y: 2}; groups == ["enemies"] compares a list. Paths: a.b.c, list[0], map["key"]; .member on a list maps over its elements. List methods: .any(pred) .all(pred) .count([pred]) .sum(e) .min(e) .max(e) .first([pred]); inside them bare names refer to the element, falling back to the outer item. Meta: $ (current item), $ref (stable id such as Node3D#1234), $type, $path, $index, $depth. Enums compare to strings; == on strings is case-sensitive. Godot: every node has name, path (below the scene root, "." = root), class (the Godot class, e.g. "OmniLight3D"), parent, owner, children (list), childCount, index, script (res:// path), groups (list) and id; signals, connections (persistent: signal target method flags), incoming and overrides (how a scene instance differs from its scene file: node property value template) are read on request (select connections, get overrides). Members are the C# properties (Position, Rotation, Scale, Visible, Text …), matched case-insensitively, so position works too; Rotation is in radians, RotationDegrees in degrees. Every Godot property, including GDScript script variables (in the editor: @export ones, every one of a @tool script) and GDExtension properties, is in props["snake_name"] (e.g. props["light_energy"], props["speed"] on a scripted node); a Godot array lists the values it holds (get props["items"] | where props["price"] > 10). Vectors / colors print as (x, y, z); write "(1, 2, 3)", [1, 2, 3] or {x: 1, y: 2, z: 3}; colors also "#ff8800". Vector2/3/4, their integer variants (whole results only) and Color compute component-wise (set Scale = Scale * 1.5, Position + [0, 1, 0]); Quaternion, Basis and transforms do not. Lists of numbers compute too ([Position.X, 0, Position.Z] * 2), and length(v), normalize(v), distance(a, b), dot(a, b), lerp(a, b, t) take vectors or lists (distance(Position, godot.find("Player").Position) < 5). Units: meters in 3D, pixels in 2D. A resource path "res://…" works wherever a resource is expected, a node path or name wherever a node is expected. Freed nodes read as null. Input events are text: "Key:Space", "Key:Ctrl+S", "PhysicalKey:W", "Mouse:Left", "JoypadButton:A", "JoypadMotion:LeftX-". A TileSet has sources (id kind atlas texture tileCount), physicsLayers, terrainSets, customDataLayers; an atlas source has tiles (coords size data). A Theme has maps keyed "ControlType/item": colors, constants, fonts, fontSizes, icons, styleboxes. A ShaderMaterial has uniforms (name type default value overridden) and params["uniform"] (typed by the shader; null reverts to the default); a MeshLibrary has items (id name mesh meshTransform shapes navigationMesh preview). Layer masks by name: layerNames["collision_mask"] on any object with layer properties is a list of layer names (a number for an unnamed layer). Callables are "Player::_on_hit" (target::method); RIDs are read-only "RID(…)"; a Projection is 16 numbers. Navigate with paths or get; select only projects output columns. Results come back as compact rows (header line, then col|col). Long results are truncated — refine with where/first or use count. Unknown members produce warnings with suggestions (in rql-mutate a misspelt member that leaves no target is an error); rql-schema(type) lists a type's members and Godot properties.

Language rules:

  • Conditions are booleans. where, and / or / not, ?: and the predicates of .any() .all() .count() .first() take a comparison or has(), never a bare value: where material != null (or has(material)), where count > 0, where name != "", where len(tags) > 0. A number, string, object or list there fails with type-mismatch; null counts as false.

  • cond ? a : b (the same as if(cond, a, b)) picks a value and evaluates only the taken branch: set y = (y < 0 ? 0 : y). Inside select, wrap computed values in parentheses: select kind = (intensity > 2 ? "bright" : "dim").

  • Lists compare by value, in order: tags == ["a", "b"], x in [[1, 2], [3, 4]]; distinct and group-by merge equal lists.

  • group-by gives rows with key (or one column per key), count and the group's items: group-by kind | select key, count, avg = items.avg(weight); also items.sum(e), items.min(e), items.max(e), items.count(pred), items.first().name. Group by the item's type with $type (group-by $type); a bare type is a member that most items do not have.

  • Receipts list only real changes: a set row whose value stayed the same is left out and counted as unchanged, so no rows with N unchanged means the values already matched.

  • call Method(args) runs the method of each item (the items must be objects). call source.Method(args) runs the method of that one source object once per stream item, with $, $index, $path and $depth in the arguments being the item's; every call counts against the target limit, and every argument is checked before the first call (a dry run checks without calling).

  • Bulk create: range(start, end[, step]) streams numbers (end excluded), and a list literal streams its elements, so one call creates many objects: range(0, 150) | call editor.CreateNode("Node3D", {name: "Tile" + $, position: [($ % 15) * 2, 0, floor($ / 15) * 2]}) builds a 15-wide grid, [["Lamp", [0, 2, 0]], ["Sun", [6, 4, 0]]] | call editor.CreateNode("OmniLight3D", {name: $[0], position: $[1]}) one object per entry. The receipt has one row per call with the $ref of the created object.

  • Vectors and colors the host marks as component-wise compute component by component: set Scale = Scale * 1.5, Position + [0, 1, 0], Position - Offset, -Velocity, v / 2. Adding a number to a vector is a type-mismatch (add [x, y, z] instead); quaternions and other types without that mark stay type-mismatch. Lists of numbers are vectors too: [x, 0, z] * k, [1, 2] + [3, 4]. Vector functions work on both: length(v) (or v.length(), magnitude), normalize(v), distance(a, b), dot(a, b), lerp(a, b, t); len(x) stays the size of a string or list.

  • Trigonometry is in radians: sin, cos, tan, asin, acos, atan, atan2(y, x), pi(), deg(x), rad(x); trigonometric results are rounded (sin(pi()) is 0). In a set value or call argument $count is the number of items entering the stage ($index runs 0 to $count - 1), so set position = [5 * cos(2 * pi() * $index / $count), 0, 5 * sin(2 * pi() * $index / $count)] lays the items out on a circle of radius 5.

  • A missing map entry reads as null: props["k"] == null and props["k"] is null are true without the entry, and has(props["k"]) tests whether it exists. A misspelt member is not null: it stays unknown and is reported.

  • Arguments by name: call Method({name: value, ...}) with one object literal that no overload takes as a whole binds each key to the parameter of that name; parameters left out keep their defaults, and an unknown name fails listing the parameters: range(0, 10) | call editor.CreateNode({className: "Node3D", options: {name: "Tile" + $, position: [$ * 2, 0, 0]}}). Engine objects (scene objects, nodes, actors) are never built from an object literal: pass an existing one, e.g. ref("Type#id"). Value-like assets and owned subobjects that the host marks are built from a typed literal: set mesh = {$type: "TypeName", size: 2}.

Examples: nodes | where name contains "Enemy" | select $ref name class path nodes | where $ is Node3D and Position.Y > 0 | select name Position nodes | where class == "OmniLight3D" | select name props["light_energy"] props["light_color"] root | recurse children | where childCount == 0 | select path class godot.resources("StandardMaterial3D") | select path props["albedo_color"] files | where ext == "tscn" | select path scenes | select name path active classes | where parent == "Light3D" | select name console | where logType == "Error" | sort sequence desc | first 5 | select message editor | select isPlaying editedScene selection godot.group("enemies") | select name path nodes | where name == "Player" | get incoming | select source signal method settings | where section == "display" and changed | select name value inputActions | where builtIn == false | select name events autoloads | select name path singleton godot.resources("TileSet") | get sources | select id kind texture tileCount godot.resource("res://materials/water.tres") | get uniforms | select name type default value overridden nodes | where class == "StaticBody3D" | select name layerNames["collision_layer"] layerNames["collision_mask"] layers | where kind == "3d_physics" and name != "" | select index name godot.resources("MeshLibrary") | get items | select id name mesh nodes | group-by class, scripted=(script is not null) nodes | where $ is Node3D | group-by class | select key, count, avgY = items.avg(Position.Y) nodes | where $ is Node3D | sort distance(Position, [0, 0, 0]) | first 3 | select name dist = (distance(Position, [0, 0, 0])) nodes | where $ is Node3D | select name kind=($ is MeshInstance3D ? "mesh" : $ is Light3D ? "light" : "other") files | where type contains "Material" and usedBy.count() == 0 | select path files | where missingDeps.count() > 0 | select path missingDeps files | where not (path starts-with "res://addons/") | sort size desc | first 5 | select path size importedSize nodes | where SceneFilePath != "" and overrides.count() > 0 | select name overrides editor | select undoHistory redoHistory

Input Schema

NameRequiredDescriptionDefault
queryYesRQL query, e.g. nodes | where $ is Node3D | select $ref name Position

Schema Changelog

Changes observed during successful MCP inspections.

  1. Addedv0.35.1

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already establish the read-only, idempotent, non-destructive profile, but the description adds substantial behavior: compact row output format, truncation of long results, refinement via where/first/count, warning-and-suggestion behavior for unknown members, and call-limit receipts.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness2/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is organized into labeled sections and examples, but it is extremely long and reads like a full language reference rather than a tool description. The linked RQL documentation is mentioned but much of its content is duplicated inline, making the definition excessively verbose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex DSL-based query tool with no output schema, the description is exceptionally complete: it covers return format, truncation, error behavior, source types, operators, and many worked examples. An agent has enough information to construct and interpret queries correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and the only parameter is the query string, but the description goes far beyond the schema by documenting sources, stages, expression syntax, paths, meta fields, type-specific members, and language rules. This is necessary semantic detail for constructing a valid query.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence states a specific verb and resource: 'Run a read-only RQL query against the live Godot editor and return compact rows.' It also distinguishes the tool from its sibling by noting 'Changes need rql-mutate.'

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly advises 'One precise query beats many exploratory calls,' routes mutations to rql-mutate, and points to rql-schema(type) for member discovery. This gives clear when-to-use and when-to-use-something-else guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.