204 Commits

Author SHA1 Message Date
Shutong Wu 07e8680df9 Merge pull request #1298 from asavs/fix/gameobject-create-component-properties
fix: make component properties reachable on manage_gameobject create
2026-08-02 16:25:25 -04:00
Shutong Wu 267b465109 fix: restore HTTP transport for Codex (#1193)
#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.
2026-07-29 00:18:25 -04:00
asavschaeffer 503d938b4a fix: make manage_gameobject component properties reachable on create
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.
2026-07-28 15:59:38 -07:00
Shutong Wu 70a96ec362 Merge pull request #1290 from Scriptwonder/fix/1279-windows-headless-stdin
fix: redirect stdin from NUL when launching the server on Windows (#1279)
2026-07-28 14:42:01 -04:00
Shutong Wu 334f3c0805 Merge pull request #1292 from Scriptwonder/fix/1193-codex-stdio-only
fix: advertise Codex as stdio-only (#1193)
2026-07-28 14:41:28 -04:00
Shutong Wu afc51b53e9 fix: force stdio in the Codex manual snippet, assert the exact transport set
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.
2026-07-28 13:12:00 -04:00
Shutong Wu aaef1df968 test: report unavailable-pipeline graphics tests as Skipped, not Inconclusive
The 18 environment guards in ManageGraphicsTests used Assume.That, which
yields Inconclusive. Three consumers disagree about what that means: the
Test Runner window paints it with a failure icon, run_tests drops it from
summary.total while progress.total still counts it (1150 vs 1168), and
failures_so_far ignores it. A clean suite therefore reads as 18 failures.

Use Assert.Ignore behind an explicit condition instead, matching the
existing convention in WriteToConfigTests, StdioBridgeReconnectTests and
ManageSceneMultiSceneTests. The helpers are renamed Require* since Assume*
named the very API being dropped.

Full EditMode suite on 2021.3.45f2 before: 1150 total / 1094 passed / 0 failed
/ 56 skipped, with 18 inconclusive unaccounted for. After: 1168 / 1094 / 0 / 74,
and the two totals reconcile.
2026-07-28 12:06:01 -04:00
Shutong Wu 4ff8ff85b1 fix: advertise Codex as stdio-only (#1193)
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.
2026-07-28 12:05:43 -04:00
Shutong Wu b954897efd fix: redirect stdin from NUL when launching the server on Windows (#1279)
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.
2026-07-28 12:05:19 -04:00
Shutong Wu 26f8a1002e fix(asset-gen): address Copilot review — floor duration, fail-closed allowlist
- 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
2026-07-13 00:44:28 -07:00
Shutong Wu 4faf7a527b fix(asset-gen): security hardening + correctness fixes (+19 tests)
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
2026-07-13 00:29:48 -07:00
Shutong Wu 236e718bab feat(asset-gen): GUI model dropdowns + fal audio row + refresh
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
2026-07-12 11:21:25 -07:00
Shutong Wu 8c0a5dfe0d feat(asset-gen): model catalog + generate_audio tool + default-model wiring
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
2026-07-12 11:20:16 -07:00
Shutong Wu 66b96d2626 feat(asset-gen): audio backend — fal adapter, import pipeline, job manager entry
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
2026-07-12 10:59:53 -07:00
Shutong Wu 0eee1568d8 feat(asset-gen): add animation_type rig mode to import_model_file
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
2026-07-11 22:59:48 -07:00
Shutong Wu e67d653502 Merge pull request #1234 from Scriptwonder/fix/issue-1229-reload-resume-race
fix(editor): survive domain reloads in HTTP auto-start and reload-resume (#1229)
2026-07-11 10:05:56 -07:00
Shutong Wu 9eaadd8dcd Merge beta (b7213383) into #1207 orphan-detector branch
Semantic coexistence resolution in McpConnectionSection.cs: keep both
sibling debounce mechanisms intact — beta's health-check verification
debounce (UnhealthyVerificationThreshold / ShouldReportUnhealthy /
consecutiveVerifyFailures, c344625e) and this branch's orphaned-session
down-poll debounce (OrphanedSessionDownPollThreshold /
ShouldEndOrphanedSession / consecutiveServerDownPolls + reset on
session start). Both EditMode test files
(McpConnectionSectionHealthDebounceTests, McpConnectionSectionOrphanDetectionTests)
are preserved.

Claude-Session: https://claude.ai/code/session_01C8TU8ibk8gv3h4LqBxPbQi
2026-07-10 22:53:48 -07:00
Shutong Wu afb1a4d56e Merge pull request #1236 from Scriptwonder/fix/issue-1023-session-isolation
fix(server): stop silent cross-instance routing when multiple editors are connected (#1023)
2026-07-06 14:22:18 -07:00
Shutong Wu c344625e4c fix(editor): stop stdio connection UI showing false "Bridge not running"
Two related stdio connection-UI reliability bugs surfaced while testing
with a live domain-reload cycle:

1. Start Session race (severe). StdioTransportClient.StartAsync returned
   true unconditionally, then callers immediately verified — but
   VerifyAsync only reads StdioBridgeHost.IsRunning, which is still false
   while the previous port releases after a reload (Start() defers the
   bind to an editor-idle retry, or falls back to a new port after
   BusyPortFallbackWindowSeconds). Result: "Connection verification
   failed: Bridge not running", and Start Session only connected after
   several clicks. StartAsync now waits (bounded, ReadyWaitTimeoutSeconds)
   for the bridge to actually bind before reporting success.

2. Health-indicator flash. VerifyBridgeConnectionInternalAsync flipped the
   indicator to Unhealthy on a single transient verify miss during a
   reload/port-hop, then recovered — misleading. It now debounces via
   UnhealthyVerificationThreshold (mirrors the #1207 orphan-session
   debounce), resetting on any reachable result.

Both decisions are pure, unit-tested helpers (ShouldKeepWaitingForReady,
ShouldReportUnhealthy). 16 EditMode tests green (4 readiness + 5 debounce
+ 7 existing #1207 orphan, no regression).

Claude-Session: https://claude.ai/code/session_015JRaRFZy4piZzZtW5NabJS
2026-07-06 11:06:01 -07:00
Shutong Wu d72859700f Merge pull request #1227 from GallopingDino/fix/multiline-console-logs
fix: preserve multi-line message body in read_console
2026-07-05 21:37:47 -07:00
Shutong Wu 18158fa353 fix: stop tearing down healthy HTTP sessions on transient probe misses (#1207)
The orphaned-session detector ended an active session on a SINGLE stale
reachability reading, and the reading came from a lone 50ms TCP connect
cached for 0.75s — trivially false-negative on a machine busy with test
runs or domain reloads. Evidence bundles in #1207 show 13 teardowns and
147 socket closures in one session from exactly this loop, wedging the
bridge in no_unity_session churn until manual recovery.

- Require 3 consecutive failed polls (0.75s cadence) before declaring a
  session orphaned; probe readings taken while the editor is compiling or
  importing don't count toward teardown, and detection is skipped entirely
  while busy.
- Raise the probe's connect wait 50ms -> 250ms, as an overall budget shared
  across candidate hosts so the worst-case main-thread wait cannot multiply.
- Honor UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S above 20s (ceiling now 120s;
  default unchanged): the old ceiling equalled the default, silently
  neutering the documented escape hatch for projects whose reloads or test
  boundaries legitimately exceed 20s. Same treatment for
  UNITY_MCP_SESSION_READY_WAIT_SECONDS, and both now share one bounded
  env-read helper.

The remaining piece of #1207 (keepalive reload-awareness in
WebSocketTransportClient) is untouched here: the resume machinery reworked
in #1234 already covers reload boundaries, and the detector debounce
removes the dominant churn source dsarno identified.
2026-07-04 15:42:52 -07:00
Shutong Wu 7cfb1afc07 fix(editor): skip shutdown cleanup in batch mode so CI instances don't stop the interactive server
OnEditorQuitting stopped the managed HTTP server unconditionally; the pidfile+port handshake is global per-user state, so a -batchmode/CI instance killed the interactive editor's server on quit. Add an early-return guard mirroring HttpAutoStartHandler/StdioBridgeHost (skip in batch unless UNITY_MCP_ALLOW_BATCH), extracted into a testable ShouldRunCleanup predicate + EditMode regression test.

Closes #1196, #1010
2026-07-04 15:33:14 -07:00
Shutong Wu 301511700c fix(editor): survive domain reloads in HTTP auto-start and reload-resume (#1229)
Auto-start died for the whole session whenever startup included a compile:
the ctor latched SessionState before the delayCall ran, and the reload wiped
the delayCall. Reload-resume died at multi-pass compiles: the one-shot flag
was consumed before the deferred (delayCall) resume ran, and the next
boundary deleted it again.

- Replace delayCall with EditorApplication.update ticks that the
  [InitializeOnLoad] ctor re-arms on every domain load; latch only when the
  deferred work actually dispatches, retry (bounded per domain) while editor
  services are still initializing, and skip the subscription entirely in the
  common case where auto-start is off and nothing is pending.
- Move the resume flag from EditorPrefs (per-user machine-global, survives
  crashes, leaks across concurrently open editors) to SessionState; keep it
  until the resume succeeds, is cancelled, or exhausts its retries, instead
  of consuming it at boundaries where the bridge is down. Manual Connect,
  End Session, transport switch, and orphan cleanup cancel a pending resume
  through a named seam (CancelPendingResume), which also aborts an in-flight
  retry loop; exhaustion erases the flag so later reloads don't replay 49s
  failure loops.
- Serialize TransportManager.StartAsync per mode: concurrent starts coalesce
  onto one in-flight attempt, so a manual Connect can no longer race the
  resume/auto-start loops into bouncing a just-established session
  (WebSocketTransportClient.StartAsync tears down a live connection first).
- A SessionState connect-pending marker lets the next domain load finish an
  auto-start whose connect phase a reload killed — connect-only, never
  re-spawning (StartLocalHttpServer stops a still-booting server first).
  Whether a launch-process handle exists is now answered live by
  ServerManagementService.HasManagedServerLaunchHandle; without one (post-
  reload, or an externally started server) the wait polls to the 5-minute
  hard cap instead of fail-fasting.
- Busy gate uses EditorStateCache.GetActualIsCompiling (now internal, with
  the CompilationPipeline reflection bound once as a delegate): raw
  isCompiling stays true all play session under
  Recompile-After-Finished-Playing (#549).
- One-time (per session) migration deletes the legacy EditorPrefs flag.

The stdio sibling has the same defect class (StdioBridgeReloadHandler.cs:65
delete-when-not-running, :133 delayCall) — follow-up, kept out of scope here,
along with the remaining stdio copies of the isCompiling probe.
2026-07-04 10:46:24 -07:00
Shutong Wu 42d9faaaf2 refactor(editor): drop OceanMark test, tuck version-gated members into the #if
- Remove OceanMarkTests (geometry/color mapping + construction) per request;
  the mark is verified visually in-Editor.
- With the test gone, MapSvgPoint/FromHex no longer need to be `internal`,
  and they (plus the SvgOrigin/SvgSize consts) are only used by the Painter2D
  path — move them inside `#if UNITY_2022_1_OR_NEWER` and make them `private`,
  so the 2021.3 fallback carries no unused members.

Result of a /simplify pass; reuse/altitude/efficiency angles came back clean
(logo-injection duplication left at 2 call sites per the repo's 3+-uses rule).
2026-07-03 17:16:46 -07:00
Shutong Wu e677663497 feat(editor): embed Ocean brand mark in Editor UI + setup polish
Add the Ocean split-cube brand mark to the Editor UI and land three
low-risk installation-flow improvements.

Logo:
- OceanMark: reusable UI Toolkit control drawing the mark in Painter2D
  vector (geometry transcribed from logo-mark.svg, brand colors baked in),
  crisp at any DPI with no new package dependency. Painter2D is 2022.1+,
  so it is guarded by #if UNITY_2022_1_OR_NEWER with a raster fallback
  (package-icon.png) on the 2021.3 floor.
- Embedded in the main window header (#header-left group) and the
  setup-wizard header.
- Ship package-icon.png inside the package (listing icon + 2021.3 fallback).

Installation polish:
- One-click uv install: UvInstaller builds the official installer command
  per platform; setup wizard runs it off-thread with a confirm dialog and
  re-checks dependencies.
- Claude CLI auto-discovery: consolidate the weaker resolver into
  ExecPath.ResolveClaude() and add the ~/.claude/local (migrate-installer)
  location both resolvers missed.
- Setup wizard clarity: success/next-step copy after configuring clients.

Tests: EditMode coverage for the geometry/color mapping, per-platform uv
command, and the Claude override contract.

Verified: compiles clean on 2021.3.45f2 (fallback path) and 6000.4.11f1
(Painter2D path).
2026-07-02 22:09:23 -07:00
Vladimir Kuznetsov 85b693958a fix: preserve multi-line message body in read_console 2026-07-01 23:24:01 +07:00
Shutong Wu 36fcefbcbf Merge remote-tracking branch 'upstream/beta' into revamp/brand-distribution-analytics
# Conflicts:
#	README.md
2026-06-30 11:33:02 -07:00
Shutong Wu 39eb562d9a Harden asset generation file paths 2026-06-29 09:15:59 -07:00
Shutong Wu 4b4b7bd4dd fix(asset-gen): address image_path code-review findings
Follow-up to 2efb7860, from a focused AI review of the image_path work:

- Validate image input synchronously in the handlers: reject unsupported
  extensions (LocalImage.ResolveExisting) and Tripo local-image up front, so bad
  input fails immediately instead of returning a fake "pending" that only faults
  on the next poll.
- Meshy: the "no task id" submit/refine error again includes the response body
  (the PostTask refactor had dropped it).
- Sketchfab archive allowlist: add .dds/.ktx (real GPU texture formats) so glTF
  textures in those formats aren't silently skipped on extract.
- fal: don't send image_size to the /edit (image->image) endpoint, which derives
  size from the source image; build the request_id fallback poll URL from the base
  model id rather than the /edit sub-path.
- Meshy two-phase preview+refine now runs under a 600s job timeout (was 300s) so a
  default textured text->3D has room for both tasks.

Deferred (perf-only, noted on the PR): the blanket AssetDatabase.Refresh
double-import and the synchronous main-thread base64 of large local images.

Verified: package compiles clean; EditMode 977/1023 pass, 0 failures via
tools/local_harness.py.

Claude-Session: https://claude.ai/code/session_015DAUrMR5UaSEzEn2wNPrEP
2026-06-28 23:38:02 -07:00
Shutong Wu 5ad4e5eb55 feat(asset-gen): Blender -> Unity handoff row in the Asset Gen tab
Informational row (not a keyed provider) with a best-effort "Blender app detected"
status and a pointer to the blender-to-unity workflow / import_model_file. BlenderMCP
runs in the AI client and isn't detectable from Unity, so this reports only the
locally installed Blender app.

- BlenderDetection helper: checks well-known install paths + PATH per platform; the
  pure DetectIn(candidates, exists) core is unit-tested with a fake predicate.
- McpAssetGenSection: AddBlenderHandoffRow() appended after the provider rows.
- README: note the tab's best-effort detection + that BlenderMCP lives in the AI client.

Verified: package compiles clean; EditMode 972/1018 pass, 0 failures (4 new
BlenderDetection tests) via tools/local_harness.py.

Claude-Session: https://claude.ai/code/session_015DAUrMR5UaSEzEn2wNPrEP
2026-06-28 23:19:59 -07:00
Shutong Wu 2efb786042 fix(asset-gen): security hardening + provider correctness + local image_path
Security review + code review of the asset-gen feature surfaced concrete issues;
this fixes them and adds regression tests (request-shaping layer, FakeHttpTransport).

Security
- SafeZipExtractor enforces an extension allowlist; ModelImportPipeline passes an
  inert model/texture allowlist so a provider archive can't drop a .cs/.dll under
  Assets/ and have the Editor compile/load it (code execution on import).
- AssetGenJobManager refuses non-http(s) download URLs before fetching
  (file:// SSRF / local-file read into the project).

Provider correctness
- Meshy image->3D polls /openapi/v1/image-to-3d/{id} (was the v2 text URL).
- Meshy text->3D honors texture=true via the preview->refine two-phase flow.
- OpenRouter image->image attaches the reference image (content image_url part).
- fal image->image uses the /edit endpoint + image_urls array; width/height
  forwarded as image_size.
- Sketchfab search forwards categories/count/cursor/downloadable; preview doc
  corrected (returns metadata, not a base64 thumbnail).
- Job import calls AssetDatabase.Refresh() before importing a freshly written file.

Local image input (image_path)
- New LocalImage helper; image_path is read and sent inline as a base64 data URI
  for Meshy / fal / OpenRouter. Tripo rejects local images with a clear error
  (needs a hosted image_url; its upload flow is not wired).

Cleanup (no behavior change)
- Shared AssetGenPaths + ProviderHttp helpers, HttpResult.Ok, MissingKeyMessage,
  cached glTFast probe, dead-field / per-frame-alloc removal, CLI _emit.

Docs: README + manual-verification updated (image_path support; transparency is
import-flag-only; width/height fal-only).

Verified: package compiles clean; Python 1306 passed / 3 skipped. Meshy refine,
fal /edit, and image_path data-URI paths are unit-tested at the request layer
only -- live smoke per provider (real keys) still pending.

Claude-Session: https://claude.ai/code/session_015DAUrMR5UaSEzEn2wNPrEP
2026-06-28 21:30:00 -07:00
Shutong Wu 86cd4ee35a feat(asset-gen): import_model_file C# handler (local model import)
Adds ImportModelFile tool (import_model_file command) that copies an
on-disk model file (FBX/OBJ/GLB/glTF/zip) under Assets/ and runs it
through the shared ModelImportPipeline. Returns {asset_path, asset_guid}.
Adds ImportModelFileHandlerTests covering missing-source, unsupported
extension, and a real OBJ import.

Claude-Session: https://claude.ai/code/session_01NoHk4f7N1vUFs7gu817ihm
2026-06-28 19:25:21 -07:00
Shutong Wu 71887b9adc fix(asset-gen): import_model handler async to avoid Unity main-thread deadlock
ImportModel.HandleCommand was synchronous and blocked on .GetAwaiter().GetResult() for the
Sketchfab search/preview UnityWebRequest calls. UnityWebRequest completes on the editor
loop, which a blocked main thread cannot pump — freezing the editor. Make HandleCommand
(and Search/Preview) async Task<object> (CommandRegistry detects the Task return type) and
add a HandleCommand_IsAsync regression guard. Surfaced via live in-editor testing.

Claude-Session: https://claude.ai/code/session_01Tjpb5gYgUe2AUJuRdXr7Lv
2026-06-28 19:25:21 -07:00
Shutong Wu 48863078e2 feat(asset-gen): remove Hunyuan; update to current SOTA model defaults
Drop Hunyuan (Tencent TC3-HMAC) — high effort and not testable by the maintainer:
delete HunyuanAdapter + TencentCloud3Signer (+tests), unwire from AssetGenProviders,
SecureKeyStoreConstants, the GUI provider list, and Python/CLI/README/manual-verify docs.

Refresh model defaults to current (2026) SOTA, verified against provider docs:
- fal: fal-ai/flux/dev -> fal-ai/flux-2 (FLUX.2 dev; cheaper and higher quality)
- Tripo: v2.5-20250123 -> v3.1-20260211 (current recommended model)
- Meshy: drop deprecated art_style (errors on Meshy-6); pin ai_model=meshy-6
- OpenRouter: gemini-2.5-flash-image-preview (delisted) -> gemini-2.5-flash-image

Fix two tests that asserted the now-implemented 'meshy' provider throws (repointed to the
now-removed 'hunyuan'). They were silently wrong: the headless --full runs never executed
EditMode tests (Unity -quit exits before -runTests; compile-only). A real -runTests run
now passes 62/62 AssetGen EditMode tests; Python suite 1299 passed.

Claude-Session: https://claude.ai/code/session_01Tjpb5gYgUe2AUJuRdXr7Lv
2026-06-28 19:25:21 -07:00
Shutong Wu 8513ff8e92 feat(asset-gen): Meshy + Sketchfab import + Hunyuan providers (Phase 6)
MeshyAdapter (text/image->3D, model_urls by format). SketchfabAdapter
(IMarketplaceProviderAdapter: search/preview/resolve-download) + ImportModel.cs
[McpForUnityTool import_model] + AssetGenJobManager.StartMarketplaceImport. Hunyuan:
TencentCloud3Signer (TC3-HMAC-SHA256, golden test vs python hmac oracle) + HunyuanAdapter
(multi-secret JSON, Submit/Query Job). SafeZipExtractor (path-traversal guarded) +
ModelImportPipeline .zip handling (extract -> find fbx/obj/glb -> import) for Hunyuan/
Sketchfab; ProviderPollResult.ResultExt override. Adapters wired in AssetGenProviders.
Tests on FakeHttpTransport incl zip guard + TC3 known-answer. Compiles on 2021.3.45f2.

Claude-Session: https://claude.ai/code/session_01Tjpb5gYgUe2AUJuRdXr7Lv
2026-06-28 19:25:21 -07:00
Shutong Wu c5e3d85260 feat(asset-gen): 2D image generation — fal.ai + OpenRouter (Phase 7)
Generalized AssetGenJobManager to submit/poll/import delegates so model & image share
one state machine; added an inline-bytes path (ProviderPollResult.InlineData) for sync
providers that return base64. FalAdapter (queue API: submit -> poll status -> fetch
result image url) and OpenRouterAdapter (chat/completions, base64 image inline).
ImageImportPipeline (TextureImporter: Sprite vs Default, alphaIsTransparency, sRGB vs
linear). GenerateImage.cs [McpForUnityTool generate_image]. AssetGenProviders.Image
wired. 7 EditMode tests (adapters via FakeHttpTransport, handler routing, OpenRouter
inline end-to-end). Key read once at submit, never persisted. Compiles on 2021.3.45f2.

Claude-Session: https://claude.ai/code/session_01Tjpb5gYgUe2AUJuRdXr7Lv
2026-06-28 19:25:21 -07:00
Shutong Wu ca5f961b5d feat(asset-gen): job manager + generate_model handler + model import (Phase 3)
AssetGenJobManager: in-memory submit->poll->download->import state machine driven by
EditorApplication.update (asset import does not recompile, so no domain reload mid-job);
SessionState snapshots keep status queryable across unrelated reloads. Key read once at
submit, held in memory only, never persisted/logged/serialized onto the job.
ModelImportPipeline: ModelImporter settings for FBX/OBJ, glTFast-gated GLB with an
actionable error, best-effort scale normalize. GenerateModel.cs [McpForUnityTool
RequiresPolling] with generate/status/cancel/list_providers using PendingResponse polling
contract. 13 EditMode tests (state machine via FakeHttpTransport, handler routing,
no-key-leak, import guards). Compiles clean on Unity 2021.3.45f2.

Claude-Session: https://claude.ai/code/session_01Tjpb5gYgUe2AUJuRdXr7Lv
2026-06-28 19:25:20 -07:00
Shutong Wu 9de38c6dc0 feat(asset-gen): provider abstraction + HTTP seam + Tripo adapter (Phase 2)
IHttpTransport seam (UnityWebRequestTransport prod, FakeHttpTransport for tests) so
adapters are unit-testable without network. IModelProviderAdapter/IImageProviderAdapter/
IMarketplaceProviderAdapter + DTOs (ModelGenRequest/ImageGenRequest/ProviderPollResult/
ProviderInfo). AssetGenProviders registry (Configured=SecureKeyStore.Has, key-free).
TripoAdapter submit (text/image_to_model, Bearer) + poll with defensive output-url
parsing (pbr_model/model, flat or nested). 8 EditMode tests on FakeHttpTransport.
Compiles clean on Unity 2021.3.45f2.

Claude-Session: https://claude.ai/code/session_01Tjpb5gYgUe2AUJuRdXr7Lv
2026-06-28 19:25:20 -07:00
Shutong Wu e728c35adb feat(asset-gen): SecureKeyStore — theft-resistant at-rest key storage (Phase 1)
OS secure store per platform: macOS Keychain (/usr/bin/security), Windows Credential
Manager (advapi32 P/Invoke), Linux secret-tool; AES-256-CBC+HMAC encrypt-then-MAC
fallback (CI-safe, master secret + machine id via PBKDF2, ciphertext under user
app-data, never in repo). Env override (MCPFORUNITY_<P>_API_KEY, read-only) layered on
top; SecretRedactor scrubs auth tokens. Keys never touch EditorPrefs/bridge/logs/git.
EditMode tests for fallback round-trip, encryption-at-rest, env override, redaction.
Compiles clean on Unity 2021.3.45f2 (floor).

Claude-Session: https://claude.ai/code/session_01Tjpb5gYgUe2AUJuRdXr7Lv
2026-06-28 19:25:20 -07:00
Shutong Wu d6604a571e feat(asset-gen): scaffold asset_gen tool group + non-secret prefs (Phase 0)
- Register 'asset_gen' tool group (off by default, parity with vfx/animation)
- EditorPrefKeys.AssetGen.* consts (non-secret config only; keys go to secure store)
- AssetGenPrefs helper (provider/format/output-root/normalize/enabled) + EditMode tests
- Python scaffold test asserts group present and disabled by default

Claude-Session: https://claude.ai/code/session_01Tjpb5gYgUe2AUJuRdXr7Lv
2026-06-28 19:25:20 -07:00
Shutong Wu 69eb51c661 chore(revamp): checkpoint WIP before testing feat/3d-asset-generation
README-zh.md edits + .meta/uv.lock churn on brand-distribution-analytics, committed
(not stashed) so the feature branch can be checked out in the main worktree for editor
testing. Untracked .agents/ and AGENTS.md intentionally left out.

Claude-Session: https://claude.ai/code/session_01Tjpb5gYgUe2AUJuRdXr7Lv
2026-06-28 11:11:59 -07:00
Shutong Wu 60f43e04b4 chore(tests): add missing EditMode test .meta sidecars; ignore test-project runtime artifacts
The 8 EditMode/Tools test scripts were tracked without their .meta files, so
every contributor's Unity regenerated them with random GUIDs (perpetual untracked
churn). Commit the metas so Unity reuses a stable GUID. Also ignore the MCP
bridge / UIToolkit runtime artifacts (Assets/UnityMCP, Assets/UI) the test
project emits when run.
2026-06-14 22:23:01 -07:00
Shutong Wu afa447ef38 fix(client): configure Kilo Code with its kilo.jsonc MCP format (#1120)
Kilo Code v7.0.33+ moved MCP config out of the VS Code extension's
globalStorage/mcp_settings.json to a CLI-style kilo.jsonc under ~/.config/kilo,
with a new schema (https://app.kilo.ai/config.json): an "mcp" container,
type:"remote" for HTTP servers (type:"local" for stdio), and an "enabled" flag.
Writing the legacy mcpServers / type:"http" / disabled config left the server
showing as stdio + disabled.

- KiloCodeConfigurator targets ~/.config/kilo/kilo.jsonc on every OS and declares
  the new format (mcp container, type:remote/local, enabled:true, $schema)
- Generalize the per-client HTTP "type" override: replace the UsesStreamableHttpType
  bool with McpClient.HttpTypeValue (Cline/Roo => "streamableHttp", Kilo => "remote",
  default "http"); add StdioTypeValue, ServerContainerKey and SchemaUrl fields
- ConfigJsonBuilder honors ServerContainerKey ("mcp") and writes a root $schema;
  JsonFileMcpConfigurator.CheckStatus/Unregister read/remove from the configured
  container so status detection and teardown work for Kilo
- Replace StreamableHttpTypeTests with ClientConfigFormatTests covering Kilo's
  remote/mcp/enabled format, Cline's streamableHttp, and generic http

Verified: 5 new tests + 13 existing config tests pass (EditMode, Unity 2021.3).
2026-06-14 21:32:04 -07:00
Shutong Wu d109211836 fix(server): headless local HTTP server launch with per-port logs and liveness polling
Convert the local MCP HTTP server launch from a visible-terminal model to a
headless background launch and make startup diagnosable and robust.

- Launch windowless via TerminalLauncher.CreateHeadlessProcessStartInfo;
  combined stdout/stderr redirected to a per-port log at
  Library/MCPForUnity/Logs/server-launch-{port}.log (truncated each launch)
- Replace the per-launch confirmation dialog with a one-time confirm gated on
  the new EditorPrefs key HttpServerLaunchConfirmed; the quiet auto-start path
  skips it and does not set the flag
- Prepend platform uv/uvx PATH entries so bare uvx/uv resolves under
  GUI-launched Unity's minimal non-login PATH (notably macOS)
- Replace the fixed ~30-attempt reachability waits with open-ended polling
  tied to the launched process's liveness (5-minute hard cap), emitting a
  tail-of-log failure report when the server dies — in both
  HttpAutoStartHandler and McpConnectionSection
- Simplify quit-time cleanup to a handshake-scoped StopManagedLocalHttpServer
  so headless servers are not left as invisible orphans
- Transport-aware UI button labels (Connect/Disconnect vs Start/End Session),
  a transient "Starting…" state, and clearer lifecycle logging

Tests: TerminalLauncherTests covers the headless start-info contract;
ServerManagementServiceCharacterizationTests covers the one-time-confirm gate,
quiet-path bypass, and per-port log redirection.
2026-06-14 16:44:08 -07:00
Shutong Wu eb9a4d6094 Merge remote-tracking branch 'upstream/beta' into chore/triage-quickwins-and-e2e 2026-06-14 11:49:30 -07:00
Shutong Wu 6e1a87e4bb fix(stdio): retry same port on bind race instead of silent fallback (#1173)
After a domain reload the previous TcpListener's OS socket is not always
released by the time Start() rebinds, so listener.Start() throws
AddressAlreadyInUse. The old catch silently switched to a new port via
DiscoverNewPort(), which stranded the Python client (pinned to the configured
port) on the orphan listener and returned busy/timeout indefinitely.

Instead, keep the configured port and fail the start attempt WITHOUT blocking;
the reload handler's async resume schedule and the editor-idle retry re-invoke
Start() on the same port within ~1s, by which point the OS has released it.
Only after the port stays busy past a short window (PortManager.
ShouldAbandonBusyPort, 3s) is it treated as a foreign occupant and the bridge
falls back to a new port — loudly (McpLog.Warn) instead of the previous
debug-only switch.

This keeps Stop()'s 500ms wait intact (no #787 backgrounded-stall regression)
and is platform-agnostic (the race also reproduces on macOS, per #1173).

Also classify ObjectDisposedException from a torn-down NetworkStream as a benign
client disconnect so it no longer spams the Console as a red error (#1187).
2026-06-13 15:57:01 -07:00
Shutong Wu 7b73f3e538 Merge remote-tracking branch 'upstream/beta' into chore/triage-quickwins-and-e2e 2026-06-07 22:22:51 +08:00
Shutong Wu 21953a09f2 fix: resolve unqualified generic names in unity_reflect; fix URP/ProBuilder EditMode test assumptions
UnityTypeResolver.FindCandidates treated a short name as ambiguous when an internal type shared it (richer assembly sets), so unqualified generics like List<T>/Dictionary<TKey,TValue> failed to resolve while fully-qualified names worked. Add DisambiguateByIdentity: de-dupe by FullName, then prefer a single public type, then a single core/BCL type, narrowing only to a unique survivor so genuine clashes still report ambiguous.

Fix EditMode tests that encoded built-in-pipeline/contract assumptions and only failed under URP + ProBuilder (the package test project uses the built-in pipeline and lacks ProBuilder, so these were never exercised):
- ManageGraphics negative tests read result["error"] (ErrorResponse has no "message" field); feature_add uses the handler's "type" param, not "feature_type"
- ManageMaterialProperties uses the pipeline-appropriate color property (_BaseColor on URP/HDRP)
- MCPToolParameterTests asserts pipeline-appropriate shader name and reads _Smoothness on URP/HDRP
- ManageProBuilder get_mesh_info test requests include:"faces"

Verified in TestbedMCP (URP 17.2 + ProBuilder 6.0.9): full EditMode suite 887 passed / 0 failed / 46 skipped.
2026-06-07 22:20:47 +08:00
cyanxwh 8ec7a64c86 Merge remote-tracking branch 'origin/beta' into codex/fusion-unsafe-type-serialization 2026-05-27 17:42:54 +08:00
Shutong Wu 67445dff3d fix(execute_code): route CodeDom references through a response file (#1144)
`CodeDomCompile` in `MCPForUnity/Editor/Tools/ExecuteCode.cs` pushed every
filtered assembly path into `CompilerParameters.ReferencedAssemblies`. Mono's
`CSharpCodeCompiler.BuildArgs` (verified at
mcs/class/System/Microsoft.CSharp/CSharpCodeCompiler.cs:388-392) turns each
reference into a literal `/r:"<absolute_path>"` flag and concatenates them
inline on the `mono.exe csc ...` command line.

Projects with ~100+ asmdefs (a perfectly normal large Unity project) overflow
Windows' 32 KB CreateProcess argument limit and `Process.Start` throws
Win32 `ERROR_FILENAME_EXCED_RANGE`, which Mono surfaces as:

    SystemException: Error running …mono.exe: The filename or extension is too long.

…exactly the failure reported in #1144 at ExecuteCode.cs:115 on Windows
10/11 + Unity 2022.3.62f2 + MCP for Unity 9.6.9-beta.8.

Fix: write all `/r:"…"` lines to a GUID-named temp response file and pass
`@"<path>"` via `CompilerParameters.CompilerOptions` (which `BuildArgs`
appends verbatim, confirmed at line 396 of the same Mono source). One short
argument regardless of reference count — the 32 KB ceiling is no longer
reachable.

Both legacy mcs and Roslyn csc accept `@responsefile`, so the change is
cross-platform: macOS/Linux Mono behaves identically, and the path doesn't
have a 32 KB limit there to begin with. Response file is cleaned up in a
`finally` block (best-effort; OS reaps temp on its own otherwise).

Tests: added two EditMode regression tests in `ExecuteCodeTests.cs` that
exercise the codedom backend end-to-end:
- `Execute_CodedomBackend_CompilesAndRuns` — basic compile + execute.
- `Execute_CodedomBackend_ResolvesUnityTypes` — verifies Unity references
  resolve through the response file.

Could not reproduce the exact 32 KB failure on macOS (Mono on POSIX doesn't
hit the limit), but the response-file path is the only path in the new code,
so the fix is mechanically equivalent for any reference-set size on any
platform.

Sources:
- Mono CSharpCodeCompiler.cs — BuildArgs converts ReferencedAssemblies to
  `/r:"…"` inline and appends CompilerOptions verbatim.
- C# compiler — ResponseFiles option (`@responsefile` syntax).
2026-05-26 16:43:18 +08:00