feat: add command gateway for multi-agent concurrent access#826
feat: add command gateway for multi-agent concurrent access#826Lint111 wants to merge 47 commits intoCoplayDev:betafrom
Conversation
Add ExecutionTier enum (Instant/Smooth/Heavy) to classify tool execution cost. Extend McpForUnityToolAttribute with Tier property (default Smooth). Annotate built-in tools: Heavy for manage_script, manage_shader, refresh_unity, run_tests. Instant for find_gameobjects, read_console, get_test_job. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CommandClassifier.Classify() inspects action parameters to override the declared tool tier (e.g., manage_scene.get_hierarchy -> Instant, manage_scene.load -> Heavy). ClassifyBatch() returns the highest tier across a set of commands for queue scheduling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BatchJob tracks ticket, agent, commands, results, and status. TicketStore manages CRUD operations, ticket generation, expiry cleanup, and per-agent statistics. Foundation for the command gateway queue. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CommandQueue provides FIFO dispatch with tier awareness: instant jobs execute inline, smooth jobs run when no heavy is active, heavy jobs get exclusive access. CommandGatewayState hooks into EditorApplication .update for per-frame queue processing. CommandRegistry extended with GetToolTier() and tier storage in HandlerInfo. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When async=true is passed, BatchExecute now routes through CommandGatewayState.Queue instead of executing inline. Returns a ticket for non-instant batches (poll via poll_job) or results directly for instant batches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PollJob (Instant tier) returns job status with position/progress/results. QueueStatus (Instant tier) returns queue depth, active heavy job info, smooth in-flight count, and per-agent statistics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
45 tests covering: - ExecutionTier enum ordering and attribute defaults (5) - CommandClassifier action-level overrides and batching (13) - TicketStore CRUD, expiry, agent stats (10) - CommandQueue submit, poll, cancel, queue depth (10) - BatchExecute async path, PollJob, QueueStatus integration (7) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Design for two fixes to the command gateway: 1. Domain-reload-causing operations (refresh_unity, play mode) are held in queue while tests are running or compilation is active 2. Queue state persists across domain reloads via SessionState Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7-task TDD plan for making the command gateway aware of async Unity operations (test runs, compilation) so domain-reload-causing commands are held until safe, and queue state survives domain reloads. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only refresh_unity (compile!=none) and manage_editor (play) trigger domain reload. Script/shader file ops are just disk I/O. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…hJob Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Domain-reload-causing jobs (refresh_unity compile, manage_editor play) are held in queue while TestJobManager.HasRunningJob or EditorApplication.isCompiling is true. Non-reload heavy jobs proceed normally. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a queued domain-reload job is blocked by editor activity (tests running, compiling), the poll_job response now includes a blocked_by field indicating the reason. Non-reload jobs return null. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TicketStore now supports ToJson/FromJson serialization. Running jobs are marked failed on restore (interrupted by reload), queued jobs survive and re-enter the heavy queue. CommandGatewayState hooks AssemblyReloadEvents to persist via SessionState. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When run_tests completes instantly (fire-and-forget) but the TestRunner is still executing, the heavy slot was released immediately, allowing domain-reload jobs like refresh_unity to be dequeued before the guard could catch them. Three fixes: 1. Hold the heavy slot after batch completion while IsEditorBusy() is true — prevents next heavy dequeue until async operations settle 2. One-frame cooldown between heavy job completions — gives async state one editor frame to propagate before the guard check 3. Add TestRunStatus.IsRunning to IsEditorBusy predicate for defense-in- depth (covers manual UI test runs alongside TestJobManager tracking) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instant tier (never queued, non-blocking). Returns all tickets with compact field names (t/s/a/l/p/b/e) for token-efficient batch polling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New Queue tab for MCP editor window with real-time job list, status bar, and 1s auto-refresh. UIElements-based section component. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
6 tasks: expose GetAllJobs, UXML layout, USS styles, C# controller, wire into editor window with 1s refresh, integration verification. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…job list Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…refresh Adds Queue as 6th tab in the MCP For Unity editor window. Loads McpQueueSection UXML/USS/controller, wires tab toggle and panel switching, adds independent 1-second refresh timer that only fires while the Queue tab is visible (before the 2-second connection status throttle). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Queue tab was blank because UIAssetSync didn't copy the new UXML/USS files from WSL to Assets/MCPForUnityUI/. Added both files to the sync list. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an agent polls a completed/failed/cancelled job, the ticket is removed from the queue after building the response. If the GatewayJobLogging EditorPref is enabled, job data is serialized to Logs/mcp-gateway-jobs.jsonl before removal. - Add Remove(ticket) to TicketStore and CommandQueue - Add GatewayJobLogger utility (JSON-lines file output) - Add GatewayJobLogging EditorPref key - Modify PollJob to auto-remove terminal jobs after response Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Unity .meta files for Queue UI components and UIAssetSync - AssetPathUtility returns UIAssetSync.SyncedBasePath when WSL detected Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Toggle to enable/disable gateway job logging - Text field showing current log file path (default: Logs/mcp-gateway-jobs.jsonl) - Browse button opens file dialog to choose custom log path - Path row hidden when logging is disabled - GatewayJobLogger now supports configurable path via EditorPrefs - Light/dark theme support for logging box Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reviewer's GuideImplements a tier-aware command gateway with async batch execution, persistent job queue, and an in-editor Queue tab, including new tooling for job polling, queue inspection, domain-reload safety, and optional JSONL job logging. Sequence diagram for async batch_execute submission via command gatewaysequenceDiagram
actor Agent
participant MCPClient
participant BatchExecute
participant CommandRegistry
participant CommandClassifier
participant CommandGatewayState
participant CommandQueue
Agent->>MCPClient: Send batch_execute(async=true, commands)
MCPClient->>BatchExecute: HandleCommand(params)
BatchExecute->>BatchExecute: Validate commands, atomic, agent, label
BatchExecute->>CommandRegistry: GetToolTier(toolName) for each command
CommandRegistry-->>BatchExecute: ExecutionTier
loop For_each_command
BatchExecute->>CommandClassifier: Classify(toolName, toolTier, cmdParams)
CommandClassifier-->>BatchExecute: effectiveTier
BatchExecute->>CommandClassifier: CausesDomainReload(toolName, cmdParams)
CommandClassifier-->>BatchExecute: causesDomainReload
BatchExecute->>BatchExecute: Build BatchCommand(Tool, Params, Tier, CausesDomainReload)
end
BatchExecute->>CommandGatewayState: Queue.Submit(agent, label, atomic, commands)
CommandGatewayState->>CommandQueue: Submit(agent, label, atomic, commands)
CommandQueue->>CommandQueue: ClassifyBatch(commands)
CommandQueue->>CommandQueue: CreateJob + enqueue heavy if needed
CommandGatewayState-->>BatchExecute: BatchJob(job)
alt job.Tier == Instant
BatchExecute->>CommandRegistry: InvokeCommandAsync for each command
CommandRegistry-->>BatchExecute: results
BatchExecute->>BatchExecute: job.Results, Status=Done
BatchExecute-->>MCPClient: SuccessResponse(ticket, results)
else Non_instant (Smooth or Heavy)
BatchExecute->>CommandQueue: GetAheadOf(job.Ticket)
CommandQueue-->>BatchExecute: jobsAhead
BatchExecute-->>MCPClient: PendingResponse(ticket, status=queued, position, tier, agent, label)
end
MCPClient-->>Agent: Return ticket for later polling
Sequence diagram for poll_job lifecycle and auto-cleanupsequenceDiagram
actor Agent
participant MCPClient
participant PollJob
participant CommandGatewayState
participant CommandQueue
participant TicketStore
participant GatewayJobLogger
Agent->>MCPClient: Send poll_job(ticket)
MCPClient->>PollJob: HandleCommand(params)
PollJob->>CommandGatewayState: Queue.Poll(ticket)
CommandGatewayState->>CommandQueue: Poll(ticket)
CommandQueue->>TicketStore: GetJob(ticket)
TicketStore-->>CommandQueue: BatchJob(job)
CommandQueue-->>PollJob: BatchJob(job)
alt job.Status == Queued
PollJob->>CommandQueue: GetAheadOf(ticket)
CommandQueue-->>PollJob: aheadJobs
PollJob->>CommandQueue: GetBlockedReason(ticket)
CommandQueue-->>PollJob: blockedBy
PollJob-->>MCPClient: PendingResponse(status=queued, position, blocked_by)
else job.Status == Running
PollJob-->>MCPClient: PendingResponse(status=running, progress)
else job.Status == Done
PollJob-->>MCPClient: SuccessResponse(status=done, results)
PollJob->>GatewayJobLogger: Log(job) [if enabled]
PollJob->>CommandGatewayState: Queue.Remove(ticket)
CommandGatewayState->>CommandQueue: Remove(ticket)
CommandQueue->>TicketStore: Remove(ticket)
TicketStore-->>CommandQueue: removedJob
else job.Status == Failed or Cancelled
PollJob-->>MCPClient: ErrorResponse(status, error, results)
PollJob->>GatewayJobLogger: Log(job) [if enabled]
PollJob->>CommandGatewayState: Queue.Remove(ticket)
CommandGatewayState->>CommandQueue: Remove(ticket)
CommandQueue->>TicketStore: Remove(ticket)
TicketStore-->>CommandQueue: removedJob
end
MCPClient-->>Agent: Present job status or results
Updated class diagram for the command gateway and queueing modelclassDiagram
direction LR
class ExecutionTier {
<<enum>>
Instant
Smooth
Heavy
}
class JobStatus {
<<enum>>
Queued
Running
Done
Failed
Cancelled
}
class McpForUnityToolAttribute {
+string CommandName
+string PollAction
+ExecutionTier Tier
}
class CommandClassifier {
+ExecutionTier Classify(toolName, attributeTier, @params)
+ExecutionTier ClassifyBatch(commands)
+bool CausesDomainReload(toolName, @params)
}
class BatchCommand {
+string Tool
+JObject Params
+ExecutionTier Tier
+bool CausesDomainReload
}
class BatchJob {
+string Ticket
+string Agent
+string Label
+bool Atomic
+ExecutionTier Tier
+bool CausesDomainReload
+JobStatus Status
+List~BatchCommand~ Commands
+List~object~ Results
+int CurrentIndex
+string Error
+DateTime CreatedAt
+DateTime CompletedAt
+int UndoGroup
}
class AgentStats {
+int Active
+int Queued
+int Completed
}
class TicketStore {
-Dictionary~string,BatchJob~ _jobs
-int _nextId
+BatchJob CreateJob(agent, label, atomic, tier)
+BatchJob GetJob(ticket)
+BatchJob Remove(ticket)
+void CleanExpired(expiry)
+List~BatchJob~ GetQueuedJobs()
+List~BatchJob~ GetRunningJobs()
+Dictionary~string,AgentStats~ GetAgentStats()
+List~BatchJob~ GetAllJobs()
+int QueueDepth
+string ToJson()
+void FromJson(json)
}
class CommandQueue {
-TicketStore _store
-Queue~string~ _heavyQueue
-List~string~ _smoothInFlight
-string _activeHeavyTicket
+Func~bool~ IsEditorBusy
+bool HasActiveHeavy
+int QueueDepth
+int SmoothInFlight
+BatchJob Submit(agent, label, atomic, commands)
+BatchJob Poll(ticket)
+bool Cancel(ticket, agent)
+List~BatchJob~ GetAheadOf(ticket)
+string GetBlockedReason(ticket)
+BatchJob Remove(ticket)
+object GetSummary()
+List~BatchJob~ GetAllJobs()
+object GetStatus()
+string PersistToJson()
+void RestoreFromJson(json)
+void ProcessTick(executeCommand)
-Task ExecuteJob(job, executeCommand)
}
class CommandGatewayState {
<<static>>
+CommandQueue Queue
-const string SessionKey
-static void OnUpdate()
}
class GatewayJobLogger {
<<static>>
+string DefaultLogPath
+bool IsEnabled
+string LogPath
+void Log(job)
}
class PollJob {
<<tool>>
+object HandleCommand(@params)
}
class QueueStatus {
<<tool>>
+object HandleCommand(@params)
}
class BatchExecute {
+Task~object~ HandleCommand(@params)
-object HandleAsyncSubmit(@params, commandsToken)
}
class CommandRegistry {
+ExecutionTier GetToolTier(commandName)
+Task~object~ InvokeCommandAsync(commandName, @params)
}
class McpQueueSection {
+VisualElement Root
+McpQueueSection(root)
+void Refresh()
-void CacheUIElements()
-void SetupLoggingControls()
-void UpdateStatusBar(queue, allJobs)
-void UpdateJobList(allJobs, queue)
-VisualElement CreateJobRow(job, queue)
}
class MCPForUnityEditorWindow {
-McpQueueSection queueSection
-ToolbarToggle queueTabToggle
-VisualElement queuePanel
-double _lastQueueRefreshTime
-ActivePanel _currentPanel
-void OnEditorUpdate()
-void SetupTabs()
-void SwitchPanel(panel)
-void CreateGUI()
}
class ExecutionTierTests
class CommandClassifierTests
class CommandQueueTests
class TicketStoreTests
class BatchExecuteAsyncTests
McpForUnityToolAttribute --> ExecutionTier
CommandClassifier --> ExecutionTier
CommandClassifier --> BatchCommand
BatchJob --> ExecutionTier
BatchJob --> JobStatus
BatchJob --> BatchCommand
TicketStore --> BatchJob
TicketStore --> AgentStats
CommandQueue --> TicketStore
CommandQueue --> BatchJob
CommandQueue --> BatchCommand
CommandGatewayState --> CommandQueue
GatewayJobLogger --> BatchJob
PollJob --> CommandGatewayState
PollJob --> GatewayJobLogger
QueueStatus --> CommandGatewayState
BatchExecute --> CommandRegistry
BatchExecute --> CommandClassifier
BatchExecute --> CommandGatewayState
McpQueueSection --> CommandGatewayState
MCPForUnityEditorWindow --> McpQueueSection
ExecutionTierTests --> ExecutionTier
CommandClassifierTests --> CommandClassifier
CommandQueueTests --> CommandQueue
TicketStoreTests --> TicketStore
BatchExecuteAsyncTests --> BatchExecute
Updated class diagram for the Queue tab UI componentsclassDiagram
direction LR
class MCPForUnityEditorWindow {
-McpQueueSection queueSection
-ToolbarToggle queueTabToggle
-VisualElement queuePanel
-double _lastQueueRefreshTime
-double QueueRefreshIntervalSeconds
-double _lastEditorUpdateTime
-double EditorUpdateIntervalSeconds
-ActivePanel _currentPanel
+void CreateGUI()
-void SetupTabs()
-void SwitchPanel(panel)
-void OnEditorUpdate()
}
class McpQueueSection {
+VisualElement Root
-Label heavyTicketLabel
-Label queuedCount
-Label runningCount
-Label doneCount
-Label failedCount
-VisualElement jobListContainer
-VisualElement jobListHeader
-Label emptyLabel
-Toggle loggingToggle
-TextField logPathField
-Button browseLogPathBtn
-VisualElement loggingPathRow
+McpQueueSection(root)
+void Refresh()
-void CacheUIElements()
-void SetupLoggingControls()
-void UpdateLoggingPathVisibility()
-void UpdateStatusBar(queue, allJobs)
-void UpdateJobList(allJobs, queue)
-VisualElement CreateJobRow(job, queue)
-string Truncate(text, maxLength)
}
class GatewayJobLogger {
<<static>>
+string DefaultLogPath
+bool IsEnabled
+string LogPath
+void Log(job)
}
class CommandGatewayState {
<<static>>
+CommandQueue Queue
}
class CommandQueue {
+List~BatchJob~ GetAllJobs()
+bool HasActiveHeavy
+int QueueDepth
+int SmoothInFlight
+string GetBlockedReason(ticket)
+Func~bool~ IsEditorBusy
}
class TicketStore {
+List~BatchJob~ GetAllJobs()
}
class BatchJob {
+string Ticket
+string Agent
+string Label
+ExecutionTier Tier
+bool CausesDomainReload
+JobStatus Status
+List~BatchCommand~ Commands
+List~object~ Results
+int CurrentIndex
+string Error
}
class UIAssetSync {
<<static>>
+string SyncedBasePath
+bool NeedsSync()
}
class AssetPathUtility {
+string GetMcpPackageRootPath()
}
MCPForUnityEditorWindow --> McpQueueSection
MCPForUnityEditorWindow --> UIAssetSync
UIAssetSync --> AssetPathUtility
McpQueueSection --> CommandGatewayState
CommandGatewayState --> CommandQueue
CommandQueue --> TicketStore
CommandQueue --> BatchJob
McpQueueSection --> GatewayJobLogger
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- In
CommandQueue,GetBlockedReasonreaches intoTestJobManager/TestRunStatus/EditorApplicationdirectly even thoughIsEditorBusyis already injected; consider routing both the boolean and reason through a single delegate/interface so the queue remains decoupled from editor/test services and easier to fake in tests. - The
HandleAsyncSubmitpath inBatchExecuteruns "instant" commands synchronously on the editor thread viaInvokeCommandAsync(...).GetAwaiter().GetResult(), which risks UI stalls or deadlocks for long-running async tools; consider making this path fully async (usingawait) or explicitly constraining/documenting the tools that can be treated asInstant.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `CommandQueue`, `GetBlockedReason` reaches into `TestJobManager`/`TestRunStatus`/`EditorApplication` directly even though `IsEditorBusy` is already injected; consider routing both the boolean and reason through a single delegate/interface so the queue remains decoupled from editor/test services and easier to fake in tests.
- The `HandleAsyncSubmit` path in `BatchExecute` runs "instant" commands synchronously on the editor thread via `InvokeCommandAsync(...).GetAwaiter().GetResult()`, which risks UI stalls or deadlocks for long-running async tools; consider making this path fully async (using `await`) or explicitly constraining/documenting the tools that can be treated as `Instant`.
## Individual Comments
### Comment 1
<location path="TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteAsyncTests.cs" line_range="59" />
<code_context>
+ [Test]
+ public void BatchExecuteMaxCommands_DefaultIs25()
</code_context>
<issue_to_address>
**suggestion (testing):** Add targeted tests for the new async batch submission path (instant vs queued behaviour and error conditions).
The new `BatchExecute.HandleAsyncSubmit` path is core to this gateway, but it isn’t covered by the current tests. Please add tests that drive the async path via the `batch_execute` tool, including:
- An `async=true` batch with only `ExecutionTier.Instant` commands, asserting a `SuccessResponse` (`status="done"`, `ticket`, and populated `results`) and that the `BatchJob` is completed rather than queued.
- An `async=true` batch that includes `Smooth`/`Heavy` commands, asserting a `PendingResponse` (`ticket`, `status="queued"`, `position`, `tier`, `agent`, `label`, `atomic`), and ideally verifying the queued job state (e.g. via `CommandGatewayState.Queue.Poll(ticket)`).
- Error cases: only invalid commands (e.g. missing `tool`) should return `ErrorResponse("No valid commands in async batch.")`, and a failing command in an atomic instant batch should fail with partial `results` and the `ticket` present in the error data.
This will exercise the new async semantics end-to-end and validate the instant-vs-queued behaviour.
</issue_to_address>
### Comment 2
<location path="docs/plans/2026-02-25-gateway-async-awareness-plan.md" line_range="394-395" />
<code_context>
+ }
+
+ _activeHeavyTicket = ticket;
+ _ = ExecuteJob(job, executeCommand);
+ return;
+ }
</code_context>
<issue_to_address>
**nitpick (typo):** Clarify grammar in the inline comment about re-enqueuing peeked items.
The inline comment `// Re-enqueue any remaining peeked items were already handled by the loop` is grammatically awkward. Please rephrase, e.g. `// Remaining peeked items are already handled by the loop` or similar.
Suggested implementation:
```
if (job == null || job.Status == JobStatus.Cancelled) continue;
// Guard: domain-reload jobs must wait when editor is busy
if (job.CausesDomainReload && editorBusy)
{
_heavyQueue.Enqueue(ticket); // put back at end
continue;
}
// Remaining peeked items are already handled by the loop
_activeHeavyTicket = ticket;
```
If the original inline comment you referenced (`// Re-enqueue any remaining peeked items were already handled by the loop`) appears elsewhere in the file or in related files, apply a similar replacement there:
<<<<<<< SEARCH
// Re-enqueue any remaining peeked items were already handled by the loop
=======
// Remaining peeked items are already handled by the loop
>>>>>>> REPLACE
Adjust capitalization or wording to match your team's commenting style if needed.
</issue_to_address>
### Comment 3
<location path="docs/plans/2026-02-25-queue-summary-ui-design.md" line_range="34-36" />
<code_context>
+
+### Status bar
+- Active heavy ticket with agent + label (or "No active heavy job")
+- Three counters: Queued / Running / Done+Failed
+- Uses existing `.section` and `.setting-row` styles
+
</code_context>
<issue_to_address>
**suggestion:** Align the documented counter list with the implemented four counters.
This line still refers to “Three counters: Queued / Running / Done+Failed”, but the UI and UXML/USS show four separate counters: queued, running, done, and failed. Please either list all four here or reword this bullet so it accurately matches the final UI behaviour.
```suggestion
### Status bar
- Active heavy ticket with agent + label (or "No active heavy job")
- Four counters: Queued / Running / Done / Failed
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteAsyncTests.cs
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
This pull request introduces a sophisticated command gateway system that enables safe concurrent access to Unity from multiple MCP clients. The implementation addresses the critical problem of domain reloads interrupting parallel agent operations by classifying tools into execution tiers (Instant/Smooth/Heavy) and implementing intelligent queue scheduling with domain-reload awareness.
Changes:
- Core gateway infrastructure with tier-based command classification, queue management, and SessionState persistence for domain-reload survival
- New
poll_jobandqueue_statusMCP tools for asynchronous batch execution with ticket-based polling - Real-time Queue tab in the MCP editor window with auto-refresh, job logging, and status visualization
- WSL compatibility workaround (UIAssetSync) for UXML/USS assets
- Extended batch_execute with async=true parameter for non-blocking command submission
Reviewed changes
Copilot reviewed 50 out of 50 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| ExecutionTier.cs | Enum defining three-tier classification system (Instant/Smooth/Heavy) |
| CommandClassifier.cs | Action-level tier refinement logic with domain-reload detection |
| BatchJob.cs | Job model with lifecycle tracking, undo support, and domain-reload flag |
| TicketStore.cs | Job storage with ticket generation, expiry, and SessionState serialization |
| CommandQueue.cs | Tier-aware FIFO dispatcher with exclusive heavy scheduling and editor-busy guards |
| CommandGatewayState.cs | Singleton wiring queue to EditorApplication.update with persistence hooks |
| PollJob.cs | MCP tool for polling job status with auto-cleanup of terminal jobs |
| QueueStatus.cs | Compressed queue introspection tool |
| GatewayJobLogger.cs | Optional JSON-lines audit logging with configurable path |
| McpQueueSection.uxml/uss/cs | Queue tab UI with status bar, job list, and logging controls |
| BatchExecute.cs | Extended with async gateway path alongside synchronous execution |
| UIAssetSync.cs | WSL UNC path workaround for Unity's UXML/USS importer limitations |
| 5 test files | 47 unit tests covering classifier, queue, store, and async tools |
| Tool files (7) | ExecutionTier annotations for proper queue dispatch |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteAsyncTests.cs
Show resolved
Hide resolved
|
@copilot open a new pull request to apply changes based on the comments in this thread |
📝 WalkthroughWalkthroughAdds an async-aware, tiered batch execution system and persistent queue to the Unity editor MCP gateway, including ExecutionTier classification, command queue/ticketing, ticket polling, gateway job logging, Queue UI, WSL UI-asset sync, transport deduplication, and extensive unit tests. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as MCP Client
participant BatchExec as BatchExecute
participant Classifier as CommandClassifier
participant Queue as CommandQueue
participant Store as TicketStore
participant EditorLoop as Editor.update
participant Poll as PollJob
Client->>BatchExec: async submit(commands)
BatchExec->>Classifier: classify each command
Classifier-->>BatchExec: (tier, causesDomainReload)
BatchExec->>Queue: Submit(agent,label,atomic,commands)
Queue->>Store: CreateJob(...)
Store-->>Queue: BatchJob(ticket)
Queue-->>BatchExec: PendingResponse(ticket)
Client->>Poll: poll_job(ticket)
Poll->>Queue: Poll(ticket)
Queue->>Store: GetJob(ticket)
Store-->>Queue: BatchJob(status)
Queue-->>Poll: Pending/Success/Error (includes blocked_by if applicable)
EditorLoop->>Queue: ProcessTick(executeCommand)
Queue->>EditorLoop: executeCommand(tool,params)
EditorLoop-->>Queue: command result
Queue->>Store: Update job status/results
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
MCPForUnity/Editor/Tools/BatchExecute.cs (1)
35-66: 🛠️ Refactor suggestion | 🟠 MajorUse
ToolParamsfor top-level batch parameter parsing.Line 56 and Lines 250-253 parse tool inputs directly from
JObject. Please switch these toToolParamsso required/optional validation is consistent across editor tools.As per coding guidelines
MCPForUnity/Editor/Tools/*.cs: UseToolParamsfor consistent parameter validation in C# Editor tools with methods likeGetInt(),RequireString().Also applies to: 248-253
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/BatchExecute.cs` around lines 35 - 66, The top-level parameter parsing in HandleCommand is using JObject.Value/ indexing directly; replace this with a ToolParams instance (e.g. new ToolParams(`@params`)) and call its helpers (GetInt(), GetBool() / GetString() / RequireString() as appropriate) to read "async", "failFast", "parallel", "maxParallelism" and the "commands" array so validation is consistent with other editor tools; specifically, change the commandsToken extraction to use ToolParams to require/validate the "commands" entry and replace usages of `@params.Value`<int?>("maxParallelism") and `@params.Value`<bool?>("async") etc. with the matching ToolParams methods while preserving the async vs legacy flow inside HandleCommand and when calling HandleAsyncSubmit.
🟡 Minor comments (13)
docs/plans/2026-02-25-gateway-async-awareness-plan.md-90-94 (1)
90-94:⚠️ Potential issue | 🟡 MinorAdd language identifiers to fenced code blocks (MD040).
Lines 90, 121, 886, 893, and 904 start unlabeled fences. Please add a language (
text,bash,csharp,json, etc.) to keep markdownlint clean.Suggested fix pattern
-``` +```text run_tests(mode=EditMode, test_names=[ "MCPForUnity.Tests.Editor.CommandClassifierTests.CausesDomainReload_RefreshUnity_CompileRequest_ReturnsTrue" ]) -``` +```Also applies to: 121-123, 886-889, 893-896, 904-933
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/plans/2026-02-25-gateway-async-awareness-plan.md` around lines 90 - 94, The markdown contains unlabeled fenced code blocks (e.g., the run_tests(...) snippet and other blocks referenced at lines 90, 121, 886, 893, 904) causing MD040; update each triple-backtick fence to include an appropriate language identifier (for the run_tests snippet use text or bash, and for other blocks choose csharp/json/text as appropriate) so every code fence is labeled (e.g., ```text or ```csharp) while keeping the block contents unchanged.docs/plans/2026-02-25-gateway-async-awareness-plan.md-13-13 (1)
13-13:⚠️ Potential issue | 🟡 MinorFix heading level jump to satisfy markdown linting.
Line 13 jumps from
#to###; use##here to avoid MD001 violations.Suggested fix
-### Task 1: Add `CausesDomainReload` to CommandClassifier +## Task 1: Add `CausesDomainReload` to CommandClassifier🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/plans/2026-02-25-gateway-async-awareness-plan.md` at line 13, Heading level jumps from H1 to H3 for the section titled "Task 1: Add `CausesDomainReload` to CommandClassifier"; change the heading token from "### Task 1: Add `CausesDomainReload` to CommandClassifier" to "## Task 1: Add `CausesDomainReload` to CommandClassifier" so the document uses a proper H2 and satisfies markdown lint MD001.docs/plans/2026-02-25-gateway-async-awareness-design.md-27-31 (1)
27-31:⚠️ Potential issue | 🟡 MinorSpecify a language on the fenced block to satisfy markdownlint.
Line 27 opens an unlabeled fence; add a language token (for example
text).Suggested fix
-``` +```text if job.CausesDomainReload AND (TestJobManager.HasRunningJob OR EditorApplication.isCompiling) → skip, keep queued -``` +```🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/plans/2026-02-25-gateway-async-awareness-design.md` around lines 27 - 31, The fenced code block containing the conditional snippet with symbols job.CausesDomainReload, TestJobManager.HasRunningJob, and EditorApplication.isCompiling is missing a language specifier; update the opening fence to include a language token (for example "text") so the block becomes ```text ... ``` to satisfy markdownlint while preserving the exact contents of the snippet.docs/plans/2026-02-25-queue-summary-ui-design.md-13-13 (1)
13-13:⚠️ Potential issue | 🟡 MinorAdd language specifiers to fenced code blocks to resolve MD040 lint warnings.
Both ASCII-art/pseudo-code blocks are missing a language tag (e.g.,
```text).Also applies to: 53-53
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/plans/2026-02-25-queue-summary-ui-design.md` at line 13, Add explicit language specifiers to the fenced code blocks containing ASCII-art/pseudo-code (the two triple-backtick blocks flagged by MD040) by changing their opening fences from ``` to a tagged form like ```text (or ```ascii) so the linter recognizes them; update both code fences so each opening triple-backtick includes the chosen language tag.MCPForUnity/Editor/Tools/PollJob.cs-57-68 (1)
57-68:⚠️ Potential issue | 🟡 MinorGuard against
nullCommandsin theRunningbranch.
job.Commands.Countandjob.Commands.Countat Lines 59 and 65 will throw aNullReferenceExceptionifCommandsis evernullon aRunningjob. While the current execution flow guarantees it's set before a job entersRunning, defensive guarding avoids a silent regression if that invariant is weakened later.🛡️ Proposed fix
- response = new PendingResponse( - $"Running command {job.CurrentIndex + 1}/{job.Commands.Count}.", + int total = job.Commands?.Count ?? 0; + response = new PendingResponse( + $"Running command {job.CurrentIndex + 1}/{total}.", pollIntervalSeconds: 1.0, data: new { ticket = job.Ticket, status = "running", - progress = $"{job.CurrentIndex + 1}/{job.Commands.Count}", + progress = $"{job.CurrentIndex + 1}/{total}", agent = job.Agent, label = job.Label });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/PollJob.cs` around lines 57 - 68, Guard against job.Commands being null in the JobStatus.Running branch: before using job.Commands.Count or constructing the progress string, check for null (or use a safe fallback) and compute safeCount = job.Commands?.Count ?? 0 and safeIndex = Math.Min(job.CurrentIndex + 1, safeCount); then build the PendingResponse using safeCount and safeIndex for the message and progress fields (update the Running case that creates the PendingResponse to reference these safe values rather than job.Commands.Count directly).TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/TicketStoreTests.cs-113-121 (1)
113-121:⚠️ Potential issue | 🟡 MinorPotential flaky ordering —
CreatedAttimestamps may collide.
GetQueuedJobs()sorts byCreatedAt. If both jobs are created in the same millisecond (common in unit tests),OrderByis not guaranteed to be stable between runs, and the positional assertions on lines 119–120 can fail non-deterministically.Consider setting
CreatedAtexplicitly or asserting on ticket presence without relying on index position:🔧 Proposed fix
[Test] public void GetQueuedJobs_OrderedByCreationTime() { var j1 = _store.CreateJob("a", "first", false, ExecutionTier.Heavy); + j1.CreatedAt = System.DateTime.UtcNow.AddSeconds(-1); var j2 = _store.CreateJob("b", "second", false, ExecutionTier.Smooth); var queued = _store.GetQueuedJobs(); Assert.That(queued[0].Ticket, Is.EqualTo(j1.Ticket)); Assert.That(queued[1].Ticket, Is.EqualTo(j2.Ticket)); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/TicketStoreTests.cs` around lines 113 - 121, The test GetQueuedJobs_OrderedByCreationTime is flaky because CreateJob may assign identical CreatedAt timestamps; update the test to avoid relying on millisecond ordering by either explicitly setting distinct CreatedAt values on j1 and j2 after creation (e.g., j1.CreatedAt = now; j2.CreatedAt = now.AddMilliseconds(1)) before calling _store.GetQueuedJobs(), or change the assertions to check that both tickets are present without requiring specific positions (e.g., Assert.That(queued.Select(q => q.Ticket), Is.EquivalentTo(new[] { j1.Ticket, j2.Ticket }))); modify only the test logic around CreateJob, j1/j2, and the assertions so GetQueuedJobs and _store remain unchanged.MCPForUnity/Editor/Tools/PollJob.cs-18-21 (1)
18-21:⚠️ Potential issue | 🟡 MinorUse
RequireString()per theToolParamscoding guideline.
p.GetRequired("ticket")deviates from the establishedToolParamsAPI convention. As per coding guidelines, C# Editor tools should use methods likeRequireString()for string parameters.🔧 Proposed fix
- var ticketResult = p.GetRequired("ticket"); - if (!ticketResult.IsSuccess) - return new ErrorResponse(ticketResult.ErrorMessage); - - string ticket = ticketResult.Value; + var ticketResult = p.RequireString("ticket"); + if (!ticketResult.IsSuccess) + return new ErrorResponse(ticketResult.ErrorMessage); + + string ticket = ticketResult.Value;As per coding guidelines: "Use
ToolParamsfor consistent parameter validation in C# Editor tools with methods likeGetInt(),RequireString()".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/PollJob.cs` around lines 18 - 21, The code is using p.GetRequired("ticket") instead of the ToolParams guideline method; replace the call with p.RequireString("ticket") (or the equivalent RequireString method on ToolParams) and update handling to use the returned result (e.g., check success and use its Value or return new ErrorResponse(result.ErrorMessage)) so the Ticket extraction uses ToolParams.RequireString(), referenced variable p, and the existing ErrorResponse/ticketResult flow.docs/plans/2026-02-25-queue-summary-ui-plan.md-35-38 (1)
35-38:⚠️ Potential issue | 🟡 MinorAdd language identifiers to fenced code blocks.
These fenced blocks are unlabeled and trigger MD040. Please annotate each block with an explicit language (for example
bash,json, ortext).Also applies to: 491-494, 767-770, 797-799, 811-813
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/plans/2026-02-25-queue-summary-ui-plan.md` around lines 35 - 38, The Markdown fenced code blocks like the one containing "refresh_unity(scope=all, compile=request, wait_for_ready=true)" and the "read_console(types=[\"error\"]) → 0 errors" output are unlabeled and trigger MD040; edit each such fenced block (including the other occurrences mentioned) to add an explicit language identifier (e.g., ```bash, ```text or ```console) after the opening backticks so the block is annotated and the linter no longer flags MD040.MCPForUnity/Editor/Helpers/UIAssetSync.cs-71-72 (1)
71-72:⚠️ Potential issue | 🟡 MinorAnchor destination path to project root explicitly.
Line 71 builds a full path from a relative base (
Assets/...) using current working directory semantics. UsePath.GetDirectoryName(Application.dataPath)as the root so writes are always deterministic to the active project.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Helpers/UIAssetSync.cs` around lines 71 - 72, The code currently computes destPath with Path.GetFullPath(Path.Combine(SyncedBasePath, relativePath)) which uses the process CWD; change the construction to anchor against the Unity project root by using Path.GetDirectoryName(Application.dataPath) as the base before combining with SyncedBasePath and relativePath so destPath and destDir are deterministic. Update the logic that sets destPath and destDir (the variables destPath, destDir and the expression using SyncedBasePath/relativePath) to first compute projectRoot = Path.GetDirectoryName(Application.dataPath) and then combine projectRoot with SyncedBasePath and relativePath to produce the final paths.docs/plans/2026-02-25-queue-summary-ui-plan.md-15-15 (1)
15-15:⚠️ Potential issue | 🟡 MinorFix heading-level jump in task sections.
Line 15 jumps from H1 to H3. Use H2 for
Task 1(and peers) to satisfy MD001.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/plans/2026-02-25-queue-summary-ui-plan.md` at line 15, The markdown heading "Task 1: Expose GetAllJobs on CommandQueue" is currently H3 (###) causing a jump from the document H1; change this and all peer task headings ("Task 1", "Task 2", etc.) from H3 to H2 (##) so the section hierarchy is H1 -> H2 -> H3 and MD001 is satisfied; update the lines with the exact heading texts to use "##" instead of "###".MCPForUnity/Editor/Tools/TicketStore.cs-39-45 (1)
39-45:⚠️ Potential issue | 🟡 Minor
CleanExpiredshould also clean cancelled terminal jobs.The expiry filter currently excludes
JobStatus.Cancelled, so cancelled jobs can accumulate when they are not removed immediately.Proposed fix
- .Where(kvp => (kvp.Value.Status == JobStatus.Done || kvp.Value.Status == JobStatus.Failed) + .Where(kvp => (kvp.Value.Status == JobStatus.Done + || kvp.Value.Status == JobStatus.Failed + || kvp.Value.Status == JobStatus.Cancelled) && kvp.Value.CompletedAt.HasValue && DateTime.UtcNow - kvp.Value.CompletedAt.Value > expiry)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/TicketStore.cs` around lines 39 - 45, The CleanExpired logic in TicketStore.CleanExpired currently filters terminal jobs by JobStatus.Done or JobStatus.Failed and thus misses cancelled terminal jobs; update the expiry predicate used when building expired (the LINQ Where over _jobs entries) to also include kvp.Value.Status == JobStatus.Cancelled so cancelled jobs with CompletedAt older than expiry are selected and removed from _jobs; ensure the selection still checks CompletedAt.HasValue and the time-based comparison remains unchanged.MCPForUnity/Editor/Windows/Components/Queue/McpQueueSection.cs-216-221 (1)
216-221:⚠️ Potential issue | 🟡 MinorQueued jobs show incorrect progress (
1/N) before execution starts.Line 219 uses
job.CurrentIndex + 1for non-done jobs, so queued jobs render as if one command already ran. Render queued as0/N.Proposed fix
- int idx = job.Status == JobStatus.Done ? job.Commands.Count : job.CurrentIndex + 1; + int idx = job.Status switch + { + JobStatus.Queued => 0, + JobStatus.Done => job.Commands.Count, + _ => System.Math.Clamp(job.CurrentIndex + 1, 1, job.Commands.Count) + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Windows/Components/Queue/McpQueueSection.cs` around lines 216 - 221, The progress calculation currently uses job.CurrentIndex + 1 for non-done jobs which makes queued jobs show "1/N"; change the logic in McpQueueSection (the block that builds progressText) to use job.CurrentIndex for non-Done jobs and leave Done as job.Commands.Count, i.e. set idx = job.Status == JobStatus.Done ? job.Commands.Count : job.CurrentIndex so queued jobs render as "0/N".MCPForUnity/Editor/Tools/CommandQueue.cs-144-146 (1)
144-146:⚠️ Potential issue | 🟡 MinorDone-job progress is off by one in summary output.
Completed jobs currently report progress from
CurrentIndexdirectly, which yieldsN-1/Nafter a normal full run.Proposed fix
if (j.Commands != null && j.Commands.Count > 0) - entry["p"] = $"{j.CurrentIndex + (j.Status == JobStatus.Done ? 0 : 1)}/{j.Commands.Count}"; +{ + var completed = j.Status == JobStatus.Done + ? j.Commands.Count + : Math.Min(j.CurrentIndex + 1, j.Commands.Count); + entry["p"] = $"{completed}/{j.Commands.Count}"; +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/CommandQueue.cs` around lines 144 - 146, The progress string in CommandQueue is off by one for completed jobs because it uses j.CurrentIndex + (j.Status == JobStatus.Done ? 0 : 1); change the assignment that sets entry["p"] so it always adds 1 to j.CurrentIndex (e.g. $"{j.CurrentIndex + 1}/{j.Commands.Count}") to ensure a finished job reports N/N; update the line inside the block that checks j.Commands to use j.CurrentIndex + 1 unconditionally.
🧹 Nitpick comments (10)
MCPForUnity/Editor/Tools/QueueStatus.cs (1)
14-18: Consider wrapping@paramswithToolParamsfor consistency with other tools.This tool doesn't require any input parameters, so there's nothing to validate. However, some tools in this directory use
ToolParamsto wrap the incomingJObject. Adopting the same pattern here — even if justvar tp = new ToolParams(@params);— keeps the tool surface consistent and makes it trivial to add optional parameters (e.g., filters) later.Per coding guidelines for
MCPForUnity/Editor/Tools/*.cs: "UseToolParamsfor consistent parameter validation in C# Editor tools with methods likeGetInt(),RequireString()".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/QueueStatus.cs` around lines 14 - 18, Wrap the incoming JObject in a ToolParams instance inside HandleCommand to match other tools' patterns: create a local variable like tp = new ToolParams(`@params`) at the start of the method and use tp (even if not used further) to preserve consistency and make future optional parameter handling trivial; update the HandleCommand method signature/body so it constructs ToolParams before calling CommandGatewayState.Queue.GetSummary() and returning the SuccessResponse.MCPForUnity/Editor/Tools/CommandClassifier.cs (2)
48-58:CausesDomainReloadreturnstruewhencompileparam is absent — worth documenting explicitly.
@params.Value<string>("compile")returnsnullwhen the key is missing, andnull != "none"evaluates totrue. This meansrefresh_unitywith nocompilekey is treated as domain-reload-causing, which is the correct conservative default — but it's a non-obvious null comparison. A short inline comment would prevent future readers from misreading it as a bug.♻️ Proposed comment
- "refresh_unity" => `@params.Value`<string>("compile") != "none", + // null (omitted) or any value other than "none" triggers a reload + "refresh_unity" => `@params.Value`<string>("compile") != "none",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/CommandClassifier.cs` around lines 48 - 58, The CausesDomainReload method treats a missing "compile" key as causing a domain reload because `@params.Value`<string>("compile") returns null and null != "none" is true; add a brief inline comment above the "refresh_unity" case (referencing CausesDomainReload and the `@params.Value`<string>("compile") expression) explaining that returning true on missing/unknown compile values is an intentional conservative default to avoid silently skipping reloads.
33-43:ClassifyBatchon an empty array returnsInstant— consider documenting this sentinel.Initialising
maxtoExecutionTier.Instantmeans a zero-command batch resolves toInstant. This is likely intentional (no commands = no restriction), but a brief comment guards against someone assuming a "neutral" default should beSmooth.♻️ Proposed comment
- var max = ExecutionTier.Instant; + // Start at the lowest tier; an empty batch resolves to Instant (no restriction). + var max = ExecutionTier.Instant;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/CommandClassifier.cs` around lines 33 - 43, ClassifyBatch currently initializes max to ExecutionTier.Instant so an empty commands array returns Instant; add a short comment above ClassifyBatch explaining that ExecutionTier.Instant is being used intentionally as the sentinel/default for "no commands" (and reference that ClassifyBatch calls Classify for each command), so future readers understand that an empty batch represents no restrictions rather than a neutral Smooth value.MCPForUnity/Editor/Tools/CommandGatewayState.cs (1)
37-41: Cache theProcessTickdelegate to avoid a per-frame allocation.
OnUpdateconstructs a newasynclambda every editor tick (potentially 100 + calls/second), causing continuous heap pressure. The lambda closes over no mutable instance state, so it can be lifted to a static field.♻️ Proposed fix
+ static readonly Func<string, JObject, Task<object>> _executeCommand = + (tool, `@params`) => CommandRegistry.InvokeCommandAsync(tool, `@params`); + static void OnUpdate() { - Queue.ProcessTick(async (tool, `@params`) => - await CommandRegistry.InvokeCommandAsync(tool, `@params`)); + Queue.ProcessTick(_executeCommand); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/CommandGatewayState.cs` around lines 37 - 41, OnUpdate currently creates a new async lambda each tick which allocates; pull that lambda out into a static readonly delegate and pass the cached delegate to Queue.ProcessTick instead. Create a static readonly Func<ToolType, ParamType, Task> (or matching delegate signature used by Queue.ProcessTick) field (name e.g. s_processTickHandler) that calls CommandRegistry.InvokeCommandAsync(tool, `@params`) and replace the inline async lambda in OnUpdate with that field to avoid per-frame allocations; keep the call to CommandRegistry.InvokeCommandAsync unchanged.TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandClassifierTests.cs (1)
27-40: Consider adding coverage forcreate(manage_scene) andpause/stop(manage_editor).
ClassifyManageSceneshares theHeavyarm across"create" or "load" or "save", butcreatehas no dedicated test. LikewiseClassifyManageEditormaps"pause"and"stop"toHeavywith no test coverage. If the switch arms are ever edited, the missing cases won't be caught by the suite.Also applies to: 59-72
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandClassifierTests.cs` around lines 27 - 40, Add unit tests to cover the missing branches by invoking CommandClassifier.Classify with the same patterns used in existing tests: add a test Classify_ManageScene_Create_ReturnsHeavy that calls CommandClassifier.Classify("manage_scene", ExecutionTier.Smooth, new JObject { ["action"] = "create" }) and asserts ExecutionTier.Heavy, and add tests Classify_ManageEditor_Pause_ReturnsHeavy and Classify_ManageEditor_Stop_ReturnsHeavy that call CommandClassifier.Classify("manage_editor", ExecutionTier.Smooth, new JObject { ["action"] = "pause" }) and new JObject { ["action"] = "stop" } respectively and assert ExecutionTier.Heavy to ensure the "create", "pause", and "stop" switch arms are covered.MCPForUnity/Editor/Tools/CommandRegistry.cs (1)
198-207:GetToolTiersilently returnsSmoothfor unregistered commands — consider whether that's always correct.Callers (e.g.,
BatchExecutebuilding a command list beforeCommandRegistry.Initialize()has run) will silently receiveSmoothrather than an error. If this sentinel is intentional, a brief XML-doc note that "unregistered or not-yet-initialized tools default to Smooth" would prevent future confusion.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/CommandRegistry.cs` around lines 198 - 207, GetToolTier currently returns ExecutionTier.Smooth when a command isn't found in _handlers, which can hide callers' initialization/order issues (e.g., BatchExecute calling GetToolTier before CommandRegistry.Initialize); update the XML doc summary for GetToolTier to explicitly state that unregistered or not-yet-initialized commands will default to ExecutionTier.Smooth so callers are aware of the sentinel behavior and can change their flow or call Initialize first.MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs (1)
332-349: Add explicit warning when Queue section assets fail to load.The Queue load block has no
elsediagnostic, unlike Tools/Resources. A warning here makes empty Queue-tab failures much easier to diagnose.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs` around lines 332 - 349, The Queue section load lacks diagnostic warnings when assets fail to load; update the block that references queueTree and queueStyleSheet (and the creation of McpQueueSection) to log explicit warnings when queueTree is null and when queueStyleSheet is null so failures are visible—use the same logging strategy as the Tools/Resources block (e.g., call Debug.LogWarning or an existing logger) to report "McpQueueSection.uxml not found" and "McpQueueSection.uss not found" and ensure you do not attempt to instantiate McpQueueSection or add a null stylesheet to rootVisualElement when the assets are missing.TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteAsyncTests.cs (1)
26-52: Strengthen response assertions to avoid false positives.These tests currently only check
Is.Not.Null, so contract regressions can slip through. Please assert expected response kind/status fields (e.g., error for missing/invalid ticket, structured status payload for queue status).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteAsyncTests.cs` around lines 26 - 52, Update the tests to assert on the response contract instead of only non-null: for PollJob.HandleCommand in PollJob_NullParams_ReturnsError, PollJob_MissingTicket_ReturnsError, and PollJob_InvalidTicket_ReturnsNotFound assert the returned JObject contains an error/ kind/status field (e.g., result["kind"] == "error" or result["status"] == "error") and for PollJob_InvalidTicket also assert an appropriate not-found indicator (status code or message). For QueueStatus.HandleCommand in QueueStatus_ReturnsStatusObject assert the response is a structured status payload (e.g., contains expected keys like "queue", "length", or "items") and validate their types (JObject/JArray or numeric) to prevent false positives.TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandQueueTests.cs (2)
79-89: Add a null/empty-agent cancellation authorization test.Given cancellation is ownership-sensitive, add a regression test that
Cancel(ticket, null)andCancel(ticket, "")are rejected.Proposed additional test
+ [Test] + public void Cancel_NullOrEmptyAgent_Fails() + { + var cmds = new List<BatchCommand> + { + new() { Tool = "refresh_unity", Params = new JObject(), Tier = ExecutionTier.Heavy } + }; + var job = _queue.Submit("agent-1", "heavy", false, cmds); + + Assert.That(_queue.Cancel(job.Ticket, null), Is.False); + Assert.That(_queue.Cancel(job.Ticket, ""), Is.False); + Assert.That(job.Status, Is.EqualTo(JobStatus.Queued)); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandQueueTests.cs` around lines 79 - 89, Update the Cancel_WrongAgent_Fails test to also assert cancellation is rejected when the caller agent is null or empty: call _queue.Cancel(job.Ticket, null) and _queue.Cancel(job.Ticket, "") and assert both return false and that job.Status remains JobStatus.Queued; keep the existing wrong-agent assertion. Use the same job created in Cancel_WrongAgent_Fails so you validate authorization logic in _queue.Cancel for null and empty caller values.
203-204: Tighten “proceeds” assertions to avoid false positives.
Is.Not.EqualTo(JobStatus.Queued)can still pass if a job fails or is cancelled. These tests should assert expected terminal/running states explicitly (and preferablyErroris null).Suggested assertion pattern
- Assert.That(job.Status, Is.Not.EqualTo(JobStatus.Queued)); + Assert.That(job.Status, Is.EqualTo(JobStatus.Running).Or.EqualTo(JobStatus.Done)); + Assert.That(job.Error, Is.Null);Also applies to: 217-218, 237-238
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandQueueTests.cs` around lines 203 - 204, Replace the loose negative assertion "Assert.That(job.Status, Is.Not.EqualTo(JobStatus.Queued))" with explicit positive assertions of the expected state (e.g., Assert.That(job.Status, Is.EqualTo(JobStatus.Running)) or Assert.That(job.Status, Is.EqualTo(JobStatus.Completed)) depending on the test) and add an assertion that the job has no error (e.g., Assert.IsNull(job.Error)). Update all occurrences in CommandQueueTests.cs where the loose check appears (the instance using the local variable job at the shown lines and the other two occurrences) so each test asserts the precise terminal or running state and verifies Error is null.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@MCPForUnity/Editor/Helpers/AssetPathUtility.cs`:
- Around line 106-112: GetMcpPackageRootPath currently returns
UIAssetSync.SyncedBasePath when UIAssetSync.NeedsSync() is true, which changes
the package root semantics and breaks downstream callers like GetPackageJson
that expect the real package root (so package.json can be found); instead,
preserve the true package root return value from GetMcpPackageRootPath and limit
the WSL workaround to UI-specific lookups only: revert the early return in
GetMcpPackageRootPath to always return the actual package root, and update any
code that loads UXML/USS (or GetPackageJson) to check UIAssetSync.NeedsSync()
and use UIAssetSync.SyncedBasePath only for UI asset resolution while keeping
GetPackageJson pointed at the real package root.
In `@MCPForUnity/Editor/Helpers/UIAssetSync.cs`:
- Around line 52-87: The static initializer UIAssetSync.UIAssetSync() currently
performs unguarded file I/O (File.ReadAllText, File.WriteAllText,
Directory.CreateDirectory) which can throw and break type initialization; wrap
the per-file operations inside a try/catch that catches IOException and general
Exception, log the error with Debug.LogError or Debug.LogException including the
relativePath and exception, skip that file and continue so the static ctor never
throws, and ensure the existing anyUpdated/AssetDatabase.Refresh logic remains
intact; keep the early returns from NeedsSync() and GetPackagePhysicalRoot()
unchanged.
In `@MCPForUnity/Editor/Tools/BatchExecute.cs`:
- Around line 255-273: The async batch loop currently skips malformed or
missing-tool entries and doesn't enforce disabled-tool policy; change it to
validate every command token and fail the entire batch if any invalid entry or a
disabled tool is found: for each token parsed in the loop (the block using
NormalizeParameterKeys, CommandRegistry.GetToolTier, CommandClassifier.Classify,
CommandClassifier.CausesDomainReload and creating new BatchCommand) detect
missing/empty toolName or non-JObject params and collect/return an ErrorResponse
listing the invalid entries instead of continuing, and check the
tool-enabled/disabled status the same way the sync path does (use the same
CommandRegistry/CommandClassifier call used in the sync path) and return an
ErrorResponse if any tool is disabled before adding commands to the commands
list.
- Around line 280-304: The current loop in BatchExecute (using
CommandRegistry.InvokeCommandAsync and collecting job.Results) unconditionally
sets job.Status = JobStatus.Done and returns a SuccessResponse even if some
commands produced ErrorResponse; after the foreach, inspect job.Results for any
ErrorResponse (or exceptions collected), and if any exist set job.Status =
JobStatus.Failed, populate job.Error/CompletedAt and return an ErrorResponse
(including ticket and results); only set Done and return SuccessResponse when no
ErrorResponse entries are present. Ensure you reference the existing symbols:
CommandRegistry.InvokeCommandAsync, ErrorResponse, SuccessResponse, job.Results,
JobStatus, atomic, and cmd.Tool when building the error payload.
- Around line 284-286: The blocking call in HandleAsyncSubmit uses
.ConfigureAwait(true).GetAwaiter().GetResult() which can deadlock the Unity
editor; change HandleAsyncSubmit to be async (e.g., Task or Task<TResult>
return) and replace that blocking call with await
CommandRegistry.InvokeCommandAsync(cmd.Tool, cmd.Params) so the call yields to
the Unity synchronization context; update any callers of HandleAsyncSubmit to
await the new async signature (or otherwise handle the returned Task), and keep
job.Results.Add(result) after the awaited call.
In `@MCPForUnity/Editor/Tools/CommandQueue.cs`:
- Around line 117-123: The Remove method currently clears _activeHeavyTicket
directly which bypasses the busy-hold and cooldown logic in ProcessTick; to fix
this, stop clearing _activeHeavyTicket inside Remove (leave
_smoothInFlight.Remove(ticket) and return _store.Remove(ticket) only) and let
ProcessTick handle clearing the active heavy slot and enforcing
cooldown/busy-hold, or if immediate clearing is absolutely required add a guard
that only clears _activeHeavyTicket when not in the editor-busy state/cooldown
(use the same busy/cooldown checks used by ProcessTick) so the
busy-hold/cooldown semantics remain consistent.
- Around line 61-66: In the Cancel method, the current check lets agent==null
bypass owner checks; change the authorization so a null agent cannot cancel
others: after retrieving job via _store.GetJob(ticket) and confirming job.Status
== JobStatus.Queued, require agent to be non-null and equal to job.Agent before
proceeding (i.e., return false if agent is null or agent != job.Agent). Update
the conditional that currently reads "if (job.Agent != agent && agent != null)
return false;" to enforce this stricter check using Cancel, job.Agent, and
JobStatus.
- Around line 199-208: RestoreFromJson currently repopulates _heavyQueue from
_store but doesn't clear transient runtime state first, causing duplicates on
repeated restores; before calling _store.FromJson or before re-enqueuing jobs in
RestoreFromJson, clear/reset the runtime-only fields: call _heavyQueue.Clear()
(or recreate it), clear the _smoothInFlight collection (e.g.,
_smoothInFlight.Clear() or new empty instance), and reset _activeHeavyTicket to
its default/null value, then proceed to call _store.FromJson and re-enqueue
tickets from _store.GetQueuedJobs() as you already do.
- Around line 37-45: Submit currently assigns the caller-owned list to
job.Commands which allows external mutation; instead make a defensive copy in
Submit by creating a new List<BatchCommand> and deep-cloning each element before
assigning to job.Commands (e.g., construct new BatchCommand instances copying
primitive fields and using JToken/JObject.DeepClone() or a BatchCommand.Clone
method for Params), so references to the original list or JObjects cannot modify
the queued job; update Submit in CommandQueue and add/use a Clone method on
BatchCommand if needed.
In `@MCPForUnity/Editor/Tools/GatewayJobLogger.cs`:
- Around line 82-87: The GatewayJobLogger.Log() call performs a synchronous
File.AppendAllText using LogPath on the Unity editor main thread (invoked from
PollJob.HandleCommand), which can block; change Log to perform non-blocking
background writes (e.g., move the file write and directory-creation logic into
Task.Run or an internal background writer/queue) so the main thread only
enqueues the log entry and returns immediately; ensure you reference and use
LogPath, the existing entry.ToString(Formatting.None) payload, and preserve
directory creation semantics (create dir inside the background task or use a
thread-safe check) and handle exceptions from the background task so failures
are logged without throwing on the main thread.
---
Outside diff comments:
In `@MCPForUnity/Editor/Tools/BatchExecute.cs`:
- Around line 35-66: The top-level parameter parsing in HandleCommand is using
JObject.Value/ indexing directly; replace this with a ToolParams instance (e.g.
new ToolParams(`@params`)) and call its helpers (GetInt(), GetBool() / GetString()
/ RequireString() as appropriate) to read "async", "failFast", "parallel",
"maxParallelism" and the "commands" array so validation is consistent with other
editor tools; specifically, change the commandsToken extraction to use
ToolParams to require/validate the "commands" entry and replace usages of
`@params.Value`<int?>("maxParallelism") and `@params.Value`<bool?>("async") etc.
with the matching ToolParams methods while preserving the async vs legacy flow
inside HandleCommand and when calling HandleAsyncSubmit.
---
Minor comments:
In `@docs/plans/2026-02-25-gateway-async-awareness-design.md`:
- Around line 27-31: The fenced code block containing the conditional snippet
with symbols job.CausesDomainReload, TestJobManager.HasRunningJob, and
EditorApplication.isCompiling is missing a language specifier; update the
opening fence to include a language token (for example "text") so the block
becomes ```text ... ``` to satisfy markdownlint while preserving the exact
contents of the snippet.
In `@docs/plans/2026-02-25-gateway-async-awareness-plan.md`:
- Around line 90-94: The markdown contains unlabeled fenced code blocks (e.g.,
the run_tests(...) snippet and other blocks referenced at lines 90, 121, 886,
893, 904) causing MD040; update each triple-backtick fence to include an
appropriate language identifier (for the run_tests snippet use text or bash, and
for other blocks choose csharp/json/text as appropriate) so every code fence is
labeled (e.g., ```text or ```csharp) while keeping the block contents unchanged.
- Line 13: Heading level jumps from H1 to H3 for the section titled "Task 1: Add
`CausesDomainReload` to CommandClassifier"; change the heading token from "###
Task 1: Add `CausesDomainReload` to CommandClassifier" to "## Task 1: Add
`CausesDomainReload` to CommandClassifier" so the document uses a proper H2 and
satisfies markdown lint MD001.
In `@docs/plans/2026-02-25-queue-summary-ui-design.md`:
- Line 13: Add explicit language specifiers to the fenced code blocks containing
ASCII-art/pseudo-code (the two triple-backtick blocks flagged by MD040) by
changing their opening fences from ``` to a tagged form like ```text (or
```ascii) so the linter recognizes them; update both code fences so each opening
triple-backtick includes the chosen language tag.
In `@docs/plans/2026-02-25-queue-summary-ui-plan.md`:
- Around line 35-38: The Markdown fenced code blocks like the one containing
"refresh_unity(scope=all, compile=request, wait_for_ready=true)" and the
"read_console(types=[\"error\"]) → 0 errors" output are unlabeled and trigger
MD040; edit each such fenced block (including the other occurrences mentioned)
to add an explicit language identifier (e.g., ```bash, ```text or ```console)
after the opening backticks so the block is annotated and the linter no longer
flags MD040.
- Line 15: The markdown heading "Task 1: Expose GetAllJobs on CommandQueue" is
currently H3 (###) causing a jump from the document H1; change this and all peer
task headings ("Task 1", "Task 2", etc.) from H3 to H2 (##) so the section
hierarchy is H1 -> H2 -> H3 and MD001 is satisfied; update the lines with the
exact heading texts to use "##" instead of "###".
In `@MCPForUnity/Editor/Helpers/UIAssetSync.cs`:
- Around line 71-72: The code currently computes destPath with
Path.GetFullPath(Path.Combine(SyncedBasePath, relativePath)) which uses the
process CWD; change the construction to anchor against the Unity project root by
using Path.GetDirectoryName(Application.dataPath) as the base before combining
with SyncedBasePath and relativePath so destPath and destDir are deterministic.
Update the logic that sets destPath and destDir (the variables destPath, destDir
and the expression using SyncedBasePath/relativePath) to first compute
projectRoot = Path.GetDirectoryName(Application.dataPath) and then combine
projectRoot with SyncedBasePath and relativePath to produce the final paths.
In `@MCPForUnity/Editor/Tools/CommandQueue.cs`:
- Around line 144-146: The progress string in CommandQueue is off by one for
completed jobs because it uses j.CurrentIndex + (j.Status == JobStatus.Done ? 0
: 1); change the assignment that sets entry["p"] so it always adds 1 to
j.CurrentIndex (e.g. $"{j.CurrentIndex + 1}/{j.Commands.Count}") to ensure a
finished job reports N/N; update the line inside the block that checks
j.Commands to use j.CurrentIndex + 1 unconditionally.
In `@MCPForUnity/Editor/Tools/PollJob.cs`:
- Around line 57-68: Guard against job.Commands being null in the
JobStatus.Running branch: before using job.Commands.Count or constructing the
progress string, check for null (or use a safe fallback) and compute safeCount =
job.Commands?.Count ?? 0 and safeIndex = Math.Min(job.CurrentIndex + 1,
safeCount); then build the PendingResponse using safeCount and safeIndex for the
message and progress fields (update the Running case that creates the
PendingResponse to reference these safe values rather than job.Commands.Count
directly).
- Around line 18-21: The code is using p.GetRequired("ticket") instead of the
ToolParams guideline method; replace the call with p.RequireString("ticket") (or
the equivalent RequireString method on ToolParams) and update handling to use
the returned result (e.g., check success and use its Value or return new
ErrorResponse(result.ErrorMessage)) so the Ticket extraction uses
ToolParams.RequireString(), referenced variable p, and the existing
ErrorResponse/ticketResult flow.
In `@MCPForUnity/Editor/Tools/TicketStore.cs`:
- Around line 39-45: The CleanExpired logic in TicketStore.CleanExpired
currently filters terminal jobs by JobStatus.Done or JobStatus.Failed and thus
misses cancelled terminal jobs; update the expiry predicate used when building
expired (the LINQ Where over _jobs entries) to also include kvp.Value.Status ==
JobStatus.Cancelled so cancelled jobs with CompletedAt older than expiry are
selected and removed from _jobs; ensure the selection still checks
CompletedAt.HasValue and the time-based comparison remains unchanged.
In `@MCPForUnity/Editor/Windows/Components/Queue/McpQueueSection.cs`:
- Around line 216-221: The progress calculation currently uses job.CurrentIndex
+ 1 for non-done jobs which makes queued jobs show "1/N"; change the logic in
McpQueueSection (the block that builds progressText) to use job.CurrentIndex for
non-Done jobs and leave Done as job.Commands.Count, i.e. set idx = job.Status ==
JobStatus.Done ? job.Commands.Count : job.CurrentIndex so queued jobs render as
"0/N".
In `@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/TicketStoreTests.cs`:
- Around line 113-121: The test GetQueuedJobs_OrderedByCreationTime is flaky
because CreateJob may assign identical CreatedAt timestamps; update the test to
avoid relying on millisecond ordering by either explicitly setting distinct
CreatedAt values on j1 and j2 after creation (e.g., j1.CreatedAt = now;
j2.CreatedAt = now.AddMilliseconds(1)) before calling _store.GetQueuedJobs(), or
change the assertions to check that both tickets are present without requiring
specific positions (e.g., Assert.That(queued.Select(q => q.Ticket),
Is.EquivalentTo(new[] { j1.Ticket, j2.Ticket }))); modify only the test logic
around CreateJob, j1/j2, and the assertions so GetQueuedJobs and _store remain
unchanged.
---
Nitpick comments:
In `@MCPForUnity/Editor/Tools/CommandClassifier.cs`:
- Around line 48-58: The CausesDomainReload method treats a missing "compile"
key as causing a domain reload because `@params.Value`<string>("compile") returns
null and null != "none" is true; add a brief inline comment above the
"refresh_unity" case (referencing CausesDomainReload and the
`@params.Value`<string>("compile") expression) explaining that returning true on
missing/unknown compile values is an intentional conservative default to avoid
silently skipping reloads.
- Around line 33-43: ClassifyBatch currently initializes max to
ExecutionTier.Instant so an empty commands array returns Instant; add a short
comment above ClassifyBatch explaining that ExecutionTier.Instant is being used
intentionally as the sentinel/default for "no commands" (and reference that
ClassifyBatch calls Classify for each command), so future readers understand
that an empty batch represents no restrictions rather than a neutral Smooth
value.
In `@MCPForUnity/Editor/Tools/CommandGatewayState.cs`:
- Around line 37-41: OnUpdate currently creates a new async lambda each tick
which allocates; pull that lambda out into a static readonly delegate and pass
the cached delegate to Queue.ProcessTick instead. Create a static readonly
Func<ToolType, ParamType, Task> (or matching delegate signature used by
Queue.ProcessTick) field (name e.g. s_processTickHandler) that calls
CommandRegistry.InvokeCommandAsync(tool, `@params`) and replace the inline async
lambda in OnUpdate with that field to avoid per-frame allocations; keep the call
to CommandRegistry.InvokeCommandAsync unchanged.
In `@MCPForUnity/Editor/Tools/CommandRegistry.cs`:
- Around line 198-207: GetToolTier currently returns ExecutionTier.Smooth when a
command isn't found in _handlers, which can hide callers' initialization/order
issues (e.g., BatchExecute calling GetToolTier before
CommandRegistry.Initialize); update the XML doc summary for GetToolTier to
explicitly state that unregistered or not-yet-initialized commands will default
to ExecutionTier.Smooth so callers are aware of the sentinel behavior and can
change their flow or call Initialize first.
In `@MCPForUnity/Editor/Tools/QueueStatus.cs`:
- Around line 14-18: Wrap the incoming JObject in a ToolParams instance inside
HandleCommand to match other tools' patterns: create a local variable like tp =
new ToolParams(`@params`) at the start of the method and use tp (even if not used
further) to preserve consistency and make future optional parameter handling
trivial; update the HandleCommand method signature/body so it constructs
ToolParams before calling CommandGatewayState.Queue.GetSummary() and returning
the SuccessResponse.
In `@MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs`:
- Around line 332-349: The Queue section load lacks diagnostic warnings when
assets fail to load; update the block that references queueTree and
queueStyleSheet (and the creation of McpQueueSection) to log explicit warnings
when queueTree is null and when queueStyleSheet is null so failures are
visible—use the same logging strategy as the Tools/Resources block (e.g., call
Debug.LogWarning or an existing logger) to report "McpQueueSection.uxml not
found" and "McpQueueSection.uss not found" and ensure you do not attempt to
instantiate McpQueueSection or add a null stylesheet to rootVisualElement when
the assets are missing.
In
`@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteAsyncTests.cs`:
- Around line 26-52: Update the tests to assert on the response contract instead
of only non-null: for PollJob.HandleCommand in PollJob_NullParams_ReturnsError,
PollJob_MissingTicket_ReturnsError, and PollJob_InvalidTicket_ReturnsNotFound
assert the returned JObject contains an error/ kind/status field (e.g.,
result["kind"] == "error" or result["status"] == "error") and for
PollJob_InvalidTicket also assert an appropriate not-found indicator (status
code or message). For QueueStatus.HandleCommand in
QueueStatus_ReturnsStatusObject assert the response is a structured status
payload (e.g., contains expected keys like "queue", "length", or "items") and
validate their types (JObject/JArray or numeric) to prevent false positives.
In
`@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandClassifierTests.cs`:
- Around line 27-40: Add unit tests to cover the missing branches by invoking
CommandClassifier.Classify with the same patterns used in existing tests: add a
test Classify_ManageScene_Create_ReturnsHeavy that calls
CommandClassifier.Classify("manage_scene", ExecutionTier.Smooth, new JObject {
["action"] = "create" }) and asserts ExecutionTier.Heavy, and add tests
Classify_ManageEditor_Pause_ReturnsHeavy and
Classify_ManageEditor_Stop_ReturnsHeavy that call
CommandClassifier.Classify("manage_editor", ExecutionTier.Smooth, new JObject {
["action"] = "pause" }) and new JObject { ["action"] = "stop" } respectively and
assert ExecutionTier.Heavy to ensure the "create", "pause", and "stop" switch
arms are covered.
In `@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandQueueTests.cs`:
- Around line 79-89: Update the Cancel_WrongAgent_Fails test to also assert
cancellation is rejected when the caller agent is null or empty: call
_queue.Cancel(job.Ticket, null) and _queue.Cancel(job.Ticket, "") and assert
both return false and that job.Status remains JobStatus.Queued; keep the
existing wrong-agent assertion. Use the same job created in
Cancel_WrongAgent_Fails so you validate authorization logic in _queue.Cancel for
null and empty caller values.
- Around line 203-204: Replace the loose negative assertion
"Assert.That(job.Status, Is.Not.EqualTo(JobStatus.Queued))" with explicit
positive assertions of the expected state (e.g., Assert.That(job.Status,
Is.EqualTo(JobStatus.Running)) or Assert.That(job.Status,
Is.EqualTo(JobStatus.Completed)) depending on the test) and add an assertion
that the job has no error (e.g., Assert.IsNull(job.Error)). Update all
occurrences in CommandQueueTests.cs where the loose check appears (the instance
using the local variable job at the shown lines and the other two occurrences)
so each test asserts the precise terminal or running state and verifies Error is
null.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (50)
MCPForUnity/Editor/Constants/EditorPrefKeys.csMCPForUnity/Editor/Helpers/AssetPathUtility.csMCPForUnity/Editor/Helpers/UIAssetSync.csMCPForUnity/Editor/Helpers/UIAssetSync.cs.metaMCPForUnity/Editor/Tools/BatchExecute.csMCPForUnity/Editor/Tools/BatchJob.csMCPForUnity/Editor/Tools/BatchJob.cs.metaMCPForUnity/Editor/Tools/CommandClassifier.csMCPForUnity/Editor/Tools/CommandClassifier.cs.metaMCPForUnity/Editor/Tools/CommandGatewayState.csMCPForUnity/Editor/Tools/CommandGatewayState.cs.metaMCPForUnity/Editor/Tools/CommandQueue.csMCPForUnity/Editor/Tools/CommandQueue.cs.metaMCPForUnity/Editor/Tools/CommandRegistry.csMCPForUnity/Editor/Tools/ExecutionTier.csMCPForUnity/Editor/Tools/ExecutionTier.cs.metaMCPForUnity/Editor/Tools/FindGameObjects.csMCPForUnity/Editor/Tools/GatewayJobLogger.csMCPForUnity/Editor/Tools/GatewayJobLogger.cs.metaMCPForUnity/Editor/Tools/GetTestJob.csMCPForUnity/Editor/Tools/ManageScript.csMCPForUnity/Editor/Tools/ManageShader.csMCPForUnity/Editor/Tools/McpForUnityToolAttribute.csMCPForUnity/Editor/Tools/PollJob.csMCPForUnity/Editor/Tools/PollJob.cs.metaMCPForUnity/Editor/Tools/QueueStatus.csMCPForUnity/Editor/Tools/QueueStatus.cs.metaMCPForUnity/Editor/Tools/ReadConsole.csMCPForUnity/Editor/Tools/RefreshUnity.csMCPForUnity/Editor/Tools/RunTests.csMCPForUnity/Editor/Tools/TicketStore.csMCPForUnity/Editor/Tools/TicketStore.cs.metaMCPForUnity/Editor/Windows/Components/Queue.metaMCPForUnity/Editor/Windows/Components/Queue/McpQueueSection.csMCPForUnity/Editor/Windows/Components/Queue/McpQueueSection.cs.metaMCPForUnity/Editor/Windows/Components/Queue/McpQueueSection.ussMCPForUnity/Editor/Windows/Components/Queue/McpQueueSection.uss.metaMCPForUnity/Editor/Windows/Components/Queue/McpQueueSection.uxmlMCPForUnity/Editor/Windows/Components/Queue/McpQueueSection.uxml.metaMCPForUnity/Editor/Windows/MCPForUnityEditorWindow.csMCPForUnity/Editor/Windows/MCPForUnityEditorWindow.uxmlTestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BatchExecuteAsyncTests.csTestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandClassifierTests.csTestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandQueueTests.csTestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecutionTierTests.csTestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/TicketStoreTests.csdocs/plans/2026-02-25-gateway-async-awareness-design.mddocs/plans/2026-02-25-gateway-async-awareness-plan.mddocs/plans/2026-02-25-queue-summary-ui-design.mddocs/plans/2026-02-25-queue-summary-ui-plan.md
|
@copilot open a new pull request to apply changes based on the comments in this thread |
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 50 out of 50 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@copilot open a new pull request to apply changes based on the comments in in the thread in general |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
docs/plans/2026-02-25-queue-summary-ui-design.md (2)
53-60: Add a language specifier to the fenced code block.The data-flow block has no language tag, triggering MD040. Use
textorplaintext.✏️ Proposed fix
-``` +```text CommandGatewayState.Queue🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/plans/2026-02-25-queue-summary-ui-design.md` around lines 53 - 60, The fenced code block containing the data-flow diagram (starting with CommandGatewayState.Queue and lines referencing TicketStore.GetAllJobs, Queue.HasActiveHeavy, Queue.QueueDepth, Queue.SmoothInFlight, Queue.IsEditorBusy) is missing a language specifier, which triggers MD040; fix it by adding a language tag (e.g., text or plaintext) to the opening fence so the block reads ```text followed by the existing lines and the closing ``` to satisfy the linter.
13-30: Add a language specifier to the fenced code block.The ASCII art layout block has no language tag, triggering MD040. Use
textorplaintextto silence the warning.✏️ Proposed fix
-``` +```text ┌───────────────────────────────────────────────┐🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/plans/2026-02-25-queue-summary-ui-design.md` around lines 13 - 30, The fenced ASCII-art code block is missing a language specifier, triggering MD040; update the opening triple-backticks for the ASCII diagram (the block starting with the box diagram/Status Bar and Job List) to include a language tag like ```text or ```plaintext so the linter accepts it and the diagram remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/plans/2026-02-25-queue-summary-ui-design.md`:
- Line 19: The ASCII layout at the given diagram is missing the "Failed" counter
declared in the spec (line 36); update the diagram to include all four counters
— "Queued", "Running", "Done", and "Failed" — e.g., change the line showing
"Queued: 2 │ Running: 1 │ Done: 5" to include "│ Failed: 0" (or the appropriate
example value) and ensure separators/spacing match the existing style, or if the
design intentionally omits failures, instead update the component spec to remove
"Failed" so both the spec and the ASCII layout are consistent.
---
Nitpick comments:
In `@docs/plans/2026-02-25-queue-summary-ui-design.md`:
- Around line 53-60: The fenced code block containing the data-flow diagram
(starting with CommandGatewayState.Queue and lines referencing
TicketStore.GetAllJobs, Queue.HasActiveHeavy, Queue.QueueDepth,
Queue.SmoothInFlight, Queue.IsEditorBusy) is missing a language specifier, which
triggers MD040; fix it by adding a language tag (e.g., text or plaintext) to the
opening fence so the block reads ```text followed by the existing lines and the
closing ``` to satisfy the linter.
- Around line 13-30: The fenced ASCII-art code block is missing a language
specifier, triggering MD040; update the opening triple-backticks for the ASCII
diagram (the block starting with the box diagram/Status Bar and Job List) to
include a language tag like ```text or ```plaintext so the linter accepts it and
the diagram remains unchanged.
Normalize file permissions by changing several files from executable (100755) to non-executable (100644). Affected files include: .github/scripts/mark_skipped.py, mcp_source.py, scripts/validate-nlt-coverage.sh, tools/docker_publish.sh, tools/generate_mcpb.py, tools/pypi_publish.sh, tools/update_fork.bat, tools/update_fork.sh, and tools/update_versions.py. This avoids accidental execution and keeps repository file modes consistent.
…ty freezes Three targeted fixes for the stuck queue problem: 1. TestJobManager: reduce stale job threshold from 5 min to 60s on domain reload restore. Orphaned test jobs are detected and cleared faster. 2. CommandQueue: add 90s watchdog timer. If IsEditorBusy blocks the heavy queue continuously for >90s, auto-clears stuck test jobs via ClearStuckJob(). Logs a warning when triggered. Resets when editor becomes non-busy. 3. BatchExecute: add fail_fast support to Instant-tier async path. Previously only atomic was checked — now fail_fast also stops on first failure result (not just exceptions). Root cause: domain reload during test run orphans the TestJobManager _currentJobId in SessionState. HasRunningJob stays true, IsEditorBusy blocks the heavy queue, and all subsequent MCP commands spin indefinitely. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ueue Direct transport commands now get the same tier-aware scheduling, heavy exclusivity, domain-reload guards, CancellationToken support, and emergency flush coverage as batch_execute(async=true) commands. Instant-tier commands still execute directly for zero-overhead reads. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Export(SDFGraphAsset, FlowDirection = LR) has 2 parameters due to the optional default. Use name-based lookup instead of exact parameter type matching, and pass Type.Missing for the optional parameter. Also add System.Linq for the LINQ query. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When identical commands are retried on timeout, the dispatcher and queue now detect duplicates and share the existing result instead of creating new entries. Prevents Unity editor freezes from backed-up command queues. - Dispatcher-level dedup: identical pending JSON shares TaskCompletionSource - Queue-level dedup: Submit/SubmitSingle return existing job if match found - BatchJob.Deduplicated flag surfaces dedup status in responses - Gateway responses include _queue metadata (ticket, tier, queued) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts: # MCPForUnity/Editor/Constants/EditorPrefKeys.cs # MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs # MCPForUnity/Editor/Tools/GetTestJob.cs # MCPForUnity/Editor/Tools/ManageShader.cs # MCPForUnity/Editor/Tools/RunTests.cs # MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
…ure/command-gateway
chore: bump version to 9.6.5
Two bugs reported when running Unity MCP under multiple concurrent Claude Code agents: 1. **Stalls under heavy agent load** (TransportCommandDispatcher + CommandQueue) The content-hash dedup added in 86bd1a6 leaked waiters when the original pending command hung: every subsequent identical request returned the original's TaskCompletionSource without registering its own cancellation, so cancelled callers still blocked, and the ContentHashToPendingId entry kept catching new commands that piled onto the dead TCS. Additionally, CommandQueue.Submit/SubmitSingle deduped against Queued jobs whose Cts had not yet been initialised (Cts is created inside ExecuteJob at line 511, after Queued->Running transition). If dispatch failed between Submit and ExecuteJob, waiters that deduped onto that job would hang forever. Fixes: - Add 60s DedupTtl on TransportCommandDispatcher — entries older than the TTL are evicted and new callers get a fresh PendingCommand instead of piling onto a stuck TCS. - Propagate the new caller's cancellation token into a linked waiter TCS so each dedup waiter can bail independently. - Evict stale dedup mappings whose original was cancelled. - CommandQueue.Submit and SubmitSingle now only dedup against Running jobs (not Queued), guaranteeing Cts is initialised at dedup time. 2. **Wrong Unity instance focused** (unity_instance_middleware) Multiple Claude Code processes sharing the same HTTP server on port 8080 all collapsed to the "global" session key when client_id was unpopulated, so set_active_instance from process A would overwrite the active instance for process B. Additionally, get_active_instance returned its cached ID without verifying the underlying Unity editor was still connected, so stale entries could route calls to disconnected instances. Fixes: - get_session_key now falls back to ctx.session_id (per-HTTP-connection token from FastMCP) before the last-resort "global" key, eliminating cross-process collision. - get_active_instance performs a throttled liveness check (5s TTL, primed by set_active_instance so the hot path stays free) and evicts entries whose ID is no longer in the discovered instance list. - Warning log when falling back to "global" so multi-client setups get an early signal. Tests: 934/934 Server-side Python tests pass, including the 46 instance-routing integration tests. Two characterization tests that codified the old "fall back to global" behavior were updated to assert the new session_id fallback. C# changes are not compiled here (no Unity project) — they will be verified on next Unity MCP restart. The transport dispatcher changes add no new API surface and touch only the dedup path, so risk is scoped to commands that currently trigger dedup.
Brings fork up to date with 178 upstream commits while preserving the CommandGateway tier-aware dispatch and stall/instance-routing fixes from c0f2802. Conflict resolutions: - McpForUnityToolAttribute.cs: additive union of ExecutionTier (HEAD) and MaxPollSeconds (origin/main). - MCPForUnityEditorWindow.cs: accept upstream removal of validation panel; keep independent queue panel in null-check list. ToolDiscoveryService.cs auto-merged cleanly with MaxPollSeconds propagation. 1203/1204 Python tests pass (the 1 failure is a pre-existing upstream prefab CLI test unrelated to the gateway). Backup tag: backup/pre-merge-main-20260406-162401
The Python execute_custom_tool tool is a server-side façade that resolves project_id/user_id before sending the inner tool name over the bridge. batch_execute runs entirely in Unity C# and dispatches through CommandRegistry, which has no handler named 'execute_custom_tool' — so batching it threw "Unknown or unsupported command type". Fix: in BatchExecute.HandleCommand (both legacy sync path and async gateway path), detect tool == "execute_custom_tool" and rewrite the entry to target the inner tool_name with its nested parameters. The inner params are re-normalized to camelCase so downstream handlers see consistent key casing. Caveat documented in code: this bypasses the façade's project_id / user_id resolution, so project-scoped custom tools that depend on multi-project scoping should still be called through execute_custom_tool outside of a batch. Also bumps Server/uv.lock mcpforunityserver 9.6.4 → 9.6.5. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (8)
MCPForUnity/Editor/Helpers/AssetPathUtility.cs (1)
110-116:⚠️ Potential issue | 🟠 MajorKeep
GetMcpPackageRootPath()semantics stable (actual package root only).Line 115 returns
UIAssetSync.SyncedBasePath, which changes this API’s meaning and breaks non-UI callers. For example, Line 164 (GetPackageJson()) will resolvepackage.jsonunder the UI mirror path in WSL mode and can fall back to"unknown"metadata unexpectedly.💡 Proposed fix
- // WSL workaround: when the package lives on a WSL UNC path and Unity - // runs on Windows, UXML/USS files cannot be parsed from the UNC path. - // UIAssetSync copies them to Assets/MCPForUnityUI/ on domain reload. - if (UIAssetSync.NeedsSync()) - { - return UIAssetSync.SyncedBasePath; - } + // Keep this API returning the actual package root. + // Callers that need synced UI assets should opt in to + // UIAssetSync.SyncedBasePath explicitly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Helpers/AssetPathUtility.cs` around lines 110 - 116, GetMcpPackageRootPath() must always return the actual package root path, not the UI mirrored path; remove the early return of UIAssetSync.SyncedBasePath when UIAssetSync.NeedsSync() is true and ensure GetMcpPackageRootPath() only computes and returns the real package root; callers that need the mirrored UI path (e.g., UXML/USS parsing) should explicitly call UIAssetSync.SyncedBasePath or a new helper (do not change GetPackageJson() behavior), so keep references to UIAssetSync limited to UI-specific code and preserve GetMcpPackageRootPath() semantics.MCPForUnity/Editor/Tools/BatchExecute.cs (3)
322-324:⚠️ Potential issue | 🔴 CriticalCritical: Blocking
.GetAwaiter().GetResult()on editor main thread risks deadlock.
CommandRegistry.InvokeCommandAsyncusesConfigureAwait(true)internally (per context snippet), which posts continuations back to Unity'sSynchronizationContext. Calling.GetAwaiter().GetResult()on the main thread blocks it, preventing those continuations from executing — a classic deadlock scenario.Convert
HandleAsyncSubmitto async and useawaitinstead:🐛 Proposed fix
- private static object HandleAsyncSubmit(JObject `@params`, JArray commandsToken) + private static async Task<object> HandleAsyncSubmit(JObject `@params`, JArray commandsToken) { // ... existing setup code ... if (job.Tier == ExecutionTier.Instant) { foreach (var cmd in commands) { try { - var result = CommandRegistry.InvokeCommandAsync(cmd.Tool, cmd.Params) - .ConfigureAwait(true).GetAwaiter().GetResult(); + var result = await CommandRegistry.InvokeCommandAsync(cmd.Tool, cmd.Params) + .ConfigureAwait(true); job.Results.Add(result);Also update the caller at line 59:
if (isAsync) { - return HandleAsyncSubmit(`@params`, commandsToken); + return await HandleAsyncSubmit(`@params`, commandsToken).ConfigureAwait(true); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/BatchExecute.cs` around lines 322 - 324, The code is blocking the Unity main thread by calling .ConfigureAwait(true).GetAwaiter().GetResult() on CommandRegistry.InvokeCommandAsync inside HandleAsyncSubmit; change HandleAsyncSubmit to be async and replace the blocking call with an awaited call (e.g., var result = await CommandRegistry.InvokeCommandAsync(cmd.Tool, cmd.Params);) and then job.Results.Add(result); and propagate async/await to its caller(s) (update the call site that invokes HandleAsyncSubmit to await it and mark that method async as well) so continuations run on the Unity context without deadlock.
291-311:⚠️ Potential issue | 🟠 MajorAsync path silently drops invalid command entries without validation errors.
Lines 293-295 and 300 use
continueto skip malformed/missing-tool entries, allowing partial batch execution. Unlike the sync path (lines 77-116), the async path doesn't record errors for invalid entries or enforce disabled-tool checks before queuing.🐛 Proposed fix sketch
+ var validationErrors = new List<string>(); foreach (var token in commandsToken) { - if (token is not JObject cmdObj) continue; + if (token is not JObject cmdObj) + { + validationErrors.Add("Command entries must be JSON objects."); + continue; + } string toolName = cmdObj["tool"]?.ToString(); - if (string.IsNullOrWhiteSpace(toolName)) continue; + if (string.IsNullOrWhiteSpace(toolName)) + { + validationErrors.Add("Each command must include a non-empty 'tool' field."); + continue; + } + + // Check disabled tools like sync path + var toolMeta = MCPServiceLocator.ToolDiscovery.GetToolMetadata(toolName); + if (toolMeta != null && !MCPServiceLocator.ToolDiscovery.IsToolEnabled(toolName)) + { + validationErrors.Add($"Tool '{toolName}' is disabled."); + continue; + } // ... rest of processing } + + if (validationErrors.Count > 0) + return new ErrorResponse($"Invalid commands in batch: {string.Join("; ", validationErrors)}");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/BatchExecute.cs` around lines 291 - 311, The async loop currently skips malformed or disallowed commands silently; update the foreach that processes commandsToken (using NormalizeParameterKeys, UnwrapExecuteCustomTool, CommandRegistry.GetToolTier, CommandClassifier.Classify, CommandClassifier.CausesDomainReload and creating BatchCommand) to perform the same validation as the sync path: collect validation errors for missing/empty tool names, malformed params, and disabled tools instead of using continue, enforce the disabled-tool check before adding to commands, and if any invalid entries are found return an ErrorResponse summarizing those errors (similar to the sync path behavior) rather than silently dropping entries.
349-352:⚠️ Potential issue | 🟠 MajorInstant async batch always reports success even when individual commands fail.
After the foreach loop,
job.Status = JobStatus.DoneandSuccessResponseare returned regardless of whether any commands producedErrorResponseresults (unlessatomic/failFasttriggered early exit). This masks failures from callers.🐛 Proposed fix
+ bool anyFailed = job.Results.Any(r => r is IMcpResponse resp && !resp.Success); - job.Status = JobStatus.Done; + job.Status = anyFailed ? JobStatus.Failed : JobStatus.Done; job.CompletedAt = DateTime.UtcNow; - return new SuccessResponse("Batch completed (instant).", - new { ticket = job.Ticket, results = job.Results }); + return anyFailed + ? new ErrorResponse("One or more instant commands failed.", + new { ticket = job.Ticket, results = job.Results }) + : new SuccessResponse("Batch completed (instant).", + new { ticket = job.Ticket, results = job.Results });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/BatchExecute.cs` around lines 349 - 352, The code unconditionally sets job.Status = JobStatus.Done and returns a SuccessResponse even if some entries in job.Results are ErrorResponse; update the end-of-batch logic to scan job.Results for any ErrorResponse (or error indicator), if any exist set job.Status = JobStatus.Failed (and job.CompletedAt = DateTime.UtcNow) and return an appropriate failure response containing the ticket and error details, otherwise set Done and return the SuccessResponse as before; refer to job.Results, JobStatus.Done/Failed, SuccessResponse, and the atomic/failFast behavior when implementing the conditional return.MCPForUnity/Editor/Tools/CommandQueue.cs (4)
371-381:⚠️ Potential issue | 🟠 MajorRestoreFromJson doesn't clear transient runtime state before restore.
Calling
RestoreFromJsonmultiple times (e.g., during repeated domain reloads in tests) leaves stale entries in_heavyQueue,_smoothInFlight, and_activeHeavyTicket, causing duplicate scheduling.🐛 Proposed fix
public void RestoreFromJson(string json) { + _activeHeavyTicket = null; + _heavyQueue.Clear(); + _smoothInFlight.Clear(); + _store.FromJson(json); // Re-populate the heavy queue from restored queued jobs foreach (var job in _store.GetQueuedJobs()) { if (job.Tier == ExecutionTier.Heavy) _heavyQueue.Enqueue(job.Ticket); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/CommandQueue.cs` around lines 371 - 381, RestoreFromJson currently calls _store.FromJson and then repopulates _heavyQueue without clearing transient runtime state, causing duplicates on repeated restores; update RestoreFromJson to first clear/reset all transient fields (_heavyQueue.Clear() or reinstantiate, _smoothInFlight.Clear() or reinstantiate, and set _activeHeavyTicket to null/default) before calling _store.FromJson or before re-enqueueing, then repopulate _heavyQueue by iterating over _store.GetQueuedJobs() as done now so restored state is deterministic.
219-225:⚠️ Potential issue | 🟠 MajorClearing
_activeHeavyTicketinRemovebypasses busy-hold logic.
ProcessTickintentionally holds the heavy slot whileIsEditorBusy()returns true (lines 449-450). Directly clearing_activeHeavyTicketinRemove(called during poll cleanup) can release the slot prematurely, allowing domain-reload jobs to dequeue while async side-effects are still running.🐛 Proposed fix
public BatchJob Remove(string ticket) { - if (_activeHeavyTicket == ticket) - _activeHeavyTicket = null; _smoothInFlight.Remove(ticket); return _store.Remove(ticket); }Let
ProcessTickhandle clearing_activeHeavyTicketwhen the job is removed (line 432-440 already handles null job case).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/CommandQueue.cs` around lines 219 - 225, The Remove method currently clears the heavy-slot marker (_activeHeavyTicket) which can bypass the busy-hold logic in ProcessTick; update Remove (the method that returns BatchJob and calls _smoothInFlight.Remove and _store.Remove) to stop touching _activeHeavyTicket (remove the if (_activeHeavyTicket == ticket) _activeHeavyTicket = null line) so that ProcessTick remains responsible for clearing the heavy slot when it observes the job is gone/null; leave the calls to _smoothInFlight.Remove(ticket) and return _store.Remove(ticket) intact.
45-68:⚠️ Potential issue | 🟠 MajorDefensively copy submitted commands to prevent post-submit mutation.
job.Commands = commands(line 60) aliases the caller-owned mutable list. Any later mutation to the list or its JObjects can alter queued execution behavior unexpectedly.🐛 Proposed fix
public BatchJob Submit(string agent, string label, bool atomic, List<BatchCommand> commands) { + if (commands == null || commands.Count == 0) + throw new ArgumentException("At least one command is required.", nameof(commands)); + + var normalizedCommands = commands.Select(c => new BatchCommand + { + Tool = c.Tool, + Params = c.Params != null ? (JObject)c.Params.DeepClone() : new JObject(), + Tier = c.Tier, + CausesDomainReload = c.CausesDomainReload + }).ToList(); + // --- Dedup check for multi-command batches --- - var existing = FindDuplicateBatch(agent, commands); + var existing = FindDuplicateBatch(agent, normalizedCommands); // ... var batchTier = CommandClassifier.ClassifyBatch( - commands.Select(c => (c.Tool, c.Tier, c.Params)).ToArray()); + normalizedCommands.Select(c => (c.Tool, c.Tier, c.Params)).ToArray()); var job = _store.CreateJob(agent, label, atomic, batchTier); - job.Commands = commands; - job.CausesDomainReload = commands.Any(c => c.CausesDomainReload); + job.Commands = normalizedCommands; + job.CausesDomainReload = normalizedCommands.Any(c => c.CausesDomainReload);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/CommandQueue.cs` around lines 45 - 68, Submit currently assigns the caller-owned mutable list to job.Commands, allowing post-submit mutation to affect queued jobs; modify Submit to create a defensive deep copy: instantiate a new List<BatchCommand>, and for each command in the incoming commands list create a new BatchCommand (copying Tool, Tier, atomic flags, etc.) and deep-clone any mutable payloads (e.g., Params JObjects via DeepClone or equivalent) before adding to the new list, then assign that new list to job.Commands (keep ClassifyBatch call using the copied/immutable data where needed); this ensures BatchJob and its job.Commands are insulated from caller-side mutations.
163-172:⚠️ Potential issue | 🟠 MajorNull agent bypasses owner-only cancellation check.
The condition
job.Agent != agent && agent != nullallows a null agent to cancel any job, which violates the documented owner-only constraint.🐛 Proposed fix
public bool Cancel(string ticket, string agent) { var job = _store.GetJob(ticket); if (job == null || job.Status != JobStatus.Queued) return false; - if (job.Agent != agent && agent != null) return false; + if (string.IsNullOrWhiteSpace(agent)) return false; + if (!string.Equals(job.Agent, agent, StringComparison.Ordinal)) return false; job.Status = JobStatus.Cancelled; job.CompletedAt = DateTime.UtcNow; return true; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/CommandQueue.cs` around lines 163 - 172, The Cancel method allows a null agent to bypass the owner check; change the guard so a null agent is rejected and only the job owner can cancel: in Cancel (which calls _store.GetJob and checks job.Status and job.Agent) replace the line "if (job.Agent != agent && agent != null) return false;" with a check that returns false when agent is null or when agent does not equal job.Agent (e.g. "if (agent == null || job.Agent != agent) return false;") so only the owner may cancel.
🧹 Nitpick comments (3)
MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs (1)
341-358: Consider adding a warning if Queue section UXML fails to load.Other sections (tools at line 318, resources at line 338) log warnings when UXML fails to load. The queue section silently continues if
queueTreeis null, which could make debugging harder.📝 Suggested fix for consistency
if (queueTree != null) { var queueRoot = queueTree.Instantiate(); queueContainer.Add(queueRoot); // Load Queue section USS var queueStyleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>( $"{basePath}/Editor/Windows/Components/Queue/McpQueueSection.uss" ); if (queueStyleSheet != null) rootVisualElement.styleSheets.Add(queueStyleSheet); queueSection = new McpQueueSection(queueRoot); } + else + { + McpLog.Warn("Failed to load queue section UXML. Queue monitoring will be unavailable."); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs` around lines 341 - 358, The Queue section currently skips silently when the UXML (queueTree) fails to load; update the initialization in MCPForUnityEditorWindow so that if queueTree is null you log a warning (e.g., using UnityEngine.Debug.LogWarning or the same logger used for tools/resources sections) indicating the McpQueueSection UXML failed to load, and similarly warn if the USS (queueStyleSheet) fails to load; locate the queueTree/queueRoot/queueContainer/queueStyleSheet/queueSection usage in the code and add the warning messages consistent with the other section checks.MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs (1)
566-579: CleanContentHash is O(n) per removal — acceptable but could be optimized.Iterating
ContentHashToPendingIdto find the hash by value is O(n). For the typical MCP command workload this is fine, but if dedup volume grows significantly, consider maintaining a reverse mappingPendingIdToContentHash.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs` around lines 566 - 579, CleanContentHash currently scans ContentHashToPendingId (O(n)) to remove an entry by value; add and maintain a reverse dictionary PendingIdToContentHash and update it wherever ContentHashToPendingId is modified (e.g., where entries are added/removed/updated) so CleanContentHash can look up the content hash in O(1) via PendingIdToContentHash and then remove from both maps; update all code paths that insert or delete entries to keep both maps in sync and use the new PendingIdToContentHash in TransportCommandDispatcher.CleanContentHash.MCPForUnity/Editor/Tools/ManageSDFGraph.cs (1)
100-128: Consider usingToolParamsfor parameter validation.The handler manually extracts and validates parameters. Per coding guidelines, using
ToolParamsclass provides consistent validation with methods likeRequireString().♻️ Example refactor
public static object HandleCommand(JObject `@params`) { - if (`@params` == null) - return new ErrorResponse("Parameters required."); + var p = new ToolParams(`@params`); - string action = `@params.Value`<string>("action"); - if (string.IsNullOrWhiteSpace(action)) - return new ErrorResponse("'action' is required. Options: export_mermaid, apply_mermaid, get_graph, compile_hlsl"); + string action = p.RequireString("action", + errorMessage: "'action' is required. Options: export_mermaid, apply_mermaid, get_graph, compile_hlsl"); + if (action == null) return p.ValidationError; if (!ResolveReflection()) return new ErrorResponse(_reflectionError); - string assetPath = `@params.Value`<string>("asset_path"); - if (string.IsNullOrWhiteSpace(assetPath)) - return new ErrorResponse("'asset_path' is required (e.g., 'Assets/SDF Graphs/MyGraph.asset')."); + string assetPath = p.RequireString("asset_path", + errorMessage: "'asset_path' is required (e.g., 'Assets/SDF Graphs/MyGraph.asset')."); + if (assetPath == null) return p.ValidationError;As per coding guidelines: "Use
ToolParamsclass for consistent parameter validation in C# tool handlers with methods likeGetInt(),RequireString(), etc."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@MCPForUnity/Editor/Tools/ManageSDFGraph.cs` around lines 100 - 128, The HandleCommand method manually extracts and validates parameters; change it to construct a ToolParams (e.g., var tp = new ToolParams(`@params`)) and use tp.RequireString("action") and tp.RequireString("asset_path") to validate and fetch values, then use those returned strings instead of manual `@params.Value`<string>("...") calls; retain the ResolveReflection check and asset loading (use assetPath from ToolParams) and forward the original JObject or ToolParams to ExecuteApplyMermaid as needed (update ExecuteApplyMermaid signature if you opt to accept ToolParams) so all parameter validation uses ToolParams.RequireString for consistent behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs`:
- Around line 36-46: The XML doc for EmergencyFlushQueue is out of sync with the
MenuItem shortcut: the attribute uses "%#&F5" (includes Alt via '&') but the
comment says "Ctrl+Shift+F5"; either update the comment to state
"Ctrl+Shift+Alt+F5 (Cmd+Shift+Alt+F5 on Mac)" to match the MenuItem, or remove
the '&' from the MenuItem attribute (change "%#&F5" to "%#F5") if the intended
shortcut is Ctrl/Cmd+Shift+F5; adjust the string in the MenuItem attribute or
the XML summary accordingly for the EmergencyFlushQueue method to keep them
consistent.
In `@MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs`:
- Around line 425-469: AwaitGateway currently awaits
CommandGatewayState.AwaitJob(job) without propagating pending.CancellationToken,
so cancellation of pending is ignored; fix by adding a CancellationToken-aware
AwaitJob overload on CommandGatewayState (e.g., AwaitJob(BatchJob job,
CancellationToken ct)) that observes ct while polling job.Status
(unsubscribe/update loop and throw OperationCanceledException on cancellation),
then call CommandGatewayState.AwaitJob(job, pending.CancellationToken) from
AwaitGateway and ensure any job-level cleanup (e.g., job.Cts.Cancel or
unsubscription) happens on cancellation; also update/remove the misleading
"CancellationToken support" comment to reflect true behavior.
In `@MCPForUnity/Editor/Tools/CommandQueue.cs`:
- Around line 45-68: Submit and SubmitSingle race with ProcessTick because
TransportCommandDispatcher.ProcessCommand can call Submit from background
threads while ProcessTick (EditorApplication.update) mutates the same shared
collections; protect accesses to _store, _heavyQueue, and _smoothInFlight by
either (A) adding a dedicated lock object and wrapping all mutations/reads in
Submit, SubmitSingle and ProcessTick (and any helper methods like
FindDuplicateBatch) with lock(...) to ensure mutual exclusion, or (B) marshal
all Submit/SubmitsSingle work to the main thread using a captured
SynchronizationContext (e.g. SynchronizationContext.Current on editor init) and
post the job-creation/queue-mutation logic there; update references to
Submit/SubmitSingle callers accordingly so background threads enqueue work
rather than directly mutating _store/_heavyQueue/_smoothInFlight.
In `@MCPForUnity/Editor/Tools/ManageSDFGraph.cs`:
- Around line 38-83: ResolveReflection currently checks and sets shared static
state (_reflectionResolved, _reflectionError, _graphAssetType,
_mermaidCodecType, _exportMethod, _parseMethod, _hlslCompilerType,
_compileMethod) without synchronization which can race if HandleCommand is
called concurrently; make the initialization thread-safe by either turning the
initialization into a Lazy<bool> (e.g. Lazy<bool> that runs the existing
ResolveReflection body) or by adding a private static readonly object
_reflectionLock and wrapping the ResolveReflection body with
lock(_reflectionLock) and re-checking _reflectionResolved inside the lock before
populating fields; ensure calls from HandleCommand use the new thread-safe
ResolveReflection (or force evaluation of the Lazy via .Value) so only one
thread performs reflection setup and others see the populated static fields.
---
Duplicate comments:
In `@MCPForUnity/Editor/Helpers/AssetPathUtility.cs`:
- Around line 110-116: GetMcpPackageRootPath() must always return the actual
package root path, not the UI mirrored path; remove the early return of
UIAssetSync.SyncedBasePath when UIAssetSync.NeedsSync() is true and ensure
GetMcpPackageRootPath() only computes and returns the real package root; callers
that need the mirrored UI path (e.g., UXML/USS parsing) should explicitly call
UIAssetSync.SyncedBasePath or a new helper (do not change GetPackageJson()
behavior), so keep references to UIAssetSync limited to UI-specific code and
preserve GetMcpPackageRootPath() semantics.
In `@MCPForUnity/Editor/Tools/BatchExecute.cs`:
- Around line 322-324: The code is blocking the Unity main thread by calling
.ConfigureAwait(true).GetAwaiter().GetResult() on
CommandRegistry.InvokeCommandAsync inside HandleAsyncSubmit; change
HandleAsyncSubmit to be async and replace the blocking call with an awaited call
(e.g., var result = await CommandRegistry.InvokeCommandAsync(cmd.Tool,
cmd.Params);) and then job.Results.Add(result); and propagate async/await to its
caller(s) (update the call site that invokes HandleAsyncSubmit to await it and
mark that method async as well) so continuations run on the Unity context
without deadlock.
- Around line 291-311: The async loop currently skips malformed or disallowed
commands silently; update the foreach that processes commandsToken (using
NormalizeParameterKeys, UnwrapExecuteCustomTool, CommandRegistry.GetToolTier,
CommandClassifier.Classify, CommandClassifier.CausesDomainReload and creating
BatchCommand) to perform the same validation as the sync path: collect
validation errors for missing/empty tool names, malformed params, and disabled
tools instead of using continue, enforce the disabled-tool check before adding
to commands, and if any invalid entries are found return an ErrorResponse
summarizing those errors (similar to the sync path behavior) rather than
silently dropping entries.
- Around line 349-352: The code unconditionally sets job.Status = JobStatus.Done
and returns a SuccessResponse even if some entries in job.Results are
ErrorResponse; update the end-of-batch logic to scan job.Results for any
ErrorResponse (or error indicator), if any exist set job.Status =
JobStatus.Failed (and job.CompletedAt = DateTime.UtcNow) and return an
appropriate failure response containing the ticket and error details, otherwise
set Done and return the SuccessResponse as before; refer to job.Results,
JobStatus.Done/Failed, SuccessResponse, and the atomic/failFast behavior when
implementing the conditional return.
In `@MCPForUnity/Editor/Tools/CommandQueue.cs`:
- Around line 371-381: RestoreFromJson currently calls _store.FromJson and then
repopulates _heavyQueue without clearing transient runtime state, causing
duplicates on repeated restores; update RestoreFromJson to first clear/reset all
transient fields (_heavyQueue.Clear() or reinstantiate, _smoothInFlight.Clear()
or reinstantiate, and set _activeHeavyTicket to null/default) before calling
_store.FromJson or before re-enqueueing, then repopulate _heavyQueue by
iterating over _store.GetQueuedJobs() as done now so restored state is
deterministic.
- Around line 219-225: The Remove method currently clears the heavy-slot marker
(_activeHeavyTicket) which can bypass the busy-hold logic in ProcessTick; update
Remove (the method that returns BatchJob and calls _smoothInFlight.Remove and
_store.Remove) to stop touching _activeHeavyTicket (remove the if
(_activeHeavyTicket == ticket) _activeHeavyTicket = null line) so that
ProcessTick remains responsible for clearing the heavy slot when it observes the
job is gone/null; leave the calls to _smoothInFlight.Remove(ticket) and return
_store.Remove(ticket) intact.
- Around line 45-68: Submit currently assigns the caller-owned mutable list to
job.Commands, allowing post-submit mutation to affect queued jobs; modify Submit
to create a defensive deep copy: instantiate a new List<BatchCommand>, and for
each command in the incoming commands list create a new BatchCommand (copying
Tool, Tier, atomic flags, etc.) and deep-clone any mutable payloads (e.g.,
Params JObjects via DeepClone or equivalent) before adding to the new list, then
assign that new list to job.Commands (keep ClassifyBatch call using the
copied/immutable data where needed); this ensures BatchJob and its job.Commands
are insulated from caller-side mutations.
- Around line 163-172: The Cancel method allows a null agent to bypass the owner
check; change the guard so a null agent is rejected and only the job owner can
cancel: in Cancel (which calls _store.GetJob and checks job.Status and
job.Agent) replace the line "if (job.Agent != agent && agent != null) return
false;" with a check that returns false when agent is null or when agent does
not equal job.Agent (e.g. "if (agent == null || job.Agent != agent) return
false;") so only the owner may cancel.
---
Nitpick comments:
In `@MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs`:
- Around line 566-579: CleanContentHash currently scans ContentHashToPendingId
(O(n)) to remove an entry by value; add and maintain a reverse dictionary
PendingIdToContentHash and update it wherever ContentHashToPendingId is modified
(e.g., where entries are added/removed/updated) so CleanContentHash can look up
the content hash in O(1) via PendingIdToContentHash and then remove from both
maps; update all code paths that insert or delete entries to keep both maps in
sync and use the new PendingIdToContentHash in
TransportCommandDispatcher.CleanContentHash.
In `@MCPForUnity/Editor/Tools/ManageSDFGraph.cs`:
- Around line 100-128: The HandleCommand method manually extracts and validates
parameters; change it to construct a ToolParams (e.g., var tp = new
ToolParams(`@params`)) and use tp.RequireString("action") and
tp.RequireString("asset_path") to validate and fetch values, then use those
returned strings instead of manual `@params.Value`<string>("...") calls; retain
the ResolveReflection check and asset loading (use assetPath from ToolParams)
and forward the original JObject or ToolParams to ExecuteApplyMermaid as needed
(update ExecuteApplyMermaid signature if you opt to accept ToolParams) so all
parameter validation uses ToolParams.RequireString for consistent behavior.
In `@MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs`:
- Around line 341-358: The Queue section currently skips silently when the UXML
(queueTree) fails to load; update the initialization in MCPForUnityEditorWindow
so that if queueTree is null you log a warning (e.g., using
UnityEngine.Debug.LogWarning or the same logger used for tools/resources
sections) indicating the McpQueueSection UXML failed to load, and similarly warn
if the USS (queueStyleSheet) fails to load; locate the
queueTree/queueRoot/queueContainer/queueStyleSheet/queueSection usage in the
code and add the warning messages consistent with the other section checks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 62772b68-4d7b-4f89-ab13-c62fcc00dffe
📒 Files selected for processing (24)
MCPForUnity/Editor/Constants/EditorPrefKeys.csMCPForUnity/Editor/Helpers/AssetPathUtility.csMCPForUnity/Editor/MenuItems/MCPForUnityMenu.csMCPForUnity/Editor/Services/TestJobManager.csMCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.csMCPForUnity/Editor/Tools/BatchExecute.csMCPForUnity/Editor/Tools/BatchJob.csMCPForUnity/Editor/Tools/CommandGatewayState.csMCPForUnity/Editor/Tools/CommandQueue.csMCPForUnity/Editor/Tools/CommandRegistry.csMCPForUnity/Editor/Tools/GetTestJob.csMCPForUnity/Editor/Tools/ManageSDFGraph.csMCPForUnity/Editor/Tools/ManageSDFGraph.cs.metaMCPForUnity/Editor/Tools/ManageScript.csMCPForUnity/Editor/Tools/ManageShader.csMCPForUnity/Editor/Tools/McpForUnityToolAttribute.csMCPForUnity/Editor/Tools/ReadConsole.csMCPForUnity/Editor/Tools/RunTests.csMCPForUnity/Editor/Windows/MCPForUnityEditorWindow.csMCPForUnity/Editor/Windows/MCPForUnityEditorWindow.uxmlMCPForUnity/package.jsonServer/README.mdServer/pyproject.tomlServer/src/transport/unity_instance_middleware.py
✅ Files skipped from review due to trivial changes (8)
- MCPForUnity/package.json
- MCPForUnity/Editor/Tools/ManageSDFGraph.cs.meta
- MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.uxml
- Server/pyproject.toml
- Server/README.md
- MCPForUnity/Editor/Constants/EditorPrefKeys.cs
- MCPForUnity/Editor/Tools/ReadConsole.cs
- MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs
🚧 Files skipped from review as they are similar to previous changes (7)
- MCPForUnity/Editor/Tools/GetTestJob.cs
- MCPForUnity/Editor/Tools/ManageShader.cs
- MCPForUnity/Editor/Tools/RunTests.cs
- MCPForUnity/Editor/Tools/ManageScript.cs
- MCPForUnity/Editor/Tools/CommandGatewayState.cs
- MCPForUnity/Editor/Tools/BatchJob.cs
- MCPForUnity/Editor/Tools/CommandRegistry.cs
| /// <summary> | ||
| /// Emergency flush: cancels all queued/running MCP commands and clears stuck test jobs. | ||
| /// Use when the editor appears frozen due to a stuck MCP queue. | ||
| /// Shortcut: Ctrl+Shift+F5 (Cmd+Shift+F5 on Mac) | ||
| /// </summary> | ||
| [MenuItem("Window/MCP For Unity/Emergency Flush Queue %#&F5", priority = 100)] | ||
| public static void EmergencyFlushQueue() | ||
| { | ||
| CommandGatewayState.EmergencyFlush(); | ||
| Debug.LogWarning("[MCP] Emergency flush completed. Queue cleared, stuck test jobs removed."); | ||
| } |
There was a problem hiding this comment.
Minor: Shortcut documentation mismatch.
The XML comment says Ctrl+Shift+F5 but the actual shortcut %#&F5 includes Alt modifier (&), making it Ctrl+Shift+Alt+F5 on Windows / Cmd+Shift+Alt+F5 on Mac.
📝 Suggested fix
/// <summary>
/// Emergency flush: cancels all queued/running MCP commands and clears stuck test jobs.
/// Use when the editor appears frozen due to a stuck MCP queue.
- /// Shortcut: Ctrl+Shift+F5 (Cmd+Shift+F5 on Mac)
+ /// Shortcut: Ctrl+Shift+Alt+F5 (Cmd+Shift+Alt+F5 on Mac)
/// </summary>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs` around lines 36 - 46, The
XML doc for EmergencyFlushQueue is out of sync with the MenuItem shortcut: the
attribute uses "%#&F5" (includes Alt via '&') but the comment says
"Ctrl+Shift+F5"; either update the comment to state "Ctrl+Shift+Alt+F5
(Cmd+Shift+Alt+F5 on Mac)" to match the MenuItem, or remove the '&' from the
MenuItem attribute (change "%#&F5" to "%#F5") if the intended shortcut is
Ctrl/Cmd+Shift+F5; adjust the string in the MenuItem attribute or the XML
summary accordingly for the EmergencyFlushQueue method to keep them consistent.
| // --- Tier-aware dispatch --- | ||
| var declaredTier = CommandRegistry.GetToolTier(command.type); | ||
| var effectiveTier = CommandClassifier.Classify(command.type, declaredTier, parameters); | ||
|
|
||
| if (effectiveTier != ExecutionTier.Instant) | ||
| { | ||
| // Route Smooth/Heavy through the gateway queue for tier-aware scheduling, | ||
| // heavy exclusivity, domain-reload guards, and CancellationToken support. | ||
| var job = CommandGatewayState.Queue.SubmitSingle(command.type, parameters, "transport"); | ||
| var gatewaySw = McpLogRecord.IsEnabled ? System.Diagnostics.Stopwatch.StartNew() : null; | ||
|
|
||
| async void AwaitGateway() | ||
| { | ||
| try | ||
| { | ||
| var gatewayResult = await CommandGatewayState.AwaitJob(job); | ||
| gatewaySw?.Stop(); | ||
| if (gatewayResult is IMcpResponse mcpResp && !mcpResp.Success) | ||
| { | ||
| McpLogRecord.Log(command.type, parameters, logType, "ERROR", gatewaySw?.ElapsedMilliseconds ?? 0, (gatewayResult as ErrorResponse)?.Error); | ||
| var errResponse = new { status = "error", result = gatewayResult, _queue = new { ticket = job.Ticket, tier = job.Tier.ToString().ToLowerInvariant(), queued = true } }; | ||
| pending.TrySetResult(JsonConvert.SerializeObject(errResponse)); | ||
| } | ||
| else | ||
| { | ||
| McpLogRecord.Log(command.type, parameters, logType, "SUCCESS", gatewaySw?.ElapsedMilliseconds ?? 0, null); | ||
| var okResponse = new { status = "success", result = gatewayResult, _queue = new { ticket = job.Ticket, tier = job.Tier.ToString().ToLowerInvariant(), queued = true } }; | ||
| pending.TrySetResult(JsonConvert.SerializeObject(okResponse)); | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| gatewaySw?.Stop(); | ||
| McpLogRecord.Log(command.type, parameters, logType, "ERROR", gatewaySw?.ElapsedMilliseconds ?? 0, ex.Message); | ||
| pending.TrySetResult(SerializeError(ex.Message, command.type, ex.StackTrace)); | ||
| } | ||
| finally | ||
| { | ||
| EditorApplication.delayCall += () => RemovePending(id, pending); | ||
| } | ||
| } | ||
|
|
||
| AwaitGateway(); | ||
| return; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if AwaitJob has any CancellationToken integration
rg -nC5 'AwaitJob' MCPForUnity/Editor/Tools/CommandGatewayState.csRepository: CoplayDev/unity-mcp
Length of output: 617
🏁 Script executed:
cat -n MCPForUnity/Editor/Tools/CommandGatewayState.cs | sed -n '61,120p'Repository: CoplayDev/unity-mcp
Length of output: 2250
🏁 Script executed:
rg -n 'CancellationToken' MCPForUnity/Editor/Tools/CommandGatewayState.csRepository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
rg -n 'pending.CancellationToken\|job.Cts' MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.csRepository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
rg -nB10 'AwaitGateway\(\)' MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs | head -50Repository: CoplayDev/unity-mcp
Length of output: 1424
🏁 Script executed:
rg -n 'var pending\|TaskCompletionSource' MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.csRepository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
rg -nB30 'var job = CommandGatewayState.Queue.SubmitSingle' MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs | head -80Repository: CoplayDev/unity-mcp
Length of output: 1939
🏁 Script executed:
rg -n 'TaskCompletionSource\|pending =' MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.csRepository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
rg -nB50 'var resourceMeta = MCPServiceLocator.ResourceDiscovery' MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs | head -100Repository: CoplayDev/unity-mcp
Length of output: 2284
🏁 Script executed:
cat -n MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs | sed -n '350,410p'Repository: CoplayDev/unity-mcp
Length of output: 2992
🏁 Script executed:
cat -n MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs | sed -n '300,360p'Repository: CoplayDev/unity-mcp
Length of output: 2368
🏁 Script executed:
rg -n 'public.*void.*Dispatch\|private.*void.*Dispatch\|async.*Dispatch' MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.csRepository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
rg -n 'class PendingCommand\|record PendingCommand\|struct PendingCommand' MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.csRepository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
cat -n MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs | sed -n '1,100p'Repository: CoplayDev/unity-mcp
Length of output: 4726
🏁 Script executed:
rg -n 'job.Cts\|\.Cts' MCPForUnity/Editor/Tools/CommandGatewayState.csRepository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
rg -n 'SubmitSingle' MCPForUnity/Editor/Tools/CommandGatewayQueue.cs -A 15Repository: CoplayDev/unity-mcp
Length of output: 150
🏁 Script executed:
fd CommandGatewayQueue MCPForUnity/Repository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
rg -l 'SubmitSingle' MCPForUnity/Repository: CoplayDev/unity-mcp
Length of output: 171
🏁 Script executed:
rg -n 'class.*BatchJob\|record.*BatchJob\|struct.*BatchJob' MCPForUnity/Repository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
cat -n MCPForUnity/Editor/Tools/CommandQueue.cs | head -150Repository: CoplayDev/unity-mcp
Length of output: 7750
🏁 Script executed:
rg -n 'SubmitSingle' MCPForUnity/Editor/Tools/CommandQueue.cs -A 20Repository: CoplayDev/unity-mcp
Length of output: 1449
🏁 Script executed:
rg -n 'class.*Job\|record.*Job\|struct.*Job' MCPForUnity/Editor/Tools/CommandQueue.csRepository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
rg -n 'class BatchJob' MCPForUnity/Repository: CoplayDev/unity-mcp
Length of output: 200
🏁 Script executed:
cat -n MCPForUnity/Editor/Tools/CommandQueue.cs | sed -n '150,200p'Repository: CoplayDev/unity-mcp
Length of output: 2063
🏁 Script executed:
cat -n MCPForUnity/Editor/Tools/BatchJob.csRepository: CoplayDev/unity-mcp
Length of output: 2515
🏁 Script executed:
rg -n 'job.Cts' MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.csRepository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
rg -n 'CancellationRegistration\|registration =' MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs -B 5 -A 5Repository: CoplayDev/unity-mcp
Length of output: 45
🏁 Script executed:
rg -n 'new PendingCommand' MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs -B 10 -A 5Repository: CoplayDev/unity-mcp
Length of output: 722
🏁 Script executed:
rg -n 'public static.*Dispatch\|DispatchAsync' MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.csRepository: CoplayDev/unity-mcp
Length of output: 45
AwaitJob does not propagate the caller's CancellationToken after execution begins.
The early cancellation check (line 332) prevents issues for synchronous command validation, but once AwaitGateway() is invoked and awaits AwaitJob, cancellation propagation breaks. AwaitJob accepts only BatchJob and has no CancellationToken parameter—it polls job.Status via EditorApplication.update indefinitely. If pending.CancellationToken is cancelled while AwaitGateway awaits AwaitJob, the async void continues until the job independently completes or fails, ignoring the cancellation request.
The comment at line 432 claiming "CancellationToken support" is misleading. Either pass pending.CancellationToken to AwaitJob and check it during polling, or explicitly document that job-level cancellation via job.Cts must be used instead.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs` around
lines 425 - 469, AwaitGateway currently awaits CommandGatewayState.AwaitJob(job)
without propagating pending.CancellationToken, so cancellation of pending is
ignored; fix by adding a CancellationToken-aware AwaitJob overload on
CommandGatewayState (e.g., AwaitJob(BatchJob job, CancellationToken ct)) that
observes ct while polling job.Status (unsubscribe/update loop and throw
OperationCanceledException on cancellation), then call
CommandGatewayState.AwaitJob(job, pending.CancellationToken) from AwaitGateway
and ensure any job-level cleanup (e.g., job.Cts.Cancel or unsubscription)
happens on cancellation; also update/remove the misleading "CancellationToken
support" comment to reflect true behavior.
| public BatchJob Submit(string agent, string label, bool atomic, List<BatchCommand> commands) | ||
| { | ||
| // --- Dedup check for multi-command batches --- | ||
| var existing = FindDuplicateBatch(agent, commands); | ||
| if (existing != null) | ||
| { | ||
| existing.Deduplicated = true; | ||
| McpLog.Info($"[CommandQueue] Dedup: identical batch already queued/running as {existing.Ticket}. Returning existing job."); | ||
| return existing; | ||
| } | ||
|
|
||
| var batchTier = CommandClassifier.ClassifyBatch( | ||
| commands.Select(c => (c.Tool, c.Tier, c.Params)).ToArray()); | ||
|
|
||
| var job = _store.CreateJob(agent, label, atomic, batchTier); | ||
| job.Commands = commands; | ||
| job.CausesDomainReload = commands.Any(c => c.CausesDomainReload); | ||
|
|
||
| if (batchTier == ExecutionTier.Heavy) | ||
| _heavyQueue.Enqueue(job.Ticket); | ||
| // Smooth and Instant are handled differently (see ProcessTick) | ||
|
|
||
| return job; | ||
| } |
There was a problem hiding this comment.
Thread-safety: Submit/SubmitSingle may race with ProcessTick.
Per context snippet, SubmitSingle is called from TransportCommandDispatcher.ProcessCommand which can execute from background websocket threads, while ProcessTick runs on EditorApplication.update (main thread). The shared collections (_store, _heavyQueue, _smoothInFlight) are accessed without synchronization, risking data corruption.
Consider adding a lock or ensuring all queue mutations happen on the main thread.
🔒️ Proposed fix sketch
+ private readonly object _queueLock = new();
+
public BatchJob Submit(string agent, string label, bool atomic, List<BatchCommand> commands)
{
+ lock (_queueLock)
+ {
// ... existing code ...
+ }
}
public void ProcessTick(Func<string, JObject, CancellationToken, Task<object>> executeCommand)
{
+ lock (_queueLock)
+ {
// ... existing code ...
+ }
}Alternatively, marshal Submit calls to the main thread via SynchronizationContext.
Also applies to: 114-153
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@MCPForUnity/Editor/Tools/CommandQueue.cs` around lines 45 - 68, Submit and
SubmitSingle race with ProcessTick because
TransportCommandDispatcher.ProcessCommand can call Submit from background
threads while ProcessTick (EditorApplication.update) mutates the same shared
collections; protect accesses to _store, _heavyQueue, and _smoothInFlight by
either (A) adding a dedicated lock object and wrapping all mutations/reads in
Submit, SubmitSingle and ProcessTick (and any helper methods like
FindDuplicateBatch) with lock(...) to ensure mutual exclusion, or (B) marshal
all Submit/SubmitsSingle work to the main thread using a captured
SynchronizationContext (e.g. SynchronizationContext.Current on editor init) and
post the job-creation/queue-mutation logic there; update references to
Submit/SubmitSingle callers accordingly so background threads enqueue work
rather than directly mutating _store/_heavyQueue/_smoothInFlight.
| static bool ResolveReflection() | ||
| { | ||
| if (_reflectionResolved) return _reflectionError == null; | ||
|
|
||
| _reflectionResolved = true; | ||
|
|
||
| _graphAssetType = FindType("Utility.SDF.Graph.SDFGraphAsset"); | ||
| if (_graphAssetType == null) | ||
| { | ||
| _reflectionError = "SDFGraphAsset type not found. Is the com.utility.sdf package installed?"; | ||
| return false; | ||
| } | ||
|
|
||
| _mermaidCodecType = FindType("Utility.SDF.Mermaid.SDFMermaidCodec"); | ||
| if (_mermaidCodecType == null) | ||
| { | ||
| _reflectionError = "SDFMermaidCodec type not found. Is the com.utility.sdf.Editor assembly loaded?"; | ||
| return false; | ||
| } | ||
|
|
||
| // Export has optional FlowDirection param — find by name only | ||
| var exportMethods = _mermaidCodecType.GetMethods(BindingFlags.Public | BindingFlags.Static) | ||
| .Where(m => m.Name == "Export").ToArray(); | ||
| _exportMethod = exportMethods.Length > 0 ? exportMethods[0] : null; | ||
|
|
||
| _parseMethod = _mermaidCodecType.GetMethod("Parse", | ||
| BindingFlags.Public | BindingFlags.Static, | ||
| null, new[] { typeof(string), _graphAssetType }, null); | ||
|
|
||
| if (_exportMethod == null || _parseMethod == null) | ||
| { | ||
| _reflectionError = "SDFMermaidCodec.Export/Parse methods not found. API may have changed."; | ||
| return false; | ||
| } | ||
|
|
||
| // HLSL compiler is optional | ||
| _hlslCompilerType = FindType("Utility.SDF.Compiler.SDFGraphCompiler"); | ||
| if (_hlslCompilerType != null) | ||
| { | ||
| _compileMethod = _hlslCompilerType.GetMethod("CompileToHLSL", | ||
| BindingFlags.Public | BindingFlags.Static, | ||
| null, new[] { _graphAssetType }, null); | ||
| } | ||
|
|
||
| return true; | ||
| } |
There was a problem hiding this comment.
Minor: Potential race in lazy reflection initialization.
ResolveReflection() uses a simple if (_reflectionResolved) return check without synchronization. If HandleCommand is called concurrently from multiple threads during first use, multiple threads could race to initialize the static fields. Consider using Lazy<T> or a lock.
In practice, Unity editor commands typically run on the main thread, so this is low risk.
🛡️ Optional fix using Lazy
+ static readonly Lazy<string> _reflectionInit = new Lazy<string>(() =>
+ {
+ _graphAssetType = FindType("Utility.SDF.Graph.SDFGraphAsset");
+ if (_graphAssetType == null)
+ return "SDFGraphAsset type not found. Is the com.utility.sdf package installed?";
+ // ... rest of init ...
+ return null; // success
+ });
+
static bool ResolveReflection()
{
- if (_reflectionResolved) return _reflectionError == null;
- _reflectionResolved = true;
- // ... init code ...
+ _reflectionError = _reflectionInit.Value;
+ return _reflectionError == null;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@MCPForUnity/Editor/Tools/ManageSDFGraph.cs` around lines 38 - 83,
ResolveReflection currently checks and sets shared static state
(_reflectionResolved, _reflectionError, _graphAssetType, _mermaidCodecType,
_exportMethod, _parseMethod, _hlslCompilerType, _compileMethod) without
synchronization which can race if HandleCommand is called concurrently; make the
initialization thread-safe by either turning the initialization into a
Lazy<bool> (e.g. Lazy<bool> that runs the existing ResolveReflection body) or by
adding a private static readonly object _reflectionLock and wrapping the
ResolveReflection body with lock(_reflectionLock) and re-checking
_reflectionResolved inside the lock before populating fields; ensure calls from
HandleCommand use the new thread-safe ResolveReflection (or force evaluation of
the Lazy via .Value) so only one thread performs reflection setup and others see
the populated static fields.
Summary
batch_executewith anasync=truepath that queues commands and returns tickets for pollingMotivation
When multiple AI agents interact with Unity simultaneously (e.g., parallel coding agents via Claude Code), they compete for the single-threaded editor. Without coordination, domain reloads from one agent break another's in-flight operations, causing cascading failures. This gateway provides safe concurrent access by:
Architecture
Execution Tiers
find_gameobjects,read_console,get_test_jobmanage_gameobject,manage_materialmanage_script.create,refresh_unity,run_testsAction-Level Overrides
The
CommandClassifierdowngrades tool tiers based on specific actions. For example:manage_sceneis Heavy (can create scenes), butmanage_scene.get_hierarchyis Instantmanage_assetis Smooth (can modify assets), butmanage_asset.searchis InstantQueue Scheduling
Domain Reload Safety
CausesDomainReloadflag propagated from classifier through commands and jobsSessionStatebefore assembly reload, restored afterDone(the reload IS the success signal); other interrupted jobs are markedFailed: "Interrupted by domain reload"Job Lifecycle & Auto-Cleanup
Terminal jobs (Done, Failed, Cancelled) are removed from the store when polled.
If gateway job logging is enabled, job metadata is appended to a JSON-lines file before removal.
Uncollected jobs expire after 5 minutes as a safety net.
New Tools
poll_job— Poll a queued job by ticket. Returns status, progress, position in queue, blocked reason, and results when done. Auto-removes terminal jobs after returning data.queue_status— Compressed queue summary: per-job status, active heavy ticket, queue depth, smooth in-flight count.Queue Tab (Editor UI)
Real-time dashboard in the MCP For Unity editor window:
Files Changed (50 files, +4,721 lines)
New Production Code
ExecutionTier.cs— Enum +[ExecutionTier]attribute for tool registrationCommandClassifier.cs— Action-level tier overrides (tool + action -> effective tier)BatchJob.cs— Job model with status lifecycle, undo group tracking, results collectionTicketStore.cs— Job storage with ticket-based lookup, expiry, removal, and domain-reload-aware restoreCommandQueue.cs— Tier-aware FIFO dispatcher with exclusive heavy scheduling, domain-reload guards, editor-busy holdCommandGatewayState.cs—[InitializeOnLoad]singleton wiring queue toEditorApplication.updatewith SessionState persistencePollJob.cs— MCP tool for polling job status with auto-cleanup of terminal jobsQueueStatus.cs— MCP tool for compressed queue introspectionGatewayJobLogger.cs— JSON-lines audit logger with configurable path via EditorPrefsQueue Tab UI
McpQueueSection.uxml— Layout: status bar, logging box, job list header, dynamic rows, empty stateMcpQueueSection.uss— Styles: status dots, columns, blocked badge, logging box, light/dark themesMcpQueueSection.cs— Controller: refresh from queue state, logging toggle/path wiring, file browserUIAssetSync.cs— WSL UNC path workaround: copies UXML/USS to Assets/ on domain reloadAssetPathUtility.cs— Returns synced base path when WSL detectedModified Production Code
BatchExecute.cs— Addedasync=truegateway path alongside existing synchronous pathMcpForUnityToolAttribute.cs— Extended withExecutionTierpropertyCommandRegistry.cs— AddedGetToolTier()helperMCPForUnityEditorWindow.cs— Wired Queue as 6th tab with 1s independent refresh timerMCPForUnityEditorWindow.uxml— Added queue-tab toggle and queue-panel scroll viewEditorPrefKeys.cs— AddedGatewayJobLoggingandGatewayJobLogPathkeys[ExecutionTier(Heavy)]annotationsTests (5 test files, 47 tests)
ExecutionTierTests.cs— 5 tests: tier enum values, attribute defaults, attribute with tier parameterCommandClassifierTests.cs— 13 tests: action-level classification, override logic, null/missing params, default fallbackTicketStoreTests.cs— 12 tests: job creation, ticket lookup, expiry cleanup, agent stats, queue orderingCommandQueueTests.cs— 10 tests: submit, poll, cancel, queue depth, agent validation, heavy schedulingBatchExecuteAsyncTests.cs— 7 tests: gateway state singleton, PollJob null/missing/invalid params, QueueStatus response, BatchExecute constantsDocumentation
2026-02-25-gateway-async-awareness-design.md— Domain-reload safety design doc2026-02-25-gateway-async-awareness-plan.md— Implementation plan for async guards2026-02-25-queue-summary-ui-design.md— Queue tab UI design doc2026-02-25-queue-summary-ui-plan.md— Queue tab implementation planUsage
Async batch submission
{ "tool": "batch_execute", "params": { "async": true, "agent": "agent-1", "label": "Create player scripts", "atomic": true, "commands": [ {"tool": "manage_script", "params": {"action": "create", "name": "Player", "path": "Assets/Scripts"}}, {"tool": "manage_script", "params": {"action": "create", "name": "Enemy", "path": "Assets/Scripts"}} ] } }Poll for results
{"tool": "poll_job", "params": {"ticket": "t-000001"}}Returns full results on completion, then auto-removes the ticket from the queue.
Check queue status
{"tool": "queue_status", "params": {}}Job log output (when enabled)
{"ticket":"t-000010","agent":"agent-1","label":"run-tests","tier":"heavy","status":"done","created_at":"2026-02-25T11:09:14Z","completed_at":"2026-02-25T11:09:14Z","command_count":1,"tools":["run_tests"]}Test Plan
run_testsstays "running" during async test execution, Agent 2refresh_unityblocked withtests_runninguntil Agent 1 completes, then executesrefresh_unityjob correctly shows Done (not Failed) after the domain reload it triggeredSummary by Sourcery
Introduce a tier-aware command gateway with async batch execution, queue persistence, and an in-editor Queue tab for monitoring and logging multi-agent MCP jobs.
New Features:
Enhancements:
Documentation:
Tests:
Summary by CodeRabbit