-
-
Notifications
You must be signed in to change notification settings - Fork 269
feat: Add event subscription system (event-subscribe, event-watch, event-list) #639
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
djdcks12
wants to merge
9
commits into
IvanMurzak:main
Choose a base branch
from
djdcks12:feature/event-system
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
113f0ed
feat: Add event subscription system (event-subscribe, event-watch, ev…
djdcks12 e4747d8
fix: Add missing using directive for McpEventBus in script-execute ex…
djdcks12 3104046
fix: Address Copilot review — Channel<T> sync, error messages, struct…
djdcks12 b7216ef
Merge branch 'main' into feature/event-system
djdcks12 8b4f465
Merge branch 'main' into feature/event-system
IvanMurzak f6524d1
Merge branch 'main' into feature/event-system
djdcks12 a6c08c6
Merge branch 'main' into feature/event-system
djdcks12 8d6df1b
Merge branch 'main' into feature/event-system
IvanMurzak 7bfad6d
Merge branch 'main' into feature/event-system
IvanMurzak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
59 changes: 59 additions & 0 deletions
59
Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Event.List.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| /* | ||
| ┌──────────────────────────────────────────────────────────────────┐ | ||
| │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ | ||
| │ Licensed under the Apache License, Version 2.0. │ | ||
| │ See the LICENSE file in the project root for more information. │ | ||
| └──────────────────────────────────────────────────────────────────┘ | ||
| */ | ||
|
|
||
| #nullable enable | ||
| using System.ComponentModel; | ||
| using System.Linq; | ||
| using com.IvanMurzak.McpPlugin; | ||
|
|
||
| namespace com.IvanMurzak.Unity.MCP.Editor.API | ||
| { | ||
| public partial class Tool_Event | ||
| { | ||
| public const string EventListToolId = "event-list"; | ||
|
|
||
| [McpPluginTool | ||
| ( | ||
| EventListToolId, | ||
| Title = "Event / List", | ||
| ReadOnlyHint = true, | ||
| IdempotentHint = true | ||
| )] | ||
| [Description( | ||
| "Lists all event types available for event-subscribe.\n\n" + | ||
| "Shows built-in Unity Editor events AND custom game events.\n" + | ||
| "Custom events are registered automatically when McpEventBus.Push() is called from game code,\n" + | ||
| "or manually via script-execute: McpEventBus.RegisterCustomType(\"event_name\", \"description\").\n\n" + | ||
| "If only built-in events appear, the project may need custom events added to game code:\n" + | ||
| " #if UNITY_EDITOR\n" + | ||
| " McpEventBus.Push(\"server_response_done\", source: \"NetworkHelper\");\n" + | ||
| " #endif\n" + | ||
| "See event-subscribe tool description for full custom event guide." | ||
| )] | ||
| public McpEventTypeInfo[] ListEvents | ||
| ( | ||
| [Description( | ||
| "Filter event types. " + | ||
| "'all' = show everything (default), " + | ||
| "'builtin' = only built-in Unity events, " + | ||
| "'custom' = only user-registered custom events." | ||
| )] | ||
| string? filter = "all" | ||
| ) | ||
| { | ||
| var allTypes = McpEventBus.GetAllEventTypes(); | ||
|
|
||
| return (filter?.ToLowerInvariant()) switch | ||
| { | ||
| "builtin" => allTypes.Where(t => t.IsBuiltIn).ToArray(), | ||
| "custom" => allTypes.Where(t => !t.IsBuiltIn).ToArray(), | ||
| _ => allTypes | ||
| }; | ||
| } | ||
| } | ||
| } |
98 changes: 98 additions & 0 deletions
98
Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Event.Subscribe.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| /* | ||
| ┌──────────────────────────────────────────────────────────────────┐ | ||
| │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ | ||
| │ Licensed under the Apache License, Version 2.0. │ | ||
| │ See the LICENSE file in the project root for more information. │ | ||
| └──────────────────────────────────────────────────────────────────┘ | ||
| */ | ||
|
|
||
| #nullable enable | ||
| using System; | ||
| using System.ComponentModel; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using com.IvanMurzak.McpPlugin; | ||
|
|
||
| namespace com.IvanMurzak.Unity.MCP.Editor.API | ||
| { | ||
| public partial class Tool_Event | ||
| { | ||
| public const string EventSubscribeToolId = "event-subscribe"; | ||
|
|
||
| [McpPluginTool | ||
| ( | ||
| EventSubscribeToolId, | ||
| Title = "Event / Subscribe" | ||
| )] | ||
| [Description( | ||
| "Waits for a Unity event and returns it. BLOCKING — holds response until event fires or timeout.\n\n" + | ||
| "== BUILT-IN EVENTS (no code changes needed) ==\n" + | ||
| "- play_mode_changed: Play/Pause/Stop transitions\n" + | ||
| "- scene_loaded / scene_opened: Scene loading\n" + | ||
| "- compilation_started / compilation_finished: Script compilation (hasErrors flag)\n" + | ||
| "- error_logged / warning_logged: Console messages\n" + | ||
| "- pause_state_changed: Editor pause toggle\n" + | ||
| "- hierarchy_changed: GO created/destroyed/reparented (NOTE: fires on ANY hierarchy change, not just yours)\n" + | ||
| "- selection_changed: Editor selection\n\n" + | ||
| "== CUSTOM EVENTS (for game logic — RECOMMENDED) ==\n" + | ||
| "Built-in events CANNOT detect game-specific moments like 'server callback done' or 'popup closed'.\n" + | ||
| "Do NOT use hierarchy_changed as a proxy for server responses — it fires on unrelated changes.\n\n" + | ||
| "Dynamic hook via script-execute (NO source code modification, auto-cleanup on play mode exit):\n" + | ||
| " Step 1 — Hook game event to McpEventBus (run once before test):\n" + | ||
| " script-execute (IMPORTANT: add 'using com.IvanMurzak.Unity.MCP.Editor.API;' for McpEventBus):\n" + | ||
| " SomeManager.Instance.OnDataLoaded += () => McpEventBus.Push(\"data_loaded\");\n" + | ||
| " Step 2 — Trigger async action (e.g. server request, UI navigation):\n" + | ||
| " script-execute(trigger the action that will eventually fire the callback)\n" + | ||
| " Step 3 — Subscribe and wait for the callback event:\n" + | ||
| " event-subscribe(type='data_loaded', timeoutMs=30000)\n" + | ||
| " Step 4 — Hooks die automatically when play mode stops. No cleanup needed.\n\n" + | ||
| "ALTERNATIVE: Add McpEventBus.Push() directly in game code (persistent, survives sessions):\n" + | ||
| " #if UNITY_EDITOR\n" + | ||
| " McpEventBus.Push(\"friend_data_loaded\", source: \"FriendManager\");\n" + | ||
| " #endif\n\n" + | ||
| "Custom events appear in event-list(filter='custom') after first Push.\n\n" + | ||
| "== USAGE PATTERNS ==\n" + | ||
| "1. Custom game events (RECOMMENDED — sequential, callback is async):\n" + | ||
| " script-execute(hook + trigger async action) → event-subscribe(type='custom_event')\n" + | ||
| "2. Built-in events (parallel subscribe + trigger):\n" + | ||
| " [parallel] event-subscribe(type='play_mode_changed') + editor-application-set-state(...)\n" + | ||
| "3. Compilation wait:\n" + | ||
| " script-update-or-create(...) → event-subscribe(type='compilation_finished')\n" + | ||
| "4. Background monitoring (non-blocking): use event-watch instead" | ||
| )] | ||
| public async Task<McpEventSubscribeResult> Subscribe | ||
| ( | ||
| [Description( | ||
| "Event type to filter for. " + | ||
| "Use a specific type like 'error_logged' or 'compilation_finished'. " + | ||
| "Empty string or null matches ANY event type. " + | ||
| "Use 'event-list' tool to see all available types." | ||
| )] | ||
| string? type = null, | ||
|
|
||
| [Description( | ||
| "Maximum wait time in milliseconds. " + | ||
| "Range: 0-120000. Default: 30000 (30 seconds). " + | ||
| "Set to 0 to drain pending events without waiting." | ||
| )] | ||
| int timeoutMs = 30000, | ||
|
|
||
| [Description( | ||
| "If true, collects ALL matching events until timeout. " + | ||
| "If false (default), returns immediately after the FIRST matching event." | ||
| )] | ||
| bool collectAll = false | ||
| ) | ||
| { | ||
| // Special case: timeoutMs=0 means drain pending, no wait | ||
| if (timeoutMs == 0) | ||
| return McpEventBus.DrainPending(type); | ||
|
|
||
| if (timeoutMs < 1000 || timeoutMs > 120000) | ||
| throw new ArgumentException(Error.InvalidSubscribeTimeout(timeoutMs)); | ||
|
|
||
| using var cts = new CancellationTokenSource(timeoutMs + 1000); // Grace period | ||
| return await McpEventBus.WaitAsync(type, timeoutMs, collectAll, cts.Token); | ||
| } | ||
| } | ||
| } | ||
123 changes: 123 additions & 0 deletions
123
Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Event.Watch.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| /* | ||
| ┌──────────────────────────────────────────────────────────────────┐ | ||
| │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ | ||
| │ Licensed under the Apache License, Version 2.0. │ | ||
| │ See the LICENSE file in the project root for more information. │ | ||
| └──────────────────────────────────────────────────────────────────┘ | ||
| */ | ||
|
|
||
| #nullable enable | ||
| using System; | ||
| using System.ComponentModel; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using com.IvanMurzak.McpPlugin; | ||
| using com.IvanMurzak.McpPlugin.Common.Model; | ||
|
|
||
| namespace com.IvanMurzak.Unity.MCP.Editor.API | ||
| { | ||
| public partial class Tool_Event | ||
| { | ||
| public const string EventWatchToolId = "event-watch"; | ||
|
|
||
| [McpPluginTool | ||
| ( | ||
| EventWatchToolId, | ||
| Title = "Event / Watch" | ||
| )] | ||
| [Description( | ||
| "NON-BLOCKING event watcher at MCP server level. Returns 'Processing' status immediately, " + | ||
| "then sends a notification via NotifyToolRequestCompleted when a matching event fires.\n\n" + | ||
| "IMPORTANT LIMITATION: Some MCP clients (including Claude Code) wait for the notification " + | ||
| "before proceeding to the next turn, making this effectively blocking from the AI agent's perspective. " + | ||
| "If your client blocks on Processing responses, use event-subscribe instead.\n\n" + | ||
| "This tool is useful for MCP clients that can handle Processing responses asynchronously, " + | ||
| "or for scenarios where you want the server to notify you without keeping a blocking tool call open.\n\n" + | ||
| "USE CASES:\n" + | ||
| "- Clients that support async Processing: background error monitoring\n" + | ||
| "- Long-running watches where you want server-side timeout management\n\n" + | ||
| "For most use cases, prefer event-subscribe:\n" + | ||
| " script-execute(trigger async action) → event-subscribe(type='my_event', timeoutMs=30000)\n\n" + | ||
| "CUSTOM EVENTS: Same as event-subscribe — see event-subscribe description for full guide.\n\n" + | ||
| "NOTE: Watch is ephemeral — if Unity domain reloads (e.g. script recompile), the watcher is lost." | ||
| )] | ||
| public ResponseCallTool Watch | ||
| ( | ||
| [Description( | ||
| "Event type to watch for. " + | ||
| "Use a specific type like 'error_logged' or 'compilation_finished'. " + | ||
| "Empty string or null matches ANY event type. " + | ||
| "Use 'event-list' tool to see all available types." | ||
| )] | ||
| string? type = null, | ||
|
|
||
| [Description( | ||
| "Maximum watch time in milliseconds. " + | ||
| "Range: 5000-120000. Default: 60000 (60 seconds). " + | ||
| "The watcher is automatically cancelled after this timeout." | ||
| )] | ||
| int timeoutMs = 60000, | ||
|
|
||
| [Description( | ||
| "If true, collects ALL matching events until timeout, then notifies once. " + | ||
| "If false (default), notifies immediately on the FIRST matching event." | ||
| )] | ||
| bool collectAll = false, | ||
|
|
||
| [RequestID] | ||
| string? requestId = null | ||
| ) | ||
| { | ||
| if (requestId == null || string.IsNullOrWhiteSpace(requestId)) | ||
| return ResponseCallTool.Error("Original request with valid RequestID must be provided."); | ||
|
|
||
| if (timeoutMs < 5000 || timeoutMs > 120000) | ||
| return ResponseCallTool.Error(Error.InvalidWatchTimeout(timeoutMs)).SetRequestID(requestId); | ||
|
|
||
| var watchType = string.IsNullOrEmpty(type) ? "any" : type; | ||
|
|
||
| // Start background watcher — does NOT block the tool response | ||
| _ = Task.Run(async () => | ||
| { | ||
| try | ||
| { | ||
| using var cts = new CancellationTokenSource(timeoutMs + 1000); | ||
| var watchResult = await McpEventBus.WaitAsync(type, timeoutMs, collectAll, cts.Token); | ||
|
|
||
| var json = System.Text.Json.JsonSerializer.SerializeToNode(watchResult); | ||
| var response = ResponseCallValueTool<McpEventSubscribeResult> | ||
| .SuccessStructured(json) | ||
| .SetRequestID(requestId); | ||
|
|
||
| await UnityMcpPluginEditor.NotifyToolRequestCompleted(new RequestToolCompletedData | ||
| { | ||
| RequestId = requestId, | ||
| Result = response | ||
| }); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| try | ||
| { | ||
| await UnityMcpPluginEditor.NotifyToolRequestCompleted(new RequestToolCompletedData | ||
| { | ||
| RequestId = requestId, | ||
| Result = ResponseCallTool.Error( | ||
| $"[event-watch] Watcher failed: {ex.Message}" | ||
| ).SetRequestID(requestId) | ||
| }); | ||
| } | ||
| catch | ||
| { | ||
| // Connection lost — nothing we can do | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| return ResponseCallTool.Processing( | ||
| $"Watching for '{watchType}' events (timeout: {timeoutMs}ms). " + | ||
| "You will be notified when a matching event occurs. Continue working." | ||
| ).SetRequestID(requestId); | ||
| } | ||
| } | ||
| } |
29 changes: 29 additions & 0 deletions
29
Unity-MCP-Plugin/Assets/root/Editor/Scripts/API/Tool/Event.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /* | ||
| ┌──────────────────────────────────────────────────────────────────┐ | ||
| │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ | ||
| │ Licensed under the Apache License, Version 2.0. │ | ||
| │ See the LICENSE file in the project root for more information. │ | ||
| └──────────────────────────────────────────────────────────────────┘ | ||
| */ | ||
|
|
||
| #nullable enable | ||
| using com.IvanMurzak.McpPlugin; | ||
|
|
||
| namespace com.IvanMurzak.Unity.MCP.Editor.API | ||
| { | ||
| [McpPluginToolType] | ||
| public partial class Tool_Event | ||
| { | ||
| public static class Error | ||
| { | ||
| public static string InvalidSubscribeTimeout(int timeoutMs) | ||
| => $"Invalid timeout value '{timeoutMs}'. Must be between 1000 and 120000 milliseconds."; | ||
|
|
||
| public static string InvalidWatchTimeout(int timeoutMs) | ||
| => $"Invalid timeout value '{timeoutMs}'. Must be between 5000 and 120000 milliseconds."; | ||
|
|
||
| public static string EventBusNotAvailable() | ||
| => "McpEventBus is not available."; | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR introduces new event tools (
event-subscribe,event-watch,event-list) and core infra (McpEventBus), but there are no corresponding Editor tests (no matches forevent-subscribeunderAssets/root/Tests). Given the repo’s existing tool test harness (BaseTest.RunTool(...)) and the constitution’s TDD/coverage requirement, please add tests covering at least: timeout vs success,timeoutMs=0drain behavior, and filtering bytype.