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,1330 @@
|
|
|
1
|
+
#include "McpBridgeWebSocket.h"
|
|
2
|
+
#include "McpAutomationBridgeSubsystem.h"
|
|
3
|
+
|
|
4
|
+
#include "Async/Async.h"
|
|
5
|
+
#include "Containers/StringConv.h"
|
|
6
|
+
#include "HAL/Event.h"
|
|
7
|
+
#include "HAL/PlatformAtomics.h"
|
|
8
|
+
#include "HAL/PlatformProcess.h"
|
|
9
|
+
#include "HAL/RunnableThread.h"
|
|
10
|
+
#include "IPAddress.h"
|
|
11
|
+
#include "Logging/LogMacros.h"
|
|
12
|
+
#include "Math/UnrealMathUtility.h"
|
|
13
|
+
#include "Misc/Base64.h"
|
|
14
|
+
#include "Misc/ScopeLock.h"
|
|
15
|
+
#include "Misc/SecureHash.h"
|
|
16
|
+
#include "Misc/StringBuilder.h"
|
|
17
|
+
#include "Misc/Timespan.h"
|
|
18
|
+
#include "SocketSubsystem.h"
|
|
19
|
+
#include "Sockets.h"
|
|
20
|
+
#include "String/LexFromString.h"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
namespace {
|
|
24
|
+
constexpr const TCHAR *WebSocketGuid =
|
|
25
|
+
TEXT("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
|
26
|
+
constexpr uint8 OpCodeContinuation = 0x0;
|
|
27
|
+
constexpr uint8 OpCodeText = 0x1;
|
|
28
|
+
constexpr uint8 OpCodeBinary = 0x2;
|
|
29
|
+
constexpr uint8 OpCodeClose = 0x8;
|
|
30
|
+
constexpr uint8 OpCodePing = 0x9;
|
|
31
|
+
constexpr uint8 OpCodePong = 0xA;
|
|
32
|
+
|
|
33
|
+
struct FParsedWebSocketUrl {
|
|
34
|
+
FString Host;
|
|
35
|
+
int32 Port = 80;
|
|
36
|
+
FString PathWithQuery;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
bool ParseWebSocketUrl(const FString &InUrl, FParsedWebSocketUrl &OutParsed,
|
|
40
|
+
FString &OutError) {
|
|
41
|
+
const FString Trimmed = InUrl.TrimStartAndEnd();
|
|
42
|
+
if (Trimmed.IsEmpty()) {
|
|
43
|
+
OutError = TEXT("WebSocket URL is empty.");
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static const FString SchemePrefix(TEXT("ws://"));
|
|
48
|
+
if (!Trimmed.StartsWith(SchemePrefix, ESearchCase::IgnoreCase)) {
|
|
49
|
+
OutError = TEXT("Only ws:// scheme is supported.");
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const FString Remainder = Trimmed.Mid(SchemePrefix.Len());
|
|
54
|
+
FString HostPort;
|
|
55
|
+
FString PathRemainder;
|
|
56
|
+
if (!Remainder.Split(TEXT("/"), &HostPort, &PathRemainder,
|
|
57
|
+
ESearchCase::CaseSensitive, ESearchDir::FromStart)) {
|
|
58
|
+
HostPort = Remainder;
|
|
59
|
+
PathRemainder.Reset();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
HostPort = HostPort.TrimStartAndEnd();
|
|
63
|
+
if (HostPort.IsEmpty()) {
|
|
64
|
+
OutError = TEXT("WebSocket URL missing host.");
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
FString Host;
|
|
69
|
+
int32 Port = 80;
|
|
70
|
+
|
|
71
|
+
if (HostPort.StartsWith(TEXT("["))) {
|
|
72
|
+
int32 ClosingBracketIndex = INDEX_NONE;
|
|
73
|
+
if (!HostPort.FindChar(TEXT(']'), ClosingBracketIndex)) {
|
|
74
|
+
OutError = TEXT("Invalid IPv6 WebSocket host.");
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Host = HostPort.Mid(1, ClosingBracketIndex - 1);
|
|
79
|
+
const int32 PortSeparatorIndex = HostPort.Find(
|
|
80
|
+
TEXT(":"), ESearchCase::CaseSensitive, ESearchDir::FromEnd);
|
|
81
|
+
if (PortSeparatorIndex > ClosingBracketIndex) {
|
|
82
|
+
const FString PortString = HostPort.Mid(PortSeparatorIndex + 1);
|
|
83
|
+
if (!PortString.IsEmpty() && !LexTryParseString(Port, *PortString)) {
|
|
84
|
+
OutError = TEXT("Invalid WebSocket port.");
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
FString PortString;
|
|
90
|
+
if (HostPort.Split(TEXT(":"), &Host, &PortString,
|
|
91
|
+
ESearchCase::CaseSensitive, ESearchDir::FromEnd)) {
|
|
92
|
+
Host = Host.TrimStartAndEnd();
|
|
93
|
+
PortString = PortString.TrimStartAndEnd();
|
|
94
|
+
|
|
95
|
+
if (!PortString.IsEmpty()) {
|
|
96
|
+
if (!LexTryParseString(Port, *PortString)) {
|
|
97
|
+
OutError = TEXT("Invalid WebSocket port.");
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
Host = HostPort;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Host = Host.TrimStartAndEnd();
|
|
107
|
+
if (Host.IsEmpty()) {
|
|
108
|
+
OutError = TEXT("WebSocket URL missing host.");
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (Port <= 0) {
|
|
113
|
+
OutError = TEXT("WebSocket port must be positive.");
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
FString PathWithQuery;
|
|
118
|
+
if (PathRemainder.IsEmpty()) {
|
|
119
|
+
PathWithQuery = TEXT("/");
|
|
120
|
+
} else {
|
|
121
|
+
PathWithQuery = TEXT("/") + PathRemainder;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
OutParsed.Host = Host;
|
|
125
|
+
OutParsed.Port = Port;
|
|
126
|
+
OutParsed.PathWithQuery = PathWithQuery;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
uint16 ToNetwork16(uint16 Value) {
|
|
131
|
+
#if PLATFORM_LITTLE_ENDIAN
|
|
132
|
+
return static_cast<uint16>(((Value & 0x00FF) << 8) | ((Value & 0xFF00) >> 8));
|
|
133
|
+
#else
|
|
134
|
+
return Value;
|
|
135
|
+
#endif
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
uint16 FromNetwork16(uint16 Value) { return ToNetwork16(Value); }
|
|
139
|
+
|
|
140
|
+
uint64 ToNetwork64(uint64 Value) {
|
|
141
|
+
#if PLATFORM_LITTLE_ENDIAN
|
|
142
|
+
return ((Value & 0x00000000000000FFULL) << 56) |
|
|
143
|
+
((Value & 0x000000000000FF00ULL) << 40) |
|
|
144
|
+
((Value & 0x0000000000FF0000ULL) << 24) |
|
|
145
|
+
((Value & 0x00000000FF000000ULL) << 8) |
|
|
146
|
+
((Value & 0x000000FF00000000ULL) >> 8) |
|
|
147
|
+
((Value & 0x0000FF0000000000ULL) >> 24) |
|
|
148
|
+
((Value & 0x00FF000000000000ULL) >> 40) |
|
|
149
|
+
((Value & 0xFF00000000000000ULL) >> 56);
|
|
150
|
+
#else
|
|
151
|
+
return Value;
|
|
152
|
+
#endif
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
uint64 FromNetwork64(uint64 Value) { return ToNetwork64(Value); }
|
|
156
|
+
|
|
157
|
+
FString BytesToStringView(const TArray<uint8> &Data) {
|
|
158
|
+
if (Data.Num() == 0) {
|
|
159
|
+
return FString();
|
|
160
|
+
}
|
|
161
|
+
// Convert UTF-8 bytes to TCHAR using an explicit length-aware converter
|
|
162
|
+
// to avoid reading beyond the provided buffer. The previous implementation
|
|
163
|
+
// used a null-terminated style conversion which could read past the
|
|
164
|
+
// payload and include stray bytes from subsequent socket reads, causing
|
|
165
|
+
// JSON parse failures on the receiving side.
|
|
166
|
+
const ANSICHAR *Utf8Ptr = reinterpret_cast<const ANSICHAR *>(Data.GetData());
|
|
167
|
+
FUTF8ToTCHAR Converter(Utf8Ptr, Data.Num());
|
|
168
|
+
if (Converter.Length() <= 0) {
|
|
169
|
+
return FString();
|
|
170
|
+
}
|
|
171
|
+
return FString(Converter.Length(), Converter.Get());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
void DispatchOnGameThread(TFunction<void()> &&Fn) {
|
|
175
|
+
if (IsInGameThread()) {
|
|
176
|
+
Fn();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
AsyncTask(ENamedThreads::GameThread, MoveTemp(Fn));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
FString DescribeSocketError(ISocketSubsystem *SocketSubsystem,
|
|
184
|
+
const TCHAR *Context) {
|
|
185
|
+
if (!SocketSubsystem) {
|
|
186
|
+
return FString::Printf(TEXT("%s (no socket subsystem)"), Context);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const ESocketErrors LastErrorCode = SocketSubsystem->GetLastErrorCode();
|
|
190
|
+
const FString Description = SocketSubsystem->GetSocketError(LastErrorCode);
|
|
191
|
+
return FString::Printf(TEXT("%s (error=%d, %s)"), Context,
|
|
192
|
+
static_cast<int32>(LastErrorCode), *Description);
|
|
193
|
+
}
|
|
194
|
+
} // namespace
|
|
195
|
+
|
|
196
|
+
FMcpBridgeWebSocket::FMcpBridgeWebSocket(
|
|
197
|
+
const FString &InUrl, const FString &InProtocols,
|
|
198
|
+
const TMap<FString, FString> &InHeaders)
|
|
199
|
+
: Url(InUrl), Socket(nullptr), Port(0), Protocols(InProtocols),
|
|
200
|
+
Headers(InHeaders), ListenHost(), PendingReceived(),
|
|
201
|
+
FragmentAccumulator(), bFragmentMessageActive(false), SelfWeakPtr(),
|
|
202
|
+
bServerMode(false), bServerAcceptedConnection(false),
|
|
203
|
+
ListenSocket(nullptr), Thread(nullptr), StopEvent(nullptr),
|
|
204
|
+
ClientSockets(), ListenBacklog(10), AcceptSleepSeconds(0.01f),
|
|
205
|
+
bConnected(false), bListening(false), bStopping(false) {
|
|
206
|
+
HandlerReadyEvent = nullptr;
|
|
207
|
+
bHandlerRegistered = false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
FMcpBridgeWebSocket::FMcpBridgeWebSocket(int32 InPort, const FString &InHost,
|
|
211
|
+
int32 InListenBacklog,
|
|
212
|
+
float InAcceptSleepSeconds)
|
|
213
|
+
: Url(), Socket(nullptr), Port(InPort), Protocols(TEXT("mcp-automation")),
|
|
214
|
+
Headers(), ListenHost(InHost), PendingReceived(), FragmentAccumulator(),
|
|
215
|
+
bFragmentMessageActive(false), SelfWeakPtr(), bServerMode(true),
|
|
216
|
+
bServerAcceptedConnection(false), ListenSocket(nullptr), Thread(nullptr),
|
|
217
|
+
StopEvent(nullptr), ClientSockets(), ListenBacklog(InListenBacklog),
|
|
218
|
+
AcceptSleepSeconds(InAcceptSleepSeconds), bConnected(false),
|
|
219
|
+
bListening(false), bStopping(false) {
|
|
220
|
+
HandlerReadyEvent = nullptr;
|
|
221
|
+
bHandlerRegistered = false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
FMcpBridgeWebSocket::FMcpBridgeWebSocket(FSocket *InClientSocket)
|
|
225
|
+
: Url(), Socket(InClientSocket), Port(0), Protocols(TEXT("mcp-automation")),
|
|
226
|
+
Headers(), ListenHost(), PendingReceived(), FragmentAccumulator(),
|
|
227
|
+
bFragmentMessageActive(false), SelfWeakPtr(), bServerMode(false),
|
|
228
|
+
bServerAcceptedConnection(true), ListenSocket(nullptr), Thread(nullptr),
|
|
229
|
+
StopEvent(nullptr), ClientSockets(), ListenBacklog(10),
|
|
230
|
+
AcceptSleepSeconds(0.01f), bConnected(true), bListening(false),
|
|
231
|
+
bStopping(false) {
|
|
232
|
+
HandlerReadyEvent = nullptr;
|
|
233
|
+
bHandlerRegistered = false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
FMcpBridgeWebSocket::~FMcpBridgeWebSocket() {
|
|
237
|
+
Close();
|
|
238
|
+
if (HandlerReadyEvent) {
|
|
239
|
+
FPlatformProcess::ReturnSynchEventToPool(HandlerReadyEvent);
|
|
240
|
+
HandlerReadyEvent = nullptr;
|
|
241
|
+
}
|
|
242
|
+
if (Thread) {
|
|
243
|
+
Thread->WaitForCompletion();
|
|
244
|
+
delete Thread;
|
|
245
|
+
Thread = nullptr;
|
|
246
|
+
}
|
|
247
|
+
if (StopEvent) {
|
|
248
|
+
FPlatformProcess::ReturnSynchEventToPool(StopEvent);
|
|
249
|
+
StopEvent = nullptr;
|
|
250
|
+
}
|
|
251
|
+
if (FSocket *LocalSocket = DetachSocket()) {
|
|
252
|
+
LocalSocket->Close();
|
|
253
|
+
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(LocalSocket);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
FSocket *FMcpBridgeWebSocket::DetachSocket() {
|
|
258
|
+
return static_cast<FSocket *>(FPlatformAtomics::InterlockedExchangePtr(
|
|
259
|
+
reinterpret_cast<void **>(&Socket), nullptr));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
void FMcpBridgeWebSocket::NotifyMessageHandlerRegistered() {
|
|
263
|
+
bHandlerRegistered = true;
|
|
264
|
+
if (HandlerReadyEvent) {
|
|
265
|
+
HandlerReadyEvent->Trigger();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
void FMcpBridgeWebSocket::InitializeWeakSelf(
|
|
270
|
+
const TSharedPtr<FMcpBridgeWebSocket> &InShared) {
|
|
271
|
+
SelfWeakPtr = InShared;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
void FMcpBridgeWebSocket::Connect() {
|
|
275
|
+
if (Thread) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
bStopping = false;
|
|
280
|
+
StopEvent = FPlatformProcess::GetSynchEventFromPool(true);
|
|
281
|
+
Thread = FRunnableThread::Create(this, TEXT("FMcpBridgeWebSocketWorker"), 0,
|
|
282
|
+
TPri_Normal);
|
|
283
|
+
if (!Thread) {
|
|
284
|
+
DispatchOnGameThread([WeakThis = SelfWeakPtr] {
|
|
285
|
+
if (TSharedPtr<FMcpBridgeWebSocket> Pinned = WeakThis.Pin()) {
|
|
286
|
+
Pinned->ConnectionErrorDelegate.Broadcast(
|
|
287
|
+
TEXT("Failed to create WebSocket worker thread."));
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
void FMcpBridgeWebSocket::Listen() {
|
|
294
|
+
if (Thread || !bServerMode) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
bStopping = false;
|
|
299
|
+
StopEvent = FPlatformProcess::GetSynchEventFromPool(true);
|
|
300
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Log,
|
|
301
|
+
TEXT("Spawning MCP automation server thread for %s:%d"), *ListenHost,
|
|
302
|
+
Port);
|
|
303
|
+
Thread = FRunnableThread::Create(
|
|
304
|
+
this, TEXT("FMcpBridgeWebSocketServerWorker"), 0, TPri_Normal);
|
|
305
|
+
if (!Thread) {
|
|
306
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Error,
|
|
307
|
+
TEXT("Failed to create server thread for MCP automation bridge."));
|
|
308
|
+
DispatchOnGameThread([WeakThis = SelfWeakPtr] {
|
|
309
|
+
if (TSharedPtr<FMcpBridgeWebSocket> Pinned = WeakThis.Pin()) {
|
|
310
|
+
Pinned->ConnectionErrorDelegate.Broadcast(
|
|
311
|
+
TEXT("Failed to create WebSocket server worker thread."));
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
void FMcpBridgeWebSocket::Close(int32 StatusCode, const FString &Reason) {
|
|
318
|
+
bStopping = true;
|
|
319
|
+
if (StopEvent) {
|
|
320
|
+
StopEvent->Trigger();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (FSocket *LocalSocket = DetachSocket()) {
|
|
324
|
+
LocalSocket->Close();
|
|
325
|
+
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(LocalSocket);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
bool FMcpBridgeWebSocket::Send(const FString &Data) {
|
|
330
|
+
FTCHARToUTF8 Converter(*Data);
|
|
331
|
+
return Send(Converter.Get(), Converter.Length());
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
bool FMcpBridgeWebSocket::Send(const void *Data, SIZE_T Length) {
|
|
335
|
+
if (!IsConnected() || !Socket) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return SendTextFrame(Data, Length);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
bool FMcpBridgeWebSocket::IsConnected() const { return bConnected; }
|
|
343
|
+
|
|
344
|
+
bool FMcpBridgeWebSocket::IsListening() const { return bListening; }
|
|
345
|
+
|
|
346
|
+
void FMcpBridgeWebSocket::SendHeartbeatPing() {
|
|
347
|
+
SendControlFrame(OpCodePing, TArray<uint8>());
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
bool FMcpBridgeWebSocket::Init() { return true; }
|
|
351
|
+
|
|
352
|
+
uint32 FMcpBridgeWebSocket::Run() {
|
|
353
|
+
if (bServerMode) {
|
|
354
|
+
return RunServer();
|
|
355
|
+
} else {
|
|
356
|
+
return RunClient();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
uint32 FMcpBridgeWebSocket::RunClient() {
|
|
361
|
+
if (bServerAcceptedConnection) {
|
|
362
|
+
if (!PerformServerHandshake()) {
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
if (!PerformHandshake()) {
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
bConnected = true;
|
|
372
|
+
UE_LOG(
|
|
373
|
+
LogMcpAutomationBridgeSubsystem, Log,
|
|
374
|
+
TEXT("FMcpBridgeWebSocket connection established (serverAccepted=%s)."),
|
|
375
|
+
bServerAcceptedConnection ? TEXT("true") : TEXT("false"));
|
|
376
|
+
DispatchOnGameThread([WeakThis = SelfWeakPtr] {
|
|
377
|
+
if (TSharedPtr<FMcpBridgeWebSocket> Pinned = WeakThis.Pin()) {
|
|
378
|
+
Pinned->ConnectedDelegate.Broadcast(Pinned);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// If this connection was accepted by the server thread (i.e. a remote
|
|
383
|
+
// client connected to the plugin), wait a short time for the game
|
|
384
|
+
// thread to attach message handlers. The client is likely to send the
|
|
385
|
+
// application-level 'bridge_hello' immediately after the upgrade; if
|
|
386
|
+
// the game thread hasn't attached its OnMessage handler yet we risk
|
|
387
|
+
// losing that first frame. Wait up to a moderate timeout for the
|
|
388
|
+
// handler registration signal.
|
|
389
|
+
if (bServerAcceptedConnection) {
|
|
390
|
+
// Lazily create the event used to wait for the handler if it
|
|
391
|
+
// hasn't been allocated yet. Use the event pool to avoid
|
|
392
|
+
// continuously allocating objects.
|
|
393
|
+
if (!HandlerReadyEvent) {
|
|
394
|
+
HandlerReadyEvent = FPlatformProcess::GetSynchEventFromPool(true);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
constexpr double MaxWaitSeconds = 0.5; // 500 ms
|
|
398
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
|
|
399
|
+
TEXT("Awaiting message handler registration for new client "
|
|
400
|
+
"connection (max %.0f ms)."),
|
|
401
|
+
MaxWaitSeconds * 1000.0);
|
|
402
|
+
if (HandlerReadyEvent->Wait(FTimespan::FromSeconds(MaxWaitSeconds))) {
|
|
403
|
+
// Event triggered by game thread
|
|
404
|
+
}
|
|
405
|
+
if (!bHandlerRegistered) {
|
|
406
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
|
|
407
|
+
TEXT("Message handler registration not observed in time; "
|
|
408
|
+
"proceeding without explicit synchronization."));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
while (!bStopping) {
|
|
413
|
+
if (!ReceiveFrame()) {
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
TearDown(TEXT("Socket loop finished."), true, 1000);
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
uint32 FMcpBridgeWebSocket::RunServer() {
|
|
423
|
+
ISocketSubsystem *SocketSubsystem =
|
|
424
|
+
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
|
|
425
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Log,
|
|
426
|
+
TEXT("FMcpBridgeWebSocket::RunServer begin (host=%s, port=%d)"),
|
|
427
|
+
*ListenHost, Port);
|
|
428
|
+
ListenSocket = SocketSubsystem->CreateSocket(
|
|
429
|
+
NAME_Stream, TEXT("McpAutomationBridgeListenSocket"), false);
|
|
430
|
+
if (!ListenSocket) {
|
|
431
|
+
const FString ErrorMessage = DescribeSocketError(
|
|
432
|
+
SocketSubsystem, TEXT("Failed to create listen socket"));
|
|
433
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Error, TEXT("%s"), *ErrorMessage);
|
|
434
|
+
DispatchOnGameThread([WeakThis = SelfWeakPtr, ErrorMessage] {
|
|
435
|
+
if (TSharedPtr<FMcpBridgeWebSocket> Pinned = WeakThis.Pin()) {
|
|
436
|
+
Pinned->ConnectionErrorDelegate.Broadcast(ErrorMessage);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
return 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
ListenSocket->SetReuseAddr(true);
|
|
443
|
+
ListenSocket->SetNonBlocking(false);
|
|
444
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Log, TEXT("Listen socket created."));
|
|
445
|
+
|
|
446
|
+
TSharedRef<FInternetAddr> ListenAddr = SocketSubsystem->CreateInternetAddr();
|
|
447
|
+
|
|
448
|
+
bool bResolvedHost = false;
|
|
449
|
+
if (!ListenHost.IsEmpty()) {
|
|
450
|
+
FString HostToBind = ListenHost;
|
|
451
|
+
if (HostToBind.Equals(TEXT("localhost"), ESearchCase::IgnoreCase)) {
|
|
452
|
+
HostToBind = TEXT("127.0.0.1");
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
bool bIsValidIp = false;
|
|
456
|
+
ListenAddr->SetIp(*HostToBind, bIsValidIp);
|
|
457
|
+
if (bIsValidIp) {
|
|
458
|
+
bResolvedHost = true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!bResolvedHost) {
|
|
463
|
+
ListenAddr->SetAnyAddress();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
ListenAddr->SetPort(Port);
|
|
467
|
+
|
|
468
|
+
if (!ListenSocket->Bind(*ListenAddr)) {
|
|
469
|
+
const FString ErrorMessage = DescribeSocketError(
|
|
470
|
+
SocketSubsystem, TEXT("Failed to bind listen socket"));
|
|
471
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Error, TEXT("%s"), *ErrorMessage);
|
|
472
|
+
DispatchOnGameThread([WeakThis = SelfWeakPtr, ErrorMessage] {
|
|
473
|
+
if (TSharedPtr<FMcpBridgeWebSocket> Pinned = WeakThis.Pin()) {
|
|
474
|
+
Pinned->ConnectionErrorDelegate.Broadcast(ErrorMessage);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
return 0;
|
|
478
|
+
}
|
|
479
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Log,
|
|
480
|
+
TEXT("Listen socket bound to %s."), *ListenAddr->ToString(false));
|
|
481
|
+
|
|
482
|
+
if (!ListenSocket->Listen(ListenBacklog > 0 ? ListenBacklog : 10)) {
|
|
483
|
+
const FString ErrorMessage = DescribeSocketError(
|
|
484
|
+
SocketSubsystem, TEXT("Failed to listen on socket"));
|
|
485
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Error, TEXT("%s"), *ErrorMessage);
|
|
486
|
+
DispatchOnGameThread([WeakThis = SelfWeakPtr, ErrorMessage] {
|
|
487
|
+
if (TSharedPtr<FMcpBridgeWebSocket> Pinned = WeakThis.Pin()) {
|
|
488
|
+
Pinned->ConnectionErrorDelegate.Broadcast(ErrorMessage);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
return 0;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
bListening = true;
|
|
495
|
+
const FString BoundAddress = ListenAddr->ToString(false);
|
|
496
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Log,
|
|
497
|
+
TEXT("MCP Automation Bridge listening on %s"), *BoundAddress);
|
|
498
|
+
DispatchOnGameThread([WeakThis = SelfWeakPtr, BoundAddress] {
|
|
499
|
+
if (TSharedPtr<FMcpBridgeWebSocket> Pinned = WeakThis.Pin()) {
|
|
500
|
+
Pinned->ConnectedDelegate.Broadcast(Pinned); // Server ready event
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
while (!bStopping) {
|
|
505
|
+
// Accept incoming connections
|
|
506
|
+
FSocket *ClientSocket =
|
|
507
|
+
ListenSocket->Accept(TEXT("McpAutomationBridgeClient"));
|
|
508
|
+
if (ClientSocket) {
|
|
509
|
+
// Create a new WebSocket instance for this client connection
|
|
510
|
+
auto ClientWebSocket = MakeShared<FMcpBridgeWebSocket>(ClientSocket);
|
|
511
|
+
ClientWebSocket->InitializeWeakSelf(ClientWebSocket);
|
|
512
|
+
ClientWebSocket->bServerMode =
|
|
513
|
+
false; // Client connections are not in server mode
|
|
514
|
+
ClientWebSocket->bServerAcceptedConnection =
|
|
515
|
+
true; // This is a server-accepted connection
|
|
516
|
+
// Annotate the accepted client socket with the server listening port
|
|
517
|
+
// so diagnostic logs and handshake acknowledgements report a
|
|
518
|
+
// meaningful activePort instead of 0.
|
|
519
|
+
ClientWebSocket->Port = Port;
|
|
520
|
+
|
|
521
|
+
{
|
|
522
|
+
FScopeLock Lock(&ClientSocketsMutex);
|
|
523
|
+
ClientSockets.Add(ClientWebSocket);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
TWeakPtr<FMcpBridgeWebSocket> LocalWeakThis = SelfWeakPtr;
|
|
527
|
+
auto RemoveFromClientList = [LocalWeakThis, ClientWebSocket] {
|
|
528
|
+
if (TSharedPtr<FMcpBridgeWebSocket> Pinned = LocalWeakThis.Pin()) {
|
|
529
|
+
FScopeLock Lock(&Pinned->ClientSocketsMutex);
|
|
530
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, VeryVerbose,
|
|
531
|
+
TEXT("Removing client socket from server tracking (remaining "
|
|
532
|
+
"before remove: %d)."),
|
|
533
|
+
Pinned->ClientSockets.Num());
|
|
534
|
+
Pinned->ClientSockets.Remove(ClientWebSocket);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
ClientWebSocket->OnConnected().AddLambda(
|
|
539
|
+
[LocalWeakThis, ClientWebSocket](TSharedPtr<FMcpBridgeWebSocket>) {
|
|
540
|
+
if (TSharedPtr<FMcpBridgeWebSocket> Pinned = LocalWeakThis.Pin()) {
|
|
541
|
+
DispatchOnGameThread(
|
|
542
|
+
[ParentWeak = LocalWeakThis, ClientSocket = ClientWebSocket] {
|
|
543
|
+
if (TSharedPtr<FMcpBridgeWebSocket> DispatchPinned =
|
|
544
|
+
ParentWeak.Pin()) {
|
|
545
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Log,
|
|
546
|
+
TEXT("Broadcasting client connected delegate."));
|
|
547
|
+
DispatchPinned->ClientConnectedDelegate.Broadcast(
|
|
548
|
+
ClientSocket);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
ClientWebSocket->OnClosed().AddLambda(
|
|
555
|
+
[RemoveFromClientList](TSharedPtr<FMcpBridgeWebSocket>, int32,
|
|
556
|
+
const FString &,
|
|
557
|
+
bool) { RemoveFromClientList(); });
|
|
558
|
+
|
|
559
|
+
ClientWebSocket->OnConnectionError().AddLambda(
|
|
560
|
+
[RemoveFromClientList](const FString &) { RemoveFromClientList(); });
|
|
561
|
+
|
|
562
|
+
// Start the client WebSocket thread to handle the handshake and
|
|
563
|
+
// communication
|
|
564
|
+
ClientWebSocket->Connect();
|
|
565
|
+
} else {
|
|
566
|
+
// Sleep briefly to avoid busy waiting
|
|
567
|
+
FPlatformProcess::Sleep(AcceptSleepSeconds > 0.0f ? AcceptSleepSeconds
|
|
568
|
+
: 0.01f);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (ListenSocket) {
|
|
573
|
+
ListenSocket->Close();
|
|
574
|
+
SocketSubsystem->DestroySocket(ListenSocket);
|
|
575
|
+
ListenSocket = nullptr;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return 0;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
void FMcpBridgeWebSocket::Stop() {
|
|
582
|
+
bStopping = true;
|
|
583
|
+
if (StopEvent) {
|
|
584
|
+
StopEvent->Trigger();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
void FMcpBridgeWebSocket::TearDown(const FString &Reason, bool bWasClean,
|
|
589
|
+
int32 StatusCode) {
|
|
590
|
+
if (FSocket *LocalSocket = DetachSocket()) {
|
|
591
|
+
LocalSocket->Close();
|
|
592
|
+
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(LocalSocket);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const bool bWasConnected = bConnected;
|
|
596
|
+
bConnected = false;
|
|
597
|
+
ResetFragmentState();
|
|
598
|
+
|
|
599
|
+
DispatchOnGameThread([WeakThis = SelfWeakPtr, Reason, bWasClean, StatusCode,
|
|
600
|
+
bWasConnected] {
|
|
601
|
+
if (TSharedPtr<FMcpBridgeWebSocket> Pinned = WeakThis.Pin()) {
|
|
602
|
+
if (!bWasConnected) {
|
|
603
|
+
Pinned->ConnectionErrorDelegate.Broadcast(Reason);
|
|
604
|
+
}
|
|
605
|
+
Pinned->ClosedDelegate.Broadcast(Pinned, StatusCode, Reason, bWasClean);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
bool FMcpBridgeWebSocket::PerformHandshake() {
|
|
611
|
+
FParsedWebSocketUrl ParsedUrl;
|
|
612
|
+
FString ParseError;
|
|
613
|
+
if (!ParseWebSocketUrl(Url, ParsedUrl, ParseError)) {
|
|
614
|
+
TearDown(ParseError, false, 4000);
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
HostHeader = ParsedUrl.Host;
|
|
619
|
+
Port = ParsedUrl.Port;
|
|
620
|
+
HandshakePath = ParsedUrl.PathWithQuery;
|
|
621
|
+
|
|
622
|
+
TSharedPtr<FInternetAddr> Endpoint;
|
|
623
|
+
if (!ResolveEndpoint(Endpoint) || !Endpoint.IsValid()) {
|
|
624
|
+
TearDown(TEXT("Unable to resolve WebSocket host."), false, 4000);
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
ISocketSubsystem *SocketSubsystem =
|
|
629
|
+
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
|
|
630
|
+
Socket = SocketSubsystem->CreateSocket(
|
|
631
|
+
NAME_Stream, TEXT("McpAutomationBridgeSocket"), false);
|
|
632
|
+
if (!Socket) {
|
|
633
|
+
TearDown(TEXT("Failed to create socket."), false, 4000);
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
Socket->SetReuseAddr(true);
|
|
637
|
+
Socket->SetNonBlocking(false);
|
|
638
|
+
Socket->SetNoDelay(true);
|
|
639
|
+
|
|
640
|
+
Endpoint->SetPort(Port);
|
|
641
|
+
if (!Socket->Connect(*Endpoint)) {
|
|
642
|
+
TearDown(TEXT("Unable to connect to WebSocket endpoint."), false, 4000);
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
TArray<uint8> KeyBytes;
|
|
647
|
+
KeyBytes.SetNumUninitialized(16);
|
|
648
|
+
for (uint8 &Byte : KeyBytes) {
|
|
649
|
+
Byte = static_cast<uint8>(FMath::RandRange(0, 255));
|
|
650
|
+
}
|
|
651
|
+
HandshakeKey = FBase64::Encode(KeyBytes.GetData(), KeyBytes.Num());
|
|
652
|
+
|
|
653
|
+
FString HostLine = HostHeader;
|
|
654
|
+
const bool bIsIpv6Host = HostLine.Contains(TEXT(":"));
|
|
655
|
+
if (bIsIpv6Host && !HostLine.StartsWith(TEXT("["))) {
|
|
656
|
+
HostLine = FString::Printf(TEXT("[%s]"), *HostLine);
|
|
657
|
+
}
|
|
658
|
+
if (!(Port == 80 || Port == 0)) {
|
|
659
|
+
HostLine += FString::Printf(TEXT(":%d"), Port);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
TStringBuilder<512> RequestBuilder;
|
|
663
|
+
RequestBuilder << TEXT("GET ") << HandshakePath << TEXT(" HTTP/1.1\r\n");
|
|
664
|
+
RequestBuilder << TEXT("Host: ") << HostLine << TEXT("\r\n");
|
|
665
|
+
RequestBuilder << TEXT("Upgrade: websocket\r\n");
|
|
666
|
+
RequestBuilder << TEXT("Connection: Upgrade\r\n");
|
|
667
|
+
RequestBuilder << TEXT("Sec-WebSocket-Version: 13\r\n");
|
|
668
|
+
RequestBuilder << TEXT("Sec-WebSocket-Key: ") << HandshakeKey << TEXT("\r\n");
|
|
669
|
+
|
|
670
|
+
if (!Protocols.IsEmpty()) {
|
|
671
|
+
RequestBuilder << TEXT("Sec-WebSocket-Protocol: ") << Protocols
|
|
672
|
+
<< TEXT("\r\n");
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
for (const TPair<FString, FString> &HeaderPair : Headers) {
|
|
676
|
+
RequestBuilder << HeaderPair.Key << TEXT(": ") << HeaderPair.Value
|
|
677
|
+
<< TEXT("\r\n");
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
RequestBuilder << TEXT("\r\n");
|
|
681
|
+
|
|
682
|
+
FTCHARToUTF8 HandshakeUtf8(RequestBuilder.ToString());
|
|
683
|
+
int32 BytesSent = 0;
|
|
684
|
+
if (!Socket->Send(reinterpret_cast<const uint8 *>(HandshakeUtf8.Get()),
|
|
685
|
+
HandshakeUtf8.Length(), BytesSent) ||
|
|
686
|
+
BytesSent != HandshakeUtf8.Length()) {
|
|
687
|
+
TearDown(TEXT("Failed to send WebSocket handshake."), false, 4000);
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
TArray<uint8> ResponseBuffer;
|
|
692
|
+
ResponseBuffer.Reserve(512);
|
|
693
|
+
constexpr int32 TempSize = 256;
|
|
694
|
+
uint8 Temp[TempSize];
|
|
695
|
+
bool bHandshakeComplete = false;
|
|
696
|
+
while (!bHandshakeComplete) {
|
|
697
|
+
if (bStopping) {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
int32 BytesRead = 0;
|
|
701
|
+
if (!Socket->Recv(Temp, TempSize, BytesRead)) {
|
|
702
|
+
TearDown(TEXT("WebSocket handshake failed while reading response."),
|
|
703
|
+
false, 4000);
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
if (BytesRead <= 0) {
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
ResponseBuffer.Append(Temp, BytesRead);
|
|
710
|
+
if (ResponseBuffer.Num() >= 4) {
|
|
711
|
+
const int32 Count = ResponseBuffer.Num();
|
|
712
|
+
if (ResponseBuffer[Count - 4] == '\r' &&
|
|
713
|
+
ResponseBuffer[Count - 3] == '\n' &&
|
|
714
|
+
ResponseBuffer[Count - 2] == '\r' &&
|
|
715
|
+
ResponseBuffer[Count - 1] == '\n') {
|
|
716
|
+
bHandshakeComplete = true;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
FString ResponseString = FString(
|
|
722
|
+
ANSI_TO_TCHAR(reinterpret_cast<const char *>(ResponseBuffer.GetData())));
|
|
723
|
+
FString HeaderSection;
|
|
724
|
+
FString ExtraData;
|
|
725
|
+
if (!ResponseString.Split(TEXT("\r\n\r\n"), &HeaderSection, &ExtraData)) {
|
|
726
|
+
HeaderSection = ResponseString;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
TArray<FString> HeaderLines;
|
|
730
|
+
HeaderSection.ParseIntoArrayLines(HeaderLines, false);
|
|
731
|
+
if (HeaderLines.Num() == 0) {
|
|
732
|
+
TearDown(TEXT("Malformed WebSocket handshake response."), false, 4000);
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const FString &StatusLine = HeaderLines[0];
|
|
737
|
+
if (!StatusLine.Contains(TEXT("101"))) {
|
|
738
|
+
TearDown(TEXT("WebSocket server rejected handshake."), false, 4000);
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
FString ExpectedAccept;
|
|
743
|
+
{
|
|
744
|
+
FTCHARToUTF8 AcceptUtf8(*(HandshakeKey + WebSocketGuid));
|
|
745
|
+
FSHA1 Hash;
|
|
746
|
+
Hash.Update(reinterpret_cast<const uint8 *>(AcceptUtf8.Get()),
|
|
747
|
+
AcceptUtf8.Length());
|
|
748
|
+
Hash.Final();
|
|
749
|
+
uint8 Digest[FSHA1::DigestSize];
|
|
750
|
+
Hash.GetHash(Digest);
|
|
751
|
+
ExpectedAccept = FBase64::Encode(Digest, FSHA1::DigestSize);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
bool bAcceptValid = false;
|
|
755
|
+
for (int32 i = 1; i < HeaderLines.Num(); ++i) {
|
|
756
|
+
FString Key;
|
|
757
|
+
FString Value;
|
|
758
|
+
if (HeaderLines[i].Split(TEXT(":"), &Key, &Value)) {
|
|
759
|
+
Key = Key.TrimStartAndEnd();
|
|
760
|
+
Value = Value.TrimStartAndEnd();
|
|
761
|
+
if (Key.Equals(TEXT("Sec-WebSocket-Accept"), ESearchCase::IgnoreCase)) {
|
|
762
|
+
bAcceptValid = Value.Equals(ExpectedAccept, ESearchCase::CaseSensitive);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (!bAcceptValid) {
|
|
768
|
+
TearDown(TEXT("WebSocket handshake validation failed."), false, 4000);
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (!ExtraData.IsEmpty()) {
|
|
773
|
+
const FTCHARToUTF8 ExtraUtf8(*ExtraData);
|
|
774
|
+
PendingReceived.Append(reinterpret_cast<const uint8 *>(ExtraUtf8.Get()),
|
|
775
|
+
ExtraUtf8.Length());
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
bool FMcpBridgeWebSocket::PerformServerHandshake() {
|
|
782
|
+
// Read the client's WebSocket upgrade request
|
|
783
|
+
TArray<uint8> RequestBuffer;
|
|
784
|
+
RequestBuffer.Reserve(1024);
|
|
785
|
+
constexpr int32 TempSize = 256;
|
|
786
|
+
uint8 Temp[TempSize];
|
|
787
|
+
bool bRequestComplete = false;
|
|
788
|
+
FString ClientKey;
|
|
789
|
+
|
|
790
|
+
int32 HeaderEndIndex = -1;
|
|
791
|
+
while (!bRequestComplete) {
|
|
792
|
+
if (bStopping) {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
int32 BytesRead = 0;
|
|
797
|
+
if (!Socket->Recv(Temp, TempSize, BytesRead)) {
|
|
798
|
+
// This may occur when a client connects but immediately closes
|
|
799
|
+
// or when a non-WebSocket probe connects; log at Verbose to avoid
|
|
800
|
+
// spamming warnings for transient or benign network activity.
|
|
801
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
|
|
802
|
+
TEXT("Server handshake recv failed while awaiting upgrade request "
|
|
803
|
+
"(benign or client closed)."));
|
|
804
|
+
TearDown(TEXT("Failed to read WebSocket upgrade request."), false, 4000);
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (BytesRead <= 0) {
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
RequestBuffer.Append(Temp, BytesRead);
|
|
813
|
+
|
|
814
|
+
// Check if we have a complete HTTP request (double CRLF) anywhere
|
|
815
|
+
// in the buffer. Clients may send additional bytes immediately after
|
|
816
|
+
// the headers (for example, the first WebSocket frame), so search
|
|
817
|
+
// the whole buffer and capture any trailing bytes beyond the header
|
|
818
|
+
// terminator into PendingReceived for the frame parser.
|
|
819
|
+
if (RequestBuffer.Num() >= 4) {
|
|
820
|
+
for (int32 Idx = 0; Idx + 3 < RequestBuffer.Num(); ++Idx) {
|
|
821
|
+
if (RequestBuffer[Idx] == '\r' && RequestBuffer[Idx + 1] == '\n' &&
|
|
822
|
+
RequestBuffer[Idx + 2] == '\r' && RequestBuffer[Idx + 3] == '\n') {
|
|
823
|
+
HeaderEndIndex = Idx + 4;
|
|
824
|
+
bRequestComplete = true;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
FString RequestString = FString(
|
|
832
|
+
ANSI_TO_TCHAR(reinterpret_cast<const char *>(RequestBuffer.GetData())));
|
|
833
|
+
TArray<FString> RequestLines;
|
|
834
|
+
RequestString.ParseIntoArrayLines(RequestLines, false);
|
|
835
|
+
|
|
836
|
+
if (RequestLines.Num() == 0) {
|
|
837
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
|
|
838
|
+
TEXT("Server handshake received empty upgrade request."));
|
|
839
|
+
TearDown(TEXT("Malformed WebSocket upgrade request."), false, 4000);
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// If there were any bytes received after the HTTP header terminator,
|
|
844
|
+
// preserve them so the frame parser can consume an arriving WebSocket
|
|
845
|
+
// frame that arrived in the same TCP packet as the upgrade request.
|
|
846
|
+
if (HeaderEndIndex > 0 && HeaderEndIndex < RequestBuffer.Num()) {
|
|
847
|
+
const int32 ExtraCount = RequestBuffer.Num() - HeaderEndIndex;
|
|
848
|
+
if (ExtraCount > 0) {
|
|
849
|
+
FScopeLock Guard(&ReceiveMutex);
|
|
850
|
+
PendingReceived.Append(RequestBuffer.GetData() + HeaderEndIndex,
|
|
851
|
+
ExtraCount);
|
|
852
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
|
|
853
|
+
TEXT("Server handshake: preserved %d extra bytes after upgrade "
|
|
854
|
+
"request for subsequent frame parsing."),
|
|
855
|
+
ExtraCount);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Parse the request
|
|
860
|
+
bool bValidUpgrade = false;
|
|
861
|
+
bool bValidConnection = false;
|
|
862
|
+
bool bValidVersion = false;
|
|
863
|
+
FString RequestedProtocols;
|
|
864
|
+
|
|
865
|
+
for (int32 i = 1; i < RequestLines.Num(); ++i) {
|
|
866
|
+
FString Key, Value;
|
|
867
|
+
if (RequestLines[i].Split(TEXT(":"), &Key, &Value)) {
|
|
868
|
+
Key = Key.TrimStartAndEnd();
|
|
869
|
+
Value = Value.TrimStartAndEnd();
|
|
870
|
+
|
|
871
|
+
if (Key.Equals(TEXT("Upgrade"), ESearchCase::IgnoreCase) &&
|
|
872
|
+
Value.Equals(TEXT("websocket"), ESearchCase::IgnoreCase)) {
|
|
873
|
+
bValidUpgrade = true;
|
|
874
|
+
} else if (Key.Equals(TEXT("Connection"), ESearchCase::IgnoreCase) &&
|
|
875
|
+
Value.Equals(TEXT("Upgrade"), ESearchCase::IgnoreCase)) {
|
|
876
|
+
bValidConnection = true;
|
|
877
|
+
} else if (Key.Equals(TEXT("Sec-WebSocket-Version"),
|
|
878
|
+
ESearchCase::IgnoreCase) &&
|
|
879
|
+
Value.Equals(TEXT("13"), ESearchCase::CaseSensitive)) {
|
|
880
|
+
bValidVersion = true;
|
|
881
|
+
} else if (Key.Equals(TEXT("Sec-WebSocket-Key"),
|
|
882
|
+
ESearchCase::IgnoreCase)) {
|
|
883
|
+
ClientKey = Value;
|
|
884
|
+
} else if (Key.Equals(TEXT("Sec-WebSocket-Protocol"),
|
|
885
|
+
ESearchCase::IgnoreCase)) {
|
|
886
|
+
RequestedProtocols = Value;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (!bValidUpgrade || !bValidConnection || !bValidVersion ||
|
|
892
|
+
ClientKey.IsEmpty()) {
|
|
893
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
|
|
894
|
+
TEXT("Server handshake validation failed (upgrade=%s, "
|
|
895
|
+
"connection=%s, version=%s, hasKey=%s)."),
|
|
896
|
+
bValidUpgrade ? TEXT("true") : TEXT("false"),
|
|
897
|
+
bValidConnection ? TEXT("true") : TEXT("false"),
|
|
898
|
+
bValidVersion ? TEXT("true") : TEXT("false"),
|
|
899
|
+
ClientKey.IsEmpty() ? TEXT("false") : TEXT("true"));
|
|
900
|
+
TearDown(TEXT("Invalid WebSocket upgrade request."), false, 4000);
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Generate the accept key
|
|
905
|
+
const FString AcceptGuid = TEXT("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
|
906
|
+
FString AcceptKey;
|
|
907
|
+
{
|
|
908
|
+
FTCHARToUTF8 AcceptUtf8(*(ClientKey + AcceptGuid));
|
|
909
|
+
FSHA1 Hash;
|
|
910
|
+
Hash.Update(reinterpret_cast<const uint8 *>(AcceptUtf8.Get()),
|
|
911
|
+
AcceptUtf8.Length());
|
|
912
|
+
Hash.Final();
|
|
913
|
+
uint8 Digest[FSHA1::DigestSize];
|
|
914
|
+
Hash.GetHash(Digest);
|
|
915
|
+
AcceptKey = FBase64::Encode(Digest, FSHA1::DigestSize);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
FString SelectedProtocol;
|
|
919
|
+
if (!Protocols.IsEmpty() && !RequestedProtocols.IsEmpty()) {
|
|
920
|
+
TArray<FString> RequestedList;
|
|
921
|
+
RequestedProtocols.ParseIntoArray(RequestedList, TEXT(","), true);
|
|
922
|
+
|
|
923
|
+
TArray<FString> SupportedList;
|
|
924
|
+
Protocols.ParseIntoArray(SupportedList, TEXT(","), true);
|
|
925
|
+
|
|
926
|
+
for (const FString &Requested : RequestedList) {
|
|
927
|
+
const FString TrimmedRequested = Requested.TrimStartAndEnd();
|
|
928
|
+
for (const FString &Supported : SupportedList) {
|
|
929
|
+
if (TrimmedRequested.Equals(Supported.TrimStartAndEnd(),
|
|
930
|
+
ESearchCase::IgnoreCase)) {
|
|
931
|
+
SelectedProtocol = Supported.TrimStartAndEnd();
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (!SelectedProtocol.IsEmpty()) {
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (!RequestedProtocols.IsEmpty() && SelectedProtocol.IsEmpty()) {
|
|
942
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
|
|
943
|
+
TEXT("Server handshake failed: no matching subprotocol. "
|
|
944
|
+
"Requested=%s Supported=%s"),
|
|
945
|
+
*RequestedProtocols, *Protocols);
|
|
946
|
+
TearDown(TEXT("No matching WebSocket subprotocol."), false, 4403);
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Send the upgrade response
|
|
951
|
+
FString Response = FString::Printf(TEXT("HTTP/1.1 101 Switching Protocols\r\n"
|
|
952
|
+
"Upgrade: websocket\r\n"
|
|
953
|
+
"Connection: Upgrade\r\n"
|
|
954
|
+
"Sec-WebSocket-Accept: %s\r\n"),
|
|
955
|
+
*AcceptKey);
|
|
956
|
+
|
|
957
|
+
if (!SelectedProtocol.IsEmpty()) {
|
|
958
|
+
Response += FString::Printf(TEXT("Sec-WebSocket-Protocol: %s\r\n"),
|
|
959
|
+
*SelectedProtocol);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
Response += TEXT("\r\n");
|
|
963
|
+
|
|
964
|
+
FTCHARToUTF8 ResponseUtf8(*Response);
|
|
965
|
+
int32 BytesSent = 0;
|
|
966
|
+
if (!Socket->Send(reinterpret_cast<const uint8 *>(ResponseUtf8.Get()),
|
|
967
|
+
ResponseUtf8.Length(), BytesSent) ||
|
|
968
|
+
BytesSent != ResponseUtf8.Length()) {
|
|
969
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
|
|
970
|
+
TEXT("Server handshake failed: unable to send upgrade response "
|
|
971
|
+
"(sent %d expected %d)."),
|
|
972
|
+
BytesSent, ResponseUtf8.Length());
|
|
973
|
+
TearDown(TEXT("Failed to send WebSocket upgrade response."), false, 4000);
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Log,
|
|
978
|
+
TEXT("Server handshake completed; subprotocol=%s"),
|
|
979
|
+
SelectedProtocol.IsEmpty() ? TEXT("(none)") : *SelectedProtocol);
|
|
980
|
+
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
bool FMcpBridgeWebSocket::ResolveEndpoint(TSharedPtr<FInternetAddr> &OutAddr) {
|
|
985
|
+
ISocketSubsystem *SocketSubsystem =
|
|
986
|
+
ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
|
|
987
|
+
if (!SocketSubsystem) {
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const FString ServiceName = FString::FromInt(Port);
|
|
992
|
+
FAddressInfoResult AddrInfo = SocketSubsystem->GetAddressInfo(
|
|
993
|
+
*HostHeader, *ServiceName, EAddressInfoFlags::Default, NAME_None,
|
|
994
|
+
ESocketType::SOCKTYPE_Streaming);
|
|
995
|
+
if (AddrInfo.Results.Num() == 0) {
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
OutAddr = AddrInfo.Results[0].Address;
|
|
1000
|
+
if (OutAddr.IsValid()) {
|
|
1001
|
+
OutAddr->SetPort(Port);
|
|
1002
|
+
}
|
|
1003
|
+
return OutAddr.IsValid();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
bool FMcpBridgeWebSocket::SendFrame(const TArray<uint8> &Frame) {
|
|
1007
|
+
if (!Socket) {
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
int32 TotalBytesSent = 0;
|
|
1012
|
+
const int32 TotalBytesToSend = Frame.Num();
|
|
1013
|
+
|
|
1014
|
+
while (TotalBytesSent < TotalBytesToSend) {
|
|
1015
|
+
int32 BytesSent = 0;
|
|
1016
|
+
if (!Socket->Send(Frame.GetData() + TotalBytesSent,
|
|
1017
|
+
TotalBytesToSend - TotalBytesSent, BytesSent)) {
|
|
1018
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Error,
|
|
1019
|
+
TEXT("Socket Send failed after sending %d / %d bytes"),
|
|
1020
|
+
TotalBytesSent, TotalBytesToSend);
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (BytesSent <= 0) {
|
|
1025
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Error,
|
|
1026
|
+
TEXT("Socket Send returned %d bytes (expected > 0). Closing "
|
|
1027
|
+
"connection."),
|
|
1028
|
+
BytesSent);
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
TotalBytesSent += BytesSent;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
bool FMcpBridgeWebSocket::SendCloseFrame(int32 StatusCode,
|
|
1039
|
+
const FString &Reason) {
|
|
1040
|
+
TArray<uint8> Payload;
|
|
1041
|
+
Payload.Reserve(2 + Reason.Len() * 4);
|
|
1042
|
+
|
|
1043
|
+
const uint16 Code = ToNetwork16(static_cast<uint16>(StatusCode));
|
|
1044
|
+
Payload.Append(reinterpret_cast<const uint8 *>(&Code), sizeof(uint16));
|
|
1045
|
+
|
|
1046
|
+
FTCHARToUTF8 ReasonUtf8(*Reason);
|
|
1047
|
+
const int32 ReasonBytes = FMath::Min<int32>(
|
|
1048
|
+
ReasonUtf8.Length(),
|
|
1049
|
+
123); // ensure control frame payload stays within 125 bytes
|
|
1050
|
+
if (ReasonBytes > 0) {
|
|
1051
|
+
Payload.Append(reinterpret_cast<const uint8 *>(ReasonUtf8.Get()),
|
|
1052
|
+
ReasonBytes);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
return SendControlFrame(OpCodeClose, Payload);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
bool FMcpBridgeWebSocket::SendTextFrame(const void *Data, SIZE_T Length) {
|
|
1059
|
+
const uint8 *Raw = static_cast<const uint8 *>(Data);
|
|
1060
|
+
TArray<uint8> Frame;
|
|
1061
|
+
|
|
1062
|
+
const uint8 Header = 0x80 | OpCodeText;
|
|
1063
|
+
Frame.Add(Header);
|
|
1064
|
+
|
|
1065
|
+
const bool bMask = !bServerAcceptedConnection;
|
|
1066
|
+
|
|
1067
|
+
if (Length <= 125) {
|
|
1068
|
+
Frame.Add((bMask ? 0x80 : 0x00) | static_cast<uint8>(Length));
|
|
1069
|
+
} else if (Length <= 0xFFFF) {
|
|
1070
|
+
Frame.Add((bMask ? 0x80 : 0x00) | 126);
|
|
1071
|
+
const uint16 SizeShort = ToNetwork16(static_cast<uint16>(Length));
|
|
1072
|
+
Frame.Append(reinterpret_cast<const uint8 *>(&SizeShort), sizeof(uint16));
|
|
1073
|
+
} else {
|
|
1074
|
+
Frame.Add((bMask ? 0x80 : 0x00) | 127);
|
|
1075
|
+
const uint64 SizeLong = ToNetwork64(static_cast<uint64>(Length));
|
|
1076
|
+
Frame.Append(reinterpret_cast<const uint8 *>(&SizeLong), sizeof(uint64));
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (bMask) {
|
|
1080
|
+
uint8 MaskKey[4];
|
|
1081
|
+
for (uint8 &Byte : MaskKey) {
|
|
1082
|
+
Byte = static_cast<uint8>(FMath::RandRange(0, 255));
|
|
1083
|
+
}
|
|
1084
|
+
Frame.Append(MaskKey, 4);
|
|
1085
|
+
|
|
1086
|
+
const int64 Offset = Frame.Num();
|
|
1087
|
+
Frame.AddUninitialized(Length);
|
|
1088
|
+
for (SIZE_T Index = 0; Index < Length; ++Index) {
|
|
1089
|
+
Frame[Offset + Index] = Raw[Index] ^ MaskKey[Index % 4];
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
Frame.Append(Raw, static_cast<int32>(Length));
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
FScopeLock Guard(&SendMutex);
|
|
1096
|
+
return SendFrame(Frame);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
bool FMcpBridgeWebSocket::SendControlFrame(const uint8 ControlOpCode,
|
|
1100
|
+
const TArray<uint8> &Payload) {
|
|
1101
|
+
if (!Socket) {
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (Payload.Num() > 125) {
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
FScopeLock Guard(&SendMutex);
|
|
1110
|
+
|
|
1111
|
+
TArray<uint8> Frame;
|
|
1112
|
+
Frame.Reserve(2 + 4 + Payload.Num());
|
|
1113
|
+
Frame.Add(0x80 | (ControlOpCode & 0x0F));
|
|
1114
|
+
const bool bMask = !bServerAcceptedConnection;
|
|
1115
|
+
Frame.Add((bMask ? 0x80 : 0x00) | static_cast<uint8>(Payload.Num()));
|
|
1116
|
+
|
|
1117
|
+
if (bMask) {
|
|
1118
|
+
uint8 MaskKey[4];
|
|
1119
|
+
for (uint8 &Byte : MaskKey) {
|
|
1120
|
+
Byte = static_cast<uint8>(FMath::RandRange(0, 255));
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
Frame.Append(MaskKey, 4);
|
|
1124
|
+
const int32 PayloadOffset = Frame.Num();
|
|
1125
|
+
Frame.AddUninitialized(Payload.Num());
|
|
1126
|
+
for (int32 Index = 0; Index < Payload.Num(); ++Index) {
|
|
1127
|
+
Frame[PayloadOffset + Index] = Payload[Index] ^ MaskKey[Index % 4];
|
|
1128
|
+
}
|
|
1129
|
+
} else if (Payload.Num() > 0) {
|
|
1130
|
+
Frame.Append(Payload);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
return SendFrame(Frame);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
void FMcpBridgeWebSocket::HandleTextPayload(const TArray<uint8> &Payload) {
|
|
1137
|
+
const FString Message = BytesToStringView(Payload);
|
|
1138
|
+
// Dispatch message handling to the game thread.
|
|
1139
|
+
// Many automation handlers touch editor/world state and must run on the
|
|
1140
|
+
// game thread. Keeping the socket receive loop thread-free also prevents
|
|
1141
|
+
// long-running actions (e.g. export_level) from stalling the connection.
|
|
1142
|
+
DispatchOnGameThread([WeakThis = SelfWeakPtr, Message] {
|
|
1143
|
+
if (TSharedPtr<FMcpBridgeWebSocket> Pinned = WeakThis.Pin()) {
|
|
1144
|
+
Pinned->MessageDelegate.Broadcast(Pinned, Message);
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
void FMcpBridgeWebSocket::ResetFragmentState() {
|
|
1150
|
+
FragmentAccumulator.Reset();
|
|
1151
|
+
bFragmentMessageActive = false;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
bool FMcpBridgeWebSocket::ReceiveFrame() {
|
|
1155
|
+
uint8 Header[2];
|
|
1156
|
+
if (!ReceiveExact(Header, 2)) {
|
|
1157
|
+
TearDown(TEXT("Failed to read WebSocket frame header."), false, 4001);
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const bool bFinalFrame = (Header[0] & 0x80) != 0;
|
|
1162
|
+
const uint8 OpCode = Header[0] & 0x0F;
|
|
1163
|
+
uint64 PayloadLength = Header[1] & 0x7F;
|
|
1164
|
+
const bool bMasked = (Header[1] & 0x80) != 0;
|
|
1165
|
+
|
|
1166
|
+
if (PayloadLength == 126) {
|
|
1167
|
+
uint8 Extended[2];
|
|
1168
|
+
if (!ReceiveExact(Extended, sizeof(Extended))) {
|
|
1169
|
+
TearDown(TEXT("Failed to read extended payload length."), false, 4001);
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
uint16 ShortVal = 0;
|
|
1173
|
+
FMemory::Memcpy(&ShortVal, Extended, sizeof(uint16));
|
|
1174
|
+
PayloadLength = FromNetwork16(ShortVal);
|
|
1175
|
+
} else if (PayloadLength == 127) {
|
|
1176
|
+
uint8 Extended[8];
|
|
1177
|
+
if (!ReceiveExact(Extended, sizeof(Extended))) {
|
|
1178
|
+
TearDown(TEXT("Failed to read extended payload length."), false, 4001);
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
uint64 LongVal = 0;
|
|
1182
|
+
FMemory::Memcpy(&LongVal, Extended, sizeof(uint64));
|
|
1183
|
+
PayloadLength = FromNetwork64(LongVal);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
uint8 MaskKey[4] = {0, 0, 0, 0};
|
|
1187
|
+
if (bMasked) {
|
|
1188
|
+
if (!ReceiveExact(MaskKey, 4)) {
|
|
1189
|
+
TearDown(TEXT("Failed to read masking key."), false, 4001);
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
TArray<uint8> Payload;
|
|
1195
|
+
if (PayloadLength > 0) {
|
|
1196
|
+
Payload.SetNumUninitialized(static_cast<int32>(PayloadLength));
|
|
1197
|
+
if (!ReceiveExact(Payload.GetData(), PayloadLength)) {
|
|
1198
|
+
TearDown(TEXT("Failed to read WebSocket payload."), false, 4001);
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
if (bMasked) {
|
|
1202
|
+
for (uint64 Index = 0; Index < PayloadLength; ++Index) {
|
|
1203
|
+
Payload[Index] ^= MaskKey[Index % 4];
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (OpCode == OpCodeClose) {
|
|
1209
|
+
TearDown(TEXT("WebSocket closed by peer."), true, 1000);
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Handle control frames immediately (they must not be fragmented)
|
|
1214
|
+
if ((OpCode & 0x08) != 0) {
|
|
1215
|
+
if (!bFinalFrame) {
|
|
1216
|
+
TearDown(TEXT("Control frames must not be fragmented."), false, 4002);
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (OpCode == OpCodePing) {
|
|
1221
|
+
SendControlFrame(OpCodePong, Payload);
|
|
1222
|
+
return true;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (OpCode == OpCodePong) {
|
|
1226
|
+
// In server mode, receiving a pong means the client is responding to our
|
|
1227
|
+
// ping In client mode, receiving a pong means the server responded to our
|
|
1228
|
+
// ping
|
|
1229
|
+
HeartbeatDelegate.Broadcast(SelfWeakPtr.Pin());
|
|
1230
|
+
return true;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Unknown control frame
|
|
1234
|
+
return true;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (OpCode == OpCodeContinuation) {
|
|
1238
|
+
if (!bFragmentMessageActive) {
|
|
1239
|
+
TearDown(TEXT("Unexpected continuation frame."), false, 4002);
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
FragmentAccumulator.Append(Payload);
|
|
1244
|
+
|
|
1245
|
+
if (bFinalFrame) {
|
|
1246
|
+
HandleTextPayload(FragmentAccumulator);
|
|
1247
|
+
ResetFragmentState();
|
|
1248
|
+
}
|
|
1249
|
+
return true;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (bFragmentMessageActive) {
|
|
1253
|
+
TearDown(
|
|
1254
|
+
TEXT("Received new data frame before completing fragmented message."),
|
|
1255
|
+
false, 4002);
|
|
1256
|
+
return false;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (OpCode == OpCodeText) {
|
|
1260
|
+
if (bFinalFrame) {
|
|
1261
|
+
HandleTextPayload(Payload);
|
|
1262
|
+
} else {
|
|
1263
|
+
FragmentAccumulator = Payload;
|
|
1264
|
+
bFragmentMessageActive = true;
|
|
1265
|
+
}
|
|
1266
|
+
return true;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (OpCode == OpCodeBinary) {
|
|
1270
|
+
TearDown(TEXT("Binary frames are not supported."), false, 4003);
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
TearDown(TEXT("Unsupported WebSocket opcode."), false, 4003);
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
bool FMcpBridgeWebSocket::ReceiveExact(uint8 *Buffer, SIZE_T Length) {
|
|
1279
|
+
SIZE_T Collected = 0;
|
|
1280
|
+
|
|
1281
|
+
{
|
|
1282
|
+
FScopeLock Guard(&ReceiveMutex);
|
|
1283
|
+
const SIZE_T Existing =
|
|
1284
|
+
FMath::Min(static_cast<SIZE_T>(PendingReceived.Num()), Length);
|
|
1285
|
+
if (Existing > 0) {
|
|
1286
|
+
FMemory::Memcpy(Buffer, PendingReceived.GetData(), Existing);
|
|
1287
|
+
PendingReceived.RemoveAt(0, Existing, EAllowShrinking::No);
|
|
1288
|
+
Collected += Existing;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
while (Collected < Length) {
|
|
1293
|
+
if (bStopping) {
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
uint32 PendingSize = 0;
|
|
1298
|
+
if (!Socket->HasPendingData(PendingSize)) {
|
|
1299
|
+
if (StopEvent && StopEvent->Wait(FTimespan::FromMilliseconds(50))) {
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const uint32 ReadSize = FMath::Min<uint32>(PendingSize, 4096);
|
|
1306
|
+
TArray<uint8> Temp;
|
|
1307
|
+
Temp.SetNumUninitialized(ReadSize);
|
|
1308
|
+
int32 BytesRead = 0;
|
|
1309
|
+
if (!Socket->Recv(Temp.GetData(), ReadSize, BytesRead)) {
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
if (BytesRead <= 0) {
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const uint32 CopyCount =
|
|
1318
|
+
FMath::Min<uint32>(static_cast<uint32>(BytesRead),
|
|
1319
|
+
static_cast<uint32>(Length - Collected));
|
|
1320
|
+
FMemory::Memcpy(Buffer + Collected, Temp.GetData(), CopyCount);
|
|
1321
|
+
Collected += CopyCount;
|
|
1322
|
+
|
|
1323
|
+
if (static_cast<uint32>(BytesRead) > CopyCount) {
|
|
1324
|
+
FScopeLock Guard(&ReceiveMutex);
|
|
1325
|
+
PendingReceived.Append(Temp.GetData() + CopyCount, BytesRead - CopyCount);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|