unreal-engine-mcp-server 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/.env.example +1 -1
  2. package/.github/release-drafter-config.yml +51 -0
  3. package/.github/workflows/greetings.yml +5 -1
  4. package/.github/workflows/labeler.yml +2 -1
  5. package/.github/workflows/publish-mcp.yml +2 -4
  6. package/.github/workflows/release-drafter.yml +3 -2
  7. package/.github/workflows/release.yml +3 -3
  8. package/CHANGELOG.md +109 -0
  9. package/CONTRIBUTING.md +1 -1
  10. package/GEMINI.md +115 -0
  11. package/Public/Plugin_setup_guide.mp4 +0 -0
  12. package/README.md +166 -200
  13. package/dist/automation/bridge.d.ts +1 -2
  14. package/dist/automation/bridge.js +24 -23
  15. package/dist/automation/connection-manager.d.ts +1 -0
  16. package/dist/automation/connection-manager.js +10 -0
  17. package/dist/automation/message-handler.js +5 -4
  18. package/dist/automation/request-tracker.d.ts +4 -0
  19. package/dist/automation/request-tracker.js +11 -3
  20. package/dist/config.d.ts +0 -1
  21. package/dist/config.js +0 -1
  22. package/dist/constants.d.ts +4 -0
  23. package/dist/constants.js +4 -0
  24. package/dist/graphql/loaders.d.ts +64 -0
  25. package/dist/graphql/loaders.js +117 -0
  26. package/dist/graphql/resolvers.d.ts +3 -3
  27. package/dist/graphql/resolvers.js +33 -30
  28. package/dist/graphql/server.js +3 -1
  29. package/dist/graphql/types.d.ts +2 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +13 -2
  32. package/dist/server-setup.d.ts +0 -1
  33. package/dist/server-setup.js +0 -40
  34. package/dist/tools/actors.d.ts +58 -24
  35. package/dist/tools/actors.js +22 -6
  36. package/dist/tools/assets.d.ts +19 -71
  37. package/dist/tools/assets.js +28 -22
  38. package/dist/tools/base-tool.d.ts +4 -4
  39. package/dist/tools/base-tool.js +1 -1
  40. package/dist/tools/blueprint.d.ts +45 -61
  41. package/dist/tools/blueprint.js +43 -14
  42. package/dist/tools/consolidated-tool-definitions.js +2 -1
  43. package/dist/tools/consolidated-tool-handlers.js +96 -110
  44. package/dist/tools/dynamic-handler-registry.d.ts +11 -9
  45. package/dist/tools/dynamic-handler-registry.js +17 -95
  46. package/dist/tools/editor.d.ts +19 -193
  47. package/dist/tools/editor.js +11 -2
  48. package/dist/tools/environment.d.ts +8 -14
  49. package/dist/tools/foliage.d.ts +18 -143
  50. package/dist/tools/foliage.js +4 -2
  51. package/dist/tools/handlers/actor-handlers.d.ts +1 -1
  52. package/dist/tools/handlers/actor-handlers.js +14 -13
  53. package/dist/tools/handlers/asset-handlers.js +454 -454
  54. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  55. package/dist/tools/handlers/sequence-handlers.js +24 -13
  56. package/dist/tools/introspection.d.ts +1 -1
  57. package/dist/tools/introspection.js +1 -1
  58. package/dist/tools/landscape.d.ts +16 -116
  59. package/dist/tools/landscape.js +7 -3
  60. package/dist/tools/level.d.ts +22 -103
  61. package/dist/tools/level.js +26 -18
  62. package/dist/tools/lighting.d.ts +54 -7
  63. package/dist/tools/lighting.js +9 -5
  64. package/dist/tools/materials.d.ts +1 -1
  65. package/dist/tools/materials.js +5 -1
  66. package/dist/tools/niagara.js +37 -2
  67. package/dist/tools/performance.d.ts +0 -1
  68. package/dist/tools/performance.js +0 -1
  69. package/dist/tools/physics.js +5 -1
  70. package/dist/tools/sequence.d.ts +24 -24
  71. package/dist/tools/sequence.js +13 -0
  72. package/dist/tools/ui.d.ts +0 -2
  73. package/dist/types/automation-responses.d.ts +115 -0
  74. package/dist/types/automation-responses.js +2 -0
  75. package/dist/types/responses.d.ts +249 -0
  76. package/dist/types/responses.js +2 -0
  77. package/dist/types/tool-interfaces.d.ts +135 -135
  78. package/dist/types/tool-types.d.ts +2 -0
  79. package/dist/unreal-bridge.js +4 -4
  80. package/dist/utils/command-validator.js +7 -5
  81. package/dist/utils/error-handler.d.ts +24 -2
  82. package/dist/utils/error-handler.js +58 -23
  83. package/dist/utils/normalize.d.ts +7 -4
  84. package/dist/utils/normalize.js +12 -10
  85. package/dist/utils/path-security.d.ts +2 -0
  86. package/dist/utils/path-security.js +24 -0
  87. package/dist/utils/response-factory.d.ts +4 -4
  88. package/dist/utils/response-factory.js +15 -21
  89. package/dist/utils/response-validator.js +88 -73
  90. package/dist/utils/unreal-command-queue.d.ts +2 -0
  91. package/dist/utils/unreal-command-queue.js +8 -1
  92. package/docs/Migration-Guide-v0.5.0.md +1 -9
  93. package/docs/handler-mapping.md +4 -2
  94. package/docs/testing-guide.md +2 -2
  95. package/package.json +12 -6
  96. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
  97. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
  98. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
  99. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
  100. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
  101. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
  102. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
  103. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
  104. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
  105. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
  106. package/scripts/run-all-tests.mjs +25 -20
  107. package/server.json +3 -2
  108. package/src/automation/bridge.ts +27 -25
  109. package/src/automation/connection-manager.ts +18 -0
  110. package/src/automation/message-handler.ts +33 -8
  111. package/src/automation/request-tracker.ts +39 -7
  112. package/src/config.ts +1 -1
  113. package/src/constants.ts +7 -0
  114. package/src/graphql/loaders.ts +244 -0
  115. package/src/graphql/resolvers.ts +47 -49
  116. package/src/graphql/server.ts +3 -1
  117. package/src/graphql/types.ts +3 -0
  118. package/src/index.ts +15 -2
  119. package/src/resources/assets.ts +5 -4
  120. package/src/server/tool-registry.ts +3 -3
  121. package/src/server-setup.ts +3 -37
  122. package/src/tools/actors.ts +77 -44
  123. package/src/tools/animation.ts +1 -0
  124. package/src/tools/assets.ts +76 -65
  125. package/src/tools/base-tool.ts +3 -3
  126. package/src/tools/blueprint.ts +170 -104
  127. package/src/tools/consolidated-tool-definitions.ts +2 -1
  128. package/src/tools/consolidated-tool-handlers.ts +129 -150
  129. package/src/tools/dynamic-handler-registry.ts +22 -140
  130. package/src/tools/editor.ts +43 -29
  131. package/src/tools/environment.ts +21 -27
  132. package/src/tools/foliage.ts +28 -25
  133. package/src/tools/handlers/actor-handlers.ts +16 -17
  134. package/src/tools/handlers/asset-handlers.ts +484 -484
  135. package/src/tools/handlers/sequence-handlers.ts +85 -62
  136. package/src/tools/introspection.ts +7 -7
  137. package/src/tools/landscape.ts +34 -28
  138. package/src/tools/level.ts +100 -80
  139. package/src/tools/lighting.ts +25 -20
  140. package/src/tools/materials.ts +9 -3
  141. package/src/tools/niagara.ts +44 -2
  142. package/src/tools/performance.ts +1 -2
  143. package/src/tools/physics.ts +7 -1
  144. package/src/tools/sequence.ts +42 -26
  145. package/src/tools/ui.ts +1 -3
  146. package/src/types/automation-responses.ts +119 -0
  147. package/src/types/responses.ts +355 -0
  148. package/src/types/tool-interfaces.ts +135 -135
  149. package/src/types/tool-types.ts +4 -0
  150. package/src/unreal-bridge.ts +71 -26
  151. package/src/utils/command-validator.ts +47 -5
  152. package/src/utils/error-handler.ts +128 -45
  153. package/src/utils/normalize.test.ts +162 -0
  154. package/src/utils/normalize.ts +38 -16
  155. package/src/utils/path-security.ts +43 -0
  156. package/src/utils/response-factory.ts +29 -24
  157. package/src/utils/response-validator.ts +103 -87
  158. package/src/utils/safe-json.test.ts +90 -0
  159. package/src/utils/unreal-command-queue.ts +13 -1
  160. package/src/utils/validation.test.ts +184 -0
  161. package/tests/test-animation.mjs +358 -33
  162. package/tests/test-asset-graph.mjs +311 -0
  163. package/tests/test-audio.mjs +314 -116
  164. package/tests/test-behavior-tree.mjs +327 -144
  165. package/tests/test-blueprint-graph.mjs +343 -12
  166. package/tests/test-control-editor.mjs +85 -53
  167. package/tests/test-graphql.mjs +58 -8
  168. package/tests/test-input.mjs +349 -0
  169. package/tests/test-inspect.mjs +291 -61
  170. package/tests/test-landscape.mjs +304 -48
  171. package/tests/test-lighting.mjs +428 -0
  172. package/tests/test-manage-level.mjs +70 -51
  173. package/tests/test-performance.mjs +539 -0
  174. package/tests/test-sequence.mjs +82 -46
  175. package/tests/test-system.mjs +72 -33
  176. package/tests/test-wasm.mjs +98 -8
  177. package/vitest.config.ts +35 -0
  178. package/.github/release-drafter.yml +0 -148
  179. package/dist/prompts/index.d.ts +0 -21
  180. package/dist/prompts/index.js +0 -217
  181. package/dist/tools/blueprint/helpers.d.ts +0 -29
  182. package/dist/tools/blueprint/helpers.js +0 -182
  183. package/src/prompts/index.ts +0 -249
  184. package/src/tools/blueprint/helpers.ts +0 -189
  185. package/tests/test-blueprint-events.mjs +0 -35
  186. package/tests/test-extra-tools.mjs +0 -38
  187. package/tests/test-render.mjs +0 -33
  188. package/tests/test-search-assets.mjs +0 -66
