csharp-game-editor
Give it one feature for the Unity project open in your editor, and it builds the scripts, objects and scene in one plan, stops that plan when the compile reports an error CS line, and plays the scene when the compile is clean. Built on MCP for Unity (CoplayDev/unity-mcp) 10.3.0. Scenes, GameObjects, C# scripts, prefabs, play mode, the console, EditMode and PlayMode tests, and player builds stay on that same path.
There are 22 tools: 19 unity_* tools (unity_editor, unity_scene, unity_object, unity_script, unity_asset, unity_look, unity_ui, unity_vfx, unity_animation, unity_graphics, unity_physics, unity_probuilder, unity_profiler, unity_build, unity_packages, unity_code, unity_tests, unity_docs, unity_generate) plus plan_batch, apply_plan and confirm_risky_plan. Together they reach every operation and resource of MCP for Unity 10.3.0 (409). The plugin adds its own: component_values, asset_find, prefab_info, read_text, create_asmdef, and describe on every unity_* tool. Each tool definition is under 5 KB; describe returns one op's arguments in full and what it does inside.
Install
You need macOS, Hermes 0.21.6 or newer, Unity Hub with a Unity 6 editor, git, and uv. This was tried with Hermes 0.22.2 and Unity 6000.6.6f1 on a new project.
-
Add MCP for Unity 10.3.0 to the Unity project. In Unity, Window > Package Manager > + > Add package from git URL, and paste the URL below. Or add the same line to
Packages/manifest.jsonunderdependencies. Unity fetches it from GitHub."com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v10.3.0" -
Set MCP for Unity to stdio: Window > MCP for Unity, and choose stdio as the transport. That stores
MCPForUnity.UseHttpTransport = falsein EditorPrefs. With stdio the editor starts its connection every time it opens, so there is no Start button to press each session. -
Install the plugin. From this directory:
hermes plugins install "file://${PWD}" --enable --yes-deps -
Open the project in Unity and ask Hermes, for example "what is in my Unity scene?". The first call starts MCP for Unity's server (
uvxpreparesmcpforunityserver10.3.0 once).
If you keep MCP for Unity on HTTP (its default for a new install), press Start in Window > MCP for Unity each session; the plugin then uses that server on http://127.0.0.1:8080. That path was not tried end to end here.
How a change runs
- A
unity_*tool reads a.unity,.prefab,.asset, or.csfile from disk at once (read_text,asset_find,prefab_info, anddescribe).set_active_instanceonly picks which editor later plans use. Any read that asks the editor (hierarchy, console, component values, reflection, test jobs) comes back as a plan, and that step is not sent. Making the plan does not run C# in the editor.unity_docsis not marked read-only, because every docs op sends the query to docs.unity3d.com, so Hermes asks, and the call itself is a plan. plan_batchtakes the steps of one feature. It does not run the steps. It returns the plan in words, aplan_id, and a fingerprint of the editor: the selected editor, play mode, the open scenes, their objects, and each file the plan names. Component values are not in that fingerprint, because reading them runs C# in the editor.apply_planis not marked read-only, so withtrust: untrustedHermes asks once before it runs. That dialog names only the tool, so the skill tells the model to show the plan text first.apply_planruns the newest plan once, in order, within 10 minutes. A newer plan replaces an older one. If the editor no longer matches the fingerprint, nothing runs and the reply names what changed. A short failure before anything runs (Unity busy, editor reloading) keeps the plan.
A plan that deletes, runs editor code (execute_code, a menu item, a custom tool, a package's import, a script Unity runs by itself), can lose unsaved work, saves unsaved changes over a scene file, or sends or writes outside the project also asks a second time. The question is the full summary: the code is not shortened. It is sent only when Hermes 0.22.2's own approval cards can show every line. Discord and Slack count raw characters (the summary stays within 1,650, and Slack's card allows 2,800). Telegram counts the HTML-escaped text in UTF-16 code units (the card allows 3,800), so a question full of & that still fits 1,650 raw characters is not sent when the escaped card would cut it. A plan whose summary does not fit sends no question and asks for confirm_risky_plan. The plan text still has the full code. Hermes 0.22.2's terminal CLI cannot show an MCP question (it fails them closed), so there too apply_plan runs nothing and asks for confirm_risky_plan, which Hermes asks about once more. Characters that hide or reorder text (line and paragraph separators, carriage returns, bidi marks) are shown escaped, as \u2028. tests/test_gateway_render.py renders those cards.
Play mode and tests run the project's code; the plan says so, and they do not get the second question.
What a step does inside
The "inside:" lines of a plan are a draft from MCP for Unity's own C# source (tools/extract_side_effects.py, tools/side_effects.md). They are not the decision that a step succeeded. The plugin also writes what it measured:
run_testsand a player build save every open scene with unsaved changes first (TestRunnerService.cs:569,BuildRunner.cs:265). The plan names the scene. Changes made before the plan get the second question.manage_scriptable_object.createwithoverwritetrue deletes or overwrites the existing asset (ManageScriptableObject.cs:169). The plan lists every serialized field of the one MonoBehaviour document on disk, including fields inside lists. The characters a text edit removes are listed the same way, and a removal is in the second question. If that document cannot be read (not one MonoBehaviour YAML document), there is no plan. The value in the editor is read whenapply_planruns, and the replace is sent only when that read is not dirty. A dirty value is named in the error and is not overwritten. Withoutoverwrite, an existing name gets the next free name.manage_gameobject.createwithprefab_pathloads and places the prefab; the prefab file is not written.- A script that Unity runs by itself after the compile is editor code: second question. Attributes are read the way C# writes them, including several names in one pair of brackets (
[Serializable, InitializeOnLoad]) and aglobal::prefix. A script the parser cannot finish is treated as editor code. A script in anEditorfolder, and an assembly definition whoseincludePlatformsnames Editor, get the second question. A script already on disk that Unity runs by itself is named when the plan plays or runs tests. One that changed on disk after the last compile is named when the plan compiles as well.
Done means read back
Before the steps run, apply_plan copies each open scene that has unsaved changes (a copy, so the scene stays unsaved), and the bytes of the files the plan names, including .meta files. After the steps it compares the project, leaving out Library, Temp, Logs, obj, .git, and hidden directories. UserSettings and files at the project root are included. Unity's own files (UserSettings/EditorUserSettings.asset, HierarchyWindow.settings, PlayModeUserSettings.asset, Search.settings, Search.index, window layouts under UserSettings/Layouts ending in .dwlt, ProjectSettings/SceneTemplateSettings.json, ProjectSettings/ProjectSettings.asset, and ProjectSettings/EditorSettings.asset) are named and not put back. A compile may rewrite *.csproj, *.sln, and *.slnx at the project root; those are named and not put back. Any other file that changed, and a file this apply created that the plan did not name, is put back, and the reply is an error that names it. A .meta stays when the asset or folder was already on disk before the plan. plan_batch only accepts steps, so the error does not ask for the file to be named later. Assets and folders under Assets that have no .meta when the plan is made are named in the plan, and the .meta Unity writes for them is an effect of the plan. A Unity .meta (it starts with fileFormatVersion) whose asset or folder is already missing is named too, and a change Unity makes to it is an effect of the plan. A .meta that does not start that way is still put back. When Packages/manifest.json lists a package that Packages/packages-lock.json does not, the plan names the lock file and Unity's rewrite of it is an effect of the plan. A lock that already lists every manifest package is still put back if it changes. A file that was on disk when the plan was made is put back even if it was already gone when the steps started. The plan names the .meta Unity writes for a new file, the .meta of a folder the plan creates, and every file inside a folder the plan deletes, so those are not put back. A package add or remove names Packages/manifest.json and Packages/packages-lock.json, and those two are not put back. A .csproj or .sln that a compile writes is named and not put back. A scene that was unsaved before the call is marked unsaved again after its bytes are put back: writing the old bytes alone leaves the scene clean.
"Done" is a separate measurement, not upstream's success sentence. Play mode counts only when the editor state says it is playing and the frame count advanced (MCP for Unity answers "Entered play mode." either way). A compile counts only after a compile started and finished, and the console then has no error CS from it. A test run counts only from the test job's per-test rows (include_details), checked against the summary (passed + failed + skipped equals total) and against progress (completed equals total). The job must have a start time and a finish time. The status word succeeded is not the result. MCP for Unity 10.3.0 does not write a result file. A failed test, 0 tests, a row count that does not match the summary, or a job with no row per test is an error. A player build counts only when the output path named in the plan is on disk and its bytes changed. Upstream's "succeeded" is quoted and is not the result. A path that was already there and did not change is an error. The first line says done or FAILED at step N of M.
run_tests is refused when an open scene has no path (an unsaved Untitled scene). Unity's test runner would open a save dialog (SaveModifiedSceneTask) and a GUI editor stops on that dialog. Save the scene, or close it, in the plan first.
After a step that changes scripts (a .cs or .asmdef, or a folder that holds them), the plugin asks Unity to refresh and compile (refresh_unity with a compile request) and waits for that compile to finish. A Unity editor in the background does not start that compile on its own. error CS lines from that compile stop the plan; later steps are not sent. A plan that plays or runs tests while the last compile has errors is refused when it is made. When a script changed on disk after the last compile, the plan compiles first, so play mode does not run old code. A file that is not UTF-8 text, has a byte-order mark, or is a nested or stripped prefab is refused and not interpreted.
After run_tests or a player build, apply_plan waits for the job. When the client sends a progress token, long steps send MCP progress notifications.
Approvals
A small feature (three scripts, a ground, a player, three coins, the camera, and a save) is one plan and one Hermes approval. Adding EditMode and PlayMode tests with their assembly definitions is one more plan. Running one test mode is one plan. Playing is one plan, or none if play is a step of the same plan. create_asmdef writes a game, EditMode, or PlayMode assembly definition, so tests need no execute_code. The same feature through MCP for Unity's own tools under trust: untrusted asked 12 times (picking the editor, three scripts, a compile request, six objects, a save), and play, a console read, and stop asked 3 more. Those calls returned in 27 seconds in all; the plan's one apply_plan took 49 seconds, because it waits for the compile and reads the editor back after each step.
What else a plan says
Each step lists what it does, every value (a script in full), each file it creates, replaces, or deletes, the code that runs, and what is lost: unsaved changes in a scene that a new scene closes, a load Unity refuses because the open scene has unsaved changes, changes made during play when play stops, a modify Unity refuses in play mode. Steps see the effect of earlier steps of the same plan, so saving a scene the plan just created is judged against that new scene.
batch_execute is shown as its steps, one per command, each checked like a step of its own and run one at a time. A batch inside a batch is refused. execute_code replay is shown and sent as the exact code from the history; history entries cut at 500 characters are refused. execute_custom_tool shows the source file of the custom tool; a tool whose source is not in the project is refused. A text edit carries the script's current sha256, so upstream refuses it if the file changes first. create_script with overwrite true replaces the whole file through a text edit guarded by its sha256 and keeps its .meta. The plan shows the file's current text. A file with text above its first using line is refused, because upstream refuses edits there. If that text cannot be shown, there is no plan.
Paths
Paths are decoded the way Unity decodes them and checked before a plan is made: Assets/Scripts/Evi%6C.cs is Assets/Scripts/Evil.cs in the plan. The arguments checked are the ones MCP for Unity's own code hands to a file system call (extracted from its source by tools/extract_unity_ops.py). A file named by folder and name (folder_path and asset_name, a script's path and name) is checked where upstream joins them. Paths outside Assets/ and Packages/, .., symbolic links, hidden files, and key or credential files (.env, keystores, .p8, .pem, .tfvars, and names such as api_keys.yaml or secrets.json) are refused. create_script writes .cs files only. A player build goes to <project>/Builds (upstream's default) and a memory snapshot to <project>/MemoryCaptures or Unity's temporary cache folder; inside Assets they are refused. Deleting Assets, Packages, or ProjectSettings as a whole is refused. File/Quit, File/Exit and Unity/Quit are refused.
component_values is a plan. Its C# runs inside apply_plan and reads fields through SerializedObject. A component that implements ISerializationCallbackReceiver is named but not read, because building a SerializedObject calls OnBeforeSerialize. MCP for Unity's own component resource calls property getters, so it is a planned step here, not a read.
read_text, asset_find, and prefab_info read files under Assets from disk. Unity is not asked, so no asset is loaded and no project code runs. MCP for Unity's manage_asset.search and manage_prefabs.get_info load each asset (AssetDatabase.LoadAssetAtPath), which runs a ScriptableObject's OnEnable and deserialization code, so they are planned steps here. component_values is a plan too: it runs bridge C# in the editor.
When a call fails
A refusal of the request ("Nothing was sent.") comes back as a normal reply with the accepted names, so a few wrong guesses do not trip Hermes' pause on the whole server.
changes Unity, so that step was not sent means the reply is a plan. Making that plan does not run C# in the editor. unknown op lists the tool's ops and the closest ones. does not take lists the op's arguments and types. replaced by a newer plan and older than 10 minutes mean plan again. changed after the plan means nothing ran. did not approve the plan in the second question means the user said no. call confirm_risky_plan means the second question could not be shown here. FAILED at step N of M names the step, why, and that later steps were not sent. error CS lines mean the compile failed. the last compile has errors means play or tests were refused when planned. more than one Unity editor is open lists the ids for set_active_instance. No Unity Editor instances found means the editor is not connected.
What stays on disk and who can use it
EditorPrefs are read and not written. With MCPForUnity.UseHttpTransport set to 0, the bridge starts MCP for Unity as a stdio child. With any other value or no key, it uses MCP for Unity's HTTP server on this machine (default http://127.0.0.1:8080) and starts one if none is listening. The child gets no HOME variable and has telemetry and the update check forced off. Its log goes to <HERMES_HOME>/plugin-data/agent-plugin-csharp-game-editor-<hash>/logs (one directory per profile; <hash> is the eight characters Hermes appends to the plugin id). MCP for Unity keeps customer_uuid.txt under ~/Library/Application Support/UnityMCP. uv keeps its package cache. An editor read is a plan and does not run C# in the editor. unity_code runs approved C# in the editor and is not sandboxed. unity_docs sends the query to docs.unity3d.com after Hermes asks. unity_packages add and remove change Packages/manifest.json and Packages/packages-lock.json; the plan names both, and they are not put back. unity_generate can call paid providers you configured in MCP for Unity; that was not tried.
Tools are on by default on every Hermes surface, including the gateway. A person the gateway admits can call them with the keys that session holds. The plugin keeps no user list of its own. In the CLI, the person at the terminal answers. On a gateway, only users the gateway admits (pairing or allowlist) can reach the tools, and the questions go to that chat, where an admitted user answers with /approve or /deny. Cron and sessions with nobody to answer decline, so a change does not run there. Each apply_plan asks again, even after a session answer. Stop the plugin with hermes plugins disable csharp-game-editor. To turn one surface off, use hermes tools.
Windows, Linux, the HTTP transport end to end, the desktop app's question panel, a real chat delivered by Telegram, Slack, or Discord (the cards were rendered with Hermes' adapters, not sent), chat apps with a smaller card such as WhatsApp (800 characters), and a paid generation call were not part of the check.