unreal-engine-mcp-server 0.4.7 → 0.5.1
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.
- package/.env.example +26 -0
- package/.env.production +38 -7
- package/.eslintrc.json +0 -54
- package/.eslintrc.override.json +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +94 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
- package/.github/copilot-instructions.md +478 -45
- package/.github/dependabot.yml +19 -0
- package/.github/labeler.yml +24 -0
- package/.github/labels.yml +70 -0
- package/.github/pull_request_template.md +42 -0
- package/.github/release-drafter-config.yml +51 -0
- package/.github/workflows/auto-merge.yml +38 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/dependency-review.yml +17 -0
- package/.github/workflows/gemini-issue-triage.yml +172 -0
- package/.github/workflows/greetings.yml +27 -0
- package/.github/workflows/labeler.yml +17 -0
- package/.github/workflows/links.yml +80 -0
- package/.github/workflows/pr-size-labeler.yml +137 -0
- package/.github/workflows/publish-mcp.yml +13 -7
- package/.github/workflows/release-drafter.yml +23 -0
- package/.github/workflows/release.yml +112 -0
- package/.github/workflows/semantic-pull-request.yml +35 -0
- package/.github/workflows/smoke-test.yml +36 -0
- package/.github/workflows/stale.yml +28 -0
- package/CHANGELOG.md +338 -31
- package/CONTRIBUTING.md +140 -0
- package/GEMINI.md +115 -0
- package/Public/Plugin_setup_guide.mp4 +0 -0
- package/README.md +189 -128
- package/claude_desktop_config_example.json +7 -6
- package/dist/automation/bridge.d.ts +50 -0
- package/dist/automation/bridge.js +452 -0
- package/dist/automation/connection-manager.d.ts +23 -0
- package/dist/automation/connection-manager.js +107 -0
- package/dist/automation/handshake.d.ts +11 -0
- package/dist/automation/handshake.js +89 -0
- package/dist/automation/index.d.ts +3 -0
- package/dist/automation/index.js +3 -0
- package/dist/automation/message-handler.d.ts +12 -0
- package/dist/automation/message-handler.js +149 -0
- package/dist/automation/request-tracker.d.ts +25 -0
- package/dist/automation/request-tracker.js +98 -0
- package/dist/automation/types.d.ts +130 -0
- package/dist/automation/types.js +2 -0
- package/dist/cli.js +32 -5
- package/dist/config.d.ts +26 -0
- package/dist/config.js +59 -0
- package/dist/constants.d.ts +16 -0
- package/dist/constants.js +16 -0
- package/dist/graphql/loaders.d.ts +64 -0
- package/dist/graphql/loaders.js +117 -0
- package/dist/graphql/resolvers.d.ts +268 -0
- package/dist/graphql/resolvers.js +746 -0
- package/dist/graphql/schema.d.ts +5 -0
- package/dist/graphql/schema.js +437 -0
- package/dist/graphql/server.d.ts +26 -0
- package/dist/graphql/server.js +117 -0
- package/dist/graphql/types.d.ts +9 -0
- package/dist/graphql/types.js +2 -0
- package/dist/handlers/resource-handlers.d.ts +20 -0
- package/dist/handlers/resource-handlers.js +180 -0
- package/dist/index.d.ts +33 -18
- package/dist/index.js +130 -619
- package/dist/resources/actors.d.ts +17 -12
- package/dist/resources/actors.js +56 -76
- package/dist/resources/assets.d.ts +6 -14
- package/dist/resources/assets.js +115 -147
- package/dist/resources/levels.d.ts +13 -13
- package/dist/resources/levels.js +25 -34
- package/dist/server/resource-registry.d.ts +20 -0
- package/dist/server/resource-registry.js +37 -0
- package/dist/server/tool-registry.d.ts +23 -0
- package/dist/server/tool-registry.js +322 -0
- package/dist/server-setup.d.ts +20 -0
- package/dist/server-setup.js +71 -0
- package/dist/services/health-monitor.d.ts +34 -0
- package/dist/services/health-monitor.js +105 -0
- package/dist/services/metrics-server.d.ts +11 -0
- package/dist/services/metrics-server.js +105 -0
- package/dist/tools/actors.d.ts +163 -9
- package/dist/tools/actors.js +356 -311
- package/dist/tools/animation.d.ts +135 -4
- package/dist/tools/animation.js +510 -411
- package/dist/tools/assets.d.ts +75 -29
- package/dist/tools/assets.js +265 -284
- package/dist/tools/audio.d.ts +102 -42
- package/dist/tools/audio.js +272 -685
- package/dist/tools/base-tool.d.ts +17 -0
- package/dist/tools/base-tool.js +46 -0
- package/dist/tools/behavior-tree.d.ts +94 -0
- package/dist/tools/behavior-tree.js +39 -0
- package/dist/tools/blueprint.d.ts +208 -126
- package/dist/tools/blueprint.js +685 -832
- package/dist/tools/consolidated-tool-definitions.d.ts +5462 -1781
- package/dist/tools/consolidated-tool-definitions.js +829 -496
- package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
- package/dist/tools/consolidated-tool-handlers.js +198 -1027
- package/dist/tools/debug.d.ts +143 -85
- package/dist/tools/debug.js +234 -180
- package/dist/tools/dynamic-handler-registry.d.ts +13 -0
- package/dist/tools/dynamic-handler-registry.js +23 -0
- package/dist/tools/editor.d.ts +30 -83
- package/dist/tools/editor.js +247 -244
- package/dist/tools/engine.d.ts +10 -4
- package/dist/tools/engine.js +13 -5
- package/dist/tools/environment.d.ts +30 -0
- package/dist/tools/environment.js +267 -0
- package/dist/tools/foliage.d.ts +65 -99
- package/dist/tools/foliage.js +221 -331
- package/dist/tools/handlers/actor-handlers.d.ts +3 -0
- package/dist/tools/handlers/actor-handlers.js +227 -0
- package/dist/tools/handlers/animation-handlers.d.ts +3 -0
- package/dist/tools/handlers/animation-handlers.js +185 -0
- package/dist/tools/handlers/argument-helper.d.ts +16 -0
- package/dist/tools/handlers/argument-helper.js +80 -0
- package/dist/tools/handlers/asset-handlers.d.ts +3 -0
- package/dist/tools/handlers/asset-handlers.js +496 -0
- package/dist/tools/handlers/audio-handlers.d.ts +3 -0
- package/dist/tools/handlers/audio-handlers.js +166 -0
- package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
- package/dist/tools/handlers/blueprint-handlers.js +358 -0
- package/dist/tools/handlers/common-handlers.d.ts +14 -0
- package/dist/tools/handlers/common-handlers.js +56 -0
- package/dist/tools/handlers/editor-handlers.d.ts +3 -0
- package/dist/tools/handlers/editor-handlers.js +119 -0
- package/dist/tools/handlers/effect-handlers.d.ts +3 -0
- package/dist/tools/handlers/effect-handlers.js +171 -0
- package/dist/tools/handlers/environment-handlers.d.ts +3 -0
- package/dist/tools/handlers/environment-handlers.js +170 -0
- package/dist/tools/handlers/graph-handlers.d.ts +3 -0
- package/dist/tools/handlers/graph-handlers.js +90 -0
- package/dist/tools/handlers/input-handlers.d.ts +3 -0
- package/dist/tools/handlers/input-handlers.js +21 -0
- package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
- package/dist/tools/handlers/inspect-handlers.js +383 -0
- package/dist/tools/handlers/level-handlers.d.ts +3 -0
- package/dist/tools/handlers/level-handlers.js +237 -0
- package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
- package/dist/tools/handlers/lighting-handlers.js +144 -0
- package/dist/tools/handlers/performance-handlers.d.ts +3 -0
- package/dist/tools/handlers/performance-handlers.js +130 -0
- package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
- package/dist/tools/handlers/pipeline-handlers.js +110 -0
- package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
- package/dist/tools/handlers/sequence-handlers.js +376 -0
- package/dist/tools/handlers/system-handlers.d.ts +4 -0
- package/dist/tools/handlers/system-handlers.js +506 -0
- package/dist/tools/input.d.ts +19 -0
- package/dist/tools/input.js +89 -0
- package/dist/tools/introspection.d.ts +103 -40
- package/dist/tools/introspection.js +425 -568
- package/dist/tools/landscape.d.ts +54 -93
- package/dist/tools/landscape.js +284 -409
- package/dist/tools/level.d.ts +66 -27
- package/dist/tools/level.js +647 -675
- package/dist/tools/lighting.d.ts +77 -38
- package/dist/tools/lighting.js +445 -943
- package/dist/tools/logs.d.ts +3 -3
- package/dist/tools/logs.js +5 -57
- package/dist/tools/materials.d.ts +91 -24
- package/dist/tools/materials.js +194 -118
- package/dist/tools/niagara.d.ts +149 -39
- package/dist/tools/niagara.js +267 -182
- package/dist/tools/performance.d.ts +27 -13
- package/dist/tools/performance.js +203 -122
- package/dist/tools/physics.d.ts +32 -77
- package/dist/tools/physics.js +175 -582
- package/dist/tools/property-dictionary.d.ts +13 -0
- package/dist/tools/property-dictionary.js +82 -0
- package/dist/tools/sequence.d.ts +85 -60
- package/dist/tools/sequence.js +208 -747
- package/dist/tools/tool-definition-utils.d.ts +59 -0
- package/dist/tools/tool-definition-utils.js +35 -0
- package/dist/tools/ui.d.ts +64 -34
- package/dist/tools/ui.js +134 -214
- package/dist/types/automation-responses.d.ts +115 -0
- package/dist/types/automation-responses.js +2 -0
- package/dist/types/env.d.ts +0 -3
- package/dist/types/env.js +0 -7
- package/dist/types/responses.d.ts +249 -0
- package/dist/types/responses.js +2 -0
- package/dist/types/tool-interfaces.d.ts +898 -0
- package/dist/types/tool-interfaces.js +2 -0
- package/dist/types/tool-types.d.ts +183 -19
- package/dist/types/tool-types.js +0 -4
- package/dist/unreal-bridge.d.ts +24 -131
- package/dist/unreal-bridge.js +364 -1506
- package/dist/utils/command-validator.d.ts +9 -0
- package/dist/utils/command-validator.js +68 -0
- package/dist/utils/elicitation.d.ts +1 -1
- package/dist/utils/elicitation.js +12 -15
- package/dist/utils/error-handler.d.ts +2 -51
- package/dist/utils/error-handler.js +11 -87
- package/dist/utils/ini-reader.d.ts +3 -0
- package/dist/utils/ini-reader.js +69 -0
- package/dist/utils/logger.js +9 -6
- package/dist/utils/normalize.d.ts +3 -0
- package/dist/utils/normalize.js +56 -0
- package/dist/utils/path-security.d.ts +2 -0
- package/dist/utils/path-security.js +24 -0
- package/dist/utils/response-factory.d.ts +7 -0
- package/dist/utils/response-factory.js +27 -0
- package/dist/utils/response-validator.d.ts +3 -24
- package/dist/utils/response-validator.js +130 -81
- package/dist/utils/result-helpers.d.ts +4 -5
- package/dist/utils/result-helpers.js +15 -16
- package/dist/utils/safe-json.js +5 -11
- package/dist/utils/unreal-command-queue.d.ts +24 -0
- package/dist/utils/unreal-command-queue.js +120 -0
- package/dist/utils/validation.d.ts +0 -40
- package/dist/utils/validation.js +1 -78
- package/dist/wasm/index.d.ts +70 -0
- package/dist/wasm/index.js +535 -0
- package/docs/GraphQL-API.md +888 -0
- package/docs/Migration-Guide-v0.5.0.md +684 -0
- package/docs/Roadmap.md +53 -0
- package/docs/WebAssembly-Integration.md +628 -0
- package/docs/editor-plugin-extension.md +370 -0
- package/docs/handler-mapping.md +242 -0
- package/docs/native-automation-progress.md +128 -0
- package/docs/testing-guide.md +423 -0
- package/mcp-config-example.json +6 -6
- package/package.json +67 -28
- package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
- package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
- package/scripts/check-unreal-connection.mjs +19 -0
- package/scripts/clean-tmp.js +23 -0
- package/scripts/patch-wasm.js +26 -0
- package/scripts/run-all-tests.mjs +136 -0
- package/scripts/smoke-test.ts +94 -0
- package/scripts/sync-mcp-plugin.js +143 -0
- package/scripts/test-no-plugin-alternates.mjs +113 -0
- package/scripts/validate-server.js +46 -0
- package/scripts/verify-automation-bridge.js +200 -0
- package/server.json +58 -21
- package/src/automation/bridge.ts +558 -0
- package/src/automation/connection-manager.ts +130 -0
- package/src/automation/handshake.ts +99 -0
- package/src/automation/index.ts +2 -0
- package/src/automation/message-handler.ts +167 -0
- package/src/automation/request-tracker.ts +123 -0
- package/src/automation/types.ts +107 -0
- package/src/cli.ts +33 -6
- package/src/config.ts +73 -0
- package/src/constants.ts +19 -0
- package/src/graphql/loaders.ts +244 -0
- package/src/graphql/resolvers.ts +1008 -0
- package/src/graphql/schema.ts +452 -0
- package/src/graphql/server.ts +156 -0
- package/src/graphql/types.ts +10 -0
- package/src/handlers/resource-handlers.ts +186 -0
- package/src/index.ts +166 -664
- package/src/resources/actors.ts +58 -76
- package/src/resources/assets.ts +148 -134
- package/src/resources/levels.ts +28 -33
- package/src/server/resource-registry.ts +47 -0
- package/src/server/tool-registry.ts +354 -0
- package/src/server-setup.ts +114 -0
- package/src/services/health-monitor.ts +132 -0
- package/src/services/metrics-server.ts +142 -0
- package/src/tools/actors.ts +426 -323
- package/src/tools/animation.ts +672 -461
- package/src/tools/assets.ts +364 -289
- package/src/tools/audio.ts +323 -766
- package/src/tools/base-tool.ts +52 -0
- package/src/tools/behavior-tree.ts +45 -0
- package/src/tools/blueprint.ts +792 -970
- package/src/tools/consolidated-tool-definitions.ts +993 -515
- package/src/tools/consolidated-tool-handlers.ts +258 -1146
- package/src/tools/debug.ts +292 -187
- package/src/tools/dynamic-handler-registry.ts +33 -0
- package/src/tools/editor.ts +329 -253
- package/src/tools/engine.ts +14 -3
- package/src/tools/environment.ts +281 -0
- package/src/tools/foliage.ts +330 -392
- package/src/tools/handlers/actor-handlers.ts +265 -0
- package/src/tools/handlers/animation-handlers.ts +237 -0
- package/src/tools/handlers/argument-helper.ts +142 -0
- package/src/tools/handlers/asset-handlers.ts +532 -0
- package/src/tools/handlers/audio-handlers.ts +194 -0
- package/src/tools/handlers/blueprint-handlers.ts +380 -0
- package/src/tools/handlers/common-handlers.ts +87 -0
- package/src/tools/handlers/editor-handlers.ts +123 -0
- package/src/tools/handlers/effect-handlers.ts +220 -0
- package/src/tools/handlers/environment-handlers.ts +183 -0
- package/src/tools/handlers/graph-handlers.ts +116 -0
- package/src/tools/handlers/input-handlers.ts +28 -0
- package/src/tools/handlers/inspect-handlers.ts +450 -0
- package/src/tools/handlers/level-handlers.ts +252 -0
- package/src/tools/handlers/lighting-handlers.ts +147 -0
- package/src/tools/handlers/performance-handlers.ts +132 -0
- package/src/tools/handlers/pipeline-handlers.ts +127 -0
- package/src/tools/handlers/sequence-handlers.ts +415 -0
- package/src/tools/handlers/system-handlers.ts +564 -0
- package/src/tools/input.ts +101 -0
- package/src/tools/introspection.ts +493 -584
- package/src/tools/landscape.ts +418 -507
- package/src/tools/level.ts +786 -708
- package/src/tools/lighting.ts +588 -984
- package/src/tools/logs.ts +9 -57
- package/src/tools/materials.ts +237 -121
- package/src/tools/niagara.ts +335 -168
- package/src/tools/performance.ts +320 -169
- package/src/tools/physics.ts +274 -613
- package/src/tools/property-dictionary.ts +98 -0
- package/src/tools/sequence.ts +276 -820
- package/src/tools/tool-definition-utils.ts +35 -0
- package/src/tools/ui.ts +205 -283
- package/src/types/automation-responses.ts +119 -0
- package/src/types/env.ts +0 -10
- package/src/types/responses.ts +355 -0
- package/src/types/tool-interfaces.ts +250 -0
- package/src/types/tool-types.ts +243 -21
- package/src/unreal-bridge.ts +460 -1550
- package/src/utils/command-validator.ts +76 -0
- package/src/utils/elicitation.ts +10 -7
- package/src/utils/error-handler.ts +14 -90
- package/src/utils/ini-reader.ts +86 -0
- package/src/utils/logger.ts +8 -3
- package/src/utils/normalize.test.ts +162 -0
- package/src/utils/normalize.ts +60 -0
- package/src/utils/path-security.ts +43 -0
- package/src/utils/response-factory.ts +44 -0
- package/src/utils/response-validator.ts +176 -56
- package/src/utils/result-helpers.ts +21 -19
- package/src/utils/safe-json.test.ts +90 -0
- package/src/utils/safe-json.ts +14 -11
- package/src/utils/unreal-command-queue.ts +152 -0
- package/src/utils/validation.test.ts +184 -0
- package/src/utils/validation.ts +4 -1
- package/src/wasm/index.ts +838 -0
- package/test-server.mjs +100 -0
- package/tests/run-unreal-tool-tests.mjs +242 -14
- package/tests/test-animation.mjs +369 -0
- package/tests/test-asset-advanced.mjs +82 -0
- package/tests/test-asset-errors.mjs +35 -0
- package/tests/test-asset-graph.mjs +311 -0
- package/tests/test-audio.mjs +417 -0
- package/tests/test-automation-timeouts.mjs +98 -0
- package/tests/test-behavior-tree.mjs +444 -0
- package/tests/test-blueprint-graph.mjs +410 -0
- package/tests/test-blueprint.mjs +577 -0
- package/tests/test-client-mode.mjs +86 -0
- package/tests/test-console-command.mjs +56 -0
- package/tests/test-control-actor.mjs +425 -0
- package/tests/test-control-editor.mjs +112 -0
- package/tests/test-graphql.mjs +372 -0
- package/tests/test-input.mjs +349 -0
- package/tests/test-inspect.mjs +302 -0
- package/tests/test-landscape.mjs +316 -0
- package/tests/test-lighting.mjs +428 -0
- package/tests/test-manage-asset.mjs +438 -0
- package/tests/test-manage-level.mjs +89 -0
- package/tests/test-materials.mjs +356 -0
- package/tests/test-niagara.mjs +185 -0
- package/tests/test-no-inline-python.mjs +122 -0
- package/tests/test-performance.mjs +539 -0
- package/tests/test-plugin-handshake.mjs +82 -0
- package/tests/test-runner.mjs +933 -0
- package/tests/test-sequence.mjs +104 -0
- package/tests/test-system.mjs +96 -0
- package/tests/test-wasm.mjs +283 -0
- package/tests/test-world-partition.mjs +215 -0
- package/tsconfig.json +3 -3
- package/vitest.config.ts +35 -0
- package/wasm/Cargo.lock +363 -0
- package/wasm/Cargo.toml +42 -0
- package/wasm/LICENSE +21 -0
- package/wasm/README.md +253 -0
- package/wasm/src/dependency_resolver.rs +377 -0
- package/wasm/src/lib.rs +153 -0
- package/wasm/src/property_parser.rs +271 -0
- package/wasm/src/transform_math.rs +396 -0
- package/wasm/tests/integration.rs +109 -0
- package/.github/workflows/smithery-build.yml +0 -29
- package/dist/prompts/index.d.ts +0 -21
- package/dist/prompts/index.js +0 -217
- package/dist/tools/build_environment_advanced.d.ts +0 -65
- package/dist/tools/build_environment_advanced.js +0 -633
- package/dist/tools/rc.d.ts +0 -110
- package/dist/tools/rc.js +0 -437
- package/dist/tools/visual.d.ts +0 -40
- package/dist/tools/visual.js +0 -282
- package/dist/utils/http.d.ts +0 -6
- package/dist/utils/http.js +0 -151
- package/dist/utils/python-output.d.ts +0 -18
- package/dist/utils/python-output.js +0 -290
- package/dist/utils/python.d.ts +0 -2
- package/dist/utils/python.js +0 -4
- package/dist/utils/stdio-redirect.d.ts +0 -2
- package/dist/utils/stdio-redirect.js +0 -20
- package/docs/unreal-tool-test-cases.md +0 -574
- package/smithery.yaml +0 -29
- package/src/prompts/index.ts +0 -249
- package/src/tools/build_environment_advanced.ts +0 -732
- package/src/tools/rc.ts +0 -515
- package/src/tools/visual.ts +0 -281
- package/src/utils/http.ts +0 -187
- package/src/utils/python-output.ts +0 -351
- package/src/utils/python.ts +0 -3
- package/src/utils/stdio-redirect.ts +0 -18
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
|
+
import { Logger } from '../utils/logger.js';
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_AUTOMATION_HOST,
|
|
6
|
+
DEFAULT_AUTOMATION_PORT,
|
|
7
|
+
DEFAULT_NEGOTIATED_PROTOCOLS,
|
|
8
|
+
DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
9
|
+
DEFAULT_MAX_PENDING_REQUESTS
|
|
10
|
+
} from '../constants.js';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import {
|
|
13
|
+
AutomationBridgeOptions,
|
|
14
|
+
AutomationBridgeStatus,
|
|
15
|
+
AutomationBridgeMessage,
|
|
16
|
+
AutomationBridgeResponseMessage,
|
|
17
|
+
AutomationBridgeEvents
|
|
18
|
+
} from './types.js';
|
|
19
|
+
import { ConnectionManager } from './connection-manager.js';
|
|
20
|
+
import { RequestTracker } from './request-tracker.js';
|
|
21
|
+
import { HandshakeHandler } from './handshake.js';
|
|
22
|
+
import { MessageHandler } from './message-handler.js';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const packageInfo: { name?: string; version?: string } = (() => {
|
|
26
|
+
try {
|
|
27
|
+
return require('../../package.json');
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const log = new Logger('AutomationBridge');
|
|
30
|
+
log.debug('Unable to read package.json for version info', error);
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
|
|
35
|
+
export class AutomationBridge extends EventEmitter {
|
|
36
|
+
private readonly host: string;
|
|
37
|
+
private readonly port: number;
|
|
38
|
+
private readonly ports: number[];
|
|
39
|
+
private readonly negotiatedProtocols: string[];
|
|
40
|
+
private readonly capabilityToken?: string;
|
|
41
|
+
private readonly enabled: boolean;
|
|
42
|
+
private readonly serverName: string;
|
|
43
|
+
private readonly serverVersion: string;
|
|
44
|
+
private readonly clientHost: string;
|
|
45
|
+
private readonly clientPort: number;
|
|
46
|
+
private readonly serverLegacyEnabled: boolean;
|
|
47
|
+
private readonly maxConcurrentConnections: number;
|
|
48
|
+
|
|
49
|
+
private connectionManager: ConnectionManager;
|
|
50
|
+
private requestTracker: RequestTracker;
|
|
51
|
+
private handshakeHandler: HandshakeHandler;
|
|
52
|
+
private messageHandler: MessageHandler;
|
|
53
|
+
private log = new Logger('AutomationBridge');
|
|
54
|
+
|
|
55
|
+
private lastHandshakeAt?: Date;
|
|
56
|
+
private lastHandshakeMetadata?: Record<string, unknown>;
|
|
57
|
+
private lastHandshakeAck?: AutomationBridgeMessage;
|
|
58
|
+
private lastHandshakeFailure?: { reason: string; at: Date };
|
|
59
|
+
private lastDisconnect?: { code: number; reason: string; at: Date };
|
|
60
|
+
private lastError?: { message: string; at: Date };
|
|
61
|
+
private requestQueue: Array<() => void> = [];
|
|
62
|
+
private queuedRequestItems: Array<{ resolve: (v: any) => void; reject: (e: any) => void; action: string; payload: any; options: any }> = [];
|
|
63
|
+
private connectionPromise?: Promise<void>;
|
|
64
|
+
|
|
65
|
+
constructor(options: AutomationBridgeOptions = {}) {
|
|
66
|
+
super();
|
|
67
|
+
this.host = options.host ?? process.env.MCP_AUTOMATION_WS_HOST ?? DEFAULT_AUTOMATION_HOST;
|
|
68
|
+
|
|
69
|
+
const sanitizePort = (value: unknown): number | null => {
|
|
70
|
+
if (typeof value === 'number' && Number.isInteger(value)) {
|
|
71
|
+
return value > 0 && value <= 65535 ? value : null;
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
74
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
75
|
+
return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : null;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const defaultPort = sanitizePort(options.port ?? process.env.MCP_AUTOMATION_WS_PORT) ?? DEFAULT_AUTOMATION_PORT;
|
|
81
|
+
const configuredPortValues: Array<number | string> | undefined = options.ports
|
|
82
|
+
? options.ports
|
|
83
|
+
: process.env.MCP_AUTOMATION_WS_PORTS
|
|
84
|
+
?.split(',')
|
|
85
|
+
.map((token) => token.trim())
|
|
86
|
+
.filter((token) => token.length > 0);
|
|
87
|
+
|
|
88
|
+
const sanitizedPorts = Array.isArray(configuredPortValues)
|
|
89
|
+
? configuredPortValues
|
|
90
|
+
.map((value) => sanitizePort(value))
|
|
91
|
+
.filter((port): port is number => port !== null)
|
|
92
|
+
: [];
|
|
93
|
+
|
|
94
|
+
if (!sanitizedPorts.includes(defaultPort)) {
|
|
95
|
+
sanitizedPorts.unshift(defaultPort);
|
|
96
|
+
}
|
|
97
|
+
if (sanitizedPorts.length === 0) {
|
|
98
|
+
sanitizedPorts.push(DEFAULT_AUTOMATION_PORT);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.ports = Array.from(new Set(sanitizedPorts));
|
|
102
|
+
const defaultProtocols = DEFAULT_NEGOTIATED_PROTOCOLS;
|
|
103
|
+
const userProtocols = Array.isArray(options.protocols)
|
|
104
|
+
? options.protocols.filter((proto) => typeof proto === 'string' && proto.trim().length > 0)
|
|
105
|
+
: [];
|
|
106
|
+
const envProtocols = process.env.MCP_AUTOMATION_WS_PROTOCOLS
|
|
107
|
+
? process.env.MCP_AUTOMATION_WS_PROTOCOLS.split(',')
|
|
108
|
+
.map((token) => token.trim())
|
|
109
|
+
.filter((token) => token.length > 0)
|
|
110
|
+
: [];
|
|
111
|
+
this.negotiatedProtocols = Array.from(new Set([...userProtocols, ...envProtocols, ...defaultProtocols]));
|
|
112
|
+
this.port = this.ports[0];
|
|
113
|
+
this.serverLegacyEnabled =
|
|
114
|
+
options.serverLegacyEnabled ?? process.env.MCP_AUTOMATION_SERVER_LEGACY !== 'false';
|
|
115
|
+
this.capabilityToken =
|
|
116
|
+
options.capabilityToken ?? process.env.MCP_AUTOMATION_CAPABILITY_TOKEN ?? undefined;
|
|
117
|
+
this.enabled = options.enabled ?? process.env.MCP_AUTOMATION_BRIDGE_ENABLED !== 'false';
|
|
118
|
+
this.serverName = options.serverName
|
|
119
|
+
?? process.env.MCP_SERVER_NAME
|
|
120
|
+
?? packageInfo.name
|
|
121
|
+
?? 'unreal-engine-mcp';
|
|
122
|
+
this.serverVersion = options.serverVersion
|
|
123
|
+
?? process.env.MCP_SERVER_VERSION
|
|
124
|
+
?? packageInfo.version
|
|
125
|
+
?? process.env.npm_package_version
|
|
126
|
+
?? '0.0.0';
|
|
127
|
+
|
|
128
|
+
const heartbeatIntervalMs = (options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS) > 0
|
|
129
|
+
? (options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS)
|
|
130
|
+
: 0;
|
|
131
|
+
|
|
132
|
+
const maxPendingRequests = Math.max(1, options.maxPendingRequests ?? DEFAULT_MAX_PENDING_REQUESTS);
|
|
133
|
+
const maxConcurrentConnections = Math.max(1, options.maxConcurrentConnections ?? 10);
|
|
134
|
+
|
|
135
|
+
this.clientHost = options.clientHost ?? process.env.MCP_AUTOMATION_CLIENT_HOST ?? DEFAULT_AUTOMATION_HOST;
|
|
136
|
+
this.clientPort = options.clientPort ?? sanitizePort(process.env.MCP_AUTOMATION_CLIENT_PORT) ?? DEFAULT_AUTOMATION_PORT;
|
|
137
|
+
this.maxConcurrentConnections = maxConcurrentConnections;
|
|
138
|
+
|
|
139
|
+
// Initialize components
|
|
140
|
+
this.connectionManager = new ConnectionManager(heartbeatIntervalMs);
|
|
141
|
+
this.requestTracker = new RequestTracker(maxPendingRequests);
|
|
142
|
+
this.handshakeHandler = new HandshakeHandler(this.capabilityToken);
|
|
143
|
+
this.messageHandler = new MessageHandler(this.requestTracker);
|
|
144
|
+
|
|
145
|
+
// Forward events from connection manager
|
|
146
|
+
// Note: ConnectionManager doesn't emit 'connected'/'disconnected' directly in the same way,
|
|
147
|
+
// we handle socket events here and use ConnectionManager to track state.
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
override on<K extends keyof AutomationBridgeEvents>(
|
|
151
|
+
event: K,
|
|
152
|
+
listener: AutomationBridgeEvents[K]
|
|
153
|
+
): this {
|
|
154
|
+
return super.on(event, listener as (...args: unknown[]) => void);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
override once<K extends keyof AutomationBridgeEvents>(
|
|
158
|
+
event: K,
|
|
159
|
+
listener: AutomationBridgeEvents[K]
|
|
160
|
+
): this {
|
|
161
|
+
return super.once(event, listener as (...args: unknown[]) => void);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
override off<K extends keyof AutomationBridgeEvents>(
|
|
165
|
+
event: K,
|
|
166
|
+
listener: AutomationBridgeEvents[K]
|
|
167
|
+
): this {
|
|
168
|
+
return super.off(event, listener as (...args: unknown[]) => void);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
start(): void {
|
|
172
|
+
if (!this.enabled) {
|
|
173
|
+
this.log.info('Automation bridge disabled by configuration.');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.log.info(`Automation bridge connecting to Unreal server at ws://${this.clientHost}:${this.clientPort}`);
|
|
178
|
+
this.startClient();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private startClient(): void {
|
|
182
|
+
try {
|
|
183
|
+
const url = `ws://${this.clientHost}:${this.clientPort}`;
|
|
184
|
+
this.log.info(`Connecting to Unreal Engine automation server at ${url}`);
|
|
185
|
+
|
|
186
|
+
this.log.debug(`Negotiated protocols: ${JSON.stringify(this.negotiatedProtocols)}`);
|
|
187
|
+
|
|
188
|
+
// Compatibility fix: If only one protocol, pass as string to ensure ws/plugin compatibility
|
|
189
|
+
const protocols = 'mcp-automation';
|
|
190
|
+
|
|
191
|
+
this.log.debug(`Using WebSocket protocols arg: ${JSON.stringify(protocols)}`);
|
|
192
|
+
|
|
193
|
+
const socket = new WebSocket(url, protocols, {
|
|
194
|
+
headers: this.capabilityToken ? { 'X-MCP-Capability': this.capabilityToken } : undefined,
|
|
195
|
+
perMessageDeflate: false
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
this.handleClientConnection(socket);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
201
|
+
this.lastError = { message: errorObj.message, at: new Date() };
|
|
202
|
+
this.log.error('Failed to create WebSocket client connection', errorObj);
|
|
203
|
+
const errorWithPort = Object.assign(errorObj, { port: this.clientPort });
|
|
204
|
+
this.emitAutomation('error', errorWithPort);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async handleClientConnection(socket: WebSocket): Promise<void> {
|
|
209
|
+
socket.on('open', async () => {
|
|
210
|
+
this.log.info('Automation bridge client connected, starting handshake');
|
|
211
|
+
try {
|
|
212
|
+
const metadata = await this.handshakeHandler.initiateHandshake(socket);
|
|
213
|
+
|
|
214
|
+
this.lastHandshakeAt = new Date();
|
|
215
|
+
this.lastHandshakeMetadata = metadata;
|
|
216
|
+
this.lastHandshakeFailure = undefined;
|
|
217
|
+
this.connectionManager.updateLastMessageTime();
|
|
218
|
+
|
|
219
|
+
// Extract remote address/port
|
|
220
|
+
const underlying: any = (socket as any)._socket || (socket as any).socket;
|
|
221
|
+
const remoteAddr = underlying?.remoteAddress ?? undefined;
|
|
222
|
+
const remotePort = underlying?.remotePort ?? undefined;
|
|
223
|
+
|
|
224
|
+
this.connectionManager.registerSocket(socket, this.clientPort, metadata, remoteAddr, remotePort);
|
|
225
|
+
this.connectionManager.startHeartbeat();
|
|
226
|
+
this.flushQueue();
|
|
227
|
+
|
|
228
|
+
this.emitAutomation('connected', {
|
|
229
|
+
socket,
|
|
230
|
+
metadata,
|
|
231
|
+
port: this.clientPort,
|
|
232
|
+
protocol: socket.protocol || null
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Set up message handling for the authenticated socket
|
|
236
|
+
socket.on('message', (data) => {
|
|
237
|
+
try {
|
|
238
|
+
const text = typeof data === 'string' ? data : data.toString('utf8');
|
|
239
|
+
this.log.debug(`[AutomationBridge Client] Received message: ${text.substring(0, 1000)}`);
|
|
240
|
+
const parsed = JSON.parse(text) as AutomationBridgeMessage;
|
|
241
|
+
this.connectionManager.updateLastMessageTime();
|
|
242
|
+
this.messageHandler.handleMessage(parsed);
|
|
243
|
+
this.emitAutomation('message', parsed);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
this.log.error('Error handling message', error);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
} catch (error) {
|
|
250
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
251
|
+
this.lastHandshakeFailure = { reason: err.message, at: new Date() };
|
|
252
|
+
this.emitAutomation('handshakeFailed', { reason: err.message, port: this.clientPort });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
socket.on('error', (error) => {
|
|
257
|
+
this.log.error('Automation bridge client socket error', error);
|
|
258
|
+
const errObj = error instanceof Error ? error : new Error(String(error));
|
|
259
|
+
this.lastError = { message: errObj.message, at: new Date() };
|
|
260
|
+
const errWithPort = Object.assign(errObj, { port: this.clientPort });
|
|
261
|
+
this.emitAutomation('error', errWithPort);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
socket.on('close', (code, reasonBuffer) => {
|
|
265
|
+
const reason = reasonBuffer.toString('utf8');
|
|
266
|
+
const socketInfo = this.connectionManager.removeSocket(socket);
|
|
267
|
+
|
|
268
|
+
if (socketInfo) {
|
|
269
|
+
this.lastDisconnect = { code, reason, at: new Date() };
|
|
270
|
+
this.emitAutomation('disconnected', {
|
|
271
|
+
code,
|
|
272
|
+
reason,
|
|
273
|
+
port: socketInfo.port,
|
|
274
|
+
protocol: socketInfo.protocol || null
|
|
275
|
+
});
|
|
276
|
+
this.log.info(`Automation bridge client socket closed (code=${code}, reason=${reason})`);
|
|
277
|
+
|
|
278
|
+
if (!this.connectionManager.isConnected()) {
|
|
279
|
+
this.requestTracker.rejectAll(new Error(reason || 'Connection lost'));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
stop(): void {
|
|
286
|
+
if (this.isConnected()) {
|
|
287
|
+
this.broadcast({
|
|
288
|
+
type: 'bridge_shutdown',
|
|
289
|
+
timestamp: new Date().toISOString(),
|
|
290
|
+
reason: 'Server shutting down'
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
this.connectionManager.closeAll(1001, 'Server shutdown');
|
|
294
|
+
this.lastHandshakeAck = undefined;
|
|
295
|
+
this.requestTracker.rejectAll(new Error('Automation bridge server stopped'));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
isConnected(): boolean {
|
|
299
|
+
return this.connectionManager.isConnected();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
getStatus(): AutomationBridgeStatus {
|
|
303
|
+
|
|
304
|
+
const connectionInfos = Array.from(this.connectionManager.getActiveSockets().entries()).map(([socket, info]) => ({
|
|
305
|
+
connectionId: info.connectionId,
|
|
306
|
+
sessionId: info.sessionId ?? null,
|
|
307
|
+
remoteAddress: info.remoteAddress ?? null,
|
|
308
|
+
remotePort: info.remotePort ?? null,
|
|
309
|
+
port: info.port,
|
|
310
|
+
connectedAt: info.connectedAt.toISOString(),
|
|
311
|
+
protocol: info.protocol || null,
|
|
312
|
+
readyState: socket.readyState,
|
|
313
|
+
isPrimary: socket === this.connectionManager.getPrimarySocket()
|
|
314
|
+
}));
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
enabled: this.enabled,
|
|
318
|
+
host: this.host,
|
|
319
|
+
port: this.port,
|
|
320
|
+
configuredPorts: [...this.ports],
|
|
321
|
+
listeningPorts: [], // We are client-only now
|
|
322
|
+
connected: this.isConnected(),
|
|
323
|
+
connectedAt: connectionInfos.length > 0 ? connectionInfos[0].connectedAt : null,
|
|
324
|
+
activePort: connectionInfos.length > 0 ? connectionInfos[0].port : null,
|
|
325
|
+
negotiatedProtocol: connectionInfos.length > 0 ? connectionInfos[0].protocol : null,
|
|
326
|
+
supportedProtocols: [...this.negotiatedProtocols],
|
|
327
|
+
supportedOpcodes: ['automation_request'],
|
|
328
|
+
expectedResponseOpcodes: ['automation_response'],
|
|
329
|
+
capabilityTokenRequired: Boolean(this.capabilityToken),
|
|
330
|
+
lastHandshakeAt: this.lastHandshakeAt?.toISOString() ?? null,
|
|
331
|
+
lastHandshakeMetadata: this.lastHandshakeMetadata ?? null,
|
|
332
|
+
lastHandshakeAck: this.lastHandshakeAck ?? null,
|
|
333
|
+
lastHandshakeFailure: this.lastHandshakeFailure
|
|
334
|
+
? { reason: this.lastHandshakeFailure.reason, at: this.lastHandshakeFailure.at.toISOString() }
|
|
335
|
+
: null,
|
|
336
|
+
lastDisconnect: this.lastDisconnect
|
|
337
|
+
? { code: this.lastDisconnect.code, reason: this.lastDisconnect.reason, at: this.lastDisconnect.at.toISOString() }
|
|
338
|
+
: null,
|
|
339
|
+
lastError: this.lastError
|
|
340
|
+
? { message: this.lastError.message, at: this.lastError.at.toISOString() }
|
|
341
|
+
: null,
|
|
342
|
+
lastMessageAt: this.connectionManager.getLastMessageTime()?.toISOString() ?? null,
|
|
343
|
+
lastRequestSentAt: null, // TODO: Track this in RequestTracker?
|
|
344
|
+
pendingRequests: this.requestTracker.getPendingCount(),
|
|
345
|
+
pendingRequestDetails: this.requestTracker.getPendingDetails(),
|
|
346
|
+
connections: connectionInfos,
|
|
347
|
+
webSocketListening: false,
|
|
348
|
+
serverLegacyEnabled: this.serverLegacyEnabled,
|
|
349
|
+
serverName: this.serverName,
|
|
350
|
+
serverVersion: this.serverVersion,
|
|
351
|
+
maxConcurrentConnections: this.maxConcurrentConnections,
|
|
352
|
+
maxPendingRequests: 100, // TODO: Expose from RequestTracker
|
|
353
|
+
heartbeatIntervalMs: 30000 // TODO: Expose from ConnectionManager
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async sendAutomationRequest<T = AutomationBridgeResponseMessage>(
|
|
358
|
+
action: string,
|
|
359
|
+
payload: Record<string, unknown> = {},
|
|
360
|
+
options: { timeoutMs?: number } = {}
|
|
361
|
+
): Promise<T> {
|
|
362
|
+
if (!this.isConnected()) {
|
|
363
|
+
if (this.enabled) {
|
|
364
|
+
this.log.info('Automation bridge not connected, attempting lazy connection...');
|
|
365
|
+
|
|
366
|
+
// Avoid multiple simultaneous connection attempts
|
|
367
|
+
if (!this.connectionPromise) {
|
|
368
|
+
this.connectionPromise = new Promise<void>((resolve, reject) => {
|
|
369
|
+
const onConnect = () => {
|
|
370
|
+
cleanup(); resolve();
|
|
371
|
+
};
|
|
372
|
+
// We map errors to rejects, but we should be careful about which errors.
|
|
373
|
+
// A socket error might happen during connection.
|
|
374
|
+
const onError = (err: any) => {
|
|
375
|
+
cleanup(); reject(err);
|
|
376
|
+
};
|
|
377
|
+
// Also listen for handshake failure
|
|
378
|
+
const onHandshakeFail = (err: any) => {
|
|
379
|
+
cleanup(); reject(new Error(`Handshake failed: ${err.reason}`));
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const cleanup = () => {
|
|
383
|
+
this.off('connected', onConnect);
|
|
384
|
+
this.off('error', onError);
|
|
385
|
+
this.off('handshakeFailed', onHandshakeFail);
|
|
386
|
+
// If we failed, clear the promise so next attempt can try again
|
|
387
|
+
if (this.connectionPromise) this.connectionPromise = undefined;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
this.once('connected', onConnect);
|
|
391
|
+
this.once('error', onError);
|
|
392
|
+
this.once('handshakeFailed', onHandshakeFail);
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
this.startClient();
|
|
396
|
+
} catch (e) {
|
|
397
|
+
onError(e);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
// Wait for connection with a short timeout for the connection itself
|
|
404
|
+
const connectTimeout = 5000;
|
|
405
|
+
await Promise.race([
|
|
406
|
+
this.connectionPromise,
|
|
407
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Lazy connection timeout')), connectTimeout))
|
|
408
|
+
]);
|
|
409
|
+
} catch (err: any) {
|
|
410
|
+
this.log.error('Lazy connection failed', err);
|
|
411
|
+
// We don't throw here immediately, we let the isConnected check fail below
|
|
412
|
+
// or throw a specific error.
|
|
413
|
+
// Actually, if connection failed, we should probably fail the request.
|
|
414
|
+
throw new Error(`Failed to establish connection to Unreal Engine: ${err.message}`);
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
throw new Error('Automation bridge disabled');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (!this.isConnected()) {
|
|
422
|
+
throw new Error('Automation bridge not connected');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Check if we need to queue (unless it's a priority request which standard ones are not)
|
|
426
|
+
// We use requestTracker directly to check limit as it's the source of truth
|
|
427
|
+
// Note: requestTracker exposes maxPendingRequests via constructor but generic check logic isn't public
|
|
428
|
+
// We assumed getPendingCount() is available
|
|
429
|
+
if (this.requestTracker.getPendingCount() >= (this as any).requestTracker.maxPendingRequests) {
|
|
430
|
+
return new Promise<T>((resolve, reject) => {
|
|
431
|
+
this.queuedRequestItems.push({
|
|
432
|
+
resolve,
|
|
433
|
+
reject,
|
|
434
|
+
action,
|
|
435
|
+
payload,
|
|
436
|
+
options
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return this.sendRequestInternal<T>(action, payload, options);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private async sendRequestInternal<T>(
|
|
445
|
+
action: string,
|
|
446
|
+
payload: Record<string, unknown>,
|
|
447
|
+
options: { timeoutMs?: number }
|
|
448
|
+
): Promise<T> {
|
|
449
|
+
const timeoutMs = options.timeoutMs ?? 60000; // Increased default timeout to 60s
|
|
450
|
+
|
|
451
|
+
// Check for coalescing
|
|
452
|
+
const coalesceKey = this.requestTracker.createCoalesceKey(action, payload);
|
|
453
|
+
if (coalesceKey) {
|
|
454
|
+
const existing = this.requestTracker.getCoalescedRequest(coalesceKey);
|
|
455
|
+
if (existing) {
|
|
456
|
+
return existing as unknown as T;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const { requestId, promise } = this.requestTracker.createRequest(action, payload, timeoutMs);
|
|
461
|
+
|
|
462
|
+
if (coalesceKey) {
|
|
463
|
+
this.requestTracker.setCoalescedRequest(coalesceKey, promise);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const message: AutomationBridgeMessage = {
|
|
467
|
+
type: 'automation_request',
|
|
468
|
+
requestId,
|
|
469
|
+
action,
|
|
470
|
+
payload
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const resultPromise = promise as unknown as Promise<T>;
|
|
474
|
+
|
|
475
|
+
// Ensure we process the queue when this request finishes
|
|
476
|
+
resultPromise.finally(() => {
|
|
477
|
+
this.processRequestQueue();
|
|
478
|
+
}).catch(() => { }); // catch to prevent unhandled rejection during finally chain? no, finally returns new promise
|
|
479
|
+
|
|
480
|
+
if (this.send(message)) {
|
|
481
|
+
return resultPromise;
|
|
482
|
+
} else {
|
|
483
|
+
this.requestTracker.rejectRequest(requestId, new Error('Failed to send request'));
|
|
484
|
+
throw new Error('Failed to send request');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private processRequestQueue() {
|
|
489
|
+
if (this.queuedRequestItems.length === 0) return;
|
|
490
|
+
|
|
491
|
+
// while we have capacity and items
|
|
492
|
+
while (
|
|
493
|
+
this.queuedRequestItems.length > 0 &&
|
|
494
|
+
this.requestTracker.getPendingCount() < (this as any).requestTracker.maxPendingRequests
|
|
495
|
+
) {
|
|
496
|
+
const item = this.queuedRequestItems.shift();
|
|
497
|
+
if (item) {
|
|
498
|
+
this.sendRequestInternal(item.action, item.payload, item.options)
|
|
499
|
+
.then(item.resolve)
|
|
500
|
+
.catch(item.reject);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
send(payload: AutomationBridgeMessage): boolean {
|
|
506
|
+
const primarySocket = this.connectionManager.getPrimarySocket();
|
|
507
|
+
if (!primarySocket || primarySocket.readyState !== WebSocket.OPEN) {
|
|
508
|
+
this.log.warn('Attempted to send automation message without an active primary connection');
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
primarySocket.send(JSON.stringify(payload));
|
|
513
|
+
return true;
|
|
514
|
+
} catch (error) {
|
|
515
|
+
this.log.error('Failed to send automation message', error);
|
|
516
|
+
const errObj = error instanceof Error ? error : new Error(String(error));
|
|
517
|
+
const primaryInfo = this.connectionManager.getActiveSockets().get(primarySocket);
|
|
518
|
+
const errorWithPort = Object.assign(errObj, { port: primaryInfo?.port });
|
|
519
|
+
this.emitAutomation('error', errorWithPort);
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private broadcast(payload: AutomationBridgeMessage): boolean {
|
|
525
|
+
const sockets = this.connectionManager.getActiveSockets();
|
|
526
|
+
if (sockets.size === 0) {
|
|
527
|
+
this.log.warn('Attempted to broadcast automation message without any active connections');
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
let sentCount = 0;
|
|
531
|
+
for (const [socket] of sockets) {
|
|
532
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
533
|
+
try {
|
|
534
|
+
socket.send(JSON.stringify(payload));
|
|
535
|
+
sentCount++;
|
|
536
|
+
} catch (error) {
|
|
537
|
+
this.log.error('Failed to broadcast automation message to socket', error);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return sentCount > 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private flushQueue(): void {
|
|
545
|
+
if (this.requestQueue.length === 0) return;
|
|
546
|
+
this.log.info(`Flushing ${this.requestQueue.length} queued automation requests`);
|
|
547
|
+
const queue = [...this.requestQueue];
|
|
548
|
+
this.requestQueue = [];
|
|
549
|
+
queue.forEach(fn => fn());
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private emitAutomation<K extends keyof AutomationBridgeEvents>(
|
|
553
|
+
event: K,
|
|
554
|
+
...args: Parameters<AutomationBridgeEvents[K]>
|
|
555
|
+
): void {
|
|
556
|
+
this.emit(event, ...args);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import { Logger } from '../utils/logger.js';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { SocketInfo } from './types.js';
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
|
+
|
|
7
|
+
export class ConnectionManager extends EventEmitter {
|
|
8
|
+
private activeSockets = new Map<WebSocket, SocketInfo>();
|
|
9
|
+
private primarySocket?: WebSocket;
|
|
10
|
+
private heartbeatTimer?: NodeJS.Timeout;
|
|
11
|
+
private lastMessageAt?: Date;
|
|
12
|
+
private log = new Logger('ConnectionManager');
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private heartbeatIntervalMs: number
|
|
16
|
+
) {
|
|
17
|
+
super();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public registerSocket(
|
|
21
|
+
socket: WebSocket,
|
|
22
|
+
port: number,
|
|
23
|
+
metadata?: Record<string, unknown>,
|
|
24
|
+
remoteAddress?: string,
|
|
25
|
+
remotePort?: number
|
|
26
|
+
): void {
|
|
27
|
+
const connectionId = randomUUID();
|
|
28
|
+
const sessionId = metadata && typeof metadata.sessionId === 'string' ? (metadata.sessionId as string) : undefined;
|
|
29
|
+
const socketInfo: SocketInfo = {
|
|
30
|
+
connectionId,
|
|
31
|
+
port,
|
|
32
|
+
connectedAt: new Date(),
|
|
33
|
+
protocol: socket.protocol || undefined,
|
|
34
|
+
sessionId,
|
|
35
|
+
remoteAddress: remoteAddress ?? undefined,
|
|
36
|
+
remotePort: typeof remotePort === 'number' ? remotePort : undefined
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
this.activeSockets.set(socket, socketInfo);
|
|
40
|
+
|
|
41
|
+
// Set as primary socket if this is the first connection
|
|
42
|
+
if (!this.primarySocket) {
|
|
43
|
+
this.primarySocket = socket;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle WebSocket pong frames for heartbeat tracking
|
|
47
|
+
socket.on('pong', () => {
|
|
48
|
+
this.lastMessageAt = new Date();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public removeSocket(socket: WebSocket): SocketInfo | undefined {
|
|
53
|
+
const info = this.activeSockets.get(socket);
|
|
54
|
+
if (info) {
|
|
55
|
+
this.activeSockets.delete(socket);
|
|
56
|
+
if (socket === this.primarySocket) {
|
|
57
|
+
this.primarySocket = this.activeSockets.size > 0 ? this.activeSockets.keys().next().value : undefined;
|
|
58
|
+
if (this.activeSockets.size === 0) {
|
|
59
|
+
this.stopHeartbeat();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return info;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public getActiveSockets(): Map<WebSocket, SocketInfo> {
|
|
67
|
+
return this.activeSockets;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public getPrimarySocket(): WebSocket | undefined {
|
|
71
|
+
return this.primarySocket;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public isConnected(): boolean {
|
|
75
|
+
return this.activeSockets.size > 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public startHeartbeat(): void {
|
|
79
|
+
if (this.heartbeatIntervalMs <= 0) return;
|
|
80
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
81
|
+
|
|
82
|
+
this.heartbeatTimer = setInterval(() => {
|
|
83
|
+
if (this.activeSockets.size === 0) {
|
|
84
|
+
this.stopHeartbeat();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const pingPayload = JSON.stringify({
|
|
89
|
+
type: 'bridge_ping',
|
|
90
|
+
timestamp: new Date().toISOString()
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
for (const [socket] of this.activeSockets) {
|
|
94
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
95
|
+
try {
|
|
96
|
+
socket.ping();
|
|
97
|
+
socket.send(pingPayload);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
this.log.error('Failed to send heartbeat', error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}, this.heartbeatIntervalMs);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public stopHeartbeat(): void {
|
|
107
|
+
if (this.heartbeatTimer) {
|
|
108
|
+
clearInterval(this.heartbeatTimer);
|
|
109
|
+
this.heartbeatTimer = undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public updateLastMessageTime(): void {
|
|
114
|
+
this.lastMessageAt = new Date();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public getLastMessageTime(): Date | undefined {
|
|
118
|
+
return this.lastMessageAt;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public closeAll(code?: number, reason?: string): void {
|
|
122
|
+
this.stopHeartbeat();
|
|
123
|
+
for (const [socket] of this.activeSockets) {
|
|
124
|
+
socket.removeAllListeners();
|
|
125
|
+
socket.close(code, reason);
|
|
126
|
+
}
|
|
127
|
+
this.activeSockets.clear();
|
|
128
|
+
this.primarySocket = undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|