@@ -1046,7 +1046,8 @@ Supported actions: create_node, delete_node, connect_pins, break_pin_links, set_
1046
1046
  type: 'string',
1047
1047
  enum: [
1048
1048
  'create_node', 'delete_node', 'connect_pins', 'break_pin_links', 'set_node_property',
1049
- 'create_reroute_node', 'get_node_details', 'get_graph_details', 'get_pin_details'
1049
+ 'create_reroute_node', 'get_node_details', 'get_graph_details', 'get_pin_details',
1050
+ 'list_node_types', 'set_pin_default_value'
1050
1051
  ],
1051
1052
  description: 'Action'
1052
1053
  },
@@ -2,6 +2,7 @@ import { cleanObject } from '../utils/safe-json.js';
2
2
  import { Logger } from '../utils/logger.js';
3
3
  import { ResponseFactory } from '../utils/response-factory.js';
4
4
  import { ITools } from '../types/tool-interfaces.js';
5
+ import { toolRegistry } from './dynamic-handler-registry.js';
5
6
  import { executeAutomationRequest, requireAction } from './handlers/common-handlers.js';
6
7
  import { handleAssetTools } from './handlers/asset-handlers.js';
7
8
  import { handleActorTools } from './handlers/actor-handlers.js';
@@ -20,8 +21,8 @@ import { handleAudioTools } from './handlers/audio-handlers.js';
20
21
  import { handleLightingTools } from './handlers/lighting-handlers.js';
