diff --git a/.gitignore b/.gitignore index 15aecc2c5..fa8902370 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ CLAUDE.md QWEN.md Client/.claude/ dev-ab-svr/debugconfig.json +/Client/.uloop/ diff --git a/Client/Packages/io.github.hatayama.uloopmcp/CHANGELOG.md b/Client/Packages/io.github.hatayama.uloopmcp/CHANGELOG.md new file mode 100644 index 000000000..f09933fc0 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/CHANGELOG.md @@ -0,0 +1,456 @@ +# Changelog + +## [2.2.0](https://github.com/hatayama/unity-cli-loop/compare/v2.1.10...v2.2.0) (2026-06-25) + + +### Features + +* Skills use the project-matched CLI by default ([#1404](https://github.com/hatayama/unity-cli-loop/issues/1404)) ([5bb9d49](https://github.com/hatayama/unity-cli-loop/commit/5bb9d49c59e4b6166a5bb926a7c6e7be690e230f)) + +## [2.1.10](https://github.com/hatayama/unity-cli-loop/compare/v2.1.9...v2.1.10) (2026-06-20) + + +### Bug Fixes + +* get-logs works with active Console search filters ([#1379](https://github.com/hatayama/unity-cli-loop/issues/1379)) ([941cb2f](https://github.com/hatayama/unity-cli-loop/commit/941cb2fb1888a943f4218d51325f33aecaf6e57f)) +* Unity 6.5 projects compile without metadata type conflicts ([#1377](https://github.com/hatayama/unity-cli-loop/issues/1377)) ([4a33119](https://github.com/hatayama/unity-cli-loop/commit/4a3311984c0c3ea11b27dff24b1987cb1a3de383)) + +## [2.1.9](https://github.com/hatayama/unity-cli-loop/compare/v2.1.8...v2.1.9) (2026-06-14) + + +### Bug Fixes + +* Respect UI raycast priority in simulate-mouse-ui ([#1318](https://github.com/hatayama/unity-cli-loop/issues/1318)) ([acb40a6](https://github.com/hatayama/unity-cli-loop/commit/acb40a68a0e4723148892e961c3aeb95ddd32f90)) + +## [2.1.8](https://github.com/hatayama/unity-cli-loop/compare/v2.1.7...v2.1.8) (2026-06-12) + + +### Bug Fixes + +* Avoid dependency conflicts from bundled metadata DLLs ([#1288](https://github.com/hatayama/unity-cli-loop/issues/1288)) ([636439d](https://github.com/hatayama/unity-cli-loop/commit/636439d4ca5170623e5ceeb017fe8ba12d9c4c60)) + +## [2.1.7](https://github.com/hatayama/unity-cli-loop/compare/v2.1.6...v2.1.7) (2026-06-12) + + +### Bug Fixes + +* Multiline dynamic code snippets work reliably on Windows ([#1319](https://github.com/hatayama/unity-cli-loop/issues/1319)) ([aff5ddd](https://github.com/hatayama/unity-cli-loop/commit/aff5dddf53f586c2da19a571784549eb4f4079d8)) + +## [2.1.6](https://github.com/hatayama/unity-cli-loop/compare/v2.1.5...v2.1.6) (2026-06-04) + + +### Bug Fixes + +* Input simulation overlays stay usable without shipping runtime DLLs ([#1285](https://github.com/hatayama/unity-cli-loop/issues/1285)) ([e271514](https://github.com/hatayama/unity-cli-loop/commit/e2715140af298ab3698a8d02efd1cd319d448670)) + +## [2.1.5](https://github.com/hatayama/unity-cli-loop/compare/v2.1.4...v2.1.5) (2026-06-01) + + +### Bug Fixes + +* Interrupted PlayMode tests no longer leave domain reload disabled ([#1255](https://github.com/hatayama/unity-cli-loop/issues/1255)) ([551bb38](https://github.com/hatayama/unity-cli-loop/commit/551bb382c21abc4785b1b23fbb52e0e48e134906)) + +## [2.1.4](https://github.com/hatayama/unity-cli-loop/compare/v2.1.3...v2.1.4) (2026-05-21) + + +### Bug Fixes + +* Make bundled skill guidance more concise ([#1178](https://github.com/hatayama/unity-cli-loop/issues/1178)) ([dbcc38e](https://github.com/hatayama/unity-cli-loop/commit/dbcc38e7d239ba3203685b0fc606d661e5d454b0)) + +## [2.1.3](https://github.com/hatayama/unity-cli-loop/compare/v2.1.2...v2.1.3) (2026-05-14) + + +### Bug Fixes + +* **build:** exclude Editor-only assemblies from player builds ([#1120](https://github.com/hatayama/unity-cli-loop/issues/1120)) ([768b49d](https://github.com/hatayama/unity-cli-loop/commit/768b49d447126cb04f98d1fe493c162fdcec6c28)) +* Unity 6.5 projects compile and keep fast code execution available ([#1119](https://github.com/hatayama/unity-cli-loop/issues/1119)) ([1a27478](https://github.com/hatayama/unity-cli-loop/commit/1a274786e8f53afe1a74bf7b66b894842310944f)) +* Unity no longer imports stale empty folders ([#1124](https://github.com/hatayama/unity-cli-loop/issues/1124)) ([45a57c3](https://github.com/hatayama/unity-cli-loop/commit/45a57c30c1803d93d4ef823e4937d1ed7c39d53d)) +* Unity stays responsive during domain reload recovery ([#1121](https://github.com/hatayama/unity-cli-loop/issues/1121)) ([5f5ab9d](https://github.com/hatayama/unity-cli-loop/commit/5f5ab9de8bf26972aaa9ef6a971324c907d01375)) + +## [2.1.2](https://github.com/hatayama/unity-cli-loop/compare/v2.1.1...v2.1.2) (2026-05-13) + + +### Bug Fixes + +* Global skill installs work without manual folder moves ([#1112](https://github.com/hatayama/unity-cli-loop/issues/1112)) ([498e428](https://github.com/hatayama/unity-cli-loop/commit/498e428958dd4d38ee83e5ca6d027eed9be9901d)) + +## [2.1.1](https://github.com/hatayama/unity-cli-loop/compare/v2.1.0...v2.1.1) (2026-05-07) + + +### Bug Fixes + +* allow installation without Unity Test Framework ([#1062](https://github.com/hatayama/unity-cli-loop/issues/1062)) ([45d58b9](https://github.com/hatayama/unity-cli-loop/commit/45d58b944da687f8505457d1a32361c188f119a5)) + +## [2.1.0](https://github.com/hatayama/unity-cli-loop/compare/v2.0.4...v2.1.0) (2026-04-29) + + +### Features + +* UI automation can see targets clearly and bypass blocked raycasts ([#996](https://github.com/hatayama/unity-cli-loop/issues/996)) ([fe43abe](https://github.com/hatayama/unity-cli-loop/commit/fe43abea6b8e2ce02cc540b3d553b8d07da1ddc0)) +* Unity Menu Commands Consolidated to Dynamic Code Execution ([#994](https://github.com/hatayama/unity-cli-loop/issues/994)) ([ea6c95b](https://github.com/hatayama/unity-cli-loop/commit/ea6c95bb0240872f41a8ab63761657fb7f3d4fc4)) +* Unity startup recovery does less blocking work ([#990](https://github.com/hatayama/unity-cli-loop/issues/990)) ([63ce4db](https://github.com/hatayama/unity-cli-loop/commit/63ce4db9ff4c6fa15d0a2a11f99c1b0387fc8a1b)) + + +### Bug Fixes + +* AI selects the right tool for selected GameObject inspection ([#1003](https://github.com/hatayama/unity-cli-loop/issues/1003)) ([1621d1a](https://github.com/hatayama/unity-cli-loop/commit/1621d1a2d508bf46878340e6c5df76731be872b9)) +* Dynamic code starts reliably on Windows PCs ([#1006](https://github.com/hatayama/unity-cli-loop/issues/1006)) ([68893da](https://github.com/hatayama/unity-cli-loop/commit/68893da351df60be7af3300f668d5ab111cd5be2)) +* Improve uloop skill guidance and hide internal CLI metadata ([#993](https://github.com/hatayama/unity-cli-loop/issues/993)) ([ec2ba7a](https://github.com/hatayama/unity-cli-loop/commit/ec2ba7a09a9484d4869b1d7aff5338dca9a272d6)) +* Make dynamic code compilation easier to maintain ([#1007](https://github.com/hatayama/unity-cli-loop/issues/1007)) ([b966dba](https://github.com/hatayama/unity-cli-loop/commit/b966dbac38bbdaaea87c0d1a62493d9c2b08e195)) +* Settings opens faster and tool toggles stay scoped ([#992](https://github.com/hatayama/unity-cli-loop/issues/992)) ([26745ad](https://github.com/hatayama/unity-cli-loop/commit/26745ade840f6cdd6322719fa83cc4de473b1b65)) +* Skill improvements for selected Hierarchy inspection ([#1002](https://github.com/hatayama/unity-cli-loop/issues/1002)) ([626f67c](https://github.com/hatayama/unity-cli-loop/commit/626f67c1841c74650185b37c3d94efe6c2ce4367)) +* Test runs avoid unsaved editor-change prompts ([#998](https://github.com/hatayama/unity-cli-loop/issues/998)) ([1db7c37](https://github.com/hatayama/unity-cli-loop/commit/1db7c376d8bf78a50e41abb4a4ead4b5bee40077)) +* uloop launch docs now explain Unity startup waiting ([#997](https://github.com/hatayama/unity-cli-loop/issues/997)) ([bef8f2f](https://github.com/hatayama/unity-cli-loop/commit/bef8f2fa8cfd69cea58b66c0060baa8801dd1d45)) + +## [2.0.4](https://github.com/hatayama/unity-cli-loop/compare/v2.0.3...v2.0.4) (2026-04-24) + + +### Bug Fixes + +* Dynamic code execution stays fast with system .NET 6 installed ([#988](https://github.com/hatayama/unity-cli-loop/issues/988)) ([81d4d02](https://github.com/hatayama/unity-cli-loop/commit/81d4d02ebf36f6bbbcea6a01dd9aba845426f13f)) + +## [2.0.3](https://github.com/hatayama/unity-cli-loop/compare/v2.0.2...v2.0.3) (2026-04-22) + + +### Bug Fixes + +* Setup now keeps third-party skills when switching folder layouts ([#980](https://github.com/hatayama/unity-cli-loop/issues/980)) ([adfc628](https://github.com/hatayama/unity-cli-loop/commit/adfc62852b8f79b46ca02c6ed6f0fc64c1d000db)) + +## [2.0.2](https://github.com/hatayama/unity-cli-loop/compare/v2.0.1...v2.0.2) (2026-04-22) + + +### Bug Fixes + +* Setup and Settings now clean up grouped skill folders and avoid rerunning skill checks after CLI updates ([#978](https://github.com/hatayama/unity-cli-loop/issues/978)) ([51fe1f5](https://github.com/hatayama/unity-cli-loop/commit/51fe1f5384a7596823fceaaa1c583e788a352196)) + +## [2.0.1](https://github.com/hatayama/unity-cli-loop/compare/v2.0.0...v2.0.1) (2026-04-22) + + +### Bug Fixes + +* improve `uloop launch` startup behavior by keeping launch progress visible and waiting for the correct Unity project without false warnings ([#965](https://github.com/hatayama/unity-cli-loop/issues/965), [#973](https://github.com/hatayama/unity-cli-loop/issues/973), [#974](https://github.com/hatayama/unity-cli-loop/issues/974), [#975](https://github.com/hatayama/unity-cli-loop/issues/975)) +* remove the skill grouping option in Setup and Settings so skills install directly under `skills/`, and stop showing first-run setup screens during updates ([#964](https://github.com/hatayama/unity-cli-loop/issues/964), [#968](https://github.com/hatayama/unity-cli-loop/issues/968), [#977](https://github.com/hatayama/unity-cli-loop/issues/977)) + +## [2.0.0](https://github.com/hatayama/unity-cli-loop/compare/v1.7.3...v2.0.0) (2026-04-20) + + +### Features + +* execute-dynamic-code now runs more than 6x faster ([#901](https://github.com/hatayama/unity-cli-loop/issues/901)) ([f48cdaa](https://github.com/hatayama/unity-cli-loop/commit/f48cdaaee5e3df0f4035a8281a6b7fe8511df04c)) +* Setup Wizard now handles first-time skill installation, grouping skills into a subfolder, target detection, status reporting, and startup behavior more clearly and reliably ([#927](https://github.com/hatayama/unity-cli-loop/issues/927), [#950](https://github.com/hatayama/unity-cli-loop/issues/950), [#951](https://github.com/hatayama/unity-cli-loop/issues/951), [#952](https://github.com/hatayama/unity-cli-loop/issues/952), [#953](https://github.com/hatayama/unity-cli-loop/issues/953), [#954](https://github.com/hatayama/unity-cli-loop/issues/954), [#963](https://github.com/hatayama/unity-cli-loop/issues/963), [#922](https://github.com/hatayama/unity-cli-loop/issues/922)) +* Windows users can follow PowerShell-specific PlayMode automation examples ([#947](https://github.com/hatayama/unity-cli-loop/issues/947)) ([59e50b4](https://github.com/hatayama/unity-cli-loop/commit/59e50b4e26f4e49c08761108737ae5a702a22788)) + + +### Bug Fixes + +* allow installation without the new Input System package ([#938](https://github.com/hatayama/unity-cli-loop/issues/938)) ([b08d899](https://github.com/hatayama/unity-cli-loop/commit/b08d899856d8c760aae7856b33f6a5bb3cc06d7f)) +* compile commands stay more reliable while the Unity server recovers ([#925](https://github.com/hatayama/unity-cli-loop/issues/925)) ([0f63ed5](https://github.com/hatayama/unity-cli-loop/commit/0f63ed5fdd9dc20ddc6b005dc1c7a4d9d1900090)) +* Dynamic code commands recover more cleanly after Unity restarts ([#944](https://github.com/hatayama/unity-cli-loop/issues/944)) ([bdbe286](https://github.com/hatayama/unity-cli-loop/commit/bdbe286d710bb2c7415b4f401d3b65e8c98f9e13)) +* invalid EditMode test requests now return a clear error during play mode ([#940](https://github.com/hatayama/unity-cli-loop/issues/940)) ([ed0c7ea](https://github.com/hatayama/unity-cli-loop/commit/ed0c7eaebfa5430fbdf95bfbd77f36e3fa500f2c)) +* make MCP deprecation easier to notice in the settings window ([#948](https://github.com/hatayama/unity-cli-loop/issues/948)) ([0ace70d](https://github.com/hatayama/unity-cli-loop/commit/0ace70d944fca67172ce24d3bca84b91a76c36a0)) +* make the setup and settings windows easier to use ([#932](https://github.com/hatayama/unity-cli-loop/issues/932)) ([6d61a7d](https://github.com/hatayama/unity-cli-loop/commit/6d61a7dccc418b2c8a41cab4132accf27780afd9)) +* prevent installation errors when the Unity Test Framework package is missing ([#939](https://github.com/hatayama/unity-cli-loop/issues/939)) ([958e4b5](https://github.com/hatayama/unity-cli-loop/commit/958e4b5050f64a47dff00575c670ebf5a9ad3196)) +* `uloop launch` now waits for Unity to finish starting ([#955](https://github.com/hatayama/unity-cli-loop/issues/955)) ([9514cc9](https://github.com/hatayama/unity-cli-loop/commit/9514cc90dc1427452ff3f998bb5b1e956901e86d)) + +## [1.7.3](https://github.com/hatayama/unity-cli-loop/compare/v1.7.2...v1.7.3) (2026-04-10) + + +### Bug Fixes + +* Avoid false "Unity not running" errors when the editor is still open ([#919](https://github.com/hatayama/unity-cli-loop/issues/919)) ([5376279](https://github.com/hatayama/unity-cli-loop/commit/53762791b70192efcf7fea2fcdf8dc8d91aa0c2d)) +* show the setup wizard when the package version changes ([#920](https://github.com/hatayama/unity-cli-loop/issues/920)) ([4f99cbc](https://github.com/hatayama/unity-cli-loop/commit/4f99cbc777d5eb1867815a3f22965205f0e01a69)) +* stabilize npm publishing and remove CLI bin warning ([#918](https://github.com/hatayama/unity-cli-loop/issues/918)) ([1b7a7f1](https://github.com/hatayama/unity-cli-loop/commit/1b7a7f12865767f5faf27529c5aa7a8502cb9a42)) + +## [1.7.2](https://github.com/hatayama/unity-cli-loop/compare/v1.7.1...v1.7.2) (2026-04-09) + + +### Bug Fixes + +* remove deprecation warning from uloop update ([#913](https://github.com/hatayama/unity-cli-loop/issues/913)) ([61ed144](https://github.com/hatayama/unity-cli-loop/commit/61ed144ffa1c0b8e2b6a10cb9eb587dada3fc1c6)) + +## [1.7.1](https://github.com/hatayama/unity-cli-loop/compare/v1.7.0...v1.7.1) (2026-04-09) + + +### Bug Fixes + +* avoid misleading Unity editor availability errors ([#911](https://github.com/hatayama/unity-cli-loop/issues/911)) ([4c2b7a6](https://github.com/hatayama/unity-cli-loop/commit/4c2b7a63bd4a31f0e84fa0964a28c268b33eb9c3)) +* require skills directories for auto-install detection ([#912](https://github.com/hatayama/unity-cli-loop/issues/912)) ([8832050](https://github.com/hatayama/unity-cli-loop/commit/8832050f7f4d9b94072ee33be2be0aeba90edf44)) +* unblock TypeScript server dependency updates by resolving npm audit findings ([#907](https://github.com/hatayama/unity-cli-loop/issues/907)) ([ffa3863](https://github.com/hatayama/unity-cli-loop/commit/ffa386365f31a8e908d2059e986218e1b113bd70)) + +## [1.7.0](https://github.com/hatayama/unity-cli-loop/compare/v1.6.4...v1.7.0) (2026-04-07) + + +### Features + +* Improve execute-dynamic-code performance by pre-resolving using directives ([#889](https://github.com/hatayama/unity-cli-loop/issues/889)) ([ba94c26](https://github.com/hatayama/unity-cli-loop/commit/ba94c268edaa948d53952ae76baa2c99b8ba1d85)) + + +### Bug Fixes + +* restore Unity settings after interrupted atomic writes ([#898](https://github.com/hatayama/unity-cli-loop/issues/898)) ([887800d](https://github.com/hatayama/unity-cli-loop/commit/887800dc18caa695ee3926827ac51dfe7a18183a)) + +## [1.6.4](https://github.com/hatayama/unity-cli-loop/compare/v1.6.3...v1.6.4) (2026-04-04) + + +### Bug Fixes + +* Deduplicate assembly references by name and add architecture overview to CLAUDE.md ([#887](https://github.com/hatayama/unity-cli-loop/issues/887)) ([cb0415e](https://github.com/hatayama/unity-cli-loop/commit/cb0415ed913be4fcf3cb8b90a3d54e3622cc9489)) + +## [1.6.3](https://github.com/hatayama/unity-cli-loop/compare/v1.6.2...v1.6.3) (2026-04-01) + + +### Bug Fixes + +* Detect and auto-recover from silent MCP server loop exit ([#871](https://github.com/hatayama/unity-cli-loop/issues/871)) ([d0430c8](https://github.com/hatayama/unity-cli-loop/commit/d0430c8e0ff1d71e37dabef9099c654565368f61)) +* Hide --port option from help, docs, and skill descriptions ([#873](https://github.com/hatayama/unity-cli-loop/issues/873)) ([eec4831](https://github.com/hatayama/unity-cli-loop/commit/eec48313e3c17d12de424d1eeab3b1a967b1c346)) +* Prevent CLI from connecting to wrong Unity instance via stale port ([#875](https://github.com/hatayama/unity-cli-loop/issues/875)) ([2e577c8](https://github.com/hatayama/unity-cli-loop/commit/2e577c834e23706bc692d2303d3db417c6ef6098)) + +## [1.6.2](https://github.com/hatayama/unity-cli-loop/compare/v1.6.1...v1.6.2) (2026-03-30) + + +### Bug Fixes + +* Prevent SetupWizardWindow from showing on package upgrade ([#861](https://github.com/hatayama/unity-cli-loop/issues/861)) ([358a32f](https://github.com/hatayama/unity-cli-loop/commit/358a32f8b8821d02e529be104762a3fc59de45a6)) + +## [1.6.1](https://github.com/hatayama/unity-cli-loop/compare/v1.6.0...v1.6.1) (2026-03-30) + + +### Bug Fixes + +* Prevent SetupWizardWindow from showing for existing users on upgrade ([#857](https://github.com/hatayama/unity-cli-loop/issues/857)) ([c9d4346](https://github.com/hatayama/unity-cli-loop/commit/c9d43466dfb52d074e609631eac8b9b1876565e8)) + +## [1.6.0](https://github.com/hatayama/unity-cli-loop/compare/v1.5.1...v1.6.0) (2026-03-29) + + +### Features + +* Add SetupWizardWindow for step-by-step onboarding ([#855](https://github.com/hatayama/unity-cli-loop/issues/855)) ([a723059](https://github.com/hatayama/unity-cli-loop/commit/a723059b0eebeeaf2b58663ba39293b9453afcca)) +* Extract server status/controls UI into standalone ServerEditorWindow ([#853](https://github.com/hatayama/unity-cli-loop/issues/853)) ([7934fba](https://github.com/hatayama/unity-cli-loop/commit/7934fba02f42e5e042f891b25a0322ea310889fd)) + + +### Bug Fixes + +* correct skill directory mapping to match CLI target-config ([#852](https://github.com/hatayama/unity-cli-loop/issues/852)) ([4818f52](https://github.com/hatayama/unity-cli-loop/commit/4818f528102def6b7fd4d3071bd88e6400f44a12)) + +## [1.5.1](https://github.com/hatayama/unity-cli-loop/compare/v1.5.0...v1.5.1) (2026-03-27) + + +### Bug Fixes + +* clarify execute-dynamic-code skill parameters ([#847](https://github.com/hatayama/unity-cli-loop/issues/847)) ([89bbff8](https://github.com/hatayama/unity-cli-loop/commit/89bbff8fbe9107a20981d239a19f7dccf5e5ae34)) + +## [1.5.0](https://github.com/hatayama/unity-cli-loop/compare/v1.4.0...v1.5.0) (2026-03-25) + + +### Features + +* Remove 3 redundant MCP tools and add Design Philosophy section ([#837](https://github.com/hatayama/unity-cli-loop/issues/837)) ([11412b6](https://github.com/hatayama/unity-cli-loop/commit/11412b6de429023b630a24cd3fbf685ae7274048)) + + +### Bug Fixes + +* replay progress bar always appearing at 100% ([#839](https://github.com/hatayama/unity-cli-loop/issues/839)) ([2880453](https://github.com/hatayama/unity-cli-loop/commit/2880453c747fd66092081f97982867368fdb8997)) + +## [1.4.0](https://github.com/hatayama/unity-cli-loop/compare/v1.3.0...v1.4.0) (2026-03-25) + + +### Features + +* Migrate dynamic code compilation from Roslyn to AssemblyBuilder with enhanced security ([#829](https://github.com/hatayama/unity-cli-loop/issues/829)) ([0ab2b87](https://github.com/hatayama/unity-cli-loop/commit/0ab2b8768ea286e3aecae8bd866ee72e2550ce48)) + + +### Bug Fixes + +* refine MCP tool settings UI ([#836](https://github.com/hatayama/unity-cli-loop/issues/836)) ([16c3c87](https://github.com/hatayama/unity-cli-loop/commit/16c3c87e3b34ca1c3750f0923ec2ecbbfe27d82f)) +* suppress idle overlay reactivation after mouse release during input replay ([#827](https://github.com/hatayama/unity-cli-loop/issues/827)) ([7133e9c](https://github.com/hatayama/unity-cli-loop/commit/7133e9c1a53473b25bcdf6f5712d5806c2147da6)) + +## [1.3.0](https://github.com/hatayama/unity-cli-loop/compare/v1.2.1...v1.3.0) (2026-03-23) + + +### Features + +* add mouse input visualization overlay with prefab workflow ([#806](https://github.com/hatayama/unity-cli-loop/issues/806)) ([c531459](https://github.com/hatayama/unity-cli-loop/commit/c531459ac9e40c5e72f8fa250a909ec166ac5b50)) +* Input recording/replay system ([#814](https://github.com/hatayama/unity-cli-loop/issues/814)) ([d7a7f58](https://github.com/hatayama/unity-cli-loop/commit/d7a7f58096020a76caa4fe04392f96558a91f6c0)) +* Replace runtime overlay generation with Prefab-based architecture and improve visualization ([#811](https://github.com/hatayama/unity-cli-loop/issues/811)) ([3ff8b09](https://github.com/hatayama/unity-cli-loop/commit/3ff8b090041360483b6637eae5157e4084121a9c)) + + +### Bug Fixes + +* clean up keyboard overlay preview badges ([#813](https://github.com/hatayama/unity-cli-loop/issues/813)) ([4caf06a](https://github.com/hatayama/unity-cli-loop/commit/4caf06a3b7860cd63f0285aae2d39054a3ea7269)) +* Remove HideFlags.DontSave from overlay canvas to fix PlayMode exit cleanup ([#812](https://github.com/hatayama/unity-cli-loop/issues/812)) ([3957641](https://github.com/hatayama/unity-cli-loop/commit/395764134e80c131b6d70a63ce7806c2fd578032)) + +## [1.2.1](https://github.com/hatayama/unity-cli-loop/compare/v1.2.0...v1.2.1) (2026-03-19) + + +### Bug Fixes + +* narrow EditorDialog preprocessor guard to UNITY_6000_3_OR_NEWER ([#804](https://github.com/hatayama/unity-cli-loop/issues/804)) ([65fce9e](https://github.com/hatayama/unity-cli-loop/commit/65fce9e0ce67fc40cbc818322c175de0aeefd889)) +* Remove redundant .meta files causing import warnings ([#802](https://github.com/hatayama/unity-cli-loop/issues/802)) ([7d662cd](https://github.com/hatayama/unity-cli-loop/commit/7d662cd9d5de78204339903ee8e0c7521cba40dc)) + +## [1.2.0](https://github.com/hatayama/unity-cli-loop/compare/v1.1.0...v1.2.0) (2026-03-18) + + +### Features + +* add simulate-mouse-input tool and split simulate-mouse into UI/Input System tools ([#799](https://github.com/hatayama/unity-cli-loop/issues/799)) ([a465640](https://github.com/hatayama/unity-cli-loop/commit/a465640ffa16a0d136956937e25dedaa61998b7a)) + + +### Bug Fixes + +* Use informational dialog icon for skill installation success on Unity 6+ ([#797](https://github.com/hatayama/unity-cli-loop/issues/797)) ([0326b38](https://github.com/hatayama/unity-cli-loop/commit/0326b38b1bd3e0089d17744a043b1fb38a7943fa)) + +## [1.1.0](https://github.com/hatayama/unity-cli-loop/compare/v1.0.2...v1.1.0) (2026-03-17) + + +### Features + +* keyboard simulation ([#783](https://github.com/hatayama/unity-cli-loop/issues/783)) ([8d632c4](https://github.com/hatayama/unity-cli-loop/commit/8d632c4fa55b03b72fdf4ce75feca11e3ffb1060)) + + +### Bug Fixes + +* Fix submenu misrender in SkillsTarget dropdown ([#787](https://github.com/hatayama/unity-cli-loop/issues/787)) ([28731df](https://github.com/hatayama/unity-cli-loop/commit/28731df6c48681d445e47e9bc38eb521b9fd7b23)) +* Windows CLI build support ([#794](https://github.com/hatayama/unity-cli-loop/issues/794)) ([ba7d382](https://github.com/hatayama/unity-cli-loop/commit/ba7d3823762b8bc11ebe57e64caf84d1b86a5faa)) + +## [1.0.2](https://github.com/hatayama/unity-cli-loop/compare/v1.0.1...v1.0.2) (2026-03-16) + + +### Bug Fixes + +* update repository URLs from uLoopMCP to unity-cli-loop ([#779](https://github.com/hatayama/unity-cli-loop/issues/779)) ([35e56a0](https://github.com/hatayama/unity-cli-loop/commit/35e56a0751f0ec0a57b025f9a539d5918328ba0b)) + +## [1.0.1](https://github.com/hatayama/unity-cli-loop/compare/v1.0.0...v1.0.1) (2026-03-16) + + +### Bug Fixes + +* Add missing .meta files for playmode skill references ([#773](https://github.com/hatayama/unity-cli-loop/issues/773)) ([19dcf5d](https://github.com/hatayama/unity-cli-loop/commit/19dcf5d0b3aa9eeef7de62c9f7003b3457beab7a)) + +## [0.70.1](https://github.com/hatayama/uLoopMCP/compare/v0.70.0...v0.70.1) (2026-03-15) + + +### Bug Fixes + +* Classify csc.rsp compiler diagnostics correctly in get-logs LogType filter ([#761](https://github.com/hatayama/uLoopMCP/issues/761)) ([#767](https://github.com/hatayama/uLoopMCP/issues/767)) ([7069c5f](https://github.com/hatayama/uLoopMCP/commit/7069c5f5a0b7d646060200373515d65fa8d24809)) + +## [0.70.0](https://github.com/hatayama/uLoopMCP/compare/v0.69.6...v0.70.0) (2026-03-15) + + +### Features + +* Add simulate-mouse tool for PlayMode UI interaction ([#759](https://github.com/hatayama/uLoopMCP/issues/759)) ([5679f34](https://github.com/hatayama/uLoopMCP/commit/5679f342932a67aecefbff804bd7265fdd6ec00d)) + +## [0.69.6](https://github.com/hatayama/uLoopMCP/compare/v0.69.5...v0.69.6) (2026-03-06) + + +### Bug Fixes + +* apply context: fork to uloop-execute-dynamic-code skill and add wiring references ([#743](https://github.com/hatayama/uLoopMCP/issues/743)) ([4ab452b](https://github.com/hatayama/uLoopMCP/commit/4ab452ba9efde76704b18d58d403761df2128941)) + +## [0.69.5](https://github.com/hatayama/uLoopMCP/compare/v0.69.4...v0.69.5) (2026-03-06) + + +### Bug Fixes + +* isolate Roslyn dependencies via shared registry ([#741](https://github.com/hatayama/uLoopMCP/issues/741)) ([c96a3a2](https://github.com/hatayama/uLoopMCP/commit/c96a3a2e06a268cc4daa5cb0eb9936b638abf5bb)) + +## [0.69.4](https://github.com/hatayama/uLoopMCP/compare/v0.69.3...v0.69.4) (2026-03-05) + + +### Bug Fixes + +* Standardize SKILL.md descriptions and reduce verbosity ([#733](https://github.com/hatayama/uLoopMCP/issues/733)) ([a743607](https://github.com/hatayama/uLoopMCP/commit/a743607a9bfa6ecdf5c31946f3776f662f4e0e15)) +* Sync lint-staged version in package.json with package-lock.json ([#735](https://github.com/hatayama/uLoopMCP/issues/735)) ([60eff43](https://github.com/hatayama/uLoopMCP/commit/60eff43cf8762f1075b3cb8e214d483aaf55af6e)) + +## [0.69.3](https://github.com/hatayama/uLoopMCP/compare/v0.69.2...v0.69.3) (2026-03-05) + + +### Bug Fixes + +* Handle Win32Exception in skills install process start ([#730](https://github.com/hatayama/uLoopMCP/issues/730)) ([cd7a4a3](https://github.com/hatayama/uLoopMCP/commit/cd7a4a3a19ab1d4830d1131074caeab4e85969e1)) +* Update hono and @hono/node-server to resolve high severity vulnerabilities ([#731](https://github.com/hatayama/uLoopMCP/issues/731)) ([135044a](https://github.com/hatayama/uLoopMCP/commit/135044ae761ef50144fb3a23c5118d6e53367ffe)) + +## [0.69.2](https://github.com/hatayama/uLoopMCP/compare/v0.69.1...v0.69.2) (2026-03-03) + + +### Bug Fixes + +* Prevent rename migration from shadowing legacy settings migration ([#725](https://github.com/hatayama/uLoopMCP/issues/725)) ([b03337b](https://github.com/hatayama/uLoopMCP/commit/b03337be8892cc807e8f6a47beab023c7d985bc9)) + +## [0.69.1](https://github.com/hatayama/uLoopMCP/compare/v0.69.0...v0.69.1) (2026-03-03) + + +### Bug Fixes + +* Add GetVersionTool to core package and improve CLI error handling ([#723](https://github.com/hatayama/uLoopMCP/issues/723)) ([21afcc8](https://github.com/hatayama/uLoopMCP/commit/21afcc899d7b8857737a7bda9480ccb447f9083c)) + +## [0.69.0](https://github.com/hatayama/uLoopMCP/compare/v0.68.3...v0.69.0) (2026-03-03) + + +### Features + +* Add delete button for MCP configuration ([#718](https://github.com/hatayama/uLoopMCP/issues/718)) ([3e4c4fb](https://github.com/hatayama/uLoopMCP/commit/3e4c4fb907b1fc446e19f3eba98bf6afc174a3ee)) + + +### Bug Fixes + +* Prevent CLI from sending commands to wrong Unity instance ([#719](https://github.com/hatayama/uLoopMCP/issues/719)) ([d0e47b3](https://github.com/hatayama/uLoopMCP/commit/d0e47b392677af36dfba740e9c7e80579877bad8)) +* Prevent toggle ChangeEvent from collapsing parent Foldout ([#721](https://github.com/hatayama/uLoopMCP/issues/721)) ([f6caf9b](https://github.com/hatayama/uLoopMCP/commit/f6caf9b7066f5f4587788c7e3b2d9e8ce0062aa3)) +* Rename settings.security.json to settings.permissions.json ([#713](https://github.com/hatayama/uLoopMCP/issues/713)) ([ae1a21a](https://github.com/hatayama/uLoopMCP/commit/ae1a21af2a575a94fd96476feb2fe05bfbb77f88)) + +## [0.68.3](https://github.com/hatayama/uLoopMCP/compare/v0.68.2...v0.68.3) (2026-03-02) + + +### Bug Fixes + +* Group help commands by category in uloop -h ([#708](https://github.com/hatayama/uLoopMCP/issues/708)) ([dbfe539](https://github.com/hatayama/uLoopMCP/commit/dbfe5394172a80f550304b95787d36d4046d0404)) + +## [0.68.2](https://github.com/hatayama/uLoopMCP/compare/v0.68.1...v0.68.2) (2026-03-01) + + +### Bug Fixes + +* Hide disabled tools from CLI help and shell completion output ([#705](https://github.com/hatayama/uLoopMCP/issues/705)) ([3f49750](https://github.com/hatayama/uLoopMCP/commit/3f4975078e324b56e4363c510c6081b7373afa63)) + +## [0.68.1](https://github.com/hatayama/uLoopMCP/compare/v0.68.0...v0.68.1) (2026-03-01) + + +### Bug Fixes + +* stabilize Tool Settings toggle interaction during UI refresh ([#702](https://github.com/hatayama/uLoopMCP/issues/702)) ([6b4ba25](https://github.com/hatayama/uLoopMCP/commit/6b4ba25d2d7821abecf0a990c7021a9c5de64518)) + +## [0.68.0](https://github.com/hatayama/uLoopMCP/compare/v0.67.5...v0.68.0) (2026-02-28) + + +### Features + +* Add per-tool enable/disable toggle for MCP tools ([#698](https://github.com/hatayama/uLoopMCP/issues/698)) ([5ca8b4c](https://github.com/hatayama/uLoopMCP/commit/5ca8b4ca99ed618148a93d74a9dd76fb6f0cff60)) +* Migrate security settings to project-scoped .uloop/settings.security.json ([#696](https://github.com/hatayama/uLoopMCP/issues/696)) ([222d46e](https://github.com/hatayama/uLoopMCP/commit/222d46e485b5b0006f8ed9e74e19e191938f2010)) + + +### Bug Fixes + +* Improve tool settings UI message to mention context window benefit ([#700](https://github.com/hatayama/uLoopMCP/issues/700)) ([040c8bd](https://github.com/hatayama/uLoopMCP/commit/040c8bd8687d8925219285f3b401c3d6900a5f3e)) +* Rename capture-window MCP tool to screenshot ([#701](https://github.com/hatayama/uLoopMCP/issues/701)) ([b178bc8](https://github.com/hatayama/uLoopMCP/commit/b178bc899a87c9ad4fce86adc27611c56a9f2811)) + +## [0.67.5](https://github.com/hatayama/uLoopMCP/compare/v0.67.4...v0.67.5) (2026-02-26) + + +### Bug Fixes + +* add meta ([#692](https://github.com/hatayama/uLoopMCP/issues/692)) ([34b29de](https://github.com/hatayama/uLoopMCP/commit/34b29de1ea497e8755f6c8cf711eebf7b1378f15)) + +## [0.67.4](https://github.com/hatayama/uLoopMCP/compare/v0.67.3...v0.67.4) (2026-02-26) + + +### Bug Fixes + +* correct SKILL.md parameter docs to match C# implementations ([#689](https://github.com/hatayama/uLoopMCP/issues/689)) ([e76ab29](https://github.com/hatayama/uLoopMCP/commit/e76ab29372c28f6513016d11b9fb7cb18e6f025f)) +* Fix incomplete property serialization in ComponentPropertySerializer ([#691](https://github.com/hatayama/uLoopMCP/issues/691)) ([2db6eb3](https://github.com/hatayama/uLoopMCP/commit/2db6eb3f63865ab6e56c41cabbbe3a37d183b164)) + +## [0.67.3](https://github.com/hatayama/uLoopMCP/compare/v0.67.2...v0.67.3) (2026-02-26) + + +### Bug Fixes + +* update READMEs with --project-path option and comprehensive CLI reference ([#687](https://github.com/hatayama/uLoopMCP/issues/687)) ([7829c9d](https://github.com/hatayama/uLoopMCP/commit/7829c9dc6e21bc117b353251b71eab7bbe95b942)) diff --git a/Client/Packages/io.github.hatayama.uloopmcp/CHANGELOG.md.meta b/Client/Packages/io.github.hatayama.uloopmcp/CHANGELOG.md.meta new file mode 100644 index 000000000..b52281ddf --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4ca30975116b542e8867604b2096e31b +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor.meta new file mode 100644 index 000000000..9c05ee08d --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 15701dfe5e2e04a058ba013c82937a72 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api.meta new file mode 100644 index 000000000..403463bbc --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dcc9f571c2a194f2785077de39c4f31c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/Errors.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/Errors.meta new file mode 100644 index 000000000..38b9352c2 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/Errors.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a66a2fc90232147fda7ad7d724b6ba38 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/Errors/ParameterValidationException.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/Errors/ParameterValidationException.cs new file mode 100644 index 000000000..422441dcc --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/Errors/ParameterValidationException.cs @@ -0,0 +1,22 @@ +using System; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Exception thrown when tool parameter JSON fails type validation. + /// + public class ParameterValidationException : Exception + { + public ParameterValidationException(string message) + : base(message) + { + } + + public ParameterValidationException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} + + diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/Errors/ParameterValidationException.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/Errors/ParameterValidationException.cs.meta new file mode 100644 index 000000000..77f0d2164 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/Errors/ParameterValidationException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7bc2a292b0560427bb23904f40141e37 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcProcessor.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcProcessor.cs new file mode 100644 index 000000000..ab365fcd4 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcProcessor.cs @@ -0,0 +1,447 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Current client context for JSON-RPC processing + /// + public class ClientExecutionContext + { + public string Endpoint { get; } + + public ClientExecutionContext(string endpoint) + { + Endpoint = endpoint; + } + } + + /// + /// Class specialized in handling JSON-RPC 2.0 processing + /// + /// Design document reference: Packages/src/Editor/ARCHITECTURE.md + /// + /// Related classes: + /// - UnityApiHandler: Executes Unity commands based on JSON-RPC requests + /// - McpBridgeServer: TCP server that receives JSON-RPC messages from TypeScript + /// - MainThreadSwitcher: Ensures Unity API calls run on the main thread + /// - JsonRpcRequest: Request model for JSON-RPC 2.0 protocol + /// - ClientExecutionContext: Thread-local context for tracking current client + /// + /// Processing flow: + /// 1. Receives JSON message from McpBridgeServer + /// 2. Parses and validates JSON-RPC 2.0 format + /// 3. Sets client context for the current thread + /// 4. Delegates to UnityApiHandler for command execution + /// 5. Formats response according to JSON-RPC 2.0 specification + /// 6. Returns JSON response to be sent back to client + /// + public static class JsonRpcProcessor + { + /// + /// Current client context for async operations + /// + private static readonly AsyncLocal _currentClientContext = new(); + + /// + /// Get current client context (ProcessID and Endpoint) + /// + public static ClientExecutionContext CurrentClientContext => _currentClientContext.Value; + + /// + /// Process JSON-RPC request and generate response with client context + /// + public static async Task ProcessRequest(string jsonRequest, string clientEndpoint) + { + var context = new ClientExecutionContext(clientEndpoint); + _currentClientContext.Value = context; + + try + { + return await ProcessRequest(jsonRequest); + } + finally + { + _currentClientContext.Value = null; + } + } + + /// + /// Process JSON-RPC request and generate response + /// + public static async Task ProcessRequest(string jsonRequest) + { + try + { + JsonRpcRequest request = ParseRequest(jsonRequest); + + if (request.IsNotification) + { + ProcessNotification(request); + return null; + } + + return await ProcessRpcRequest(request, jsonRequest); + } + catch (JsonReaderException ex) + { + return CreateErrorResponse(null, ex); + } + catch (Exception ex) + { + return CreateErrorResponse(null, ex); + } + } + + /// + /// Parse JSON-RPC request string into structured object + /// + private static JsonRpcRequest ParseRequest(string jsonRequest) + { + JObject request = JObject.Parse(jsonRequest); + return new JsonRpcRequest + { + Method = request["method"]?.ToString(), + Params = request["params"], + Id = request["id"]?.ToObject(), + UloopMetadata = ParseUloopMetadata(request["x-uloop"]) + }; + } + + private static JsonRpcRequestUloopMetadata ParseUloopMetadata(JToken metadataToken) + { + if (metadataToken == null || metadataToken.Type == JTokenType.Null) + { + return null; + } + + return new JsonRpcRequestUloopMetadata + { + ExpectedProjectRoot = metadataToken["expectedProjectRoot"]?.ToString(), + ExpectedServerSessionId = metadataToken["expectedServerSessionId"]?.ToString() + }; + } + + /// + /// Process notification (fire-and-forget) + /// + /// Note: Notifications are one-way messages without response. + /// Currently handles: + /// - focus-window: Brings Unity Editor window to foreground (used after request timeout) + /// + private static void ProcessNotification(JsonRpcRequest request) + { + if (string.IsNullOrEmpty(request.Method)) + { + return; + } + + switch (request.Method) + { + case "focus-window": + HandleFocusWindowNotification(); + break; + default: + // Unknown notification - ignore silently + break; + } + } + + /// + /// Handle focus-window notification. + /// Note: focus-window is now handled at OS level by TypeScript MCP server using launch-unity package. + /// This notification handler remains for protocol compatibility but does nothing. + /// + private static void HandleFocusWindowNotification() + { + // Intentionally empty - focus-window is handled by TypeScript MCP server at OS level + } + + /// + /// Process RPC request and return response JSON + /// + private static async Task ProcessRpcRequest(JsonRpcRequest request, string originalJson) + { + Stopwatch requestStopwatch = Stopwatch.StartNew(); + try + { + ValidateClientIdentityIfNeeded(request); + + Stopwatch mainThreadWaitStopwatch = Stopwatch.StartNew(); + await MainThreadSwitcher.SwitchToMainThread(); + mainThreadWaitStopwatch.Stop(); + + Stopwatch toolStopwatch = Stopwatch.StartNew(); + BaseToolResponse result = await ExecuteMethod(request.Method, request.Params); + toolStopwatch.Stop(); + + AppendExecuteDynamicCodeTimingsIfSupported( + result, + mainThreadWaitStopwatch.Elapsed.TotalMilliseconds, + toolStopwatch.Elapsed.TotalMilliseconds, + requestStopwatch.Elapsed.TotalMilliseconds); + string response = CreateSuccessResponse(request.Id, result); + return response; + } + catch (JsonSerializationException ex) + { + UnityEngine.Debug.LogError($"[JsonRpcProcessor] JSON serialization error: {ex.Message}\nStack trace: {ex.StackTrace}"); + return CreateErrorResponse(request.Id, ex); + } + catch (ParameterValidationException ex) + { + LogParameterValidationException(ex); + return CreateErrorResponse(request.Id, ex); + } + catch (Exception ex) + { + UnityEngine.Debug.LogError($"[JsonRpcProcessor] Error: {ex.Message}\nStack trace: {ex.StackTrace}"); + return CreateErrorResponse(request.Id, ex); + } + } + + private static void ValidateClientIdentityIfNeeded(JsonRpcRequest request) + { + JsonRpcRequestIdentityValidator.Validate( + request?.UloopMetadata, + McpEditorSettings.GetProjectRootPath(), + McpEditorSettings.GetServerSessionId()); + } + + private static void LogParameterValidationException(ParameterValidationException exception) + { + if (JsonRpcRequestIdentityValidator.IsExpectedRetryableFailure(exception)) + { +#if ULOOPMCP_DEBUG + UnityEngine.Debug.LogWarning( + $"[JsonRpcProcessor] Expected retryable parameter validation event: {exception.Message}"); +#endif + return; + } + + UnityEngine.Debug.LogError( + $"[JsonRpcProcessor] Parameter validation error: {exception.Message}\nStack trace: {exception.StackTrace}"); + } + + private static void AppendExecuteDynamicCodeTimingsIfSupported( + BaseToolResponse result, + double mainThreadWaitMilliseconds, + double toolTotalMilliseconds, + double requestTotalMilliseconds) + { + if (result is not ExecuteDynamicCodeResponse executeDynamicCodeResponse) + { + return; + } + + ExecuteDynamicCodeResponseTimingAugmenter.AppendTimingEntries( + executeDynamicCodeResponse, + mainThreadWaitMilliseconds, + toolTotalMilliseconds, + requestTotalMilliseconds); + } + + /// + /// Create JSON-RPC success response + /// + /// Request ID - must be same type as received (string/number/null per JSON-RPC spec) + /// Command execution result + private static string CreateSuccessResponse(object id, BaseToolResponse result) + { + JsonSerializerSettings settings = new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + MaxDepth = McpServerConfig.DEFAULT_JSON_MAX_DEPTH + }; + + try + { + JsonRpcSuccessResponse response = new JsonRpcSuccessResponse( + McpServerConfig.JSONRPC_VERSION, + id, + result + ); + return JsonConvert.SerializeObject(response, Formatting.None, settings); + } + catch (Exception) + { + // Return safe fallback response for any serialization errors + object fallbackResult = new + { + error = "Serialization failed - returning safe fallback", + commandType = result != null ? result.GetType().Name : "unknown" + }; + + JsonRpcSuccessResponse fallbackResponse = new JsonRpcSuccessResponse( + McpServerConfig.JSONRPC_VERSION, + id, + fallbackResult + ); + return JsonConvert.SerializeObject(fallbackResponse, Formatting.None); + } + } + + /// + /// Create JSON-RPC error response + /// + /// Request ID - must be same type as received (string/number/null per JSON-RPC spec) + /// Exception to convert to error response + private static string CreateErrorResponse(object id, Exception ex) + { + // Centralize exception -> user-facing message via UserFriendlyErrorConverter + UserFriendlyErrorConverter handler = new UserFriendlyErrorConverter(); + UserFriendlyErrorDto exceptionResponse = handler.ProcessException(ex); + + // Map UserFriendlyErrorDto to JsonRpcError + string errorMessage = exceptionResponse.FriendlyMessage; + + JsonRpcErrorData errorData; + if (ex is McpSecurityException secEx) + { + errorData = new SecurityBlockedErrorData(secEx.ToolName, secEx.SecurityReason, exceptionResponse.Explanation ?? ex.Message); + } + else + { + errorData = new InternalErrorData(exceptionResponse.Explanation ?? ex.Message); + } + + JsonRpcErrorResponse errorResponse = new JsonRpcErrorResponse( + McpServerConfig.JSONRPC_VERSION, + id, + new JsonRpcError(McpServerConfig.INTERNAL_ERROR_CODE, errorMessage, errorData) + ); + + JsonSerializerSettings settings = new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + MaxDepth = McpServerConfig.DEFAULT_JSON_MAX_DEPTH + }; + + return JsonConvert.SerializeObject(errorResponse, Formatting.None, settings); + } + + /// + /// Execute appropriate handler according to method name + /// Use new command-based structure + /// + private static async Task ExecuteMethod(string method, JToken paramsToken) + { + return await UnityApiHandler.ExecuteCommandAsync(method, paramsToken); + } + } + + /// + /// Constants for JSON-RPC error types + /// + public static class JsonRpcErrorTypes + { + public const string SecurityBlocked = "security_blocked"; + public const string InternalError = "internal_error"; + } + + /// + /// Base class for JSON-RPC error data + /// + public abstract class JsonRpcErrorData + { + public abstract string type { get; } + + public string message { get; protected set; } + + protected JsonRpcErrorData(string message) + { + this.message = message; + } + } + + /// + /// Error data for security blocked commands + /// + public class SecurityBlockedErrorData : JsonRpcErrorData + { + public override string type => JsonRpcErrorTypes.SecurityBlocked; + + public string command { get; } + + public string reason { get; } + + public SecurityBlockedErrorData(string command, string reason, string message) : base(message) + { + this.command = command; + this.reason = reason; + } + } + + /// + /// Error data for internal errors + /// + public class InternalErrorData : JsonRpcErrorData + { + public override string type => JsonRpcErrorTypes.InternalError; + + public InternalErrorData(string message) : base(message) + { + } + } + + /// + /// JSON-RPC error object + /// + public class JsonRpcError + { + public int code { get; } + + public string message { get; } + + public JsonRpcErrorData data { get; } + + public JsonRpcError(int code, string message, JsonRpcErrorData data) + { + this.code = code; + this.message = message; + this.data = data; + } + } + + /// + /// JSON-RPC success response + /// + public class JsonRpcSuccessResponse + { + public string jsonrpc { get; } + + public object id { get; } + + public object result { get; } + + public JsonRpcSuccessResponse(string jsonRpc, object id, object result) + { + this.jsonrpc = jsonRpc; + this.id = id; + this.result = result; + } + } + + /// + /// JSON-RPC error response + /// + public class JsonRpcErrorResponse + { + public string jsonrpc { get; } + + public object id { get; } + + public JsonRpcError error { get; } + + public JsonRpcErrorResponse(string jsonRpc, object id, JsonRpcError error) + { + this.jsonrpc = jsonRpc; + this.id = id; + this.error = error; + } + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcProcessor.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcProcessor.cs.meta new file mode 100644 index 000000000..614042b18 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcProcessor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d3c8feac02c96413ba4dde4ba5990ef8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcRequest.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcRequest.cs new file mode 100644 index 000000000..5e498d9a1 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcRequest.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json.Linq; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Represents a parsed JSON-RPC request + /// + internal class JsonRpcRequest + { + public string Method { get; set; } + /// + /// JSON-RPC 2.0 spec allows params to be object, array, or null. + /// We use JToken to accept any format, then convert to strongly-typed + /// schema classes (e.g. PingSchema) in AbstractUnityCommand.ConvertToSchema. + /// This provides flexibility at the protocol layer and type safety at the command layer. + /// + public JToken Params { get; set; } + /// + /// JSON-RPC 2.0 spec requires id type to match the request. + /// Must be string, number, or null - same as received. + /// + public object Id { get; set; } + public JsonRpcRequestUloopMetadata UloopMetadata { get; set; } + /// + /// JSON-RPC 2.0 notification flag. True when id is null/missing. + /// Notifications are fire-and-forget messages that don't expect a response. + /// Regular requests (with id) expect a response, notifications do not. + /// + public bool IsNotification => Id == null; + } + + internal class JsonRpcRequestUloopMetadata + { + public string ExpectedProjectRoot { get; set; } + public string ExpectedServerSessionId { get; set; } + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcRequest.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcRequest.cs.meta new file mode 100644 index 000000000..38f627693 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b3307d57ac9144e00842cc0e4a068b06 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcRequestIdentityValidator.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcRequestIdentityValidator.cs new file mode 100644 index 000000000..738451796 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcRequestIdentityValidator.cs @@ -0,0 +1,63 @@ +using System; + +namespace io.github.hatayama.uLoopMCP +{ + internal static class JsonRpcRequestIdentityValidator + { + internal const string ServerSessionChangedMessage = "Unity CLI Loop server session changed. Retry the command."; + + public static void Validate( + JsonRpcRequestUloopMetadata metadata, + string actualProjectRoot, + string actualServerSessionId) + { + if (metadata == null) + { + return; + } + + if (string.IsNullOrWhiteSpace(metadata.ExpectedProjectRoot)) + { + throw new ParameterValidationException("Invalid x-uloop metadata: expectedProjectRoot is required."); + } + + if (string.IsNullOrWhiteSpace(metadata.ExpectedServerSessionId)) + { + throw new ParameterValidationException("Invalid x-uloop metadata: expectedServerSessionId is required."); + } + + if (string.IsNullOrWhiteSpace(actualProjectRoot)) + { + throw new ParameterValidationException("Fast project validation is unavailable. Restart Unity CLI Loop and retry."); + } + + if (string.IsNullOrWhiteSpace(actualServerSessionId)) + { + throw new ParameterValidationException(ServerSessionChangedMessage); + } + + if (!string.Equals(metadata.ExpectedProjectRoot, actualProjectRoot, StringComparison.Ordinal)) + { + throw new ParameterValidationException("Connected Unity instance belongs to a different project."); + } + + if (!string.Equals(metadata.ExpectedServerSessionId, actualServerSessionId, StringComparison.Ordinal)) + { + throw new ParameterValidationException(ServerSessionChangedMessage); + } + } + + internal static bool IsExpectedRetryableFailure(ParameterValidationException exception) + { + if (exception == null) + { + return false; + } + + return string.Equals( + exception.Message, + ServerSessionChangedMessage, + StringComparison.Ordinal); + } + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcRequestIdentityValidator.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcRequestIdentityValidator.cs.meta new file mode 100644 index 000000000..cb65165ee --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/JsonRpcRequestIdentityValidator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 105ea7a6425e04852b1f41d84dabc8ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools.meta new file mode 100644 index 000000000..3cf3df30c --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 66830065e21e249c0876bf2ef4f0776b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole.meta new file mode 100644 index 000000000..d905718a4 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1511443bffd3044d8a38e727e688c81f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleResponse.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleResponse.cs new file mode 100644 index 000000000..0aeebcbaa --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleResponse.cs @@ -0,0 +1,102 @@ +using System; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Response schema for ClearConsole command + /// Provides type-safe response structure for console clearing operation + /// + public class ClearConsoleResponse : BaseToolResponse + { + /// + /// Whether the console clear operation was successful + /// + public bool Success { get; set; } + + /// + /// Number of logs that were cleared from the console + /// + public int ClearedLogCount { get; set; } + + /// + /// Breakdown of cleared logs by type + /// + public ClearedLogCounts ClearedCounts { get; set; } + + /// + /// Message describing the clear operation result + /// + public string Message { get; set; } + + /// + /// Error message if the operation failed + /// + public string ErrorMessage { get; set; } + + /// + /// Create a new ClearConsoleResponse for successful operation + /// + public ClearConsoleResponse(bool success, int clearedLogCount, ClearedLogCounts clearedCounts, string message) + { + Success = success; + ClearedLogCount = clearedLogCount; + ClearedCounts = clearedCounts ?? new ClearedLogCounts(); + Message = message ?? string.Empty; + ErrorMessage = string.Empty; + } + + /// + /// Create a new ClearConsoleResponse for failed operation + /// + public ClearConsoleResponse(string errorMessage) + { + Success = false; + ClearedLogCount = 0; + ClearedCounts = new ClearedLogCounts(); + Message = string.Empty; + ErrorMessage = errorMessage ?? string.Empty; + } + + /// + /// Parameterless constructor for JSON deserialization + /// + public ClearConsoleResponse() + { + ClearedCounts = new ClearedLogCounts(); + Message = string.Empty; + ErrorMessage = string.Empty; + } + } + + /// + /// Breakdown of cleared logs by type + /// + public class ClearedLogCounts + { + /// + /// Number of error logs that were cleared + /// + public int ErrorCount { get; set; } + + /// + /// Number of warning logs that were cleared + /// + public int WarningCount { get; set; } + + /// + /// Number of info logs that were cleared + /// + public int LogCount { get; set; } + + public ClearedLogCounts() + { + } + + public ClearedLogCounts(int errorCount, int warningCount, int logCount) + { + ErrorCount = errorCount; + WarningCount = warningCount; + LogCount = logCount; + } + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleResponse.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleResponse.cs.meta new file mode 100644 index 000000000..fd364ec95 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dbbf8d96e00c54171b03c9c0d42822dc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleSchema.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleSchema.cs new file mode 100644 index 000000000..33c882e70 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleSchema.cs @@ -0,0 +1,21 @@ +using System.ComponentModel; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Schema for ClearConsole command parameters + /// Provides type-safe parameter access for clearing Unity Console + /// Related classes: + /// - ConsoleUtility: Service layer for console operations + /// - ClearConsoleResponse: Type-safe response structure + /// - ClearConsoleCommand: Command implementation + /// + public class ClearConsoleSchema : BaseToolSchema + { + /// + /// Whether to add a confirmation log message after clearing + /// + [Description("Whether to add a confirmation log message after clearing")] + public bool AddConfirmationMessage { get; set; } = false; + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleSchema.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleSchema.cs.meta new file mode 100644 index 000000000..7dc3e9cdc --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleSchema.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 94a230c702f0e4d75b03dc706886b935 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleTool.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleTool.cs new file mode 100644 index 000000000..d9acd1b6b --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleTool.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// ClearConsole tool handler - Type-safe implementation using Schema and Response + /// Clears Unity console logs for clean development workflow + /// Related classes: ClearConsoleUseCase, ConsoleUtility, ClearConsoleSchema, ClearConsoleResponse + /// + [McpTool(Description = "Clear Unity console logs")] + public class ClearConsoleTool : AbstractUnityTool + { + public override string ToolName => "clear-console"; + + /// + /// Execute console clear tool + /// + /// Type-safe parameters + /// Cancellation token for timeout control + /// Clear operation result + protected override async Task ExecuteAsync(ClearConsoleSchema parameters, CancellationToken cancellationToken) + { + // Create and execute ClearConsoleUseCase instance + ClearConsoleUseCase useCase = new(); + return await useCase.ExecuteAsync(parameters, cancellationToken); + } + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleTool.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleTool.cs.meta new file mode 100644 index 000000000..c469645d4 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/ClearConsoleTool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b72387c853bad490fb5cb20f551f0781 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/Skill.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/Skill.meta new file mode 100644 index 000000000..7742ac11e --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/Skill.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fc3fddf04059a4303be8a7f27aa53052 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/Skill/SKILL.md b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/Skill/SKILL.md new file mode 100644 index 000000000..40891bc38 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/Skill/SKILL.md @@ -0,0 +1,48 @@ +--- +name: uloop-clear-console +description: "Clear Unity Console entries. Use before compile, tests, or debugging when stale logs would hide the current result." +--- + +# uloop clear-console + +Clear Unity console logs. + +## Usage + +```bash +uloop clear-console [--add-confirmation-message] +``` + +## Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--add-confirmation-message` | boolean | `false` | Add confirmation message after clearing | + +## Global Options + +| Option | Description | +|--------|-------------| +| `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | + +## Examples + +```bash +# Clear console +uloop clear-console + +# Clear with confirmation +uloop clear-console --add-confirmation-message +``` + +## Output + +Returns JSON with: +- `Success` (boolean): Whether the clear operation succeeded +- `ClearedLogCount` (number): Total number of log entries that were cleared +- `ClearedCounts` (object): Breakdown by log type + - `ErrorCount` (number): Errors cleared + - `WarningCount` (number): Warnings cleared + - `LogCount` (number): Info logs cleared +- `Message` (string): Description of the result; carries the failure summary when the operation fails (e.g. `"Failed to clear console: ..."`) +- `ErrorMessage` (string): Currently always empty for this tool — read `Message` for failure details diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/Skill/SKILL.md.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/Skill/SKILL.md.meta new file mode 100644 index 000000000..b83ab9524 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ClearConsole/Skill/SKILL.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9f1d3cbc9cba645b398b9f4bbe9537c4 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common.meta new file mode 100644 index 000000000..4348234fe --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 90328140d69f84f1e925caf7f710c0eb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/ComponentInfo.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/ComponentInfo.cs new file mode 100644 index 000000000..6daef1d75 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/ComponentInfo.cs @@ -0,0 +1,22 @@ +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Represents information about a Unity component + /// + public class ComponentInfo + { + public string type { get; set; } + public string fullTypeName { get; set; } + public ComponentPropertyInfo[] properties { get; set; } + } + + /// + /// Represents a property of a Unity component + /// + public class ComponentPropertyInfo + { + public string name { get; set; } + public string type { get; set; } + public object value { get; set; } + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/ComponentInfo.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/ComponentInfo.cs.meta new file mode 100644 index 000000000..b126f7bb4 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/ComponentInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b8f54071c76614440b5775885e181d95 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem.meta new file mode 100644 index 000000000..dfc13732c --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a8a3c04e7ca654807956364da02c4e85 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem/InputSystemUpdateHelper.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem/InputSystemUpdateHelper.cs new file mode 100644 index 000000000..87ec69ecc --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem/InputSystemUpdateHelper.cs @@ -0,0 +1,208 @@ +#nullable enable +#if ULOOPMCP_HAS_INPUT_SYSTEM +using System; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.LowLevel; + +namespace io.github.hatayama.uLoopMCP +{ + // Shared helper for applying Input System state changes at the correct update phase. + // Both keyboard and mouse simulation need frame-precise timing so that + // wasPressedThisFrame / wasReleasedThisFrame detect the injected state. + internal static class InputSystemUpdateHelper + { + public static Task ApplyOnNextConfiguredUpdate(Action apply, CancellationToken ct) + { + InputUpdateType targetUpdateType = InputUpdateTypeResolver.Resolve(); + if (InputUpdateTypeResolver.RequiresExplicitUpdate()) + { + return ApplyOnExplicitUpdate(apply, targetUpdateType, ct); + } + + TaskCompletionSource tcs = new TaskCompletionSource(); + CancellationTokenRegistration registration = default; + Action? callback = null; + + callback = () => + { + InputUpdateType currentUpdateType = InputState.currentUpdateType; + if (!InputUpdateTypeResolver.IsMatch(currentUpdateType, targetUpdateType)) + { + return; + } + + Debug.Assert(callback != null, "callback must be assigned before subscription"); + InputSystem.onBeforeUpdate -= callback; + registration.Dispose(); + apply(); + tcs.TrySetResult(true); + }; + + InputSystem.onBeforeUpdate += callback; + if (ct.CanBeCanceled) + { + registration = ct.Register(() => + { + Debug.Assert(callback != null, "callback must be assigned before cancellation"); + InputSystem.onBeforeUpdate -= callback; + tcs.TrySetCanceled(ct); + }); + } + + return tcs.Task; + } + + public static int GetMinimumObservationFrameCount() + { + if (!InputUpdateTypeResolver.RequiresExplicitUpdate()) + { + return 1; + } + + InputUpdateType targetUpdateType = InputUpdateTypeResolver.Resolve(); + if (targetUpdateType != InputUpdateType.Manual) + { + return 2; + } + + // Manual-mode projects often call InputSystem.Update from their own Update loop, + // so zero-duration taps need one extra frame to remain visible to gameplay code. + return 3; + } + + public static async Task WaitForObservationFrames(CancellationToken ct) + { + await WaitForRuntimeFrames(GetMinimumObservationFrameCount(), ct); + } + + public static async Task WaitForPressLifetime(float duration, CancellationToken ct) + { + int minimumObservationFrames = GetMinimumObservationFrameCount(); + int startFrameCount = Time.frameCount; + float startTime = Time.realtimeSinceStartup; + float elapsed = 0f; + int observedFrames = 0; + + while (observedFrames < minimumObservationFrames || elapsed < duration) + { + await EditorDelay.DelayFrame(1, ct); + observedFrames = Time.frameCount - startFrameCount; + elapsed = Time.realtimeSinceStartup - startTime; + } + } + + public static void RunExplicitUpdate(InputUpdateType targetUpdateType) + { + InputSettings? settings = InputSystem.settings; + if (settings == null) + { + InputSystem.Update(); + return; + } + + InputSettings.UpdateMode originalUpdateMode = settings.updateMode; + InputSettings.UpdateMode targetUpdateMode = GetExplicitUpdateMode(targetUpdateType, originalUpdateMode); + if (targetUpdateMode == originalUpdateMode) + { + InputSystem.Update(); + return; + } + + settings.updateMode = targetUpdateMode; + try + { + InputSystem.Update(); + } + finally + { + settings.updateMode = originalUpdateMode; + } + } + + private static Task ApplyOnExplicitUpdate(Action apply, InputUpdateType targetUpdateType, CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + + TaskCompletionSource tcs = new TaskCompletionSource(); + CancellationTokenRegistration registration = default; + Action? callback = null; + + callback = () => + { + InputUpdateType currentUpdateType = InputState.currentUpdateType; + if (!InputUpdateTypeResolver.IsMatch(currentUpdateType, targetUpdateType)) + { + return; + } + + Debug.Assert(callback != null, "callback must be assigned before subscription"); + InputSystem.onBeforeUpdate -= callback; + registration.Dispose(); + apply(); + tcs.TrySetResult(true); + }; + + InputSystem.onBeforeUpdate += callback; + if (ct.CanBeCanceled) + { + registration = ct.Register(() => + { + Debug.Assert(callback != null, "callback must be assigned before cancellation"); + InputSystem.onBeforeUpdate -= callback; + tcs.TrySetCanceled(ct); + }); + } + + RunExplicitUpdate(targetUpdateType); + if (!tcs.Task.IsCompleted) + { + Debug.Assert(callback != null, "callback must be assigned before explicit update fallback"); + InputSystem.onBeforeUpdate -= callback; + registration.Dispose(); + apply(); + RunExplicitUpdate(targetUpdateType); + tcs.TrySetResult(true); + } + + return tcs.Task; + } + + private static InputSettings.UpdateMode GetExplicitUpdateMode( + InputUpdateType targetUpdateType, + InputSettings.UpdateMode fallbackUpdateMode) + { + if (targetUpdateType == InputUpdateType.Dynamic) + { + return InputSettings.UpdateMode.ProcessEventsInDynamicUpdate; + } + + if (targetUpdateType == InputUpdateType.Fixed) + { + return InputSettings.UpdateMode.ProcessEventsInFixedUpdate; + } + + if (targetUpdateType == InputUpdateType.Manual) + { + return InputSettings.UpdateMode.ProcessEventsManually; + } + + return fallbackUpdateMode; + } + + private static async Task WaitForRuntimeFrames(int frameCount, CancellationToken ct) + { + int startFrameCount = Time.frameCount; + int observedFrames = 0; + + while (observedFrames < frameCount) + { + await EditorDelay.DelayFrame(1, ct); + observedFrames = Time.frameCount - startFrameCount; + } + } + } +} +#endif diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem/InputSystemUpdateHelper.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem/InputSystemUpdateHelper.cs.meta new file mode 100644 index 000000000..607ce7ca9 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem/InputSystemUpdateHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d2cdea3342ed49e2a971e1766cbafa7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem/InputUpdateTypeResolver.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem/InputUpdateTypeResolver.cs new file mode 100644 index 000000000..faa8a373b --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem/InputUpdateTypeResolver.cs @@ -0,0 +1,64 @@ +#nullable enable +#if ULOOPMCP_HAS_INPUT_SYSTEM +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.LowLevel; + +namespace io.github.hatayama.uLoopMCP +{ + // Projects can process Input System events in dynamic, fixed, or manual mode. + // Input simulation must follow that configured loop to avoid frame mismatches. + internal static class InputUpdateTypeResolver + { + public static InputUpdateType Resolve() + { + InputSettings? settings = InputSystem.settings; + if (settings == null) + { + return InputUpdateType.Dynamic; + } + + InputSettings.UpdateMode updateMode = settings.updateMode; + switch (updateMode) + { + case InputSettings.UpdateMode.ProcessEventsInFixedUpdate: + // Paused screens commonly set timeScale to 0, which stops fixed ticks entirely. + // Falling back to Dynamic keeps input simulation responsive for those menus. + return IsPausedFixedUpdate(settings) ? InputUpdateType.Dynamic : InputUpdateType.Fixed; + + case InputSettings.UpdateMode.ProcessEventsManually: + return InputUpdateType.Manual; + + default: + return InputUpdateType.Dynamic; + } + } + + public static bool IsMatch(InputUpdateType current, InputUpdateType expected) + { + return (current & expected) == expected; + } + + public static bool RequiresExplicitUpdate() + { + InputSettings? settings = InputSystem.settings; + if (settings == null) + { + return false; + } + + if (settings.updateMode == InputSettings.UpdateMode.ProcessEventsManually) + { + return true; + } + + return IsPausedFixedUpdate(settings); + } + + private static bool IsPausedFixedUpdate(InputSettings settings) + { + return settings.updateMode == InputSettings.UpdateMode.ProcessEventsInFixedUpdate && Time.timeScale <= 0f; + } + } +} +#endif diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem/InputUpdateTypeResolver.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem/InputUpdateTypeResolver.cs.meta new file mode 100644 index 000000000..db6e0092c --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Common/InputSystem/InputUpdateTypeResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 67072e4564d6345d58ceff0440cbfeda +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile.meta new file mode 100644 index 000000000..5dd721ce9 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: acb202afcf62f40b88d846c3f1e0c373 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileResponse.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileResponse.cs new file mode 100644 index 000000000..f21b7d74e --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileResponse.cs @@ -0,0 +1,102 @@ +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Compile error or warning information + /// + public class CompileIssue + { + public string Message { get; set; } + public string File { get; set; } + public int Line { get; set; } + + public CompileIssue(string message, string file, int line) + { + Message = message; + File = file; + Line = line; + } + + public CompileIssue() { } + } + + /// + /// Response schema for Compile command + /// Provides type-safe response structure + /// + public class CompileResponse : BaseToolResponse + { + /// + /// Whether compilation was successful. + /// Null is used when the tool cannot reliably determine the result yet (e.g., forced recompile/domain reload), + /// to avoid misleading clients into treating "0 errors" as a confirmed success. + /// + public bool? Success { get; set; } + + /// + /// Number of compilation errors. + /// Null is used when the tool intentionally does not provide details (e.g., forced recompile), + /// because Unity reports errors/warnings after domain reload and clients should fetch logs later. + /// + public int? ErrorCount { get; set; } + + /// + /// Number of compilation warnings. + /// Null is used when the tool intentionally does not provide details (e.g., forced recompile), + /// because Unity reports errors/warnings after domain reload and clients should fetch logs later. + /// + public int? WarningCount { get; set; } + + /// + /// Compilation errors. + /// Null is used when the tool intentionally does not provide details (e.g., forced recompile), + /// because Unity reports errors/warnings after domain reload and clients should fetch logs later. + /// + public CompileIssue[] Errors { get; set; } + + /// + /// Compilation warnings. + /// Null is used when the tool intentionally does not provide details (e.g., forced recompile), + /// because Unity reports errors/warnings after domain reload and clients should fetch logs later. + /// + public CompileIssue[] Warnings { get; set; } + + /// + /// Optional message for additional information + /// + public string Message { get; set; } + + /// + /// Unity project root path (from Application.dataPath). + /// Set only when WaitForDomainReload=true so that TS/CLI can locate lock files + /// in the correct project even when connected to a remote Unity instance. + /// + public string ProjectRoot { get; set; } + + /// + /// Create a new CompileResponse + /// + public CompileResponse( + bool? success, + int? errorCount, + int? warningCount, + CompileIssue[] errors, + CompileIssue[] warnings, + string message = null + ) + { + Success = success; + ErrorCount = errorCount; + WarningCount = warningCount; + Errors = errors; + Warnings = warnings; + Message = message; + } + + /// + /// Parameterless constructor for JSON deserialization + /// + public CompileResponse() + { + } + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileResponse.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileResponse.cs.meta new file mode 100644 index 000000000..6219b279c --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 56090f2379ae3428bbe733a4db41e344 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileSchema.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileSchema.cs new file mode 100644 index 000000000..1edecd030 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileSchema.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Schema for Compile command parameters + /// Provides type-safe parameter access with default values + /// + public class CompileSchema : BaseToolSchema + { + /// + /// Whether to perform forced recompilation + /// + [Description("Whether to perform forced recompilation. Note: Force compilation results do not include error/warning messages in the response. Use get-logs tool after execution to retrieve compilation messages.")] + public bool ForceRecompile { get; set; } = false; + + /// + /// Whether to wait for domain reload completion before the caller returns. + /// + [Description("Whether to wait for domain reload completion before returning.")] + public bool WaitForDomainReload { get; set; } = false; + + /// + /// Internal request identifier used for delayed result recovery across domain reload. + /// + [Browsable(false)] + public string RequestId { get; set; } = ""; + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileSchema.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileSchema.cs.meta new file mode 100644 index 000000000..f148b2de8 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileSchema.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2581c0d32a90546fd9978a2f938e2f6f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileTool.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileTool.cs new file mode 100644 index 000000000..5491f02a6 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileTool.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Compile tool handler - Type-safe implementation using Schema and Response + /// Handles Unity project compilation with optional force recompile + /// Related classes: CompileUseCase, CompilationStateValidationService, CompilationExecutionService + /// + [McpTool(Description = "Execute Unity project compilation")] + public class CompileTool : AbstractUnityTool + { + public override string ToolName => "compile"; + + /// + /// Execute compile tool - delegates to CompileUseCase + /// + /// Type-safe parameters + /// Cancellation token for timeout control + /// Compile result + protected override async Task ExecuteAsync(CompileSchema parameters, CancellationToken cancellationToken) + { + // Create and execute CompileUseCase instance + CompileUseCase useCase = new(); + return await useCase.ExecuteAsync(parameters, cancellationToken); + } + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileTool.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileTool.cs.meta new file mode 100644 index 000000000..befb48a89 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/CompileTool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 60e9cabfcc7d047cbab5dfd6e9b06904 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/Skill.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/Skill.meta new file mode 100644 index 000000000..a9529eae5 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/Skill.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1186b025586e04dd794dace399e9ce35 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/Skill/SKILL.md b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/Skill/SKILL.md new file mode 100644 index 000000000..6e927b236 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/Skill/SKILL.md @@ -0,0 +1,70 @@ +--- +name: uloop-compile +description: "Compile the Unity project and report errors/warnings. Use after C# edits or when a full Domain Reload compile is needed." +--- + +# uloop compile + +Execute Unity project compilation. + +## Usage + +```bash +uloop compile [--force-recompile] [--wait-for-domain-reload] +``` + +## Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--force-recompile` | boolean | `false` | Force full recompilation (triggers Domain Reload) | +| `--wait-for-domain-reload` | boolean | `false` | Wait until Domain Reload completes before returning | + +## Global Options + +| Option | Description | +|--------|-------------| +| `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | + +## Examples + +```bash +# Check compilation +uloop compile + +# Force full recompilation +uloop compile --force-recompile + +# Force recompilation and wait for Domain Reload completion +uloop compile --force-recompile true --wait-for-domain-reload true + +# Wait for Domain Reload completion even without force recompilation +uloop compile --force-recompile false --wait-for-domain-reload true +``` + +## Output + +Returns JSON: +- `Success`: boolean +- `ErrorCount`: number +- `WarningCount`: number + +## Troubleshooting + +Diagnose the failure mode before retrying. + +**Stale lock files** (CLI hangs or shows "Unity is busy" while Unity Editor *is* running): + +```bash +uloop fix +``` + +This removes any leftover lock files (`compiling.lock`, `domainreload.lock`, `serverstarting.lock`) from the Unity project's Temp directory. Then retry `uloop compile`. + +**Unity Editor not running** (CLI returns a connection failure and no Unity process is alive): + +```bash +uloop launch +``` + +`uloop launch` auto-detects the project at the current working directory and opens it in the matching Unity Editor version. After Unity finishes launching, retry `uloop compile`. diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/Skill/SKILL.md.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/Skill/SKILL.md.meta new file mode 100644 index 000000000..dd43bd8e4 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Compile/Skill/SKILL.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3e7fbf71d98ab425486f6bd68b7a6612 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode.meta new file mode 100644 index 000000000..d770492ae --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a76e166c9de484f20b07f88305dd4b6b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeResponse.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeResponse.cs new file mode 100644 index 000000000..e93b7e573 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeResponse.cs @@ -0,0 +1,10 @@ +namespace io.github.hatayama.uLoopMCP +{ + public class ControlPlayModeResponse : BaseToolResponse + { + public bool IsPlaying { get; set; } + public bool IsPaused { get; set; } + public string Message { get; set; } + } +} + diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeResponse.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeResponse.cs.meta new file mode 100644 index 000000000..e16d6baa3 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 752e3f7c18a5649d8b6a555add78f1d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeSchema.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeSchema.cs new file mode 100644 index 000000000..5a747f94a --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeSchema.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; + +namespace io.github.hatayama.uLoopMCP +{ + public enum PlayModeAction + { + Play = 0, + Stop = 1, + Pause = 2 + } + + public class ControlPlayModeSchema : BaseToolSchema + { + [Description("Action to perform: Play(0) - Start play mode, Stop(1) - Stop play mode, Pause(2) - Pause play mode")] + public PlayModeAction Action { get; set; } = PlayModeAction.Play; + } +} + diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeSchema.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeSchema.cs.meta new file mode 100644 index 000000000..fc70c9edd --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeSchema.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5634451b383ae4ff8b0a1cd00500f342 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeTool.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeTool.cs new file mode 100644 index 000000000..67d777216 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeTool.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace io.github.hatayama.uLoopMCP +{ + [McpTool(Description = "Control Unity Editor play mode (play/stop/pause)")] + public class ControlPlayModeTool : AbstractUnityTool + { + public override string ToolName => "control-play-mode"; + + protected override async Task ExecuteAsync(ControlPlayModeSchema parameters, CancellationToken cancellationToken) + { + ControlPlayModeUseCase useCase = new(); + return await useCase.ExecuteAsync(parameters, cancellationToken); + } + } +} + diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeTool.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeTool.cs.meta new file mode 100644 index 000000000..0c17a320e --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/ControlPlayModeTool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bdce84d8140ea44e9a5fbc8b1e1cf5d6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/Skill.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/Skill.meta new file mode 100644 index 000000000..cb363cf32 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/Skill.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8809b197b1dbb42fbbabc04674f5baa9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/Skill/SKILL.md b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/Skill/SKILL.md new file mode 100644 index 000000000..5f830696e --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/Skill/SKILL.md @@ -0,0 +1,55 @@ +--- +name: uloop-control-play-mode +description: "Control Unity Editor Play Mode. Use to start, stop, or pause Play Mode for runtime behavior checks and frame inspection." +--- + +# uloop control-play-mode + +Control Unity Editor play mode (play/stop/pause). + +## Usage + +```bash +uloop control-play-mode [options] +``` + +## Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `--action` | string | `Play` | Action to perform: `Play`, `Stop`, `Pause` | + +## Global Options + +| Option | Description | +|--------|-------------| +| `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | + +## Examples + +```bash +# Start play mode +uloop control-play-mode --action Play + +# Stop play mode +uloop control-play-mode --action Stop + +# Pause play mode +uloop control-play-mode --action Pause +``` + +## Output + +Returns JSON with the current play mode state: +- `IsPlaying`: Whether Unity is currently in play mode +- `IsPaused`: Whether play mode is paused +- `Message`: Description of the action performed + +## Notes + +- Play action starts the game in the Unity Editor (also resumes from pause) +- Stop action exits play mode and returns to edit mode +- Pause action pauses the game while remaining in play mode +- Useful for automated testing workflows + +- PlayMode entry may complete on the next editor frame. If a PlayMode-dependent command reports "PlayMode is not active" immediately after `--action Play`, wait briefly and retry. diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/Skill/SKILL.md.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/Skill/SKILL.md.meta new file mode 100644 index 000000000..75d5bafd9 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ControlPlayMode/Skill/SKILL.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1e1425714444f43d1aab052bf7101893 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core.meta new file mode 100644 index 000000000..a81309008 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a4d88782b067f473a8c1bf230936d8f0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/AbstractUnityTool.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/AbstractUnityTool.cs new file mode 100644 index 000000000..9fb51d9b3 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/AbstractUnityTool.cs @@ -0,0 +1,122 @@ +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; +using System.Threading; + +namespace io.github.hatayama.uLoopMCP +{ + // Related classes: + // - IUnityTool: The interface that this class implements. + // - UnityToolRegistry: Registers and manages instances of tool implementations. + // - ToolParameterSchemaGenerator: Generates the JSON schema for tool parameters. + /// + /// Abstract base class for type-safe Unity tools using Schema and Response types + /// + /// Schema type for tool parameters + /// Response type for tool results + public abstract class AbstractUnityTool : IUnityTool + where TSchema : BaseToolSchema, new() + where TResponse : BaseToolResponse + { + public abstract string ToolName { get; } + + /// + /// Automatically generates parameter schema from TSchema type + /// + public virtual ToolParameterSchema ParameterSchema => + ToolParameterSchemaGenerator.FromDto(); + + /// + /// Execute tool with type-safe Schema parameters + /// Note: This method is called from the main thread context. + /// MainThreadSwitcher.SwitchToMainThread() is already handled by the upper layer (JsonRpcProcessor), + /// so individual tools do not need to call it again. + /// + /// Strongly typed parameters + /// Cancellation token for timeout control + /// Strongly typed tool execution result + protected abstract Task ExecuteAsync(TSchema parameters, CancellationToken cancellationToken); + + /// + /// IUnityTool implementation - converts JToken to Schema and returns BaseToolResponse + /// + public async Task ExecuteAsync(JToken paramsToken) + { + // Convert JToken to strongly typed Schema + TSchema parameters = ConvertToSchema(paramsToken); + + // Execute with type-safe parameters + TResponse response = await ExecuteAsync(parameters, CancellationToken.None); + + // Return as BaseToolResponse for IUnityTool interface compatibility + return response; + } + + /// + /// Convert JToken to strongly typed Schema with default value fallback + /// + private TSchema ConvertToSchema(JToken paramsToken) + { + if (paramsToken == null || paramsToken.Type == JTokenType.Null) + { + // Return default instance if no parameters provided + return new TSchema(); + } + + // Create JsonSerializerSettings with CamelCasePropertyNamesContractResolver + // This allows TypeScript side to use camelCase while C# uses PascalCase + JsonSerializerSettings settings = new JsonSerializerSettings + { + ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() + }; + + // Create JsonSerializer with custom settings + JsonSerializer serializer = JsonSerializer.Create(settings); + + // Try to deserialize from JToken with custom serializer + TSchema schema; + try + { + schema = paramsToken.ToObject(serializer); + } + catch (JsonSerializationException ex) + { + // Create detailed error message for type mismatches + string errorMessage = $"Parameter type mismatch for tool '{ToolName}': {ex.Message}"; + + // Check for specific Dictionary conversion errors + if (ex.Message.Contains("Dictionary") && ex.Message.Contains("Error converting value")) + { + string received = paramsToken["parameters"]?.ToString() ?? paramsToken["Parameters"]?.ToString() ?? "null"; + errorMessage = + "Parameter 'Parameters' must be an object, not a string. " + + "Do: omit 'Parameters' or use {}. " + + "Don't: \"{}\" (string). " + + $"Received: {received}"; + } + + throw new ParameterValidationException(errorMessage, ex); + } + + // If deserialization returns null, create default instance + if (schema == null) + { + schema = new TSchema(); + } + + // Apply default values for null properties + return ApplyDefaultValues(schema); + } + + /// + /// Apply default values to Schema properties if they are null + /// Override this method to provide custom default value logic + /// + protected virtual TSchema ApplyDefaultValues(TSchema schema) + { + // Default implementation - return as is + // Subclasses can override to apply specific default values + return schema; + } + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/AbstractUnityTool.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/AbstractUnityTool.cs.meta new file mode 100644 index 000000000..d64eeb7d6 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/AbstractUnityTool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8d4b53392372a454e954428caf82258a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/AbstractUseCase.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/AbstractUseCase.cs new file mode 100644 index 000000000..8e55f09e0 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/AbstractUseCase.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using System.Threading; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// UseCase base class - Responsible for temporal cohesion + /// New instances are created each time and disposed after Execute completion + /// Related classes: AbstractUnityTool, BaseToolSchema, BaseToolResponse + /// + /// Schema type for tool parameters + /// Response type for tool results + public abstract class AbstractUseCase + where TSchema : BaseToolSchema + where TResponse : BaseToolResponse + { + /// + /// UseCase execution method - The only public method + /// Responsible for temporal cohesion (executing multiple operations in sequence) + /// + /// Type-safe parameters + /// Cancellation control token + /// Type-safe execution result + public abstract Task ExecuteAsync(TSchema parameters, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/AbstractUseCase.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/AbstractUseCase.cs.meta new file mode 100644 index 000000000..8d9c4e242 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/AbstractUseCase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a74e8d5c037f4277a284f8f686edc05 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/BaseToolResponse.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/BaseToolResponse.cs new file mode 100644 index 000000000..60387da1a --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/BaseToolResponse.cs @@ -0,0 +1,13 @@ +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Base response class for all Unity MCP tool responses + /// + public abstract class BaseToolResponse + { + /// + /// uLoopMCP server version for CLI version compatibility check + /// + public string Ver => McpVersion.VERSION; + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/BaseToolResponse.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/BaseToolResponse.cs.meta new file mode 100644 index 000000000..f369b6693 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/BaseToolResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a54be1e158eda4064aa08ab9da92d980 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/BaseToolSchema.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/BaseToolSchema.cs new file mode 100644 index 000000000..22bfaef1a --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/BaseToolSchema.cs @@ -0,0 +1,9 @@ +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Base schema class for all Unity MCP tool parameters + /// + public abstract class BaseToolSchema + { + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/BaseToolSchema.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/BaseToolSchema.cs.meta new file mode 100644 index 000000000..be9a28c08 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/BaseToolSchema.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c705d28d63c1b4fb5aa6d7797c95631d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/CompileSessionState.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/CompileSessionState.cs new file mode 100644 index 000000000..75acc7e19 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/CompileSessionState.cs @@ -0,0 +1,114 @@ +using UnityEditor; +using System; +using Newtonsoft.Json; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// SessionState management class for forced recompilation + /// Preserves request information even when assembly reload occurs + /// + public static class CompileSessionState + { + // Note: Compile requests are now managed via McpSessionManager + + /// + /// Forced recompilation request information + /// + [Serializable] + public class CompileRequestInfo + { + public string requestId; + public bool forceRecompile; + public string clientEndpoint; + public DateTime requestTime; + public bool isCompleted; + + public CompileRequestInfo(string requestId, bool forceRecompile, string clientEndpoint) + { + this.requestId = requestId; + this.forceRecompile = forceRecompile; + this.clientEndpoint = clientEndpoint; + this.requestTime = DateTime.Now; + this.isCompleted = false; + } + } + + /// + /// Save forced recompilation request + /// + public static void SaveCompileRequest(string requestId, bool forceRecompile, string clientEndpoint = "unknown") + { + if (!forceRecompile) return; // Don't save normal compilation + + CompileRequestInfo requestInfo = new CompileRequestInfo(requestId, forceRecompile, clientEndpoint); + string json = JsonConvert.SerializeObject(requestInfo); + + McpEditorSettings.SetCompileRequestJson(requestId, json); + McpEditorSettings.AddPendingCompileRequest(requestId); + + } + + /// + /// Get pending request IDs + /// + public static string[] GetPendingRequestIds() + { + return McpEditorSettings.GetPendingCompileRequestIds(); + } + + /// + /// Get saved request information + /// + public static CompileRequestInfo GetCompileRequest(string requestId) + { + string json = McpEditorSettings.GetCompileRequestJson(requestId); + if (string.IsNullOrEmpty(json)) return null; + + try + { + return JsonConvert.DeserializeObject(json); + } + catch + { + return null; + } + } + + /// + /// Mark request as completed + /// + public static void MarkRequestCompleted(string requestId) + { + CompileRequestInfo requestInfo = GetCompileRequest(requestId); + if (requestInfo == null) return; + + requestInfo.isCompleted = true; + string json = JsonConvert.SerializeObject(requestInfo); + McpEditorSettings.SetCompileRequestJson(requestId, json); + McpEditorSettings.RemovePendingCompileRequest(requestId); + + } + + // Note: RemoveFromPendingRequests is now handled by McpSessionManager + + /// + /// Start forced recompilation + /// + public static void StartForceRecompile() + { + UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation( + UnityEditor.Compilation.RequestScriptCompilationOptions.CleanBuildCache + ); + } + + /// + /// Clear SessionState + /// + public static void ClearAll() + { + McpEditorSettings.ClearAllCompileRequests(); + + } + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/CompileSessionState.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/CompileSessionState.cs.meta new file mode 100644 index 000000000..2202f1ae6 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/CompileSessionState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 43fb73081d49d4a309e18955e541493f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/CustomToolManager.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/CustomToolManager.cs new file mode 100644 index 000000000..fe0a8874e --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/CustomToolManager.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Static class for managing custom tools + /// Allows users to register and manage their own tools + /// + public static class CustomToolManager + { + private static UnityToolRegistry _sharedRegistry; + + internal static event Action OnToolsChanged; + + /// + /// Get shared registry (lazy initialization) + /// + private static UnityToolRegistry SharedRegistry + { + get + { + if (_sharedRegistry == null) + { + _sharedRegistry = new UnityToolRegistry(); + // Standard tools are automatically registered in UnityToolRegistry constructor + } + return _sharedRegistry; + } + } + + /// + /// Register custom tool + /// + /// Tool to register + public static void RegisterCustomTool(IUnityTool tool) + { + SharedRegistry.RegisterTool(tool); + + // Notify tool changes for manual registration + NotifyToolChanges(); + } + + /// + /// Unregister custom tool + /// + /// Name of tool to unregister + public static void UnregisterCustomTool(string toolName) + { + SharedRegistry.UnregisterTool(toolName); + + // Notify tool changes for manual unregistration + NotifyToolChanges(); + } + + /// + /// Get list of all registered tools (standard + custom) + /// + /// Array of tool information + public static ToolInfo[] GetRegisteredCustomTools() + { + return SharedRegistry.GetRegisteredTools(); + } + + /// + /// Check if specified tool is registered + /// + /// Tool name + /// True if registered + public static bool IsCustomToolRegistered(string toolName) + { + return SharedRegistry.IsToolRegistered(toolName); + } + + /// + /// Get internal registry (for MCP server) + /// + /// UnityToolRegistry instance + internal static UnityToolRegistry GetRegistry() + { + return SharedRegistry; + } + + internal static UnityToolRegistry TryGetRegistry() + { + return _sharedRegistry; + } + + internal static void WarmupRegistry() + { + _ = SharedRegistry; + } + + /// + /// Debug: Get detailed registry information + /// + /// Debug information + public static string GetDebugInfo() + { + ToolInfo[] tools = SharedRegistry.GetRegisteredTools(); + string[] toolNames = new string[tools.Length]; + for (int i = 0; i < tools.Length; i++) + { + toolNames[i] = tools[i].Name; + } + return $"Registry instance: {SharedRegistry.GetHashCode()}, Tools: [{string.Join(", ", toolNames)}]"; + } + + /// + /// Manually notify tool changes to MCP clients + /// Public API for users to trigger notifications when needed + /// + public static void NotifyToolChanges() + { + UnityToolRegistry.TriggerToolsChangedNotification(); + OnToolsChanged?.Invoke(); + } + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/CustomToolManager.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/CustomToolManager.cs.meta new file mode 100644 index 000000000..123543ffc --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/CustomToolManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dd9b6252fa10a4edc9a5f2ee0140365d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/IUnityTool.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/IUnityTool.cs new file mode 100644 index 000000000..e456e4392 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/IUnityTool.cs @@ -0,0 +1,79 @@ +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Constants for parameter schema property names + /// Ensures consistency between Unity and TypeScript sides + /// + public static class ParameterSchemaConstants + { + public const string TYPE_PROPERTY = "Type"; + public const string DESCRIPTION_PROPERTY = "Description"; + public const string DEFAULT_VALUE_PROPERTY = "DefaultValue"; + public const string ENUM_PROPERTY = "Enum"; + public const string PROPERTIES_PROPERTY = "Properties"; + public const string REQUIRED_PROPERTY = "Required"; + } + + /// + /// Base interface for Unity MCP tool handlers + /// Following the Open-Closed principle, when adding new tools, + /// create a new class that implements this interface + /// + public interface IUnityTool + { + /// + /// Get tool name + /// + string ToolName { get; } + + /// + /// Get parameter schema information for TypeScript side + /// + ToolParameterSchema ParameterSchema { get; } + + /// + /// Execute tool + /// + /// JSON token for parameters + /// Execution result + Task ExecuteAsync(JToken paramsToken); + } + + /// + /// Parameter schema definition for tools (immutable) + /// + public class ToolParameterSchema + { + public readonly Dictionary Properties; + public readonly string[] Required; + + public ToolParameterSchema(Dictionary properties = null, string[] required = null) + { + Properties = properties ?? new Dictionary(); + Required = required ?? new string[0]; + } + } + + /// + /// Individual parameter information (immutable) + /// + public class ParameterInfo + { + public readonly string Type; + public readonly string Description; + public readonly object DefaultValue; + public readonly string[] Enum; + + public ParameterInfo(string type, string description, object defaultValue = null, string[] enumValues = null) + { + Type = type; + Description = description; + DefaultValue = defaultValue; + Enum = enumValues; + } + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/IUnityTool.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/IUnityTool.cs.meta new file mode 100644 index 000000000..f0390e56d --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/IUnityTool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4f9cf1a64741648cb8533b76ec6f3a87 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/McpToolAttribute.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/McpToolAttribute.cs new file mode 100644 index 000000000..b6f47cf10 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/McpToolAttribute.cs @@ -0,0 +1,34 @@ +using System; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Attribute to mark classes for automatic MCP tool registration + /// Classes marked with this attribute will be automatically registered as MCP commands + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class McpToolAttribute : Attribute + { + /// + /// Gets whether this tool should only be displayed in development mode + /// + public bool DisplayDevelopmentOnly { get; set; } = false; + + /// + /// Gets the specific security setting required to execute this command + /// + public SecuritySettings RequiredSecuritySetting { get; set; } = SecuritySettings.None; + + /// + /// Gets or sets the description of this tool + /// + public string Description { get; set; } = string.Empty; + + /// + /// Initialize McpTool attribute + /// + public McpToolAttribute() + { + } + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/McpToolAttribute.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/McpToolAttribute.cs.meta new file mode 100644 index 000000000..c1bbf0b06 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/McpToolAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f9f72309eee6a49528d19b182ff51ad6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/OverlayCanvasFactory.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/OverlayCanvasFactory.cs new file mode 100644 index 000000000..408fccd22 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/OverlayCanvasFactory.cs @@ -0,0 +1,64 @@ +using UnityEngine; +using UnityEditor; + +namespace io.github.hatayama.uLoopMCP +{ + // Instantiates the InputVisualizationCanvas prefab and manages its lifecycle. + internal static class OverlayCanvasFactory + { + private const string CANVAS_PREFAB_PATH = "Packages/io.github.hatayama.uloopmcp/Runtime/Common/InputVisualizationCanvas.prefab"; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticFields() + { + _instance = null; + } + + private static InputVisualizationCanvas _instance; + + public static InputVisualizationCanvas VisualizationCanvas + { + get + { + EnsureExists(); + Debug.Assert(_instance != null, "InputVisualizationCanvas instance must exist after EnsureExists"); + return _instance!; + } + } + + public static void EnsureExists() + { + if (_instance != null) + { + return; + } + + // Domain Reload resets _instance but DontDestroyOnLoad objects survive; reclaim one and destroy duplicates + InputVisualizationCanvas[] existing = + Object.FindObjectsByType(FindObjectsSortMode.None); + for (int i = 0; i < existing.Length; i++) + { + if (_instance == null) + { + _instance = existing[i]; + } + else + { + Object.DestroyImmediate(existing[i].gameObject); + } + } + if (_instance != null) + { + return; + } + + GameObject prefab = AssetDatabase.LoadAssetAtPath(CANVAS_PREFAB_PATH); + Debug.Assert(prefab != null, $"InputVisualizationCanvas prefab not found at {CANVAS_PREFAB_PATH}"); + + GameObject go = (GameObject)PrefabUtility.InstantiatePrefab(prefab); + Object.DontDestroyOnLoad(go); + _instance = go.GetComponent(); + Debug.Assert(_instance != null, "InputVisualizationCanvas component not found on prefab"); + } + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/OverlayCanvasFactory.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/OverlayCanvasFactory.cs.meta new file mode 100644 index 000000000..7c8b139f6 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/OverlayCanvasFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 76e5da6abcd484cbcbdbbf462a6fdd4b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ServiceResult.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ServiceResult.cs new file mode 100644 index 000000000..9d9116c04 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ServiceResult.cs @@ -0,0 +1,52 @@ +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Generic data model representing service execution result + /// Used in Application Service Layer + /// + /// Type of result data + public class ServiceResult + { + /// + /// Whether execution was successful + /// + public bool Success { get; } + + /// + /// Data of execution result + /// + public T Data { get; } + + /// + /// Error message when execution fails + /// + public string ErrorMessage { get; } + + /// + /// Create ServiceResult + /// + /// Execution result + /// Result data + /// Error message (null on success) + public ServiceResult(bool success, T data = default, string errorMessage = null) + { + Success = success; + Data = data; + ErrorMessage = errorMessage; + } + + /// + /// Create success result + /// + /// Result data + /// ServiceResult representing success + public static ServiceResult SuccessResult(T data) => new(true, data); + + /// + /// Create failure result + /// + /// Error message + /// ServiceResult representing failure + public static ServiceResult FailureResult(string errorMessage) => new(false, default, errorMessage); + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ServiceResult.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ServiceResult.cs.meta new file mode 100644 index 000000000..16d3bef3c --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ServiceResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de2101885e97d4499889ddbb6bb6e358 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ToolExecutionAvailability.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ToolExecutionAvailability.cs new file mode 100644 index 000000000..0fb2d1f5b --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ToolExecutionAvailability.cs @@ -0,0 +1,54 @@ +using UnityEngine; + +namespace io.github.hatayama.uLoopMCP +{ + internal static class ToolExecutionAvailability + { + internal static bool ShouldReportDependencyUnavailableBeforeDisabled(string toolName) + { + return ShouldReportDependencyUnavailableBeforeDisabled( + toolName, + isTestFrameworkAvailable: IsTestFrameworkAvailable); + } + + internal static bool ShouldReportDependencyUnavailableBeforeDisabled( + string toolName, + bool isTestFrameworkAvailable) + { + Debug.Assert(!string.IsNullOrEmpty(toolName), "toolName must not be null or empty"); + + return toolName == McpConstants.TOOL_NAME_RUN_TESTS && !isTestFrameworkAvailable; + } + + internal static bool ShouldExposeInRegisteredTools(string toolName) + { + return ShouldExposeInRegisteredTools( + toolName, + isToolEnabled: ToolSettings.IsToolEnabled(toolName), + isTestFrameworkAvailable: IsTestFrameworkAvailable); + } + + internal static bool ShouldExposeInRegisteredTools( + string toolName, + bool isToolEnabled, + bool isTestFrameworkAvailable) + { + Debug.Assert(!string.IsNullOrEmpty(toolName), "toolName must not be null or empty"); + + return isToolEnabled || + ShouldReportDependencyUnavailableBeforeDisabled(toolName, isTestFrameworkAvailable); + } + + internal static bool IsTestFrameworkAvailable + { + get + { +#if ULOOPMCP_HAS_TEST_FRAMEWORK + return true; +#else + return false; +#endif + } + } + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ToolExecutionAvailability.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ToolExecutionAvailability.cs.meta new file mode 100644 index 000000000..3af1a6151 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ToolExecutionAvailability.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 12a3cd3bb7213f14ab18fa845fe17f13 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ToolParameterSchemaGenerator.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ToolParameterSchemaGenerator.cs new file mode 100644 index 000000000..0b44b6634 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ToolParameterSchemaGenerator.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json; +using System.ComponentModel; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Utility class to generate ToolParameterSchema from DTO classes + /// Eliminates duplication between DTO default values and schema definitions + /// + public static class ToolParameterSchemaGenerator + { + /// + /// Generate ToolParameterSchema from DTO type using reflection + /// + /// DTO type + /// Generated parameter schema + public static ToolParameterSchema FromDto() where TDto : class, new() + { + Type dtoType = typeof(TDto); + Dictionary properties = new Dictionary(); + List required = new List(); + + // Create instance to get default values + TDto defaultInstance = new TDto(); + + foreach (PropertyInfo property in dtoType.GetProperties()) + { + // Skip properties without public setters + if (!property.CanWrite || property.SetMethod?.IsPublic != true) + continue; + + BrowsableAttribute browsableAttribute = property.GetCustomAttribute(); + if (browsableAttribute != null && !browsableAttribute.Browsable) + continue; + + // Get JSON property name (fallback to property name) + string parameterName = GetJsonPropertyName(property); + + // Get property type information + string parameterType = GetParameterType(property.PropertyType); + + // Get description from attributes + string description = GetDescription(property); + + // Get default value from instance + object defaultValue = property.GetValue(defaultInstance); + + // Get enum values if applicable + string[] enumValues = GetEnumValues(property.PropertyType); + + // Check if property is required + bool isRequired = IsRequired(property); + if (isRequired) + { + required.Add(parameterName); + } + + // Create parameter info + ParameterInfo paramInfo = new ParameterInfo( + parameterType, + description, + defaultValue, + enumValues + ); + + properties[parameterName] = paramInfo; + } + + return new ToolParameterSchema(properties, required.ToArray()); + } + + /// + /// Get JSON property name from JsonProperty attribute or property name + /// + private static string GetJsonPropertyName(PropertyInfo property) + { + JsonPropertyAttribute jsonAttr = property.GetCustomAttribute(); + return jsonAttr?.PropertyName ?? property.Name; + } + + /// + /// Convert .NET type to parameter type string + /// + private static string GetParameterType(Type type) + { + // Handle nullable types + Type underlyingType = Nullable.GetUnderlyingType(type) ?? type; + + if (underlyingType == typeof(string)) + return "string"; + if (underlyingType == typeof(bool)) + return "boolean"; + if (underlyingType == typeof(int) || underlyingType == typeof(long) || + underlyingType == typeof(float) || underlyingType == typeof(double) || + underlyingType == typeof(decimal)) + return "number"; + if (underlyingType.IsArray || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))) + return "array"; + if (underlyingType.IsEnum) + return "string"; // Enums are treated as strings with enum constraints + if (IsDictionaryType(underlyingType)) + return "object"; + + return "string"; // Default fallback + } + + /// + /// Check if type is a dictionary type (Dictionary, IDictionary, IReadOnlyDictionary) + /// + private static bool IsDictionaryType(Type type) + { + if (type.IsGenericType) + { + Type genericDef = type.GetGenericTypeDefinition(); + if (genericDef == typeof(Dictionary<,>) || + genericDef == typeof(IDictionary<,>) || + genericDef == typeof(IReadOnlyDictionary<,>)) + return true; + } + return type.GetInterfaces().Any(i => + i.IsGenericType && + (i.GetGenericTypeDefinition() == typeof(IDictionary<,>) || + i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>))); + } + + /// + /// Get description from Description attribute or generate default + /// + private static string GetDescription(PropertyInfo property) + { + DescriptionAttribute descAttr = property.GetCustomAttribute(); + if (descAttr != null) + return descAttr.Description; + + // Generate default description + return $"Parameter: {property.Name}"; + } + + /// + /// Get enum values if property type is enum + /// + private static string[] GetEnumValues(Type type) + { + Type underlyingType = Nullable.GetUnderlyingType(type) ?? type; + + if (underlyingType.IsEnum) + { + return Enum.GetNames(underlyingType); + } + + return null; + } + + /// + /// Check if property is required using custom Required attribute + /// For now, returns false as Unity doesn't support DataAnnotations + /// Can be extended with custom attributes if needed + /// + private static bool IsRequired(PropertyInfo property) + { + // TODO: Implement custom Required attribute if needed + return false; + } + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ToolParameterSchemaGenerator.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ToolParameterSchemaGenerator.cs.meta new file mode 100644 index 000000000..a16a05dae --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ToolParameterSchemaGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b9d06a624dd5d4129b95d39ea144a412 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/UnityToolRegistry.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/UnityToolRegistry.cs new file mode 100644 index 000000000..0c5b99e4f --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/UnityToolRegistry.cs @@ -0,0 +1,424 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Stopwatch = System.Diagnostics.Stopwatch; + +namespace io.github.hatayama.uLoopMCP +{ + // Related classes: + // - UnityToolExecutor: Uses this registry to execute _tools. + // - IUnityTool: The interface for all _tools stored in this registry. + // - AbstractUnityTool: The base class for most tool implementations. + // - McpToolAttribute: Attribute used to discover and register _tools automatically. + /// + /// Unity MCP tool registry class + /// Supports dynamic tool registration, allowing users to add their own _tools + /// + public class UnityToolRegistry + { + private readonly Dictionary _tools = new(); + + /// + /// Singleton instance for global access + /// + public static UnityToolRegistry Instance { get; private set; } + + /// + /// Default constructor + /// Auto-registers standard _tools + /// + public UnityToolRegistry() + { + Instance = this; + RegisterDefaultTools(); + } + + /// + /// Register standard _tools + /// + private void RegisterDefaultTools() + { + // Register _tools with attribute-based discovery + RegisterToolsWithAttributes(); + + // Manual registration for _tools without attributes (for backward compatibility) + RegisterManualTools(); + } + + /// + /// Register _tools using attribute-based discovery + /// + private void RegisterToolsWithAttributes() + { + try + { + // Get all assemblies in the current domain + Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); + + List toolTypes = new List(); + + foreach (Assembly assembly in assemblies) + { + // Find all types with McpTool attribute that implement IUnityTool + Type[] types = assembly.GetTypes() + .Where(type => type.GetCustomAttribute() != null) + .Where(type => typeof(IUnityTool).IsAssignableFrom(type)) + .Where(type => !type.IsAbstract && !type.IsInterface) + .ToArray(); + + toolTypes.AddRange(types); + } + + // Register all _tools - filtering will be handled by TypeScript side + foreach (Type type in toolTypes) + { + // Security: Validate type before creating instance + if (!IsValidToolType(type)) + { + UnityEngine.Debug.LogWarning($"{McpConstants.SECURITY_LOG_PREFIX} Skipping invalid tool type: {type.FullName}"); + continue; + } + + IUnityTool tool = (IUnityTool)Activator.CreateInstance(type); + RegisterTool(tool); + } + } + catch (Exception) + { + throw; + } + } + + /// + /// Register _tools manually (for backward compatibility) + /// + private void RegisterManualTools() + { + // Only register _tools that don't have the McpTool attribute + // This prevents double registration + + if (!IsToolTypeRegistered()) + { + RegisterTool(new PingTool()); + } + + if (!IsToolTypeRegistered()) + { + RegisterTool(new CompileTool()); + } + + if (!IsToolTypeRegistered()) + { + RegisterTool(new GetLogsTool()); + } + + if (!IsToolTypeRegistered()) + { + RegisterTool(new RunTestsTool()); + } + } + + /// + /// Check if a tool type is already registered + /// + private bool IsToolTypeRegistered() where T : IUnityTool + { + return _tools.Values.Any(tool => tool.GetType() == typeof(T)); + } + + /// + /// Security: Validate if the type is safe to instantiate + /// + private bool IsValidToolType(Type type) + { + try + { + // Must implement IUnityTool + if (!typeof(IUnityTool).IsAssignableFrom(type)) + { + return false; + } + + // Must not be abstract or interface + if (type.IsAbstract || type.IsInterface) + { + return false; + } + + // Must have McpTool attribute + if (type.GetCustomAttribute() == null) + { + return false; + } + + // Must have parameterless constructor + if (type.GetConstructor(Type.EmptyTypes) == null) + { + return false; + } + + return true; + } + catch (Exception ex) + { + UnityEngine.Debug.LogError($"{McpConstants.SECURITY_LOG_PREFIX} Error validating tool type {type?.FullName}: {ex.Message}"); + return false; + } + } + + /// + /// Register tool + /// + /// Tool to register + public void RegisterTool(IUnityTool tool) + { + if (tool == null) + { + throw new ArgumentNullException(nameof(tool)); + } + + if (string.IsNullOrWhiteSpace(tool.ToolName)) + { + throw new ArgumentException("Tool name cannot be null or empty", nameof(tool)); + } + + _tools[tool.ToolName] = tool; + } + + /// + /// Unregister tool + /// + /// Name of tool to unregister + public void UnregisterTool(string toolName) + { + _tools.Remove(toolName); + } + + /// + /// Execute tool + /// + /// Tool name + /// Parameters + /// Execution result + /// When tool is unknown + /// When tool is blocked by security settings + public async Task ExecuteToolAsync(string toolName, JToken paramsToken) + { + if (!_tools.TryGetValue(toolName, out IUnityTool tool)) + { + throw new ArgumentException($"Unknown tool: {toolName}"); + } + + if (!ToolSettings.IsToolEnabled(toolName) + && !ToolExecutionAvailability.ShouldReportDependencyUnavailableBeforeDisabled(toolName)) + { + throw new ToolDisabledException(toolName); + } + + // Security check - validate tool before execution + if (!McpSecurityChecker.IsToolAllowed(toolName)) + { + throw new McpSecurityException(toolName, "Tool is blocked by security settings"); + } + + Stopwatch mainThreadHopStopwatch = Stopwatch.StartNew(); + await MainThreadSwitcher.SwitchToMainThread(); + mainThreadHopStopwatch.Stop(); + + Stopwatch toolBodyStopwatch = Stopwatch.StartNew(); + BaseToolResponse response = await tool.ExecuteAsync(paramsToken); + toolBodyStopwatch.Stop(); + + AppendExecuteDynamicCodeTimingsIfSupported( + response, + $"[Perf] RegistryMainThreadHop: {mainThreadHopStopwatch.Elapsed.TotalMilliseconds:F1}ms", + $"[Perf] ToolBody: {toolBodyStopwatch.Elapsed.TotalMilliseconds:F1}ms"); + return response; + } + + private static void AppendExecuteDynamicCodeTimingsIfSupported( + BaseToolResponse response, + params string[] timingEntries) + { + if (response is not ExecuteDynamicCodeResponse executeDynamicCodeResponse) + { + return; + } + + if (executeDynamicCodeResponse.Timings == null) + { + executeDynamicCodeResponse.Timings = new List(); + } + + executeDynamicCodeResponse.Timings.AddRange(timingEntries); + } + + /// + /// Get detailed information of registered _tools + /// + /// Array of tool information + public ToolInfo[] GetRegisteredTools() + { + return _tools.Values + .Where(tool => ToolExecutionAvailability.ShouldExposeInRegisteredTools(tool.ToolName)) + .Select(tool => + { + // Check if tool has McpTool attribute with DisplayDevelopmentOnly + bool displayDevelopmentOnly = false; + McpToolAttribute attribute = tool.GetType().GetCustomAttribute(); + if (attribute != null) + { + displayDevelopmentOnly = attribute.DisplayDevelopmentOnly; + } + + // Check security settings + bool isAllowed = McpSecurityChecker.IsToolAllowed(tool.ToolName); + + // Get description from attribute + string description = attribute?.Description ?? ""; + + // Modify description for blocked _tools + if (!isAllowed) + { + description = $"[BLOCKED] {description} - Blocked by security settings"; + } + + return new ToolInfo(tool.ToolName, description, tool.ParameterSchema, displayDevelopmentOnly); + }).ToArray(); + } + + /// + /// Get all tools including disabled ones. Used by Tool Settings UI. + /// + public ToolInfo[] GetAllRegisteredToolInfos() + { + return _tools.Values.Select(tool => + { + McpToolAttribute attribute = tool.GetType().GetCustomAttribute(); + string description = attribute?.Description ?? ""; + bool displayDevelopmentOnly = attribute?.DisplayDevelopmentOnly ?? false; + return new ToolInfo(tool.ToolName, description, tool.ParameterSchema, displayDevelopmentOnly); + }).ToArray(); + } + + public ToolSettingsCatalogItem[] GetToolSettingsCatalog() + { + return _tools.Values.Select(tool => + { + Type toolType = tool.GetType(); + McpToolAttribute attribute = toolType.GetCustomAttribute(); + string description = attribute?.Description ?? ""; + bool displayDevelopmentOnly = attribute?.DisplayDevelopmentOnly ?? false; + bool isThirdParty = IsThirdPartyAssembly(toolType.Assembly.GetName().Name); + + return new ToolSettingsCatalogItem( + tool.ToolName, + description, + displayDevelopmentOnly, + isThirdParty); + }).ToArray(); + } + + /// + /// Check if a tool belongs to a third-party assembly. + /// + public bool IsThirdPartyTool(string toolName) + { + if (!_tools.TryGetValue(toolName, out IUnityTool tool)) + { + return true; + } + string assemblyName = tool.GetType().Assembly.GetName().Name; + return IsThirdPartyAssembly(assemblyName); + } + + private static bool IsThirdPartyAssembly(string assemblyName) + { + return assemblyName != "uLoopMCP.Editor"; + } + + /// + /// Get tool type by name for security checking + /// + /// Tool name + /// Tool type or null if not found + public Type GetToolType(string toolName) + { + if (_tools.TryGetValue(toolName, out IUnityTool tool)) + { + return tool.GetType(); + } + return null; + } + + /// + /// Check if specified tool is registered + /// + /// Tool name + /// True if registered + public bool IsToolRegistered(string toolName) + { + return _tools.ContainsKey(toolName); + } + + /// + /// Manually trigger _tools changed notification + /// Used for manual notifications and post-compilation notifications + /// + public static void TriggerToolsChangedNotification() + { + // Call the public method in McpServerController + McpServerController.TriggerToolChangeNotification(); + } + + } + + /// + /// Class representing tool information + /// + public class ToolInfo + { + [JsonProperty("name")] public string Name { get; } + + [JsonProperty("description")] public string Description { get; } + + [JsonProperty("parameterSchema")] public ToolParameterSchema ParameterSchema { get; } + + [JsonProperty("displayDevelopmentOnly")] public bool DisplayDevelopmentOnly { get; } + + public ToolInfo(string name, string description, ToolParameterSchema parameterSchema, bool displayDevelopmentOnly = false) + { + Name = name; + Description = description; + ParameterSchema = parameterSchema; + DisplayDevelopmentOnly = displayDevelopmentOnly; + } + } + + /// + /// Lightweight registry metadata used by Tool Settings UI without generating parameter schemas. + /// + public class ToolSettingsCatalogItem + { + public readonly string Name; + public readonly string Description; + public readonly bool DisplayDevelopmentOnly; + public readonly bool IsThirdParty; + + public ToolSettingsCatalogItem( + string name, + string description, + bool displayDevelopmentOnly, + bool isThirdParty) + { + Name = name; + Description = description; + DisplayDevelopmentOnly = displayDevelopmentOnly; + IsThirdParty = isThirdParty; + } + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/UnityToolRegistry.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/UnityToolRegistry.cs.meta new file mode 100644 index 000000000..4c8507476 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/UnityToolRegistry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b13155ca2a4cd42b2b0c2997a7925511 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ValidationResult.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ValidationResult.cs new file mode 100644 index 000000000..4951e537e --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ValidationResult.cs @@ -0,0 +1,43 @@ +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Data model representing validation result + /// Used in Application Service Layer + /// + public class ValidationResult + { + /// + /// Whether validation was successful + /// + public bool IsValid { get; } + + /// + /// Error message when validation fails + /// + public string ErrorMessage { get; } + + /// + /// Create ValidationResult + /// + /// Validation result + /// Error message (null on success) + public ValidationResult(bool isValid, string errorMessage = null) + { + IsValid = isValid; + ErrorMessage = errorMessage; + } + + /// + /// Create success result + /// + /// ValidationResult representing success + public static ValidationResult Success() => new(true); + + /// + /// Create failure result + /// + /// Error message + /// ValidationResult representing failure + public static ValidationResult Failure(string errorMessage) => new(false, errorMessage); + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ValidationResult.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ValidationResult.cs.meta new file mode 100644 index 000000000..f4feebce6 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Core/ValidationResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de99bb9f98ad74025bd8be4e41a953af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug.meta new file mode 100644 index 000000000..1f8b88ff8 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dbe57138c4a024b33bfe815d180e9068 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepResponse.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepResponse.cs new file mode 100644 index 000000000..dcbe1475e --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepResponse.cs @@ -0,0 +1,27 @@ +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Response for debug sleep tool execution + /// Related classes: + /// - SleepTool: Implementation that generates this response + /// - SleepSchema: Input parameters for the sleep tool + /// - BaseToolResponse: Base class providing timing information + /// + public class SleepResponse : BaseToolResponse + { + /// + /// Message indicating the result of the sleep operation + /// + public string Message { get; set; } + + /// + /// Actual number of seconds the tool slept before completion or timeout + /// + public int ActualSleepSeconds { get; set; } + + /// + /// Indicates whether the operation completed successfully or was cancelled + /// + public bool WasCancelled { get; set; } + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepResponse.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepResponse.cs.meta new file mode 100644 index 000000000..c1c10f3dd --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 28eaf066ee0a94a999dd67f9f4baabcd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepSchema.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepSchema.cs new file mode 100644 index 000000000..763557145 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepSchema.cs @@ -0,0 +1,19 @@ +using System.ComponentModel; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Schema for debug sleep tool parameters + /// Related classes: + /// - SleepTool: Implementation of the debug sleep tool + /// - SleepResponse: Response structure for the sleep tool + /// + public class SleepSchema : BaseToolSchema + { + /// + /// Number of seconds to sleep for testing purposes (default: 15) + /// + [Description("Number of seconds to sleep for testing purposes (default: 15)")] + public int SleepSeconds { get; set; } = 15; + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepSchema.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepSchema.cs.meta new file mode 100644 index 000000000..247d201dd --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepSchema.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 65adc65e8b27d41e889d4ccb67be267b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepTool.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepTool.cs new file mode 100644 index 000000000..86e2dea7a --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepTool.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using System.Threading; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Debug tool that sleeps for specified duration to test timeout functionality + /// Related classes: + /// - SleepSchema: Input parameters including sleep duration and timeout + /// - SleepResponse: Output containing results and timing information + /// - AbstractUnityTool: Base class providing timeout infrastructure + /// + [McpTool( + DisplayDevelopmentOnly = true, + Description = "Debug tool that sleeps for specified duration to test timeout functionality" + )] + public class SleepTool : AbstractUnityTool + { + public override string ToolName => "debug-sleep"; + + protected override async Task ExecuteAsync(SleepSchema parameters, CancellationToken cancellationToken) + { + int actualSleepSeconds = 0; + + // Sleep 1 second at a time, checking for cancellation between each sleep + for (int i = 0; i < parameters.SleepSeconds; i++) + { + // Check for cancellation before each sleep iteration + cancellationToken.ThrowIfCancellationRequested(); + + // Sleep for 1 second with cancellation support + await Task.Delay(1000, cancellationToken); + + actualSleepSeconds = i + 1; + } + + return new SleepResponse + { + Message = $"Successfully slept for {actualSleepSeconds} seconds", + ActualSleepSeconds = actualSleepSeconds, + WasCancelled = false + }; + } + } +} \ No newline at end of file diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepTool.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepTool.cs.meta new file mode 100644 index 000000000..722fae3eb --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/Debug/SleepTool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3819fd5fb05cb4309a2229a93f79f74a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode.meta new file mode 100644 index 000000000..8e4543bd3 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ae802852e2dfd47dc979543d41bb00d2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/DESIGN.md b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/DESIGN.md new file mode 100644 index 000000000..dc096cdd6 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/DESIGN.md @@ -0,0 +1,31 @@ +# execute-dynamic-code design notes + +This directory owns the user-facing `execute-dynamic-code` tool contract. +The public contract must stay stable even when the internal compiler strategy changes. + +## Fast path intent + +- The preferred compilation path is Unity-bundled Roslyn. +- `SharedRoslynCompilerWorkerHost` keeps a warm compiler process so structural cache misses stay fast. +- The shared worker is an optimization only. It must not own response shaping, security validation, or tool-specific policy. + +## Prewarm intent + +- Auto prewarm exists to hide first-use compiler startup cost after server startup or recovery. +- Prewarm must go through the same runtime facade used by normal requests. +- Prewarm must be single-flight. It should never create a parallel execution lane or bypass runtime idleness checks. +- Prewarm is allowed to improve latency only. It must not change observable tool behavior. + +## Fallback intent + +- Preferred order is: shared Roslyn worker -> one-shot Roslyn -> AssemblyBuilder fallback. +- Fallback changes performance, not semantics. +- Any fallback from the ideal fast path is a maintainer-visible error condition and must be logged through `DynamicCompilationHealthMonitor`. + +## Invariants + +- Security validation always runs in Unity after compilation and before execution. +- In restricted mode, metadata validation and IL validation must both complete before `Assembly.Load`. +- Tool entry points and use cases depend on runtime-facing facades, not compiler backend details. +- Worker state is disposable. Domain reload or worker protocol failure must be handled by rebuilding state, not by preserving it. +- The compile backend may change, but `ExecuteDynamicCodeResponse` shape and user-facing behavior must remain stable. diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/DESIGN.md.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/DESIGN.md.meta new file mode 100644 index 000000000..71056277d --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/DESIGN.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3a32019de73d24a92b23228d40898acf +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeResponse.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeResponse.cs new file mode 100644 index 000000000..c4d6c22f1 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeResponse.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Response for dynamic code execution tool + + /// Related classes: ExecuteDynamicCodeTool, ExecuteDynamicCodeSchema + /// + public class ExecuteDynamicCodeResponse : BaseToolResponse + { + private const bool EmitTimingsInJsonResponses = false; + + /// Execution success flag + public bool Success { get; set; } + + /// Execution result + public string Result { get; set; } + + /// Log messages + public List Logs { get; set; } = new(); + + /// Compilation errors + public List CompilationErrors { get; set; } = new(); + + /// Error message (on failure) + public string ErrorMessage { get; set; } + + /// Current security level + public string SecurityLevel { get; set; } + + /// Error message (alias for ErrorMessage) + public string Error + { + get => ErrorMessage; + set => ErrorMessage = value; + } + + /// + /// Code formatted for compilation + /// (After extracting/moving using statements and applying class/method wrapping) + /// Allows checking the actual compiled code during debugging + /// + public string UpdatedCode { get; set; } + + /// + /// Summary of diagnostics (unique count, total count, first error brief) + /// + public string DiagnosticsSummary { get; set; } + + /// + /// Structured diagnostics for rich clients (line/column/code/message/hint/suggestions) + /// + public List Diagnostics { get; set; } = new(); + + /// + /// Lightweight internal timings for benchmark comparison. + /// + public List Timings { get; set; } = new(); + + // Keep timings available in memory for diagnostics and readiness decisions + // while avoiding noisy default payloads for normal execute-dynamic-code users. + public bool ShouldSerializeTimings() + { + return EmitTimingsInJsonResponses; + } + } + + /// + /// DTO for compilation error information + /// + public class CompilationErrorDto + { + /// Error message + public string Message { get; set; } + + /// Line number + public int Line { get; set; } + + /// Column number + public int Column { get; set; } + + /// Compiler error code (e.g., CS0103) + public string ErrorCode { get; set; } + + /// Optional hint for resolving the error + public string Hint { get; set; } + + /// Suggested fixes (e.g., add using or qualify) + public List Suggestions { get; set; } = new(); + + /// Context lines around the error with a caret pointer + public string Context { get; set; } + + /// Pointer column for caret rendering (1-based) + public int PointerColumn { get; set; } + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeResponse.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeResponse.cs.meta new file mode 100644 index 000000000..6385ff9b0 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 88dbc381725914ce488e21b9b27a2cb8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeResponseTimingAugmenter.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeResponseTimingAugmenter.cs new file mode 100644 index 000000000..74478494f --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeResponseTimingAugmenter.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace io.github.hatayama.uLoopMCP +{ + internal static class ExecuteDynamicCodeResponseTimingAugmenter + { + public static void AppendTimingEntries( + ExecuteDynamicCodeResponse response, + double mainThreadWaitMilliseconds, + double toolTotalMilliseconds, + double requestTotalMilliseconds) + { + if (response == null) + { + return; + } + + if (response.Timings == null) + { + response.Timings = new List(); + } + + response.Timings.Add($"[Perf] MainThreadWait: {mainThreadWaitMilliseconds:F1}ms"); + response.Timings.Add($"[Perf] ToolTotal: {toolTotalMilliseconds:F1}ms"); + response.Timings.Add($"[Perf] RequestTotal: {requestTotalMilliseconds:F1}ms"); + response.Timings.AddRange(DynamicCodeStartupTelemetry.CreateTimingEntries()); + } + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeResponseTimingAugmenter.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeResponseTimingAugmenter.cs.meta new file mode 100644 index 000000000..d6e92403a --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeResponseTimingAugmenter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8683636c860b448cf93c98208c36946b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeSchema.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeSchema.cs new file mode 100644 index 000000000..8f05bcf47 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeSchema.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.ComponentModel; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Parameter schema for dynamic code execution tool + /// Related classes: ExecuteDynamicCodeTool, ExecuteDynamicCodeResponse + /// + public class ExecuteDynamicCodeSchema : BaseToolSchema + { + /// C# code to execute + [Description(@"Editor automation only — no file I/O, no script authoring. + +Direct statements only (no classes/namespaces/methods); return is optional (auto 'return null;' if omitted). + +You may include using directives at the top; they are hoisted above the wrapper. +Example: + using UnityEngine; + var x = Mathf.PI; + return x; + +Do: +- Prefab/material wiring (PrefabUtility) +- AddComponent + reference wiring (SerializedObject) +- Scene/hierarchy edits + +Don’t: +- System.IO.* (File/Directory/Path) +- AssetDatabase.CreateFolder / file writes +- Create/edit .cs/.asmdef (use Terminal/IDE instead) + +Need files/dirs? Run terminal commands.")] + public string Code { get; set; } = ""; + + /// Runtime parameters (advanced; usually unnecessary) + [EditorBrowsable(EditorBrowsableState.Advanced)] + [Description(@"Advanced, optional parameters passed to the snippet. + +- For throwaway snippets, prefer defining locals directly in code +- Keys are not preserved; values map to parameters[""param0""], parameters[""param1""], ... +- Use when injecting UnityEngine.Object or sensitive values, or reusing a snippet with varying data +- Use object values supported by the Editor; prefer fully-qualified type names for ambiguous refs (e.g., UnityEngine.Object) + +- Input type must be an OBJECT. Strings like ""{}"" are invalid. +- Do: omit 'Parameters' entirely, or use {} +- Don't: ""{}"" (string), ""{""a"":1}"" (string) + +Examples: +OK: ""Parameters"": {} +OK: (omit the 'Parameters' property) +NG: ""Parameters"": ""{}""")] + public Dictionary Parameters { get; set; } = new(); + + /// Compile only (do not execute) + [Description(@"Compile only (no execution). + +- Uses Roslyn validation to surface diagnostics +- For new MonoBehaviours: create .cs → compile (mcp__uLoopMCP__compile with ForceRecompile=false) → ensure ErrorCount=0 → AddComponent")] + public bool CompileOnly { get; set; } = false; + + [EditorBrowsable(EditorBrowsableState.Advanced)] + public bool YieldToForegroundRequests { get; set; } = false; + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeSchema.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeSchema.cs.meta new file mode 100644 index 000000000..26a9f8d13 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeSchema.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 96f1941ea53e64fbf9dca3d1b273ae72 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeTool.cs b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeTool.cs new file mode 100644 index 000000000..2900f5db9 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeTool.cs @@ -0,0 +1,47 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// MCP Dynamic C# Code Execution Tool. + /// Delegates workflow orchestration to the dedicated execute-dynamic-code use case. + /// + [McpTool(Description = @"Execute C# code dynamically in Unity Editor for editor automation. + +Use find-game-objects first for basic selected GameObject discovery or property inspection. Use this after built-in inspection tools are not enough or when you need to modify Unity state. + +Direct statements only (no classes/namespaces/methods); return is optional (auto 'return null;' if omitted). + +You may include using directives at the top; they are hoisted above the wrapper. +Example: + using UnityEngine; + var x = Mathf.PI; + return x; + +Do: +- Prefab/material wiring (PrefabUtility) +- AddComponent + reference wiring (SerializedObject) +- Scene/hierarchy edits + +Don't: +- System.IO.* (File/Directory/Path) +- AssetDatabase.CreateFolder / file writes +- Create/edit .cs/.asmdef (use Terminal/IDE instead) + +Need files/dirs? Run terminal commands. + +See examples at {project_root}/.claude/skills/unity-cli-loop/uloop-execute-dynamic-code/examples/")] + public class ExecuteDynamicCodeTool : AbstractUnityTool + { + public override string ToolName => "execute-dynamic-code"; + + protected override async Task ExecuteAsync( + ExecuteDynamicCodeSchema parameters, + CancellationToken cancellationToken) + { + IExecuteDynamicCodeUseCase useCase = await DynamicCodeServices.GetExecuteDynamicCodeUseCaseAsync(); + return await useCase.ExecuteAsync(parameters, cancellationToken); + } + } +} diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeTool.cs.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeTool.cs.meta new file mode 100644 index 000000000..9fb050127 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/ExecuteDynamicCodeTool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c0a0005c984014fe9ae1227c1e458432 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill.meta new file mode 100644 index 000000000..42d193ded --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e4545560316c242f08f6034dc0c57177 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/SKILL.md b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/SKILL.md new file mode 100644 index 000000000..189ed4d1c --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/SKILL.md @@ -0,0 +1,87 @@ +--- +name: uloop-execute-dynamic-code +description: "Execute C# with Unity APIs when existing uloop tools cannot inspect or edit enough. Use for scene, prefab, SerializedObject, AssetDatabase refresh/.meta generation, menu, or PlayMode automation." +context: fork +--- + +# Task + +Execute the following request using `uloop execute-dynamic-code`: $ARGUMENTS + +For basic selected GameObject discovery or property inspection, use `find-game-objects --search-mode Selected` before this tool. Use this tool after the built-in inspection tools are not enough or when you need to modify Unity state. + +## Workflow + +1. Read the relevant reference file(s) from the Code Examples section below +2. Construct C# code based on the reference examples +3. For multiline snippets, write the C# statements to a temporary `.csx` file and execute `uloop execute-dynamic-code --code-file ` +4. Use `uloop execute-dynamic-code --code ''` only for short one-line snippets +5. If execution fails, adjust code and retry +6. Report the execution result + +## Parameters + +- `--code ''`: Inline C# statements to execute. Use this for one-line snippets only. +- `--code-file `: Read C# statements from a UTF-8 file. Prefer this for multiline snippets, especially on PowerShell, because Windows `.cmd` shims can lose lines from multiline inline arguments before `uloop` receives them. +- **Shell quoting**: bash/zsh uses single quotes, for example `uloop execute-dynamic-code --code 'using UnityEngine; return Mathf.PI;'`. PowerShell single-quoted strings can contain normal double quotes, for example `uloop execute-dynamic-code --code 'Debug.Log("Hello!");'`. +- `--parameters {}` (advanced, optional): Pass an object when reusing a snippet with varying data or when keeping values outside the code. Values are exposed as `parameters["param0"]`, `parameters["param1"]`, and so on. Omit this flag for most snippets, and pass an object instead of a JSON string. +- `--compile-only true` (optional): Compile the snippet without executing it. Use this when you want Roslyn diagnostics before running new code. + +## Code Rules + +Write direct statements only — no class/namespace/method wrappers. Return is optional. + +```csharp +using UnityEngine; +float x = Mathf.PI; +return x; +``` + +**Forbidden** — these will be rejected at compile time: `System.IO.*`, `AssetDatabase.CreateFolder`, creating/editing `.cs`/`.asmdef` files. Use terminal commands for file operations instead. + +## Output + +Returns JSON: +- `Success`: boolean — overall execution success +- `Result`: string — value of the snippet's `return` statement (empty when omitted) +- `Logs`: string[] — execution diagnostics from the dynamic-code runner, not Unity Console entries +- `CompilationErrors`: object[] — Roslyn diagnostics with `Message`, `Line`, `Column`, `ErrorCode`, optional `Hint` and `Suggestions` +- `ErrorMessage`: string — top-level failure summary (empty on success) +- `Error`: string — alias of `ErrorMessage` +- `SecurityLevel`: string — dynamic-code security level active for the request +- `UpdatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) +- `DiagnosticsSummary`: string|null — compact summary when diagnostics are available +- `Diagnostics`: object[] — structured diagnostics; same shape as `CompilationErrors`, usually populated together with it + +Use `uloop get-logs` to retrieve `Debug.Log`, `Debug.LogWarning`, and `Debug.LogError` messages emitted by the snippet. On `Success: false`, inspect `CompilationErrors` first. If empty, read `ErrorMessage` (and `Logs` for extra context) — the failure may be a runtime exception, security violation, cancellation, or an "execution in progress" rejection, all of which return empty `CompilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. + +## Code Examples by Category + +For detailed code examples, refer to these files: + +- **Prefab operations**: See [references/prefab-operations.md](references/prefab-operations.md) + - Create prefabs, instantiate, add components, modify properties +- **Material operations**: See [references/material-operations.md](references/material-operations.md) + - Create materials, set shaders/textures, modify properties +- **Asset operations**: See [references/asset-operations.md](references/asset-operations.md) + - Find/search assets, duplicate, move, rename, load +- **ScriptableObject**: See [references/scriptableobject.md](references/scriptableobject.md) + - Create ScriptableObjects, modify with SerializedObject +- **Scene operations**: See [references/scene-operations.md](references/scene-operations.md) + - Create/modify GameObjects, set parents, wire references, load scenes +- **Batch operations**: See [references/batch-operations.md](references/batch-operations.md) + - Bulk modify objects, batch add/remove components, rename, layer/tag/material replacement +- **Cleanup operations**: See [references/cleanup-operations.md](references/cleanup-operations.md) + - Detect broken scripts, missing references, unused materials, empty GameObjects +- **Undo operations**: See [references/undo-operations.md](references/undo-operations.md) + - Undo-aware operations: RecordObject, AddComponent, SetParent, grouping +- **Selection operations**: See [references/selection-operations.md](references/selection-operations.md) + - Get/set selection, multi-select, filter by type/editability +- **PlayMode automation (zsh)**: See [references/playmode-automation-zsh.md](references/playmode-automation-zsh.md) + - Click UI buttons, invoke methods, set fields, tool combination workflows for zsh users +- **PlayMode automation (PowerShell)**: See [references/playmode-automation-powershell.md](references/playmode-automation-powershell.md) + - Click UI buttons, invoke methods, set fields, tool combination workflows for PowerShell users +- **PlayMode UI controls**: See [references/playmode-ui-controls.md](references/playmode-ui-controls.md) + - InputField, Slider, Toggle, Dropdown, drag & drop simulation, list all UI controls +- **PlayMode inspection**: See [references/playmode-inspection.md](references/playmode-inspection.md) + - Scene info, game state via reflection, physics state, raycast checks, GameObject search, position/rotation diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/SKILL.md.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/SKILL.md.meta new file mode 100644 index 000000000..f80665650 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/SKILL.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2445f075f1af349fc8bf73259f1df493 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references.meta new file mode 100644 index 000000000..589fcf4bb --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c5155f736569e440ea4679a66a5ee36b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/asset-operations.md b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/asset-operations.md new file mode 100644 index 000000000..31ddf7adc --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/asset-operations.md @@ -0,0 +1,194 @@ +# Asset Operations + +Code examples for AssetDatabase operations using `execute-dynamic-code`. + +## Find Assets by Type + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab"); +List paths = new List(); + +foreach (string guid in prefabGuids) +{ + paths.Add(AssetDatabase.GUIDToAssetPath(guid)); +} +return $"Found {paths.Count} prefabs"; +``` + +## Find Assets by Name + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string searchName = "Player"; +string[] guids = AssetDatabase.FindAssets(searchName); +List paths = new List(); + +foreach (string guid in guids) +{ + paths.Add(AssetDatabase.GUIDToAssetPath(guid)); +} +return $"Found {paths.Count} assets matching '{searchName}'"; +``` + +## Find Assets in Folder + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string folder = "Assets/Prefabs"; +string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { folder }); +List paths = new List(); + +foreach (string guid in guids) +{ + paths.Add(AssetDatabase.GUIDToAssetPath(guid)); +} +return $"Found {paths.Count} prefabs in {folder}"; +``` + +## Duplicate Asset + +```csharp +using UnityEditor; + +string sourcePath = "Assets/Materials/MyMaterial.mat"; +string destPath = "Assets/Materials/MyMaterial_Backup.mat"; + +bool success = AssetDatabase.CopyAsset(sourcePath, destPath); +return success ? $"Copied to {destPath}" : "Copy failed"; +``` + +## Move Asset + +```csharp +using UnityEditor; + +string sourcePath = "Assets/OldFolder/MyAsset.asset"; +string destPath = "Assets/NewFolder/MyAsset.asset"; + +string error = AssetDatabase.MoveAsset(sourcePath, destPath); +return string.IsNullOrEmpty(error) ? $"Moved to {destPath}" : $"Error: {error}"; +``` + +## Rename Asset + +```csharp +using UnityEditor; + +string assetPath = "Assets/Materials/OldName.mat"; +string newName = "NewName"; + +string error = AssetDatabase.RenameAsset(assetPath, newName); +return string.IsNullOrEmpty(error) ? $"Renamed to {newName}" : $"Error: {error}"; +``` + +## Rename Asset (Undo-supported) + +```csharp +using UnityEditor; + +// ObjectNames.SetNameSmart() supports Undo (AssetDatabase.RenameAsset() does NOT) +Object selected = Selection.activeObject; +if (selected == null) +{ + return "No asset selected"; +} + +string oldName = selected.name; +ObjectNames.SetNameSmart(selected, "NewName"); +AssetDatabase.SaveAssets(); +return $"Renamed {oldName} to {selected.name}"; +``` + +## Get Asset Path from Object + +```csharp +using UnityEditor; + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No object selected"; +} + +string path = AssetDatabase.GetAssetPath(selected); +if (string.IsNullOrEmpty(path)) +{ + return "Selected object is not an asset (scene object)"; +} +return $"Asset path: {path}"; +``` + +## Load Asset at Path + +```csharp +using UnityEditor; + +string path = "Assets/Prefabs/Player.prefab"; +GameObject asset = AssetDatabase.LoadAssetAtPath(path); + +if (asset == null) +{ + return $"Asset not found at {path}"; +} +return $"Loaded: {asset.name}"; +``` + +## Get All Assets of Type + +```csharp +using UnityEditor; + +string[] scriptGuids = AssetDatabase.FindAssets("t:MonoScript"); +int count = 0; + +foreach (string guid in scriptGuids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + if (path.StartsWith("Assets/")) + { + count++; + } +} +return $"Found {count} scripts in Assets folder"; +``` + +## Check if Asset Exists + +```csharp +using UnityEditor; + +string path = "Assets/Prefabs/Player.prefab"; +string guid = AssetDatabase.AssetPathToGUID(path); + +bool exists = !string.IsNullOrEmpty(guid); +return exists ? $"Asset exists: {path}" : $"Asset not found: {path}"; +``` + +## Get Asset Dependencies + +```csharp +using UnityEditor; + +string assetPath = "Assets/Prefabs/Player.prefab"; +string[] dependencies = AssetDatabase.GetDependencies(assetPath, true); + +return $"Asset has {dependencies.Length} dependencies"; +``` + +## Refresh AssetDatabase + +Use this after terminal-based asset file changes when Unity needs to import them and generate `.meta` files. + +```csharp +using UnityEditor; + +AssetDatabase.Refresh(); +return "AssetDatabase refreshed"; +``` diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/asset-operations.md.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/asset-operations.md.meta new file mode 100644 index 000000000..85d280356 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/asset-operations.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 39fd76f51419343c1bbc8844b9e1f45f +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/batch-operations.md b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/batch-operations.md new file mode 100644 index 000000000..875c3fc65 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/batch-operations.md @@ -0,0 +1,399 @@ +# Batch Operations + +Code examples for batch processing using `execute-dynamic-code`. + +## Batch Modify Selected Objects + +```csharp +using UnityEditor; +using System.Collections.Generic; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Modify"); + +foreach (GameObject obj in selected) +{ + Undo.RecordObject(obj.transform, ""); + obj.transform.localScale = Vector3.one * 2; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Scaled {selected.Length} objects (Single undo step)"; +``` + +## Edit Multiple Objects with SerializedObject + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +List transforms = new List(); +foreach (GameObject obj in selected) +{ + transforms.Add(obj.transform); +} + +SerializedObject serializedObj = new SerializedObject(transforms.ToArray()); +SerializedProperty positionProp = serializedObj.FindProperty("m_LocalPosition"); +positionProp.vector3Value = Vector3.zero; +serializedObj.ApplyModifiedProperties(); + +return $"Reset position of {selected.Length} objects"; +``` + +## Batch Add Component + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Add Rigidbody"); + +int addedCount = 0; +foreach (GameObject obj in selected) +{ + if (obj.GetComponent() == null) + { + Undo.AddComponent(obj); + addedCount++; + } +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Added Rigidbody to {addedCount} objects"; +``` + +## Batch Process Assets with StartAssetEditing + +```csharp +using UnityEditor; + +string[] guids = AssetDatabase.FindAssets("t:Material", new[] { "Assets/Materials" }); +if (guids.Length == 0) +{ + return "No materials found"; +} + +AssetDatabase.StartAssetEditing(); +try +{ + int modified = 0; + foreach (string guid in guids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + Material mat = AssetDatabase.LoadAssetAtPath(path); + if (mat != null) + { + mat.color = Color.white; + EditorUtility.SetDirty(mat); + modified++; + } + } + + AssetDatabase.SaveAssets(); + return $"Reset color of {modified} materials"; +} +finally +{ + AssetDatabase.StopAssetEditing(); +} + +``` + +## Batch Rename GameObjects + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Rename"); + +for (int i = 0; i < selected.Length; i++) +{ + Undo.RecordObject(selected[i], ""); + selected[i].name = $"Item_{i:D3}"; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Renamed {selected.Length} objects"; +``` + +## Batch Set Layer + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int layer = LayerMask.NameToLayer("Default"); + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Set Layer"); + +foreach (GameObject obj in selected) +{ + Undo.RecordObject(obj, ""); + obj.layer = layer; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Set layer of {selected.Length} objects to Default"; +``` + +## Batch Set Tag + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Set Tag"); + +foreach (GameObject obj in selected) +{ + Undo.RecordObject(obj, ""); + obj.tag = "Enemy"; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Tagged {selected.Length} objects as Enemy"; +``` + +## Batch Modify ScriptableObjects + +```csharp +using UnityEditor; + +string[] guids = AssetDatabase.FindAssets("t:ScriptableObject", new[] { "Assets/Data" }); +if (guids.Length == 0) +{ + return "No ScriptableObjects found"; +} + +int modified = 0; +foreach (string guid in guids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + ScriptableObject so = AssetDatabase.LoadAssetAtPath(path); + if (so == null) continue; + + SerializedObject serializedObj = new SerializedObject(so); + SerializedProperty prop = serializedObj.FindProperty("isEnabled"); + if (prop != null) + { + prop.boolValue = true; + serializedObj.ApplyModifiedProperties(); + EditorUtility.SetDirty(so); + modified++; + } +} + +AssetDatabase.SaveAssets(); +return $"Enabled {modified} ScriptableObjects"; +``` + +## Batch Remove Component + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Remove Rigidbody"); + +int removedCount = 0; +foreach (GameObject obj in selected) +{ + Rigidbody rb = obj.GetComponent(); + if (rb != null) + { + Undo.DestroyObjectImmediate(rb); + removedCount++; + } +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Removed Rigidbody from {removedCount} objects"; +``` + +## Batch Set Static Flags + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Set Static"); + +foreach (GameObject obj in selected) +{ + Undo.RecordObject(obj, ""); + GameObjectUtility.SetStaticEditorFlags(obj, StaticEditorFlags.BatchingStatic | StaticEditorFlags.OccludeeStatic); +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Set static flags on {selected.Length} objects"; +``` + +## Batch Process with Progress Bar + +```csharp +using UnityEditor; + +string[] guids = AssetDatabase.FindAssets("t:Texture2D"); +if (guids.Length == 0) +{ + return "No textures found"; +} + +int processed = 0; +foreach (string guid in guids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter; + if (importer != null && importer.maxTextureSize > 1024) + { + importer.maxTextureSize = 1024; + importer.SaveAndReimport(); + processed++; + } + + if (processed % 10 == 0) + { + EditorUtility.DisplayProgressBar("Processing Textures", path, (float)processed / guids.Length); + } +} + +EditorUtility.ClearProgressBar(); +return $"Resized {processed} textures to max 1024"; +``` + +## Batch Align Objects + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length < 2) +{ + return "Select at least 2 objects"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Align Objects"); + +float startX = selected[0].transform.position.x; +float spacing = 2f; + +for (int i = 0; i < selected.Length; i++) +{ + Undo.RecordObject(selected[i].transform, ""); + Vector3 pos = selected[i].transform.position; + pos.x = startX + (i * spacing); + selected[i].transform.position = pos; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Aligned {selected.Length} objects with {spacing}m spacing"; +``` + +## Batch Rename Assets (Undo-supported) + +```csharp +using UnityEditor; + +// ObjectNames.SetNameSmart() supports Undo (AssetDatabase.RenameAsset() does NOT) +Object[] selected = Selection.objects; +if (selected.Length == 0) +{ + return "No assets selected"; +} + +for (int i = 0; i < selected.Length; i++) +{ + string newName = $"{i:D3}_{selected[i].name}"; + ObjectNames.SetNameSmart(selected[i], newName); +} + +AssetDatabase.SaveAssets(); +return $"Renamed {selected.Length} assets"; +``` + +## Batch Replace Material + +```csharp +using UnityEditor; + +GameObject[] selected = Selection.gameObjects; +if (selected.Length == 0) +{ + return "No GameObjects selected"; +} + +string materialPath = "Assets/Materials/NewMaterial.mat"; +Material newMat = AssetDatabase.LoadAssetAtPath(materialPath); +if (newMat == null) +{ + return $"Material not found at {materialPath}"; +} + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Batch Replace Material"); + +int replaced = 0; +foreach (GameObject obj in selected) +{ + MeshRenderer renderer = obj.GetComponent(); + if (renderer != null) + { + Undo.RecordObject(renderer, ""); + renderer.sharedMaterial = newMat; + replaced++; + } +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Replaced material on {replaced} objects"; +``` diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/batch-operations.md.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/batch-operations.md.meta new file mode 100644 index 000000000..36f62bebf --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/batch-operations.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cd5beae8d98da49f590990720e8d86be +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/cleanup-operations.md b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/cleanup-operations.md new file mode 100644 index 000000000..40eec9a52 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/cleanup-operations.md @@ -0,0 +1,403 @@ +# Cleanup Operations + +Code examples for project cleanup operations using `execute-dynamic-code`. + +## Detect Missing Scripts on GameObject + +```csharp +using UnityEditor; + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No GameObject selected"; +} + +int missingCount = GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(selected); +return $"{selected.name} has {missingCount} missing script(s)"; +``` + +## Remove Missing Scripts from GameObject + +```csharp +using UnityEditor; + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No GameObject selected"; +} + +int removedCount = GameObjectUtility.RemoveMonoBehavioursWithMissingScript(selected); +return $"Removed {removedCount} missing script(s) from {selected.name}"; +``` + +## Scan Scene for Missing Scripts + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List objectsWithMissing = new List(); + +foreach (GameObject obj in allObjects) +{ + int count = GameObjectUtility.GetMonoBehavioursWithMissingScriptCount(obj); + if (count > 0) + { + objectsWithMissing.Add($"{obj.name} ({count})"); + } +} + +if (objectsWithMissing.Count == 0) +{ + return "No missing scripts found in scene"; +} + +return $"Objects with missing scripts: {string.Join(", ", objectsWithMissing)}"; +``` + +## Remove All Missing Scripts from Scene + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +int totalRemoved = 0; + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Remove All Missing Scripts"); + +foreach (GameObject obj in allObjects) +{ + int removed = GameObjectUtility.RemoveMonoBehavioursWithMissingScript(obj); + totalRemoved += removed; +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Removed {totalRemoved} missing scripts from scene"; +``` + +## Detect Missing References in Component + +```csharp +using UnityEditor; + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No GameObject selected"; +} + +List missingRefs = new List(); + +Component[] components = selected.GetComponents(); +foreach (Component comp in components) +{ + if (comp == null) continue; + + SerializedObject so = new SerializedObject(comp); + SerializedProperty prop = so.GetIterator(); + + while (prop.NextVisible(true)) + { + if (prop.propertyType == SerializedPropertyType.ObjectReference) + { + if (prop.objectReferenceValue == null && prop.objectReferenceInstanceIDValue != 0) + { + missingRefs.Add($"{comp.GetType().Name}.{prop.name}"); + } + } + } +} + +if (missingRefs.Count == 0) +{ + return "No missing references found"; +} + +return $"Missing references: {string.Join(", ", missingRefs)}"; +``` + +## Scan Scene for Missing References + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List results = new List(); + +foreach (GameObject obj in allObjects) +{ + Component[] components = obj.GetComponents(); + foreach (Component comp in components) + { + if (comp == null) continue; + + SerializedObject so = new SerializedObject(comp); + SerializedProperty prop = so.GetIterator(); + + while (prop.NextVisible(true)) + { + if (prop.propertyType == SerializedPropertyType.ObjectReference) + { + if (prop.objectReferenceValue == null && prop.objectReferenceInstanceIDValue != 0) + { + results.Add($"{obj.name}/{comp.GetType().Name}.{prop.name}"); + } + } + } + } +} + +if (results.Count == 0) +{ + return "No missing references found in scene"; +} + +return $"Missing references ({results.Count}): {string.Join(", ", results.Take(10))}..."; +``` + +## Find Unused Materials in Project + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string[] materialGuids = AssetDatabase.FindAssets("t:Material"); +HashSet usedMaterials = new HashSet(); + +string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab"); +foreach (string guid in prefabGuids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + string[] deps = AssetDatabase.GetDependencies(path, true); + foreach (string dep in deps) + { + if (dep.EndsWith(".mat")) + { + usedMaterials.Add(dep); + } + } +} + +// This scan only checks prefab dependencies. Verify scene and other asset +// references manually before deleting any reported materials. +List unusedMaterials = new List(); +foreach (string guid in materialGuids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + if (!usedMaterials.Contains(path)) + { + unusedMaterials.Add(path); + } +} + +return $"Found {unusedMaterials.Count} materials not referenced by prefabs. Verify scene and other asset references manually before deleting."; +``` + +## Find Empty GameObjects + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List emptyObjects = new List(); + +foreach (GameObject obj in allObjects) +{ + Component[] components = obj.GetComponents(); + if (components.Length == 1 && obj.transform.childCount == 0) + { + emptyObjects.Add(obj.name); + } +} + +if (emptyObjects.Count == 0) +{ + return "No empty GameObjects found"; +} + +return $"Empty objects ({emptyObjects.Count}): {string.Join(", ", emptyObjects.Take(20))}"; +``` + +## Find Duplicate Names in Hierarchy + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +Dictionary nameCounts = new Dictionary(); + +foreach (GameObject obj in allObjects) +{ + if (nameCounts.ContainsKey(obj.name)) + { + nameCounts[obj.name]++; + } + else + { + nameCounts[obj.name] = 1; + } +} + +List duplicates = new List(); +foreach (KeyValuePair kvp in nameCounts) +{ + if (kvp.Value > 1) + { + duplicates.Add($"{kvp.Key} ({kvp.Value})"); + } +} + +if (duplicates.Count == 0) +{ + return "No duplicate names found"; +} + +return $"Duplicate names: {string.Join(", ", duplicates.Take(15))}"; +``` + +## Check for Broken Prefab Instances + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List brokenPrefabs = new List(); + +foreach (GameObject obj in allObjects) +{ + if (PrefabUtility.IsPartOfPrefabInstance(obj)) + { + GameObject prefabAsset = PrefabUtility.GetCorrespondingObjectFromSource(obj); + if (prefabAsset == null) + { + brokenPrefabs.Add(obj.name); + } + } +} + +if (brokenPrefabs.Count == 0) +{ + return "No broken prefab instances found"; +} + +return $"Broken prefab instances: {string.Join(", ", brokenPrefabs)}"; +``` + +## Find Objects with Negative Scale + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); +List negativeScale = new List(); + +foreach (GameObject obj in allObjects) +{ + Vector3 scale = obj.transform.localScale; + if (scale.x < 0 || scale.y < 0 || scale.z < 0) + { + negativeScale.Add($"{obj.name} ({scale})"); + } +} + +if (negativeScale.Count == 0) +{ + return "No objects with negative scale found"; +} + +return $"Negative scale objects: {string.Join(", ", negativeScale.Take(10))}"; +``` + +## Remove Empty Leaf GameObjects + +```csharp +using UnityEditor; + +GameObject[] allObjects = Object.FindObjectsByType(FindObjectsSortMode.None); + +int undoGroup = Undo.GetCurrentGroup(); +Undo.SetCurrentGroupName("Remove Empty Leaves"); + +int removedCount = 0; +foreach (GameObject obj in allObjects) +{ + if (obj == null) continue; + + Component[] components = obj.GetComponents(); + if (components.Length == 1 && obj.transform.childCount == 0) + { + Undo.DestroyObjectImmediate(obj); + removedCount++; + } +} + +Undo.CollapseUndoOperations(undoGroup); +return $"Removed {removedCount} empty leaf GameObjects"; +``` + +## Find Large Meshes + +```csharp +using UnityEditor; + +string[] meshGuids = AssetDatabase.FindAssets("t:Mesh"); +List largeMeshes = new List(); +int threshold = 10000; + +foreach (string guid in meshGuids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + Mesh mesh = AssetDatabase.LoadAssetAtPath(path); + if (mesh != null && mesh.vertexCount > threshold) + { + largeMeshes.Add($"{path} ({mesh.vertexCount} verts)"); + } +} + +if (largeMeshes.Count == 0) +{ + return $"No meshes with more than {threshold} vertices found"; +} + +return $"Large meshes: {string.Join(", ", largeMeshes.Take(10))}"; +``` + +## Validate Asset References + +```csharp +using UnityEditor; + +string[] guids = AssetDatabase.FindAssets("t:ScriptableObject", new[] { "Assets/Data" }); +List invalidRefs = new List(); + +foreach (string guid in guids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + ScriptableObject so = AssetDatabase.LoadAssetAtPath(path); + if (so == null) continue; + + SerializedObject serializedObj = new SerializedObject(so); + SerializedProperty prop = serializedObj.GetIterator(); + + while (prop.NextVisible(true)) + { + if (prop.propertyType == SerializedPropertyType.ObjectReference) + { + if (prop.objectReferenceValue == null && prop.objectReferenceInstanceIDValue != 0) + { + invalidRefs.Add($"{path}: {prop.name}"); + } + } + } +} + +if (invalidRefs.Count == 0) +{ + return "All asset references are valid"; +} + +return $"Invalid references ({invalidRefs.Count}): {string.Join(", ", invalidRefs.Take(10))}"; +``` diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/cleanup-operations.md.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/cleanup-operations.md.meta new file mode 100644 index 000000000..bb954e6cb --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/cleanup-operations.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 13f19aa1f6ea44f119e9e253e14d731c +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/material-operations.md b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/material-operations.md new file mode 100644 index 000000000..3bd98697a --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/material-operations.md @@ -0,0 +1,160 @@ +# Material Operations + +Code examples for Material operations using `execute-dynamic-code`. + +## Create a New Material + +```csharp +using UnityEditor; + +Shader shader = Shader.Find("Standard"); +Material mat = new Material(shader); +mat.name = "MyMaterial"; +string path = "Assets/Materials/MyMaterial.mat"; +AssetDatabase.CreateAsset(mat, path); +AssetDatabase.SaveAssets(); +return $"Material created at {path}"; +``` + +## Set Material Color + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +Material mat = AssetDatabase.LoadAssetAtPath(matPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} + +mat.SetColor("_Color", new Color(1f, 0.5f, 0f, 1f)); +EditorUtility.SetDirty(mat); +AssetDatabase.SaveAssets(); +return "Material color set to orange"; +``` + +## Set Material Properties (Float, Vector) + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +Material mat = AssetDatabase.LoadAssetAtPath(matPath); + +mat.SetFloat("_Metallic", 0.8f); +mat.SetFloat("_Glossiness", 0.6f); +mat.SetVector("_EmissionColor", new Vector4(1, 1, 0, 1)); + +EditorUtility.SetDirty(mat); +AssetDatabase.SaveAssets(); +return "Material properties updated"; +``` + +## Assign Texture to Material + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +string texPath = "Assets/Textures/MyTexture.png"; + +Material mat = AssetDatabase.LoadAssetAtPath(matPath); +Texture2D tex = AssetDatabase.LoadAssetAtPath(texPath); +if (mat == null) +{ + return $"Material not found at {matPath}"; +} +if (tex == null) +{ + return $"Texture not found at {texPath}"; +} + +mat.SetTexture("_MainTex", tex); +EditorUtility.SetDirty(mat); +AssetDatabase.SaveAssets(); +return $"Assigned {tex.name} to material"; +``` + +## Assign Material to GameObject + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +Material mat = AssetDatabase.LoadAssetAtPath(matPath); + +GameObject selected = Selection.activeGameObject; +if (selected == null) +{ + return "No GameObject selected"; +} + +Renderer renderer = selected.GetComponent(); +if (renderer == null) +{ + return "Selected object has no Renderer"; +} + +renderer.sharedMaterial = mat; +EditorUtility.SetDirty(selected); +return $"Assigned {mat.name} to {selected.name}"; +``` + +## Enable/Disable Material Keywords + +```csharp +using UnityEditor; + +string matPath = "Assets/Materials/MyMaterial.mat"; +Material mat = AssetDatabase.LoadAssetAtPath(matPath); + +mat.EnableKeyword("_EMISSION"); +mat.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive; + +EditorUtility.SetDirty(mat); +AssetDatabase.SaveAssets(); +return "Emission enabled on material"; +``` + +## Find All Materials Using a Shader + +```csharp +using UnityEditor; +using System.Collections.Generic; + +string shaderName = "Standard"; +string[] guids = AssetDatabase.FindAssets("t:Material"); +List matchingMaterials = new List(); + +foreach (string guid in guids) +{ + string path = AssetDatabase.GUIDToAssetPath(guid); + Material mat = AssetDatabase.LoadAssetAtPath(path); + if (mat != null && mat.shader != null && mat.shader.name == shaderName) + { + matchingMaterials.Add(path); + } +} +return $"Found {matchingMaterials.Count} materials using {shaderName}"; +``` + +## Duplicate Material + +```csharp +using UnityEditor; + +string sourcePath = "Assets/Materials/MyMaterial.mat"; +string destPath = "Assets/Materials/MyMaterial_Copy.mat"; + +Material source = AssetDatabase.LoadAssetAtPath(sourcePath); +if (source == null) +{ + return $"Material not found at {sourcePath}"; +} + +Material copy = new Material(source); +AssetDatabase.CreateAsset(copy, destPath); +AssetDatabase.SaveAssets(); +return $"Material duplicated to {destPath}"; +``` diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/material-operations.md.meta b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/material-operations.md.meta new file mode 100644 index 000000000..48b907641 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/material-operations.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6b5ad08d159fb4390911b484746791d1 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/playmode-automation-powershell.md b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/playmode-automation-powershell.md new file mode 100644 index 000000000..dd6fb0173 --- /dev/null +++ b/Client/Packages/io.github.hatayama.uloopmcp/Editor/Api/McpTools/ExecuteDynamicCode/Skill/references/playmode-automation-powershell.md @@ -0,0 +1,330 @@ +# PlayMode Automation (PowerShell) + +Code examples for runtime automation during Play mode using `execute-dynamic-code`. +These examples manipulate live scene objects while the game is running. +Shell command examples in this file target `PowerShell`. + +## When to use dedicated mouse simulation tools instead + +The examples in this file call UI handlers or runtime methods from C#. +That remains the better choice when you want targeted automation, direct state control, or a quick diagnostic path. +Use dedicated mouse tools only when the input route itself is part of what you need to verify. + +Choose the tool based on what you are trying to validate: + +| Scenario | Recommended tool | Why | +|----------|------------------|-----| +| Verify that a uGUI element responds through the real EventSystem pointer path | `simulate-mouse-ui` | Fires `PointerDown` / `PointerUp` / `PointerClick` / drag events through EventSystem raycasts instead of bypassing the UI input route. | +| Test gameplay that reads `Mouse.current`, button state, delta, or scroll | `simulate-mouse-input` | Injects Input System mouse state into `Mouse.current`, so game code can observe `wasPressedThisFrame`, movement delta, and scroll like player input. This assumes the project uses the New Input System (`Input System Package (New)` or `Both`). If that is not available in the target project, prefer `execute-dynamic-code` for a project-specific workaround instead of changing project settings just to use this tool. | +| Jump straight to a known button callback, invoke a method, inspect state, or set up a test precondition | `execute-dynamic-code` | Best when you intentionally want direct automation without reproducing the full input pipeline. | +| Drive custom runtime behavior that does not map cleanly to the built-in mouse tools | `execute-dynamic-code` | Lets you call project-specific methods, inspect scene objects, and prototype one-off flows immediately. | + +In short: do not default everything to mouse simulation. +Use `execute-dynamic-code` for direct automation and diagnostics, and switch to `simulate-mouse-ui` or `simulate-mouse-input` when reproducing the real input path is the thing you need to test. + +## PowerShell Quoting Notes + +Use these patterns when you need shell-safe inline code: + +### Double quotes inside C# strings + +Single-quote the whole snippet and keep C# string literals unchanged. + +```powershell +uloop execute-dynamic-code --code 'return "Hello from PowerShell";' +``` + +### Single quotes inside inline C# code + +If the C# snippet itself contains a single quote, double it inside the PowerShell single-quoted string. + +```powershell +uloop execute-dynamic-code --code 'char initial = ''A''; return initial.ToString();' +``` + +### JSON-like values passed via `--parameters` + +Wrap the whole expression in single quotes so PowerShell passes the inner double quotes through unchanged. + +```powershell +uloop execute-dynamic-code --code 'return parameters["param0"];' --parameters '{"param0":"Hello from PowerShell"}' +``` + +### Multi-line C# snippets + +Use a here-string when the snippet spans multiple lines. + +```powershell +$code = @' +using UnityEngine; + +GameObject obj = GameObject.Find("Player"); +if (obj == null) return "Player not found"; + +return obj.name; +'@ + +uloop execute-dynamic-code --code $code +``` + +You can combine multi-line code with multi-line parameters the same way. + +```powershell +$code = @' +return parameters["param0"]; +'@ + +$parameters = @' +{"param0":"Hello from PowerShell"} +'@ + +uloop execute-dynamic-code --code $code --parameters $parameters +``` + +## Click UI Button by Path + +```csharp +using UnityEngine.UI; + +Button btn = GameObject.Find("Canvas/StartButton")?.GetComponent