Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ Made a mistake? → undo / redo
## Tool Surface

96 tools in seven groups. Full parameters in [docs/tool-reference.md](docs/tool-reference.md).
`get_capabilities` groups the same tools by what they need instead: `editor_tools` (70)
holds the Editor, Track View and Animation groups plus the four asset readiness tools, and
`asset_tools` (5) holds the asset tools that work without an editor.

| Group | Count | Editor needed | What it covers |
|-------|-------|---------------|----------------|
| Capabilities | 1 | No | `get_capabilities` — call this first |
| Editor | 41 | Yes | Entities, components, transforms, prefabs, levels, viewport/camera, console/CVARs, game mode, undo/redo, persistent sessions |
| Introspection | 3 | Partly | EBus schema (static stubs and live), RenderDoc capture |
| Introspection | 3 | Partly | EBus schema (static stubs and live), RenderDoc capture (manual steps on O3DE 26.10.0) |
| Project | 17 | No | Engines, projects, gems, templates, builds (blocking and background), export |
| Assets | 9 | Partly | Asset Processor status, refresh/wait, log tailing (no editor); per-asset readiness and build failure logs (editor, gem 0.6.0+) |
| Track View | 8 | Yes | Cinematic sequences: create/list/describe/delete, time range, add nodes, play/stop |
Expand Down
26 changes: 23 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **The last failures reported as successes, and three that never worked.** A
pre-release audit found more paths outside the error envelope, now fixed:
- `set_component_property` printed "Set ..." without checking the result, and
`get_component_property` reported a failed read as `{"value": "None"}`. Both now
check the outcome (`set_property_failed`, `get_property_failed`) and report an
unknown component or one not on the entity.
- `add_component` reported "Added" when the add raised or returned no outcome.
- `get_cvar` answered a CVAR that does not exist with `"value": "(missing)"` (the
engine's own text) and a failed read with `"value": "Error: ..."`; those are now
`cvar_not_found` and `get_cvar_failed`. `get_viewport_camera` returned a bare
`error` key; it now returns `viewport_camera_unavailable`.
- `get_bus_schema_live` answered a bus missing from a real module as a live result
with an `error` key; it now falls back to the stubs and reports `bus_not_found`.
- Three fallbacks addressed Broadcast-only events with `bus.Event` and could never
work; they are gone.
- `instantiate_prefab` rebuilt its `parent_id` with `entity.EntityId(n)`, which is
invalid on 26.10.0, so the parent was silently ignored. It now looks the parent up
(an unknown one is `entity_not_found`).
- With `O3DE_EDITOR_TLS_VERIFY=1` and no `O3DE_EDITOR_TLS_CA`, no CA was trusted, so
every handshake failed. The system CA store is now loaded, as documented.
- **`set_transform` applies non-uniform scale for real, and stops faking it.** An O3DE
Transform holds only a uniform scale, so a scale like `[50, 50, 1]` was applied as a
uniform 50 and reported as success. On AiCompanion gem API 0.5.0+ (gem 0.6.0) the
Expand Down Expand Up @@ -66,8 +86,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Compatibility

- **Entity IDs are tolerated as decimal strings.** AiCompanion gem main (API 0.4.0,
shipping as 0.6.0) emits every native 64-bit entity id as a decimal string instead
- **Entity IDs are tolerated as decimal strings.** AiCompanion gem 0.6.0 (API 0.4.0
and up) emits every native 64-bit entity id as a decimal string instead
of a JSON number, so a client with a 53-bit-float JSON parser cannot corrupt a u64
id. o3de-mcp already carries ids through unchanged and accepts a number or a string
on input; tests now pin that a full-precision u64 string round-trips through
Expand Down Expand Up @@ -123,7 +143,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Animation tools** (`tools/animation.py`, 2 tools): `list_anim_graphs` and
`get_anim_graph`, EMotion FX anim graph reads over the AiCompanion gem's native
request types. They need gem main or 0.6.0+ and the EMotionFX gem; an older gem
answers with code `unknown_request_type`. No editor-Python fallback (EMotion FX
answers `Unknown request type` (code `unknown_request_type` from gem 0.5.0). No editor-Python fallback (EMotion FX
anim graphs are not usefully reflected to Python); they work in secure mode.
`get_anim_graph` takes exactly one of `anim_graph_id` (32-bit number or digit
string) or `file_name`. `_native_request` moved to module level in `editor.py`
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

An MCP server (Model Context Protocol) that exposes Open 3D Engine (O3DE) capabilities to AI assistants. 96 tools across seven categories:
- **Capabilities tools** (`src/o3de_mcp/tools/capabilities.py`, 1 tool) — runtime detection of editor connectivity and CLI availability. Call `get_capabilities()` first to know what's available.
- **Editor tools** (`src/o3de_mcp/tools/editor.py`, 41 tools): send Python scripts to a running O3DE Editor over TCP port 4600. Covers entities, components, transforms, prefabs, levels, viewport/camera, console and CVARs, game mode, undo/redo, and persistent scripting sessions. Requires the AiCompanion and EditorPythonBindings gems active in the editor. Four of the tools (`get_scene_snapshot`, `get_entity_tree`, `get_entity`, `validate_scene`) use the AgentServer's native C++ request types instead of sending Python. `get_bus_schema_live`, `create_entity`, `set_transform` and `delete_entity` try their native request types first (`_native_bus_schema` in `tools/introspection.py`, `_native_mutation` in `tools/editor.py`) and fall back to editor Python when the gem answers `unknown_request_type` (too old to serve them) or the transport is the legacy RemoteConsole; a native refusal (validation, missing entity) is returned as an error, never retried through Python. On gem API 0.5.0+ `set_transform` sends quaternions and non-uniform scale natively (the gem uses the Non-uniform Scale component); on older gems it uses editor Python for a rotation near a gimbal pole and refuses a non-uniform scale (a Transform's own scale is uniform), and `get_bus_schema_live` falls back on any native failure, including an unknown bus. Fast-fails when the editor is unreachable.
- **Introspection tools** (`src/o3de_mcp/tools/introspection.py`, 3 tools) — EBus schema discovery (static stub parsing and live query) plus RenderDoc frame capture.
- **Editor tools** (`src/o3de_mcp/tools/editor.py`, 41 tools): send Python scripts to a running O3DE Editor over TCP port 4600. Covers entities, components, transforms, prefabs, levels, viewport/camera, console and CVARs, game mode, undo/redo, and persistent scripting sessions. Requires the AiCompanion and EditorPythonBindings gems active in the editor. Four of the tools (`get_scene_snapshot`, `get_entity_tree`, `get_entity`, `validate_scene`) use the AgentServer's native C++ request types instead of sending Python. `get_bus_schema_live`, `create_entity`, `set_transform` and `delete_entity` try their native request types first (`_native_bus_schema` in `tools/introspection.py`, `_native_mutation` in `tools/editor.py`) and fall back to editor Python when the gem answers `Unknown request type` (too old to serve them; the reply carries code `unknown_request_type` from gem 0.5.0) or the transport is the legacy RemoteConsole; a native refusal (validation, missing entity) is returned as an error, never retried through Python. On gem API 0.5.0+ `set_transform` sends quaternions and non-uniform scale natively (the gem uses the Non-uniform Scale component); on older gems it uses editor Python for a rotation near a gimbal pole and refuses a non-uniform scale (a Transform's own scale is uniform), and `get_bus_schema_live` falls back on any native failure, including an unknown bus. Fast-fails when the editor is unreachable.
- **Introspection tools** (`src/o3de_mcp/tools/introspection.py`, 3 tools): EBus schema discovery (static stub parsing and live query) plus RenderDoc frame capture (reports manual steps on O3DE 26.10.0, where the bus is not exposed to editor Python).
- **Project tools** (`src/o3de_mcp/tools/project.py`, 17 tools) — wrap the O3DE CLI (`scripts/o3de.sh` / `o3de.bat`) and CMake for project creation, gem management, engine registration, builds (blocking and background), and export.
- **Asset tools** (`src/o3de_mcp/tools/assets.py`, 9 tools): Asset Processor status, asset refresh/wait and log tailing without the editor, plus per-asset readiness through the AiCompanion gem's native request types (gem 0.6.0+): `get_asset_status`, `get_asset_jobs` (with failure logs), `get_asset_processor_connection` and `wait_for_asset`, which tells a failed build from a file not registered yet.
- **Track View tools** (`src/o3de_mcp/tools/trackview.py`, 8 tools): cinematic sequence authoring: create/list/describe/delete sequences, set time range, add nodes (Director, Event, entity, component), and play/stop. Editor-Python over `azlmbr.legacy.trackview`; no gem request type. Track and keyframe authoring are intentionally not exposed (the reflected API's per-node parameter-type strings and record-only key workflow are not reliable to drive).
Expand Down
7 changes: 6 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ Follow the `register_*_tools(mcp: MCPServer)` pattern:
Requirements:

- **Validate all user inputs at tool boundaries.** See existing validators in `editor.py` and `project.py` for examples.
- **Never interpolate raw user strings into editor Python scripts.** Use `json.dumps`/`json.loads` round-trip to safely pass values into `pyRunScript` commands.
- **Never interpolate raw user strings into editor Python scripts.** Use a `json.dumps`/`json.loads` round-trip to pass values into the script. Scripts are sent to the AiCompanion gem's AgentServer as framed `execute_python` requests; `pyRunScript` is only the legacy RemoteConsole fallback.
- **Report every failure with the one envelope** `{"status": "error", "code": "<slug>", "message": "<text>"}`: `format_error` from `utils/errors.py` in server code, and `_o3de_fail(code, message)` inside generated editor scripts. Never print a plain-text failure.
- **Get entities in editor scripts with `_resolve_entity_id(id)`**, which finds the entity's own id object through the search bus. Never build an id with `entity.EntityId(n)`: on O3DE 26.10.0 it gives an invalid id for every `n`.
- **Annotate entity-id parameters with `EntityIdArg` or `OptionalEntityIdArg`** from `tools/editor.py`, not `int | str`, so a bracketed id like `"[1234]"` is not JSON-decoded into a list.
- **Keep the surface test passing.** `tests/test_editor_scripts.py` runs every generated editor script against the reflected `azlmbr` surface in `tests/data/azlmbr_surface.json`; check that file before calling a bus event or function the tools do not already use.
- **Add a live test for any new editor-side path** in `tests/test_live_editor.py`, and run it with `scripts/live-sandbox.sh` (see `docs/releasing.md`). A mocked test proves only that the tool dispatches.

## Security

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ See the [architecture documentation](https://github.com/nickschuetz/o3de-mcp/blo

**Engine Introspection** (3 tools):
- EBus schema discovery, both from generated `azlmbr` stubs and live from the editor
- RenderDoc frame capture
- RenderDoc frame capture (on O3DE 26.10.0 the editor does not expose it to Python, so the tool reports the manual steps)

**Project & Build Management** (17 tools, CLI-based, no editor required):
- Discover local O3DE engine installations (multi-engine support), register engines, select the active one
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ The [**o3de-ai-companion-gem**](https://github.com/nickschuetz/o3de-ai-companion

Scripts are base64-encoded for safe transport and executed in the editor's embedded Python interpreter.

Besides `execute_python`, the AgentServer answers `ping`, `get_api_version`, `get_scene_snapshot`, `get_entity_tree`, `get_entity`, `validate_scene` and `get_bus_schema` natively in C++ (`get_entity` and `get_bus_schema` from gem 0.4.0), plus `create_entity`, `set_transform` and `delete_entity` from gem 0.5.0. Gem main (shipping as 0.6.0) adds the anim graph request types (`list_anim_graphs`, `get_anim_graph`, and the authoring types from `create_anim_graph` to `set_anim_graph_node`), which the animation tools call directly with no Python fallback, and the asset readiness types (`get_asset_status`, `get_asset_jobs`, `get_asset_processor_status`), which the asset tools call the same way. `get_capabilities` uses `get_api_version` to tell a real AgentServer (gem present) from a bare socket, and the snapshot tools call their request types directly, so they work even when the gem's secure mode disables `execute_python`. The three mutation tools try their native type first and send their editor-Python script only when the gem answers `Unknown request type` (or the transport is the legacy RemoteConsole, where native requests return `agent_server_required`); any other refusal is returned as an error, never retried through Python. On gem API 0.5.0+ `set_transform` sends its quaternion (`rotation_quaternion`) and a non-uniform `[x, y, z]` scale natively; the gem applies non-uniform scale with the engine's Non-uniform Scale component. On older gems it converts the quaternion to Euler degrees, goes to editor Python for a rotation within 0.02 degrees of a gimbal pole (where the Euler form cannot carry it), and refuses a non-uniform scale. `get_bus_schema_live` falls back to editor Python, then to the stub files, on any native failure, including an unknown bus. From gem 0.6.0 (API 0.4.0) every native refusal carries a `code` (`validation_failed`, `not_found`, `unavailable`, `engine_error`, `secure_mode`, `execution_failed`, `unknown_request_type`, `timeout`, `shutting_down`), which o3de-mcp relays in its failure envelope. The snapshot tools report a native failure rather than falling back.
Besides `execute_python`, the AgentServer answers `ping`, `get_api_version`, `get_scene_snapshot`, `get_entity_tree`, `get_entity`, `validate_scene` and `get_bus_schema` natively in C++ (`get_entity` and `get_bus_schema` from gem 0.4.0), plus `create_entity`, `set_transform` and `delete_entity` from gem 0.5.0. Gem 0.6.0 adds the anim graph request types (`list_anim_graphs`, `get_anim_graph`, and the authoring types from `create_anim_graph` to `set_anim_graph_node`), which the animation tools call directly with no Python fallback, and the asset readiness types (`get_asset_status`, `get_asset_jobs`, `get_asset_processor_status`), which the asset tools call the same way. `get_capabilities` uses `get_api_version` to tell a real AgentServer (gem present) from a bare socket, and the snapshot tools call their request types directly, so they work even when the gem's secure mode disables `execute_python`. The three mutation tools try their native type first and send their editor-Python script only when the gem answers `Unknown request type` (or the transport is the legacy RemoteConsole, where native requests return `agent_server_required`); any other refusal is returned as an error, never retried through Python. On gem API 0.5.0+ `set_transform` sends its quaternion (`rotation_quaternion`) and a non-uniform `[x, y, z]` scale natively; the gem applies non-uniform scale with the engine's Non-uniform Scale component. On older gems it converts the quaternion to Euler degrees, goes to editor Python for a rotation within 0.02 degrees of a gimbal pole (where the Euler form cannot carry it), and refuses a non-uniform scale. `get_bus_schema_live` falls back to editor Python, then to the stub files, on any native failure, including an unknown bus. From gem 0.6.0 (API 0.4.0 and up; 0.6.0 reports API 0.5.0) every native refusal carries a `code` (`validation_failed`, `not_found`, `unavailable`, `engine_error`, `secure_mode`, `execution_failed`, `unknown_request_type`, `timeout`, `shutting_down`), which o3de-mcp relays in its failure envelope. The snapshot tools report a native failure rather than falling back.

#### Connection lifecycle & timeouts

Expand Down
64 changes: 23 additions & 41 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,10 @@ missing, set `O3DE_ENGINE_PATH` and try again.
```
1. create_entity(name="WinZone")
2. add_component(entity_id=<id>, component_type="PhysX Primitive Collider")
3. set_component_property(entity_id=<id>, component_type="PhysX Primitive Collider",
property_path="Collider Configuration|Trigger", value="true")
```

> Configure the collider as a trigger via `run_editor_python` to set
> `IsTrigger = True`.

---

## Scripting
Expand All @@ -183,59 +182,42 @@ missing, set `O3DE_ENGINE_PATH` and try again.

### Recipe: Set entity transform (position/rotation/scale)

```python
# Use via run_editor_python
import azlmbr.components as components
import azlmbr.bus as bus
import azlmbr.math as math

eid = azlmbr.entity.EntityId('<entity_id>')
pos = math.Vector3(10.0, 5.0, 0.0)
components.TransformBus(bus.Event, 'SetWorldTranslation', eid, pos)
```
1. set_transform(entity_id=<id>, position=[10.0, 5.0, 0.0])
2. set_transform(entity_id=<id>, rotation=[0.0, 0.0, 0.7071068, 0.7071068], scale=[2.0, 2.0, 2.0])
3. get_transform(entity_id=<id>) → read it back
```

> Only the parts you pass change. `rotation` is an `[x, y, z, w]` quaternion
> (this one turns 90 degrees about Z). A non-uniform `scale` such as
> `[4.0, 4.0, 0.1]` works on the AiCompanion gem 0.6.0 (gem API 0.5.0) and
> later, through the engine's Non-uniform Scale component; older gems refuse it
> with `non_uniform_scale_unsupported`.

### Recipe: Set mesh asset on entity

```python
# Use via run_editor_python
import azlmbr.editor as editor
import azlmbr.bus as bus
import azlmbr.entity as entity
import azlmbr.asset as asset
import azlmbr.math as math

eid = azlmbr.entity.EntityId('<entity_id>')
# GetAssetIdByPath takes (path, asset type uuid, auto-register); a null uuid
# matches any type.
mesh_asset = asset.AssetCatalogRequestBus(
bus.Broadcast, 'GetAssetIdByPath',
'objects/primitives/cube.fbx.azmodel', math.Uuid(), False
)
mesh_t = editor.EditorComponentAPIBus(
bus.Broadcast, 'FindComponentTypeIdsByEntityType',
['Mesh'], entity.EntityType().Game
)[0]
outcome = editor.EditorComponentAPIBus(
bus.Broadcast, 'GetComponentOfType', eid, mesh_t
)
if outcome.IsSuccess():
pair = outcome.GetValue()
editor.EditorComponentAPIBus(
bus.Broadcast, 'SetComponentProperty', pair,
'Controller|Configuration|Model Asset', mesh_asset
)
```
assign_asset(entity_id=<id>, component_type="Mesh",
property_path="Controller|Configuration|Model Asset",
asset_path="objects/shaderball/ground_plane_4x4m.fbx.azmodel")
```

> The entity needs a Mesh component first (`add_component`). `asset_path` is
> the product path in the asset catalog. `assign_asset` reads the property back
> and reports only an assignment that holds; an unknown asset, a component the
> entity lacks, or a value that does not stick comes back as an error.

### Recipe: Batch-create multiple entities

```python
# Use via run_editor_python — efficient single call
import azlmbr.editor as editor
import azlmbr.bus as bus
import azlmbr.entity as entity
import json

entities = ['Tree_01', 'Tree_02', 'Tree_03', 'Rock_01', 'Rock_02']
parent = azlmbr.entity.EntityId()
parent = entity.EntityId() # an empty id: create at the level root
results = []
for name in entities:
eid = editor.ToolsApplicationRequestBus(bus.Broadcast, 'CreateNewEntity', parent)
Expand Down
Loading
Loading