21
22
  import { handlePerformanceTools } from './handlers/performance-handlers.js';
22
23
  import { handleInputTools } from './handlers/input-handlers.js';
23
- import { getDynamicHandlerForTool } from './dynamic-handler-registry.js';
24
- import { consolidatedToolDefinitions } from './consolidated-tool-definitions.js';
24
+ // import { getDynamicHandlerForTool } from './dynamic-handler-registry.js';
25
+ // import { consolidatedToolDefinitions } from './consolidated-tool-definitions.js';
25
26
 
26
27
  type NormalizedToolCall = {
27
28
  name: string;
@@ -116,157 +117,141 @@ function normalizeToolCall(
116
117
  };
117
118
  }
118
119
 
119
- async function invokeNamedTool(
120
- name: string,
121
- action: string,
122
- args: any,
123
- tools: ITools
124
- ) {
125
- switch (name) {
126
- // 1. ASSET MANAGER
127
- case 'manage_asset': {
128
- // Reroute merged functionality
129
- if (['create_render_target', 'nanite_rebuild_mesh'].includes(action)) {
130
- const payload = { ...args, subAction: action };
131
- return cleanObject(await executeAutomationRequest(tools, 'manage_render', payload, `Automation bridge not available for ${action}`));
132
- }
133
- if (isMaterialGraphAction(action)) {
134
- // Map new action names to legacy subActions if needed, or just pass through if plugin supports them.
135
- // The plugin expects 'manage_material_graph' for these.
136
- // We need to map 'add_material_node' -> 'add_node' etc if the plugin is strict,
137
- // or we assume we updated the plugin.
138
- // For now, let's assume we map them to the old tool 'manage_material_graph' with the old action names.
139
- const subAction = MATERIAL_GRAPH_ACTION_MAP[action] || action;
140
-
141
- return await handleGraphTools('manage_material_graph', subAction, args, tools);
142
- }
143
- if (isBehaviorTreeGraphAction(action)) {
144
- const subAction = BEHAVIOR_TREE_ACTION_MAP[action] || action;
145
- return await handleGraphTools('manage_behavior_tree', subAction, args, tools);
146
- }
147
- return await handleAssetTools(action, args, tools);
120
+ // Registration of default handlers
121
+ function registerDefaultHandlers() {
122
+ // 1. ASSET MANAGER
123
+ toolRegistry.register('manage_asset', async (args, tools) => {
124
+ const action = args.subAction || args.action || requireAction(args); // Fallback assumption
125
+ // Reroute merged functionality
126
+ if (['create_render_target', 'nanite_rebuild_mesh'].includes(action)) {
127
+ const payload = { ...args, subAction: action };
128
+ return cleanObject(await executeAutomationRequest(tools, 'manage_render', payload, `Automation bridge not available for ${action}`));
148
129
  }
149
-
150
- // 2. BLUEPRINT MANAGER
151
-
152
- case 'manage_blueprint': {
153
- if (action === 'get_blueprint') {
154
- return await handleBlueprintGet(args, tools);
155
- }
156
- // Graph actions
157
- const graphActions = ['create_node', 'delete_node', 'connect_pins', 'break_pin_links', 'set_node_property', 'create_reroute_node', 'get_node_details', 'get_graph_details', 'get_pin_details'];
158
- if (graphActions.includes(action)) {
159
- return await handleGraphTools('manage_blueprint_graph', action, args, tools);
160
- }
161
- return await handleBlueprintTools(action, args, tools);
130
+ if (isMaterialGraphAction(action)) {
131
+ const subAction = MATERIAL_GRAPH_ACTION_MAP[action] || action;
132
+ return await handleGraphTools('manage_material_graph', subAction, args, tools);
162
133
  }
163
-
164
- // 3. ACTOR CONTROL
165
- case 'control_actor':
166
- return await handleActorTools(action, args, tools);
167
-
168
- // 4. EDITOR CONTROL
169
- case 'control_editor': {
170
- if (action === 'simulate_input') {
171
- const payload = { ...args, subAction: action };
172
- return cleanObject(await executeAutomationRequest(tools, 'manage_ui', payload, 'Automation bridge not available'));
173
- }
174
- return await handleEditorTools(action, args, tools);
134
+ if (isBehaviorTreeGraphAction(action)) {
135
+ const subAction = BEHAVIOR_TREE_ACTION_MAP[action] || action;
136
+ return await handleGraphTools('manage_behavior_tree', subAction, args, tools);
175
137
  }
176
-
177
- // 5. LEVEL MANAGER
178
- case 'manage_level': {
179
- if (['load_cells', 'set_datalayer'].includes(action)) {
180
- const payload = { ...args, subAction: action };
181
- return cleanObject(await executeAutomationRequest(tools, 'manage_world_partition', payload, 'Automation bridge not available'));
182
- }
183
- return await handleLevelTools(action, args, tools);
138
+ return await handleAssetTools(action, args, tools);
139
+ });
140
+
141
+ // 2. BLUEPRINT MANAGER
142
+ toolRegistry.register('manage_blueprint', async (args, tools) => {
143
+ const action = args.action || requireAction(args);
144
+ if (action === 'get_blueprint') {
145
+ return await handleBlueprintGet(args, tools);
184
146
  }
185
-
186
- // 6. ANIMATION & PHYSICS
187
- case 'animation_physics':
188
- return await handleAnimationTools(action, args, tools);
189
-
190
- // 7. EFFECTS MANAGER (Renamed from create_effect)
191
- case 'manage_effect': {
192
- if (isNiagaraGraphAction(action)) {
193
- // Special case: set_niagara_parameter can be for actor (Effect Tool) or asset (Graph Tool)
194
- // If actorName is present, or systemName is present but no path, it's an instance operation -> handleEffectTools
195
- const isInstanceOp = action === 'set_niagara_parameter' && (args.actorName || (args.systemName && !args.assetPath && !args.systemPath));
196
- if (isInstanceOp) {
197
- return await handleEffectTools(action, args, tools);
198
- }
199
-
200
- const subAction = NIAGARA_GRAPH_ACTION_MAP[action] || action;
201
- return await handleGraphTools('manage_niagara_graph', subAction, args, tools);
147
+ const graphActions = ['create_node', 'delete_node', 'connect_pins', 'break_pin_links', 'set_node_property', 'create_reroute_node', 'get_node_details', 'get_graph_details', 'get_pin_details'];
148
+ if (graphActions.includes(action)) {
149
+ return await handleGraphTools('manage_blueprint_graph', action, args, tools);
150
+ }
151
+ return await handleBlueprintTools(action, args, tools);
152
+ });
153
+
154
+ // 3. ACTOR CONTROL
155
+ toolRegistry.register('control_actor', async (args, tools) => {
156
+ return await handleActorTools(args.action || requireAction(args), args, tools);
157
+ });
158
+
159
+ // 4. EDITOR CONTROL
160
+ toolRegistry.register('control_editor', async (args, tools) => {
161
+ const action = args.action || requireAction(args);
162
+ if (action === 'simulate_input') {
163
+ const payload = { ...args, subAction: action };
164
+ return cleanObject(await executeAutomationRequest(tools, 'manage_ui', payload, 'Automation bridge not available'));
165
+ }
166
+ return await handleEditorTools(action, args, tools);
167
+ });
168
+
169
+ // 5. LEVEL MANAGER
170
+ toolRegistry.register('manage_level', async (args, tools) => {
171
+ const action = args.action || requireAction(args);
172
+ if (['load_cells', 'set_datalayer'].includes(action)) {
173
+ const payload = { ...args, subAction: action };
174
+ return cleanObject(await executeAutomationRequest(tools, 'manage_world_partition', payload, 'Automation bridge not available'));
175
+ }
176
+ return await handleLevelTools(action, args, tools);
177
+ });
178
+
179
+ // 6. ANIMATION & PHYSICS
180
+ toolRegistry.register('animation_physics', async (args, tools) => await handleAnimationTools(args.action || requireAction(args), args, tools));
181
+
182
+ // 7. EFFECTS MANAGER
183
+ toolRegistry.register('manage_effect', async (args, tools) => {
184
+ const action = args.action || requireAction(args);
185
+ if (isNiagaraGraphAction(action)) {
186
+ // Instance check
187
+ const isInstanceOp = action === 'set_niagara_parameter' && (args.actorName || (args.systemName && !args.assetPath && !args.systemPath));
188
+ if (isInstanceOp) {
189
+ return await handleEffectTools(action, args, tools);
202
190
  }
203
- return await handleEffectTools(action, args, tools);
191
+ const subAction = NIAGARA_GRAPH_ACTION_MAP[action] || action;
192
+ return await handleGraphTools('manage_niagara_graph', subAction, args, tools);
204
193
  }
194
+ return await handleEffectTools(action, args, tools);
195
+ });
205
196
 
206
- // 8. ENVIRONMENT BUILDER
207
- case 'build_environment':
208
- return await handleEnvironmentTools(action, args, tools);
197
+ // 8. ENVIRONMENT BUILDER
198
+ toolRegistry.register('build_environment', async (args, tools) => await handleEnvironmentTools(args.action || requireAction(args), args, tools));
209
199
 
210
- // 9. SYSTEM CONTROL
211
- case 'system_control': {
212
- if (action === 'console_command') return await handleConsoleCommand(args, tools);
213
- if (action === 'run_ubt') return await handlePipelineTools(action, args, tools);
200
+ // 9. SYSTEM CONTROL
201
+ toolRegistry.register('system_control', async (args, tools) => {
202
+ const action = args.action || requireAction(args);
203
+ if (action === 'console_command') return await handleConsoleCommand(args, tools);
204
+ if (action === 'run_ubt') return await handlePipelineTools(action, args, tools);
214
205
 
215
- // Bridge forwards
216
- if (action === 'run_tests') return cleanObject(await executeAutomationRequest(tools, 'manage_tests', { ...args, subAction: action }, 'Bridge unavailable'));
217
- if (action === 'subscribe' || action === 'unsubscribe') return cleanObject(await executeAutomationRequest(tools, 'manage_logs', { ...args, subAction: action }, 'Bridge unavailable'));
218
- if (action === 'spawn_category') return cleanObject(await executeAutomationRequest(tools, 'manage_debug', { ...args, subAction: action }, 'Bridge unavailable'));
219
- if (action === 'start_session') return cleanObject(await executeAutomationRequest(tools, 'manage_insights', { ...args, subAction: action }, 'Bridge unavailable'));
220
- if (action === 'lumen_update_scene') return cleanObject(await executeAutomationRequest(tools, 'manage_render', { ...args, subAction: action }, 'Bridge unavailable'));
206
+ if (action === 'run_tests') return cleanObject(await executeAutomationRequest(tools, 'manage_tests', { ...args, subAction: action }, 'Bridge unavailable'));
207
+ if (action === 'subscribe' || action === 'unsubscribe') return cleanObject(await executeAutomationRequest(tools, 'manage_logs', { ...args, subAction: action }, 'Bridge unavailable'));
208
+ if (action === 'spawn_category') return cleanObject(await executeAutomationRequest(tools, 'manage_debug', { ...args, subAction: action }, 'Bridge unavailable'));
209
+ if (action === 'start_session') return cleanObject(await executeAutomationRequest(tools, 'manage_insights', { ...args, subAction: action }, 'Bridge unavailable'));
210
+ if (action === 'lumen_update_scene') return cleanObject(await executeAutomationRequest(tools, 'manage_render', { ...args, subAction: action }, 'Bridge unavailable'));
221
211
 
222
- return await handleSystemTools(action, args, tools);
223
- }
212
+ return await handleSystemTools(action, args, tools);
213
+ });
224
214
 
225
- // 10. SEQUENCER
226
- case 'manage_sequence':
227
- return await handleSequenceTools(action, args, tools);
215
+ // 10. SEQUENCER
216
+ toolRegistry.register('manage_sequence', async (args, tools) => await handleSequenceTools(args.action || requireAction(args), args, tools));
228
217
 
229
- // 11. INTROSPECTION
230
- case 'inspect':
231
- return await handleInspectTools(action, args, tools);
218
+ // 11. INTROSPECTION
219
+ toolRegistry.register('inspect', async (args, tools) => await handleInspectTools(args.action || requireAction(args), args, tools));
232
220
 
233
- // 12. AUDIO
234
- case 'manage_audio':
235
- return await handleAudioTools(action, args, tools);
221
+ // 12. AUDIO
222
+ toolRegistry.register('manage_audio', async (args, tools) => await handleAudioTools(args.action || requireAction(args), args, tools));
236
223
 
237
- // 13. BEHAVIOR TREE
238
- case 'manage_behavior_tree':
239
- return await handleGraphTools('manage_behavior_tree', action, args, tools);
224
+ // 13. BEHAVIOR TREE
225
+ toolRegistry.register('manage_behavior_tree', async (args, tools) => await handleGraphTools('manage_behavior_tree', args.action || requireAction(args), args, tools));
240
226
 
241
- // 14. BLUEPRINT GRAPH DIRECT
242
- case 'manage_blueprint_graph':
243
- return await handleGraphTools('manage_blueprint_graph', action, args, tools);
227
+ // 14. BLUEPRINT GRAPH DIRECT
228
+ toolRegistry.register('manage_blueprint_graph', async (args, tools) => await handleGraphTools('manage_blueprint_graph', args.action || requireAction(args), args, tools));
244
229
 
245
- // 15. RENDER TOOLS
246
- case 'manage_render':
247
- return cleanObject(await executeAutomationRequest(tools, 'manage_render', { ...args, subAction: action }, 'Bridge unavailable'));
230
+ // 15. RENDER TOOLS
231
+ toolRegistry.register('manage_render', async (args, tools) => {
232
+ const action = args.action || requireAction(args);
233
+ return cleanObject(await executeAutomationRequest(tools, 'manage_render', { ...args, subAction: action }, 'Bridge unavailable'));
234
+ });
248
235
 
249
- // 16. WORLD PARTITION
250
- case 'manage_world_partition':
251
- return cleanObject(await executeAutomationRequest(tools, 'manage_world_partition', { ...args, subAction: action }, 'Bridge unavailable'));
236
+ // 16. WORLD PARTITION
237
+ toolRegistry.register('manage_world_partition', async (args, tools) => {
238
+ const action = args.action || requireAction(args);
239
+ return cleanObject(await executeAutomationRequest(tools, 'manage_world_partition', { ...args, subAction: action }, 'Bridge unavailable'));
240
+ });
252
241
 
253
- // 17. LIGHTING
254
- case 'manage_lighting':
255
- return await handleLightingTools(action, args, tools);
242
+ // 17. LIGHTING
243
+ toolRegistry.register('manage_lighting', async (args, tools) => await handleLightingTools(args.action || requireAction(args), args, tools));
256
244
 
257
- // 18. PERFORMANCE
258
- case 'manage_performance':
259
- return await handlePerformanceTools(action, args, tools);
245
+ // 18. PERFORMANCE
246
+ toolRegistry.register('manage_performance', async (args, tools) => await handlePerformanceTools(args.action || requireAction(args), args, tools));
260
247
 
261
- // 19. INPUT
262
- case 'manage_input':
263
- return await handleInputTools(action, args, tools);
264
-
265
- default:
266
- return cleanObject({ success: false, error: 'UNKNOWN_TOOL', message: `Unknown consolidated tool: ${name}` });
267
- }
248
+ // 19. INPUT
249
+ toolRegistry.register('manage_input', async (args, tools) => await handleInputTools(args.action || requireAction(args), args, tools));
268
250
  }
269
251
 
252
+ // Initialize default handlers immediately
253
+ registerDefaultHandlers();
254
+
270
255
  // Export the main consolidated tool call handler
271
256
  export async function handleConsolidatedToolCall(
272
257
  name: string,
@@ -280,30 +265,24 @@ export async function handleConsolidatedToolCall(
280
265
  try {
281
266
  const normalized = normalizeToolCall(name, args);
282
267
  const normalizedName = normalized.name;
283
- const action = normalized.action;
284
268
  const normalizedArgs = normalized.args;
285
-
286
- const toolDef: any = (consolidatedToolDefinitions as any[]).find(t => t.name === normalizedName);
287
- const dynamicHandler = await getDynamicHandlerForTool(normalizedName, action);
288
-
289
- if (!dynamicHandler && toolDef && toolDef.inputSchema && toolDef.inputSchema.properties && toolDef.inputSchema.properties.action) {
290
- const actionProp: any = toolDef.inputSchema.properties.action;
291
- const allowed: string[] | undefined = Array.isArray(actionProp.enum) ? actionProp.enum as string[] : undefined;
292
- if (allowed && !allowed.includes(action)) {
293
- return cleanObject({
294
- success: false,
295
- error: 'UNKNOWN_ACTION',
296
- message: `Unknown action '${action}' for consolidated tool '${normalizedName}'.`
297
- });
298
- }
269
+ // Note: action extracted inside handler usually, but here we might pass it if needed.
270
+ // The handlers above re-extract or use normalizedArgs.action.
271
+ // `normalizeToolCall` puts `action` into `normalized.action` but does NOT necessarily put it into `normalized.args.action`.
272
+ // Let's ensure args has action if we relied on it above.
273
+ if (normalized.action && !normalizedArgs.action) {
274
+ normalizedArgs.action = normalized.action;
299
275
  }
300
- const defaultHandler = async () => invokeNamedTool(normalizedName, action, normalizedArgs, tools);
301
276
 
302
- if (dynamicHandler) {
303
- return await dynamicHandler({ name: normalizedName, action, args: normalizedArgs, tools, defaultHandler });
277
+ const handler = toolRegistry.getHandler(normalizedName);
278
+
279
+ if (handler) {
280
+ return await handler(normalizedArgs, tools);
304
281
  }
305
282
 
306
- return await defaultHandler();
283
+ // Fallback or Unknown
284
+ return cleanObject({ success: false, error: 'UNKNOWN_TOOL', message: `Unknown consolidated tool: ${name}` });
285
+
307
286
  } catch (err: any) {
308
287
  const duration = Date.now() - startTime;
309
288
  logger.error(`Failed execution of ${name} after ${duration}ms: ${err?.message || String(err)}`);
@@ -1,151 +1,33 @@
1
- import { Logger } from '../utils/logger.js';
2
- import type { ITools } from '../types/tool-interfaces.js';
3
- import fs from 'fs/promises';
4
- import path from 'node:path';
5
- import { pathToFileURL } from 'node:url';
1
+ import { ITools } from '../types/tool-interfaces.js';
6
2
 
7
- const log = new Logger('DynamicHandlerRegistry');
3
+ type ToolHandler = (args: any, tools: ITools) => Promise<any>;
8
4
 
9
- export interface DynamicHandlerContext {
10
- name: string;
11
- action: string;
12
- args: any;
13
- tools: ITools;
14
- defaultHandler: () => Promise<any>;
15
- }
16
-
17
- export type DynamicHandlerFn = (ctx: DynamicHandlerContext) => Promise<any>;
18
-
19
- interface RawHandlerEntry {
20
- tool: string;
21
- module: string;
22
- function?: string;
23
- actions?: string[];
24
- }
25
-
26
- interface RawRegistry {
27
- handlers?: RawHandlerEntry[];
28
- }
29
-
30
- interface LoadedHandler {
31
- tool: string;
32
- actions?: string[];
33
- fn: DynamicHandlerFn;
34
- }
5
+ export class DynamicHandlerRegistry {
6
+ private handlers = new Map<string, ToolHandler>();
35
7
 
36
- let registryLoaded = false;
37
- const loadedHandlers: LoadedHandler[] = [];
38
-
39
- async function loadRegistryIfNeeded(): Promise<void> {
40
- if (registryLoaded) return;
41
- registryLoaded = true;
42
-
43
- // Project root (works for both src/ and dist/ builds)
44
- const rootUrl = new URL('../..', import.meta.url);
45
-
46
- const envPath = (process.env.MCP_HANDLER_REGISTRY || '').trim();
47
- let configUrl: URL | null = null;
48
-
49
- try {
50
- if (envPath) {
51
- // Allow absolute paths, file:// URLs, or paths relative to project root
52
- if (envPath.startsWith('file:')) {
53
- configUrl = new URL(envPath);
54
- } else if (path.isAbsolute(envPath)) {
55
- configUrl = pathToFileURL(envPath);
56
- } else {
57
- configUrl = new URL(envPath, rootUrl);
58
- }
59
- } else {
60
- // Default: handler-registry.json at project root
61
- configUrl = new URL('handler-registry.json', rootUrl);
8
+ register(toolName: string, handler: ToolHandler) {
9
+ if (this.handlers.has(toolName)) {
10
+ console.warn(`Handler for tool '${toolName}' is being overwritten.`);
62
11
  }
63
-
64
- let jsonText: string;
65
- try {
66
- jsonText = await fs.readFile(configUrl, 'utf8');
67
- } catch (err: any) {
68
- if (err && (err.code === 'ENOENT' || err.code === 'ERR_MODULE_NOT_FOUND')) {
69
- log.debug(`Handler registry not found at ${configUrl.href}; dynamic handlers disabled.`);
70
- return;
71
- }
72
- log.warn(`Failed to read handler registry from ${configUrl.href}:`, err);
73
- return;
74
- }
75
-
76
- let raw: RawRegistry;
77
- try {
78
- raw = JSON.parse(jsonText) as RawRegistry;
79
- } catch (err) {
80
- log.warn(`Invalid JSON in handler registry at ${configUrl.href}:`, err);
81
- return;
82
- }
83
-
84
- if (!raw.handlers || !Array.isArray(raw.handlers)) {
85
- log.debug('Handler registry contains no handlers.');
86
- return;
87
- }
88
-
89
- for (const entry of raw.handlers) {
90
- if (!entry || typeof entry.tool !== 'string' || typeof entry.module !== 'string') {
91
- continue;
92
- }
93
- try {
94
- // Resolve module relative to project root by default
95
- const modUrl = new URL(entry.module, rootUrl);
96
- const mod: any = await import(modUrl.href);
97
-
98
- const exportName = entry.function && entry.function.trim().length > 0
99
- ? entry.function.trim()
100
- : 'default';
101
-
102
- const fnExport = mod[exportName];
103
- if (typeof fnExport !== 'function') {
104
- log.warn(
105
- `Dynamic handler module ${modUrl.href} for tool '${entry.tool}' does not export a callable '${exportName}'.`
106
- );
107
- continue;
108
- }
109
-
110
- const wrapped: DynamicHandlerFn = fnExport;
111
- loadedHandlers.push({
112
- tool: entry.tool,
113
- actions: Array.isArray(entry.actions) ? entry.actions.slice() : undefined,
114
- fn: wrapped
115
- });
116
-
117
- log.info(`Registered dynamic handler for tool '${entry.tool}' from ${modUrl.href}`);
118
- } catch (err) {
119
- log.warn(
120
- `Failed to load dynamic handler for tool '${entry.tool}' from module spec '${entry.module}':`,
121
- err as any
122
- );
123
- }
124
- }
125
- } catch (err) {
126
- log.warn('Unexpected error while loading handler registry:', err as any);
12
+ this.handlers.set(toolName, handler);
127
13
  }
128
- }
129
14
 
130
- /**
131
- * Resolve a dynamic handler for a given tool/action pair.
132
- * Returns null when no handler is configured.
133
- */
134
- export async function getDynamicHandlerForTool(
135
- toolName: string,
136
- action?: string
137
- ): Promise<DynamicHandlerFn | null> {
138
- await loadRegistryIfNeeded();
139
- if (!loadedHandlers.length) return null;
15
+ getHandler(toolName: string): ToolHandler | undefined {
16
+ return this.handlers.get(toolName);
17
+ }
140
18
 
141
- const name = toolName;
142
- const act = action;
19
+ hasHandler(toolName: string): boolean {
20
+ return this.handlers.has(toolName);
21
+ }
143
22
 
144
- for (const handler of loadedHandlers) {
145
- if (handler.tool !== name) continue;
146
- if (handler.actions && act && !handler.actions.includes(act)) continue;
147
- return handler.fn;
23
+ removeHandler(toolName: string): boolean {
24
+ return this.handlers.delete(toolName);
148
25
  }
149
26
 
150
- return null;
27
+ getAllRegisteredTools(): string[] {
28
+ return Array.from(this.handlers.keys());
29
+ }
151
30
  }
31
+
32
+ // Global registry instance
33
+ export const toolRegistry = new DynamicHandlerRegistry();