Long-running tool calls (asset imports, test runs, batched edits) were
cut off ~30-90s into execution, so the task could never finish. On the
stdio transport this was governed by hardcoded values on both hops:
- Unity side: StdioBridgeHost.FrameIOTimeoutMs (30s const) capped every
command's execution and frame I/O; on timeout the client reconnected
and re-sent, which force-closed the prior client and made the bridge
restart on a new port (the repeated "StdioBridgeHost started on port
6400/6402" churn).
- Server side: ServerConfig.connection_timeout (30s socket recv) and
command_total_timeout (90s cross-retry ceiling) cut the command off
first.
Unlike the WebSocket transport (WebSocketTransportClient reads a per-call
timeout off the wire), the stdio bridge had no way to raise these.
Make all three configurable with a 5-minute default:
- FrameIOTimeoutMs: 30s -> 300s, env UNITY_MCP_STDIO_COMMAND_TIMEOUT_MS.
ReceiveTimeout now scales with it (max(60s, timeout)).
- connection_timeout: 30s -> 300s, env UNITY_MCP_CONNECTION_TIMEOUT.
- command_total_timeout: 90s -> 600s, env UNITY_MCP_COMMAND_TOTAL_TIMEOUT.
Invalid/non-positive env values fall back to the default so a bad
override can't disable the timeout. Updates the config characterization
test to the new defaults.
A resource's name and its URI are deliberately different (`editor_state` vs
`mcpforunity://editor/state`), and the URI scheme is not derivable from the
name -- most resources are `category/thing` but several are flat
(`mcpforunity://instances`, `mcpforunity://menu-items`, `mcpforunity://tests`).
Several agent-facing strings still named resources without their URI, so an
agent following them built `mcpforunity://editor_state` and got a 404:
- server instructions listed resources by bare name and told the reader to
"poll the `editor_state` resource's `isCompiling` field" (that field path is
also wrong -- payloads are wrapped, so it is `data.compilation.is_compiling`)
- `refresh_unity`'s `wait_for_ready` parameter description referred to
`editor_state.advice.ready_for_tools`
- the hint Unity returns in the `refresh_unity` result said "poll editor_state
until ready_for_tools is true"
#1244 added a warning that names and URIs are not interchangeable, but left the
strings that trigger the mistake unchanged. Spell every resource reference as a
full URI instead, and correct the field paths while here.
Adds a regression test asserting that no agent-facing prose -- server
instructions, resource descriptions, tool and parameter descriptions, and
multi-word string literals under MCPForUnity/Editor -- mentions a resource by
its snake_case name without also giving that resource's URI.
#1292 declared Codex stdio-only. Tested against Codex CLI 0.47.0 with an
isolated CODEX_HOME, that is wrong: a bare
[mcp_servers.unityMCP]
url = "http://127.0.0.1:8123/mcp"
reports `transport: streamable_http` from `codex mcp get`, and Codex completes a
full MCP handshake against a live mcp-for-unity HTTP server - initialize 200,
notifications/initialized 202, SSE GET 200, tools/list 200 - with no feature flag
set at all. Adding [features] rmcp_client, the deprecated root-level
experimental_use_rmcp_client, both, or a deliberately bogus feature key all give
byte-identical results; unknown feature keys are silently ignored.
So #1292 removed a capability Codex has, for every Codex user.
Drop SupportsHttpTransport = false (the McpClient default is already true) and
delete the SupportedTransports override, since the base default is already
{ Stdio, Http }.
Delete the GetManualSnippet stdio coercion too. It was added by #1292 to stop a
stdio-only client rendering a url block, and CodexConfigurator is the only
subclass of CodexMcpConfigurator, so once Codex is HTTP-capable that branch is
unreachable.
Leave [features] rmcp_client = true alone: it is the current key name (the root
experimental_use_rmcp_client form is deprecated per openai/codex#6995), it is
harmless, and it enables the RMCP client that OAuth needs. Deliberately not
adding the deprecated key - it does nothing on current Codex and would just
linger in users' configs.
Tests now assert both transports and cover the snippet in both directions.
Caveat for review: this was verified against the Codex CLI. #1193 was reported
against Codex Desktop on Windows 11, which is untested here. #1292's remedy was
too broad, which does not mean the reporter was wrong - ask for their version and
CLI-vs-Desktop before closing #1193. If Desktop genuinely cannot do HTTP, that
belongs in Desktop-specific handling, not a blanket capability removal.
Fixes#1297. At action:"create", component_properties was accepted and
coerced by the C# dispatcher (ManageGameObject.cs) but only ever consumed
by the "modify" handler, so it silently did nothing. Meanwhile the shape
"create" already reads directly out of each componentsToAdd entry
({typeName, properties}) was rejected before it reached Unity, because the
Python schema typed components_to_add as list[str].
- GameObjectComponentHelpers.cs: factor the componentProperties loop +
error aggregation out of GameObjectModify.cs into a shared
ApplyComponentProperties helper, so both actions apply it identically.
- GameObjectCreate.cs: call the new helper after components are added,
destroying the partially-created object and returning the error if any
property fails to set (matching how component-add failures are handled).
- GameObjectModify.cs: switch to the shared helper (behavior-preserving
refactor, no functional change on the modify path).
- manage_gameobject.py: widen components_to_add to accept
{"typeName": ..., "properties": {...}} objects alongside plain strings,
matching what GameObjectCreate.cs already reads.
- Regenerated website/docs/reference/tools/core/manage_gameobject.md via
tools/generate_docs_reference.py for the updated parameter docs.
Tested: Server/tests/test_manage_gameobject.py exercises the Python
contract end-to-end, including a real fastmcp/pydantic schema validation
run of the issue's exact repro payloads (confirmed the pre-fix
ValidationError reproduces on the unmodified file, and is gone after).
Added TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/
ManageGameObjectCreateTests.cs coverage for the C# side, but this was not
run against a live Editor.
Two review findings on #1292.
Copilot: advertising Codex as stdio-only was not enough. GetManualSnippet()
calls BuildCodexServerBlock directly, which reads the global UseHttpTransport
pref itself. Configure() gets that pref coerced for it by
ClientConfigurationService.ConfigureWithTransportCoercion; the snippet path
does not. With the HTTP pref on, the copyable snippet still rendered
[features]
rmcp_client = true
[mcp_servers.unityMCP]
url = "http://127.0.0.1:8080/mcp"
reintroducing the exact silent-failure path via manual setup. Coerce to stdio
around the call for any client that does not support HTTP, restoring the pref
afterwards, mirroring ConfigureWithTransportCoercion.
Deliberately not removing the HTTP branch from CodexConfigHelper: it is covered
by BuildCodexServerBlock_HttpMode_GeneratesUrlField and is a general-purpose
helper, so narrowing the caller is the smaller and more honest change.
CodeRabbit: assert SupportedTransports equals exactly { Stdio } rather than
contains-stdio plus not-contains-http, which would also pass if a third
transport were added.
Adds Codex_ManualSnippet_IsStdio_EvenWhenHttpPreferred, which fails with the
url block above before this change.
Codex does not expose MCP tools that are configured through the HTTP block,
so writing an HTTP config produces a client that connects but surfaces no
tools.
Declare SupportsHttpTransport = false and restrict SupportedTransports to
stdio, so CoerceTransportFor settles on stdio before Configure() runs.
EditorApplication.isCompiling conflates three states: actually compiling,
compilation queued, and finished-but-reload-deferred. A project holding
EditorApplication.LockReloadAssemblies sits in the third state for as long
as the lock is held, with no compilation running, and isCompiling stays
true the whole time.
Eight call sites gated on that raw flag, so they refused work indefinitely:
the stdio bridge would not start, unity_reflect and manage_scriptable_object
returned "Unity is compiling", refresh_unity reported the wrong resulting
state and never completed its wait, the stdio reload handler deferred its
resume, and TestJobManager both mis-attributed its init timeout and reported
a bogus "compiling" block reason.
Route all eight through EditorStateCache.GetActualIsCompiling(), which falls
back to the event-tracked CompilationPipeline flag, and drop the isPlaying
gate that previously limited the workaround to play mode.
Verified live: with LockReloadAssemblies held after RequestScriptCompilation,
EditorApplication.isCompiling is true while the pipeline flag is false, so
CompilationPipeline.compilationFinished does fire while the reload is held.
The Editor is a console-less GUI process. TerminalLauncher spawned cmd.exe
with UseShellExecute=false and CreateNoWindow=true and never redirected
stdin, so uvx.exe inherited an invalid stdin handle and died with
"The handle is invalid. (os error 6)" before the server could start.
Redirect stdin from NUL inside the cmd.exe payload so the child gets a
valid handle regardless of whether the Editor has a console.
Regression from #1201, shipped in v10.1.0.
status / cancel / list_providers were near-identical across GenerateAudio,
GenerateImage and GenerateModel (differing only by a kind label + poll
interval), and NormalizeOutputFolder was a verbatim triple copy.
- New AssetGenToolHelpers.{Status,Cancel,ListProviders} — the three tools now
delegate, passing their kind label / poll interval.
- NormalizeOutputFolder moved to AssetGenPaths (it already lived on
TryGetAssetsFolder).
- Behaviour preserved (image `remove_background` arm kept; provider-list
filtering unchanged). Net -120 lines of duplication.
Verified: EditMode 1167 tests / 0 failures + in-editor smoke (audio/image/model
list_providers, status/cancel error paths, image remove_background all intact).
Claude-Session: https://claude.ai/code/session_015KYy51gwBuhDuLZXXoqc98
- FalAudioAdapter.BuildBody: floor (not Math.Round) the clamped duration so
it never exceeds the requested value, then enforce >= 1 (banker's rounding
could round 2.5 -> 2 and could exceed the request).
- AssetGenJobManager.AllowedExtensionsFor: fail closed — an unexpected/unknown
job kind now allows nothing instead of falling through to the model
allowlist, so the RCE boundary never opens by default. Explicit cases for
model + marketplace.
- Tests: pin floor semantics (10.9 -> 10) and the fail-closed behavior
(unknown/null/empty kind -> not allowed).
Verified: EditMode 1167 tests / 0 failures + in-editor smoke (BuildBody(10.9)
-> duration=10; IsAllowed("bogus","glb")=false).
Claude-Session: https://claude.ai/code/session_015KYy51gwBuhDuLZXXoqc98
- McpToolsSection: add the asset_gen -> "Asset Gen" group display name so
the tool-group tab no longer falls back to the raw "Asset_gen".
- Update the asset_gen group blurb (registry + CLI) to include audio gen.
- Regenerate the tool reference docs from the Python registry: add the
missing generate_audio.md and refresh the asset_gen landscape/index.
Claude-Session: https://claude.ai/code/session_015KYy51gwBuhDuLZXXoqc98
Security (from a dynamic security audit of the branch):
- H1: UnityWebRequestTransport disables auto-redirect on auth-bearing
requests (redirectLimit=0) so a provider 3xx can't re-send the API key
to a redirect host.
- H2/P8: per-kind result-extension allowlist in AssetGenJobManager.WriteFile
(+ defense-in-depth in the audio/image import pipelines) — a provider
can no longer land a .cs/.asmdef/.meta/.asset under Assets/ (Editor RCE).
- H3: ProviderHttp.RequireHost pins the submit URL and the provider-supplied
response_url to https://queue.fal.run before the fal key is attached
(both fal image + audio adapters).
Correctness (from code review):
- C1: Tripo image->3D now sends model_version.
- C2/C3/C6/C10: FalAudioAdapter.BuildBody is catalog-driven — duration-
required models (CassetteAI SFX/Music, Stable Audio) send a default
duration when the caller passes 0 (fixes the default-input 422),
fractional durations floor to >=1, Lyria stays prompt-only and its GUI
no longer advertises a duration it ignores, and the clamp ceilings come
from the catalog (no more duplicated 190/30/180).
- C4: an unmapped fal poll status now fails fast instead of polling to the
600s timeout (both fal adapters).
- C5: a stale/invalid selected-model pref is cleared on dropdown fallback.
- C7: the audio fal-key status refreshes when the shared 2D fal key changes.
Cleanup: extract AssetGenModelCatalog.ResolveModel (dedupes the model-
resolution chain across the three generate tools) + DefaultModelId no-alloc.
Verified: full EditMode suite 1166 tests, 0 failures (+19 new regression
tests); 34 Python asset-gen tests pass.
Claude-Session: https://claude.ai/code/session_015KYy51gwBuhDuLZXXoqc98
Wraps each category (3D Models / 2D Images / Sound) in its own darker rounded panel
so they read as distinct blocks, and moves the per-provider key status (saved/not set)
up into the header row to the right of the provider name — reclaiming a line per
provider. Verified live: 3 panels (bg alpha 0.20), status inline in each header.
Claude-Session: https://claude.ai/code/session_01GCxmdd4qo7MG6J4M6WcT9Y
The provider row is a vertical (column) container, so the setting-dropdown-inline
class's flex-grow:1 stretched the DropdownField vertically. Wrap the Model dropdown
in a horizontal .setting-row with a .setting-label (matching the Format row) and use
a label-less DropdownField. Verified live: all 5 model dropdowns now render at 18px
inside setting-row containers instead of ~400px boxes.
Claude-Session: https://claude.ai/code/session_01GCxmdd4qo7MG6J4M6WcT9Y
Phases 4-5. Extends the existing Asset Gen tab (no new window): per-provider
'Model' dropdowns on the image + 3D rows with price/duration/use-case metadata, a
fal audio row (no key field — reuses the shared fal key) with a model dropdown and
the Stable Audio license caveat, and a Refresh button that re-validates key
presence + the curated catalog. Selecting a model writes the per-(kind,provider)
pref that generate_* reads as its default. fal has no public list-models API, so
Refresh is a curated re-validate (the plan's gated fallback), never a network fetch.
Characterization tests assert the new builders + three-phase lifecycle.
Claude-Session: https://claude.ai/code/session_01GCxmdd4qo7MG6J4M6WcT9Y
Phases 2-3. Adds a curated AssetGenModelCatalog (image/3D/audio models with
price/duration/use-case metadata; defaults reference the adapter constants so the
panel default equals what an omitted model resolves to). Adds the generate_audio
MCP tool + CLI, threads a per-(kind,provider) selected-model pref so a GUI choice
becomes the default generate_* uses when no model is passed, and makes
Tripo/Meshy consume req.Model. Per-provider prefs (not per-type) so disjoint model
lists never clobber. Compiles on 2021.3 floor; Python + adapter/catalog tests green.
Claude-Session: https://claude.ai/code/session_01GCxmdd4qo7MG6J4M6WcT9Y
Phase 1 of fal.ai audio generation. Adds AudioGenRequest + IAudioProviderAdapter,
a FalAudioAdapter fronting the v1 fal audio models (stable-audio-25, cassetteai/*,
lyria2), an AudioImportPipeline that branches AudioImporter load type on clip
length, AssetGenProviders.Audio + an audio List row, and
AssetGenJobManager.StartAudioGeneration. Also threads ModelGenRequest.Model
(consumed in Phase 3). Compiles on the 2021.3 floor; adapter/import/e2e tests green.
Claude-Session: https://claude.ai/code/session_01GCxmdd4qo7MG6J4M6WcT9Y
Review responses (CodeRabbit + Copilot):
- Document the 'legacy' animation_type value in the C# XML summary, MCP tool
description, param help, and CLI help (Copilot x4).
- SKILL.md: drop multi-material zones as a GLB-only trigger (both formats keep
slots), define out_glb in the glTF branch example, document the full
animation_type contract, probe both Principled emission socket names
('Emission Color' 4.x / 'Emission' 3.x), keep use_active_scene=True in the
Notes GLB example, and distinguish FBX vertex-color data transfer from URP
Lit shader display.
- bridge-fidelity.md: make export_apply conditional (static-only; skinned/
shape-key meshes need False) and document export_animations' active/NLA-
stashed semantics.
Regenerate website/docs/reference for the updated tool description — fixes the
"Check docs reference is fresh" CI failure ("differs: asset_gen/import_model_file.md").
Claude-Session: https://claude.ai/code/session_01Cya1SZmg7CJgJS61nhjLH4
A rigged/animated FBX imported through import_model_file always arrived
with zero AnimationClips because the import pipeline hard-coded
ModelImporterAnimationType.None. Thread an optional animation_type param
(none|generic|humanoid|legacy) through the MCP tool, the CLI command, and
the C# pipeline so callers can surface a model's clips; the default stays
None, so existing imports are unchanged. glTF/GLB ignores the knob
(glTFast imports animation itself).
Also fold the tested Blender-handoff findings into the blender-to-unity
skill: GLB-vs-FBX fidelity guidance (with the new
references/bridge-fidelity.md matrix, force-added since .claude is
gitignored), FBX emission restoration steps, and the animation_type knob.
Sync uv.lock with the pyproject 10.0.0 version already on beta.
Claude-Session: https://claude.ai/code/session_01Cya1SZmg7CJgJS61nhjLH4