unreal-engine-mcp-server 0.5.4 → 0.5.5
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/dist/automation/bridge.d.ts.map +1 -0
- package/dist/automation/bridge.js.map +1 -0
- package/dist/automation/connection-manager.d.ts.map +1 -0
- package/dist/automation/connection-manager.js.map +1 -0
- package/dist/automation/handshake.d.ts.map +1 -0
- package/dist/automation/handshake.js.map +1 -0
- package/dist/automation/index.d.ts.map +1 -0
- package/dist/automation/index.js.map +1 -0
- package/dist/automation/message-handler.d.ts.map +1 -0
- package/dist/automation/message-handler.js.map +1 -0
- package/dist/automation/request-tracker.d.ts.map +1 -0
- package/dist/automation/request-tracker.js.map +1 -0
- package/dist/automation/types.d.ts.map +1 -0
- package/dist/automation/types.js.map +1 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +4 -3
- package/dist/cli.js.map +1 -0
- package/dist/config/class-aliases.d.ts.map +1 -0
- package/dist/config/class-aliases.js.map +1 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js.map +1 -0
- package/dist/graphql/loaders.d.ts.map +1 -0
- package/dist/graphql/loaders.js.map +1 -0
- package/dist/graphql/resolvers.d.ts.map +1 -0
- package/dist/graphql/resolvers.js +29 -29
- package/dist/graphql/resolvers.js.map +1 -0
- package/dist/graphql/schema.d.ts.map +1 -0
- package/dist/graphql/schema.js.map +1 -0
- package/dist/graphql/server.d.ts.map +1 -0
- package/dist/graphql/server.js.map +1 -0
- package/dist/graphql/types.d.ts.map +1 -0
- package/dist/graphql/types.js.map +1 -0
- package/dist/handlers/resource-handlers.d.ts.map +1 -0
- package/dist/handlers/resource-handlers.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +64 -7
- package/dist/index.js.map +1 -0
- package/dist/resources/actors.d.ts.map +1 -0
- package/dist/resources/actors.js.map +1 -0
- package/dist/resources/assets.d.ts.map +1 -0
- package/dist/resources/assets.js +6 -4
- package/dist/resources/assets.js.map +1 -0
- package/dist/resources/levels.d.ts.map +1 -0
- package/dist/resources/levels.js.map +1 -0
- package/dist/server/resource-registry.d.ts.map +1 -0
- package/dist/server/resource-registry.js.map +1 -0
- package/dist/server/tool-registry.d.ts.map +1 -0
- package/dist/server/tool-registry.js.map +1 -0
- package/dist/server-setup.d.ts.map +1 -0
- package/dist/server-setup.js.map +1 -0
- package/dist/services/health-monitor.d.ts.map +1 -0
- package/dist/services/health-monitor.js.map +1 -0
- package/dist/services/metrics-server.d.ts.map +1 -0
- package/dist/services/metrics-server.js.map +1 -0
- package/dist/tools/actors.d.ts.map +1 -0
- package/dist/tools/actors.js +3 -1
- package/dist/tools/actors.js.map +1 -0
- package/dist/tools/animation.d.ts.map +1 -0
- package/dist/tools/animation.js +2 -2
- package/dist/tools/animation.js.map +1 -0
- package/dist/tools/assets.d.ts.map +1 -0
- package/dist/tools/assets.js.map +1 -0
- package/dist/tools/audio.d.ts.map +1 -0
- package/dist/tools/audio.js.map +1 -0
- package/dist/tools/base-tool.d.ts.map +1 -0
- package/dist/tools/base-tool.js.map +1 -0
- package/dist/tools/behavior-tree.d.ts.map +1 -0
- package/dist/tools/behavior-tree.js.map +1 -0
- package/dist/tools/blueprint.d.ts.map +1 -0
- package/dist/tools/blueprint.js +4 -2
- package/dist/tools/blueprint.js.map +1 -0
- package/dist/tools/consolidated-tool-definitions.d.ts.map +1 -0
- package/dist/tools/consolidated-tool-definitions.js.map +1 -0
- package/dist/tools/consolidated-tool-handlers.d.ts.map +1 -0
- package/dist/tools/consolidated-tool-handlers.js.map +1 -0
- package/dist/tools/debug.d.ts.map +1 -0
- package/dist/tools/debug.js +3 -1
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/dynamic-handler-registry.d.ts.map +1 -0
- package/dist/tools/dynamic-handler-registry.js +3 -1
- package/dist/tools/dynamic-handler-registry.js.map +1 -0
- package/dist/tools/editor.d.ts.map +1 -0
- package/dist/tools/editor.js +1 -1
- package/dist/tools/editor.js.map +1 -0
- package/dist/tools/engine.d.ts.map +1 -0
- package/dist/tools/engine.js.map +1 -0
- package/dist/tools/environment.d.ts.map +1 -0
- package/dist/tools/environment.js +2 -2
- package/dist/tools/environment.js.map +1 -0
- package/dist/tools/foliage.d.ts.map +1 -0
- package/dist/tools/foliage.js.map +1 -0
- package/dist/tools/handlers/actor-handlers.d.ts +1 -1
- package/dist/tools/handlers/actor-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/actor-handlers.js +6 -5
- package/dist/tools/handlers/actor-handlers.js.map +1 -0
- package/dist/tools/handlers/animation-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/animation-handlers.js.map +1 -0
- package/dist/tools/handlers/argument-helper.d.ts.map +1 -0
- package/dist/tools/handlers/argument-helper.js.map +1 -0
- package/dist/tools/handlers/asset-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/asset-handlers.js +5 -1
- package/dist/tools/handlers/asset-handlers.js.map +1 -0
- package/dist/tools/handlers/audio-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/audio-handlers.js.map +1 -0
- package/dist/tools/handlers/blueprint-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/blueprint-handlers.js +2 -1
- package/dist/tools/handlers/blueprint-handlers.js.map +1 -0
- package/dist/tools/handlers/common-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/common-handlers.js.map +1 -0
- package/dist/tools/handlers/editor-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/editor-handlers.js +12 -2
- package/dist/tools/handlers/editor-handlers.js.map +1 -0
- package/dist/tools/handlers/effect-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/effect-handlers.js.map +1 -0
- package/dist/tools/handlers/environment-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/environment-handlers.js.map +1 -0
- package/dist/tools/handlers/graph-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/graph-handlers.js +61 -1
- package/dist/tools/handlers/graph-handlers.js.map +1 -0
- package/dist/tools/handlers/input-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/input-handlers.js.map +1 -0
- package/dist/tools/handlers/inspect-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/inspect-handlers.js.map +1 -0
- package/dist/tools/handlers/level-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/level-handlers.js.map +1 -0
- package/dist/tools/handlers/lighting-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/lighting-handlers.js +23 -1
- package/dist/tools/handlers/lighting-handlers.js.map +1 -0
- package/dist/tools/handlers/performance-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/performance-handlers.js +15 -2
- package/dist/tools/handlers/performance-handlers.js.map +1 -0
- package/dist/tools/handlers/pipeline-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/pipeline-handlers.js.map +1 -0
- package/dist/tools/handlers/sequence-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/sequence-handlers.js.map +1 -0
- package/dist/tools/handlers/system-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/system-handlers.js +16 -1
- package/dist/tools/handlers/system-handlers.js.map +1 -0
- package/dist/tools/input.d.ts.map +1 -0
- package/dist/tools/input.js +3 -1
- package/dist/tools/input.js.map +1 -0
- package/dist/tools/introspection.d.ts.map +1 -0
- package/dist/tools/introspection.js.map +1 -0
- package/dist/tools/landscape.d.ts.map +1 -0
- package/dist/tools/landscape.js +3 -1
- package/dist/tools/landscape.js.map +1 -0
- package/dist/tools/level.d.ts.map +1 -0
- package/dist/tools/level.js.map +1 -0
- package/dist/tools/lighting.d.ts.map +1 -0
- package/dist/tools/lighting.js +3 -1
- package/dist/tools/lighting.js.map +1 -0
- package/dist/tools/logs.d.ts.map +1 -0
- package/dist/tools/logs.js.map +1 -0
- package/dist/tools/materials.d.ts.map +1 -0
- package/dist/tools/materials.js +3 -1
- package/dist/tools/materials.js.map +1 -0
- package/dist/tools/niagara.d.ts.map +1 -0
- package/dist/tools/niagara.js +7 -5
- package/dist/tools/niagara.js.map +1 -0
- package/dist/tools/performance.d.ts.map +1 -0
- package/dist/tools/performance.js.map +1 -0
- package/dist/tools/physics.d.ts.map +1 -0
- package/dist/tools/physics.js +9 -7
- package/dist/tools/physics.js.map +1 -0
- package/dist/tools/property-dictionary.d.ts.map +1 -0
- package/dist/tools/property-dictionary.js.map +1 -0
- package/dist/tools/sequence.d.ts.map +1 -0
- package/dist/tools/sequence.js +3 -1
- package/dist/tools/sequence.js.map +1 -0
- package/dist/tools/tool-definition-utils.d.ts.map +1 -0
- package/dist/tools/tool-definition-utils.js.map +1 -0
- package/dist/tools/ui.d.ts.map +1 -0
- package/dist/tools/ui.js +3 -1
- package/dist/tools/ui.js.map +1 -0
- package/dist/types/automation-responses.d.ts.map +1 -0
- package/dist/types/automation-responses.js.map +1 -0
- package/dist/types/env.d.ts.map +1 -0
- package/dist/types/env.js.map +1 -0
- package/dist/types/handler-types.d.ts.map +1 -0
- package/dist/types/handler-types.js.map +1 -0
- package/dist/types/tool-interfaces.d.ts.map +1 -0
- package/dist/types/tool-interfaces.js.map +1 -0
- package/dist/types/tool-types.d.ts.map +1 -0
- package/dist/types/tool-types.js.map +1 -0
- package/dist/unreal-bridge.d.ts +1 -0
- package/dist/unreal-bridge.d.ts.map +1 -0
- package/dist/unreal-bridge.js +8 -0
- package/dist/unreal-bridge.js.map +1 -0
- package/dist/utils/command-validator.d.ts.map +1 -0
- package/dist/utils/command-validator.js.map +1 -0
- package/dist/utils/elicitation.d.ts.map +1 -0
- package/dist/utils/elicitation.js.map +1 -0
- package/dist/utils/error-handler.d.ts.map +1 -0
- package/dist/utils/error-handler.js.map +1 -0
- package/dist/utils/ini-reader.d.ts.map +1 -0
- package/dist/utils/ini-reader.js.map +1 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/normalize.d.ts.map +1 -0
- package/dist/utils/normalize.js.map +1 -0
- package/dist/utils/path-security.d.ts.map +1 -0
- package/dist/utils/path-security.js.map +1 -0
- package/dist/utils/response-factory.d.ts.map +1 -0
- package/dist/utils/response-factory.js +3 -1
- package/dist/utils/response-factory.js.map +1 -0
- package/dist/utils/response-validator.d.ts.map +1 -0
- package/dist/utils/response-validator.js.map +1 -0
- package/dist/utils/result-helpers.d.ts.map +1 -0
- package/dist/utils/result-helpers.js.map +1 -0
- package/dist/utils/safe-json.d.ts.map +1 -0
- package/dist/utils/safe-json.js.map +1 -0
- package/dist/utils/unreal-command-queue.d.ts.map +1 -0
- package/dist/utils/unreal-command-queue.js.map +1 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/wasm/index.d.ts.map +1 -0
- package/dist/wasm/index.js.map +1 -0
- package/package.json +12 -34
- package/server.json +2 -2
- package/.dockerignore +0 -57
- package/.env.example +0 -26
- package/.env.production +0 -61
- package/.eslintrc.json +0 -0
- package/.eslintrc.override.json +0 -8
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -94
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -56
- package/.github/copilot-instructions.md +0 -478
- package/.github/dependabot.yml +0 -19
- package/.github/labeler.yml +0 -24
- package/.github/labels.yml +0 -70
- package/.github/pull_request_template.md +0 -42
- package/.github/release-drafter-config.yml +0 -51
- package/.github/workflows/auto-merge.yml +0 -38
- package/.github/workflows/ci.yml +0 -38
- package/.github/workflows/dependency-review.yml +0 -17
- package/.github/workflows/gemini-issue-triage.yml +0 -172
- package/.github/workflows/greetings.yml +0 -27
- package/.github/workflows/labeler.yml +0 -17
- package/.github/workflows/links.yml +0 -80
- package/.github/workflows/pr-size-labeler.yml +0 -137
- package/.github/workflows/publish-mcp.yml +0 -79
- package/.github/workflows/release-drafter.yml +0 -24
- package/.github/workflows/release.yml +0 -112
- package/.github/workflows/semantic-pull-request.yml +0 -35
- package/.github/workflows/smoke-test.yml +0 -36
- package/.github/workflows/stale.yml +0 -28
- package/CONTRIBUTING.md +0 -140
- package/Dockerfile +0 -37
- package/GEMINI.md +0 -115
- package/Public/Plugin_setup_guide.mp4 +0 -0
- package/Public/icon.png +0 -0
- package/claude_desktop_config_example.json +0 -15
- package/dist/types/responses.d.ts +0 -249
- package/dist/types/responses.js +0 -2
- package/docs/GraphQL-API.md +0 -888
- package/docs/Migration-Guide-v0.5.0.md +0 -684
- package/docs/Roadmap.md +0 -53
- package/docs/WebAssembly-Integration.md +0 -628
- package/docs/editor-plugin-extension.md +0 -370
- package/docs/handler-mapping.md +0 -249
- package/docs/native-automation-progress.md +0 -128
- package/docs/testing-guide.md +0 -423
- package/eslint.config.mjs +0 -68
- package/mcp-config-example.json +0 -14
- package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +0 -8
- package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +0 -64
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +0 -189
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +0 -22
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +0 -30
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +0 -1983
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +0 -72
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +0 -46
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +0 -846
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +0 -2393
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +0 -300
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +0 -2807
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +0 -1087
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +0 -488
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +0 -643
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +0 -31
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +0 -1094
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +0 -5750
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +0 -152
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +0 -2614
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +0 -42
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +0 -1237
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +0 -1725
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +0 -2265
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +0 -954
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +0 -209
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +0 -41
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +0 -1164
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +0 -762
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +0 -663
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +0 -136
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +0 -494
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +0 -278
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +0 -625
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +0 -401
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +0 -67
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +0 -472
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +0 -2634
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +0 -189
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +0 -917
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +0 -39
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +0 -2706
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +0 -519
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +0 -38
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +0 -668
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +0 -346
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +0 -1345
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +0 -149
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -782
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +0 -115
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +0 -796
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +0 -117
- package/scripts/check-unreal-connection.mjs +0 -19
- package/scripts/clean-tmp.js +0 -23
- package/scripts/patch-wasm.js +0 -26
- package/scripts/run-all-tests.mjs +0 -136
- package/scripts/smoke-test.ts +0 -94
- package/scripts/sync-mcp-plugin.js +0 -143
- package/scripts/test-no-plugin-alternates.mjs +0 -113
- package/scripts/validate-server.js +0 -46
- package/scripts/verify-automation-bridge.js +0 -200
- package/src/automation/bridge.ts +0 -630
- package/src/automation/connection-manager.ts +0 -148
- package/src/automation/handshake.ts +0 -99
- package/src/automation/index.ts +0 -2
- package/src/automation/message-handler.ts +0 -192
- package/src/automation/request-tracker.ts +0 -155
- package/src/automation/types.ts +0 -108
- package/src/cli.ts +0 -34
- package/src/config/class-aliases.ts +0 -65
- package/src/config.ts +0 -73
- package/src/constants.ts +0 -29
- package/src/graphql/loaders.ts +0 -244
- package/src/graphql/resolvers.ts +0 -1008
- package/src/graphql/schema.ts +0 -452
- package/src/graphql/server.ts +0 -156
- package/src/graphql/types.ts +0 -10
- package/src/handlers/resource-handlers.ts +0 -186
- package/src/index.ts +0 -243
- package/src/resources/actors.ts +0 -127
- package/src/resources/assets.ts +0 -286
- package/src/resources/levels.ts +0 -68
- package/src/server/resource-registry.ts +0 -47
- package/src/server/tool-registry.ts +0 -354
- package/src/server-setup.ts +0 -114
- package/src/services/health-monitor.ts +0 -132
- package/src/services/metrics-server.ts +0 -176
- package/src/tools/actors.ts +0 -564
- package/src/tools/animation.ts +0 -941
- package/src/tools/assets.ts +0 -394
- package/src/tools/audio.ts +0 -499
- package/src/tools/base-tool.ts +0 -52
- package/src/tools/behavior-tree.ts +0 -45
- package/src/tools/blueprint.ts +0 -940
- package/src/tools/consolidated-tool-definitions.ts +0 -1256
- package/src/tools/consolidated-tool-handlers.ts +0 -302
- package/src/tools/debug.ts +0 -622
- package/src/tools/dynamic-handler-registry.ts +0 -33
- package/src/tools/editor.ts +0 -435
- package/src/tools/engine.ts +0 -43
- package/src/tools/environment.ts +0 -281
- package/src/tools/foliage.ts +0 -596
- package/src/tools/handlers/actor-handlers.ts +0 -244
- package/src/tools/handlers/animation-handlers.ts +0 -237
- package/src/tools/handlers/argument-helper.ts +0 -142
- package/src/tools/handlers/asset-handlers.ts +0 -550
- package/src/tools/handlers/audio-handlers.ts +0 -194
- package/src/tools/handlers/blueprint-handlers.ts +0 -380
- package/src/tools/handlers/common-handlers.ts +0 -108
- package/src/tools/handlers/editor-handlers.ts +0 -124
- package/src/tools/handlers/effect-handlers.ts +0 -224
- package/src/tools/handlers/environment-handlers.ts +0 -183
- package/src/tools/handlers/graph-handlers.ts +0 -117
- package/src/tools/handlers/input-handlers.ts +0 -28
- package/src/tools/handlers/inspect-handlers.ts +0 -450
- package/src/tools/handlers/level-handlers.ts +0 -253
- package/src/tools/handlers/lighting-handlers.ts +0 -151
- package/src/tools/handlers/performance-handlers.ts +0 -132
- package/src/tools/handlers/pipeline-handlers.ts +0 -194
- package/src/tools/handlers/sequence-handlers.ts +0 -438
- package/src/tools/handlers/system-handlers.ts +0 -564
- package/src/tools/input.ts +0 -160
- package/src/tools/introspection.ts +0 -689
- package/src/tools/landscape.ts +0 -649
- package/src/tools/level.ts +0 -989
- package/src/tools/lighting.ts +0 -1052
- package/src/tools/logs.ts +0 -219
- package/src/tools/materials.ts +0 -295
- package/src/tools/niagara.ts +0 -485
- package/src/tools/performance.ts +0 -661
- package/src/tools/physics.ts +0 -679
- package/src/tools/property-dictionary.ts +0 -98
- package/src/tools/sequence.ts +0 -385
- package/src/tools/tool-definition-utils.ts +0 -35
- package/src/tools/ui.ts +0 -452
- package/src/types/automation-responses.ts +0 -119
- package/src/types/env.ts +0 -17
- package/src/types/handler-types.ts +0 -442
- package/src/types/responses.ts +0 -355
- package/src/types/tool-interfaces.ts +0 -250
- package/src/types/tool-types.ts +0 -575
- package/src/unreal-bridge.ts +0 -693
- package/src/utils/command-validator.ts +0 -139
- package/src/utils/elicitation.ts +0 -132
- package/src/utils/error-handler.ts +0 -287
- package/src/utils/ini-reader.ts +0 -86
- package/src/utils/logger.ts +0 -35
- package/src/utils/normalize.test.ts +0 -162
- package/src/utils/normalize.ts +0 -146
- package/src/utils/path-security.ts +0 -43
- package/src/utils/response-factory.ts +0 -44
- package/src/utils/response-validator.ts +0 -395
- package/src/utils/result-helpers.ts +0 -195
- package/src/utils/safe-json.test.ts +0 -90
- package/src/utils/safe-json.ts +0 -70
- package/src/utils/unreal-command-queue.ts +0 -166
- package/src/utils/validation.test.ts +0 -184
- package/src/utils/validation.ts +0 -312
- package/src/wasm/index.ts +0 -838
- package/test-server.mjs +0 -100
- package/tests/test-animation.mjs +0 -369
- package/tests/test-asset-advanced.mjs +0 -82
- package/tests/test-asset-graph.mjs +0 -311
- package/tests/test-audio.mjs +0 -417
- package/tests/test-automation-timeouts.mjs +0 -98
- package/tests/test-behavior-tree.mjs +0 -444
- package/tests/test-blueprint-graph.mjs +0 -410
- package/tests/test-blueprint.mjs +0 -577
- package/tests/test-client-mode.mjs +0 -86
- package/tests/test-console-command.mjs +0 -56
- package/tests/test-control-actor.mjs +0 -425
- package/tests/test-control-editor.mjs +0 -112
- package/tests/test-graphql.mjs +0 -372
- package/tests/test-input.mjs +0 -349
- package/tests/test-inspect.mjs +0 -302
- package/tests/test-landscape.mjs +0 -316
- package/tests/test-lighting.mjs +0 -428
- package/tests/test-manage-asset.mjs +0 -438
- package/tests/test-manage-level.mjs +0 -89
- package/tests/test-materials.mjs +0 -356
- package/tests/test-niagara.mjs +0 -185
- package/tests/test-no-inline-python.mjs +0 -122
- package/tests/test-performance.mjs +0 -539
- package/tests/test-plugin-handshake.mjs +0 -82
- package/tests/test-runner.mjs +0 -993
- package/tests/test-sequence.mjs +0 -104
- package/tests/test-system.mjs +0 -96
- package/tests/test-wasm.mjs +0 -283
- package/tests/test-world-partition.mjs +0 -215
- package/tsconfig.json +0 -56
- package/vitest.config.ts +0 -35
- package/wasm/Cargo.lock +0 -363
- package/wasm/Cargo.toml +0 -42
- package/wasm/LICENSE +0 -21
- package/wasm/README.md +0 -253
- package/wasm/src/dependency_resolver.rs +0 -377
- package/wasm/src/lib.rs +0 -153
- package/wasm/src/property_parser.rs +0 -271
- package/wasm/src/transform_math.rs +0 -396
- package/wasm/tests/integration.rs +0 -109
package/tests/test-runner.mjs
DELETED
|
@@ -1,993 +0,0 @@
|
|
|
1
|
-
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
-
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import fs from 'node:fs/promises';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import net from 'node:net';
|
|
7
|
-
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = path.dirname(__filename);
|
|
10
|
-
const repoRoot = path.resolve(__dirname, '..');
|
|
11
|
-
const reportsDir = path.join(__dirname, 'reports');
|
|
12
|
-
|
|
13
|
-
// Common failure keywords to check against
|
|
14
|
-
const failureKeywords = ['failed', 'error', 'exception', 'invalid', 'not found', 'missing', 'timed out', 'timeout', 'unsupported', 'unknown'];
|
|
15
|
-
const successKeywords = ['success', 'created', 'updated', 'deleted', 'completed', 'done', 'ok'];
|
|
16
|
-
|
|
17
|
-
// Defaults for spawning the MCP server.
|
|
18
|
-
let serverCommand = process.env.UNREAL_MCP_SERVER_CMD ?? 'node';
|
|
19
|
-
let serverArgs = process.env.UNREAL_MCP_SERVER_ARGS ? process.env.UNREAL_MCP_SERVER_ARGS.split(',') : [path.join(repoRoot, 'dist', 'cli.js')];
|
|
20
|
-
const serverCwd = process.env.UNREAL_MCP_SERVER_CWD ?? repoRoot;
|
|
21
|
-
const serverEnv = Object.assign({}, process.env);
|
|
22
|
-
|
|
23
|
-
const DEFAULT_RESPONSE_LOG_MAX_CHARS = 6000; // default max chars
|
|
24
|
-
const RESPONSE_LOGGING_ENABLED = process.env.UNREAL_MCP_TEST_LOG_RESPONSES !== '0';
|
|
25
|
-
|
|
26
|
-
function clampString(value, maxChars) {
|
|
27
|
-
if (typeof value !== 'string') return '';
|
|
28
|
-
if (value.length <= maxChars) return value;
|
|
29
|
-
return value.slice(0, maxChars) + `\n... (truncated, ${value.length - maxChars} chars omitted)`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function tryParseJson(text) {
|
|
33
|
-
if (typeof text !== 'string') return null;
|
|
34
|
-
try {
|
|
35
|
-
return JSON.parse(text);
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function normalizeMcpResponse(response) {
|
|
42
|
-
const normalized = {
|
|
43
|
-
isError: Boolean(response?.isError),
|
|
44
|
-
structuredContent: response?.structuredContent ?? null,
|
|
45
|
-
contentText: '',
|
|
46
|
-
content: response?.content ?? undefined
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
if (normalized.structuredContent === null && Array.isArray(response?.content)) {
|
|
50
|
-
for (const entry of response.content) {
|
|
51
|
-
if (entry?.type !== 'text' || typeof entry.text !== 'string') continue;
|
|
52
|
-
const parsed = tryParseJson(entry.text);
|
|
53
|
-
if (parsed !== null) {
|
|
54
|
-
normalized.structuredContent = parsed;
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (Array.isArray(response?.content) && response.content.length > 0) {
|
|
61
|
-
normalized.contentText = response.content
|
|
62
|
-
.map((entry) => (entry && typeof entry.text === 'string' ? entry.text : ''))
|
|
63
|
-
.filter((text) => text.length > 0)
|
|
64
|
-
.join('\n');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return normalized;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function logMcpResponse(toolName, normalizedResponse) {
|
|
71
|
-
const maxChars = Number(process.env.UNREAL_MCP_TEST_RESPONSE_MAX_CHARS ?? DEFAULT_RESPONSE_LOG_MAX_CHARS);
|
|
72
|
-
const payload = {
|
|
73
|
-
isError: normalizedResponse.isError,
|
|
74
|
-
structuredContent: normalizedResponse.structuredContent,
|
|
75
|
-
contentText: normalizedResponse.contentText,
|
|
76
|
-
content: normalizedResponse.content
|
|
77
|
-
};
|
|
78
|
-
const json = JSON.stringify(payload, null, 2);
|
|
79
|
-
console.log(`[MCP RESPONSE] ${toolName}:`);
|
|
80
|
-
console.log(clampString(json, Number.isFinite(maxChars) && maxChars > 0 ? maxChars : DEFAULT_RESPONSE_LOG_MAX_CHARS));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function formatResultLine(testCase, status, detail, durationMs) {
|
|
84
|
-
const durationText = typeof durationMs === 'number' ? ` (${durationMs.toFixed(1)} ms)` : '';
|
|
85
|
-
return `[${status.toUpperCase()}] ${testCase.scenario}${durationText}${detail ? ` => ${detail}` : ''}`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function persistResults(toolName, results) {
|
|
89
|
-
await fs.mkdir(reportsDir, { recursive: true });
|
|
90
|
-
const timestamp = new Date().toISOString().replace(/[:]/g, '-');
|
|
91
|
-
const resultsPath = path.join(reportsDir, `${toolName}-test-results-${timestamp}.json`);
|
|
92
|
-
const serializable = results.map((result) => ({
|
|
93
|
-
scenario: result.scenario,
|
|
94
|
-
toolName: result.toolName,
|
|
95
|
-
arguments: result.arguments,
|
|
96
|
-
status: result.status,
|
|
97
|
-
durationMs: result.durationMs,
|
|
98
|
-
detail: result.detail
|
|
99
|
-
}));
|
|
100
|
-
await fs.writeFile(resultsPath, JSON.stringify({ generatedAt: new Date().toISOString(), toolName, results: serializable }, null, 2));
|
|
101
|
-
return resultsPath;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function summarize(toolName, results, resultsPath) {
|
|
105
|
-
const totals = results.reduce((acc, result) => { acc.total += 1; acc[result.status] = (acc[result.status] ?? 0) + 1; return acc; }, { total: 0, passed: 0, failed: 0, skipped: 0 });
|
|
106
|
-
console.log('\n' + '='.repeat(60));
|
|
107
|
-
console.log(`${toolName} Test Summary`);
|
|
108
|
-
console.log('='.repeat(60));
|
|
109
|
-
console.log(`Total cases: ${totals.total}`);
|
|
110
|
-
console.log(`✅ Passed: ${totals.passed ?? 0}`);
|
|
111
|
-
console.log(`❌ Failed: ${totals.failed ?? 0}`);
|
|
112
|
-
console.log(`⏭️ Skipped: ${totals.skipped ?? 0}`);
|
|
113
|
-
if (totals.passed && totals.total > 0) console.log(`Pass rate: ${((totals.passed / totals.total) * 100).toFixed(1)}%`);
|
|
114
|
-
console.log(`Results saved to: ${resultsPath}`);
|
|
115
|
-
console.log('='.repeat(60));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Evaluates whether a test case passed based on expected outcome
|
|
120
|
-
*/
|
|
121
|
-
function evaluateExpectation(testCase, response) {
|
|
122
|
-
const expectation = testCase.expected;
|
|
123
|
-
|
|
124
|
-
// Normalize expected into a comparable form. If expected is an object
|
|
125
|
-
// (e.g. {condition: 'success|error', errorPattern: 'SC_DISABLED'}), then
|
|
126
|
-
// we extract the condition string as the primary expectation string.
|
|
127
|
-
const expectedCondition = (typeof expectation === 'object' && expectation !== null && expectation.condition)
|
|
128
|
-
? expectation.condition
|
|
129
|
-
: (typeof expectation === 'string' ? expectation : String(expectation));
|
|
130
|
-
|
|
131
|
-
const lowerExpected = expectedCondition.toLowerCase();
|
|
132
|
-
|
|
133
|
-
// Determine failure/success intent from condition keywords
|
|
134
|
-
const containsFailure = failureKeywords.some((word) => lowerExpected.includes(word));
|
|
135
|
-
const containsSuccess = successKeywords.some((word) => lowerExpected.includes(word));
|
|
136
|
-
|
|
137
|
-
const structuredSuccess = typeof response.structuredContent?.success === 'boolean'
|
|
138
|
-
? response.structuredContent.success
|
|
139
|
-
: undefined;
|
|
140
|
-
const actualSuccess = structuredSuccess ?? !response.isError;
|
|
141
|
-
|
|
142
|
-
// Extract actual error/message from response
|
|
143
|
-
let actualError = null;
|
|
144
|
-
let actualMessage = null;
|
|
145
|
-
if (response.structuredContent) {
|
|
146
|
-
actualError = response.structuredContent.error;
|
|
147
|
-
actualMessage = response.structuredContent.message;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Also extract flattened plain-text content for matching when structured
|
|
151
|
-
// fields are missing or when MCP errors (e.g. timeouts) are only reported
|
|
152
|
-
// via the textual content array.
|
|
153
|
-
let contentText = '';
|
|
154
|
-
if (Array.isArray(response.content) && response.content.length > 0) {
|
|
155
|
-
contentText = response.content
|
|
156
|
-
.map((entry) => (entry && typeof entry.text === 'string' ? entry.text : ''))
|
|
157
|
-
.filter((t) => t.length > 0)
|
|
158
|
-
.join('\n');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Helper to get effective actual strings for matching
|
|
162
|
-
const messageStr = (actualMessage || '').toString().toLowerCase();
|
|
163
|
-
const errorStr = (actualError || '').toString().toLowerCase();
|
|
164
|
-
const contentStr = contentText.toString().toLowerCase();
|
|
165
|
-
const combined = `${messageStr} ${errorStr} ${contentStr}`;
|
|
166
|
-
|
|
167
|
-
// If expectation is an object with specific pattern constraints, apply them
|
|
168
|
-
if (typeof expectation === 'object' && expectation !== null) {
|
|
169
|
-
// If actual outcome was success, check successPattern
|
|
170
|
-
if (actualSuccess && expectation.successPattern) {
|
|
171
|
-
const pattern = expectation.successPattern.toLowerCase();
|
|
172
|
-
if (combined.includes(pattern)) {
|
|
173
|
-
return { passed: true, reason: `Success pattern matched: ${expectation.successPattern}` };
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
// If actual outcome was error/failure, check errorPattern
|
|
177
|
-
if (!actualSuccess && expectation.errorPattern) {
|
|
178
|
-
const pattern = expectation.errorPattern.toLowerCase();
|
|
179
|
-
if (combined.includes(pattern)) {
|
|
180
|
-
return { passed: true, reason: `Error pattern matched: ${expectation.errorPattern}` };
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Handle multi-condition expectations using "or" or pipe separators
|
|
186
|
-
// e.g., "success or LOAD_FAILED" or "success|no_instances|load_failed"
|
|
187
|
-
if (lowerExpected.includes(' or ') || lowerExpected.includes('|')) {
|
|
188
|
-
const separator = lowerExpected.includes(' or ') ? ' or ' : '|';
|
|
189
|
-
const conditions = lowerExpected.split(separator).map((c) => c.trim()).filter(Boolean);
|
|
190
|
-
for (const condition of conditions) {
|
|
191
|
-
if (successKeywords.some((kw) => condition.includes(kw)) && actualSuccess === true) {
|
|
192
|
-
return { passed: true, reason: JSON.stringify(response.structuredContent) };
|
|
193
|
-
}
|
|
194
|
-
if (condition === 'handled' && response.structuredContent && response.structuredContent.handled === true) {
|
|
195
|
-
return { passed: true, reason: 'Handled gracefully' };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Special-case timeout expectations so that MCP transport timeouts
|
|
199
|
-
// (e.g. "Request timed out") satisfy conditions containing "timeout".
|
|
200
|
-
if (condition === 'timeout' || condition.includes('timeout')) {
|
|
201
|
-
if (combined.includes('timeout') || combined.includes('timed out')) {
|
|
202
|
-
return { passed: true, reason: `Expected timeout condition met: ${condition}` };
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (combined.includes(condition)) {
|
|
207
|
-
return { passed: true, reason: `Expected condition met: ${condition}` };
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
// If none of the OR/pipe conditions matched, it's a failure
|
|
211
|
-
return { passed: false, reason: `None of the expected conditions matched: ${expectedCondition}` };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Also flag common automation/plugin failure phrases
|
|
215
|
-
const pluginFailureIndicators = ['does not match prefix', 'unknown', 'not implemented', 'unavailable', 'unsupported'];
|
|
216
|
-
const hasPluginFailure = pluginFailureIndicators.some(term => combined.includes(term));
|
|
217
|
-
|
|
218
|
-
if (!containsFailure && hasPluginFailure) {
|
|
219
|
-
return {
|
|
220
|
-
passed: false,
|
|
221
|
-
reason: `Expected success but plugin reported failure: ${actualMessage || actualError}`
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// CRITICAL: Check if message says "failed" but success is true (FALSE POSITIVE)
|
|
226
|
-
if (actualSuccess && (
|
|
227
|
-
messageStr.includes('failed') ||
|
|
228
|
-
messageStr.includes('python execution failed') ||
|
|
229
|
-
errorStr.includes('failed')
|
|
230
|
-
)) {
|
|
231
|
-
return {
|
|
232
|
-
passed: false,
|
|
233
|
-
reason: `False positive: success=true but message indicates failure: ${actualMessage}`
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// CRITICAL FIX: UE_NOT_CONNECTED errors should ALWAYS fail tests unless explicitly expected
|
|
238
|
-
if (actualError === 'UE_NOT_CONNECTED') {
|
|
239
|
-
const explicitlyExpectsDisconnection = lowerExpected.includes('not connected') ||
|
|
240
|
-
lowerExpected.includes('ue_not_connected') ||
|
|
241
|
-
lowerExpected.includes('disconnected');
|
|
242
|
-
if (!explicitlyExpectsDisconnection) {
|
|
243
|
-
return {
|
|
244
|
-
passed: false,
|
|
245
|
-
reason: `Test requires Unreal Engine connection, but got: ${actualError} - ${actualMessage}`
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// For tests that expect specific error types, validate the actual error matches
|
|
251
|
-
const expectedFailure = containsFailure && !containsSuccess;
|
|
252
|
-
if (expectedFailure && !actualSuccess) {
|
|
253
|
-
// Test expects failure and got failure - but verify it's the RIGHT kind of failure
|
|
254
|
-
const lowerReason = actualMessage?.toLowerCase() || actualError?.toLowerCase() || contentStr || '';
|
|
255
|
-
|
|
256
|
-
// Check for specific error types (not just generic "error" keyword)
|
|
257
|
-
const specificErrorTypes = ['not found', 'invalid', 'missing', 'already exists', 'does not exist', 'sc_disabled'];
|
|
258
|
-
const expectedErrorType = specificErrorTypes.find(type => lowerExpected.includes(type));
|
|
259
|
-
let errorTypeMatch = expectedErrorType ? lowerReason.includes(expectedErrorType) :
|
|
260
|
-
failureKeywords.some(keyword => lowerExpected.includes(keyword) && lowerReason.includes(keyword));
|
|
261
|
-
|
|
262
|
-
// Also check detail field if main error check failed (handles wrapped exceptions)
|
|
263
|
-
if (!errorTypeMatch && response.detail && typeof response.detail === 'string') {
|
|
264
|
-
const lowerDetail = response.detail.toLowerCase();
|
|
265
|
-
if (expectedErrorType) {
|
|
266
|
-
if (lowerDetail.includes(expectedErrorType)) errorTypeMatch = true;
|
|
267
|
-
} else {
|
|
268
|
-
// If no specific error type, just check if detail contains expected string
|
|
269
|
-
if (lowerDetail.includes(lowerExpected)) errorTypeMatch = true;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// If expected outcome specifies an error type, actual error should match it
|
|
274
|
-
if (lowerExpected.includes('not found') || lowerExpected.includes('invalid') ||
|
|
275
|
-
lowerExpected.includes('missing') || lowerExpected.includes('already exists') || lowerExpected.includes('sc_disabled')) {
|
|
276
|
-
const passed = errorTypeMatch;
|
|
277
|
-
let reason;
|
|
278
|
-
if (response.isError) {
|
|
279
|
-
reason = response.content?.map((entry) => ('text' in entry ? entry.text : JSON.stringify(entry))).join('\n');
|
|
280
|
-
} else if (response.structuredContent) {
|
|
281
|
-
reason = JSON.stringify(response.structuredContent);
|
|
282
|
-
} else {
|
|
283
|
-
reason = 'No structured response returned';
|
|
284
|
-
}
|
|
285
|
-
return { passed, reason };
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Default evaluation logic
|
|
290
|
-
const passed = expectedFailure ? !actualSuccess : !!actualSuccess;
|
|
291
|
-
let reason;
|
|
292
|
-
if (response.isError) {
|
|
293
|
-
reason = response.content?.map((entry) => ('text' in entry ? entry.text : JSON.stringify(entry))).join('\n');
|
|
294
|
-
} else if (response.structuredContent) {
|
|
295
|
-
reason = JSON.stringify(response.structuredContent);
|
|
296
|
-
} else if (response.content?.length) {
|
|
297
|
-
reason = response.content.map((entry) => ('text' in entry ? entry.text : JSON.stringify(entry))).join('\n');
|
|
298
|
-
} else {
|
|
299
|
-
reason = 'No structured response returned';
|
|
300
|
-
}
|
|
301
|
-
return { passed, reason };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Main test runner function
|
|
306
|
-
*/
|
|
307
|
-
export async function runToolTests(toolName, testCases) {
|
|
308
|
-
console.log(`Total test cases: ${testCases.length}`);
|
|
309
|
-
console.log('='.repeat(60));
|
|
310
|
-
console.log('');
|
|
311
|
-
|
|
312
|
-
let transport;
|
|
313
|
-
let client;
|
|
314
|
-
const results = [];
|
|
315
|
-
// callToolOnce is assigned after the MCP client is initialized. Declare here so
|
|
316
|
-
// the test loop can call it regardless of block scoping rules.
|
|
317
|
-
let callToolOnce;
|
|
318
|
-
|
|
319
|
-
try {
|
|
320
|
-
// Wait for the automation bridge ports to be available so the spawned MCP server
|
|
321
|
-
// process can successfully connect to the editor plugin.
|
|
322
|
-
const bridgeHost = process.env.MCP_AUTOMATION_WS_HOST ?? '127.0.0.1';
|
|
323
|
-
const envPorts = process.env.MCP_AUTOMATION_WS_PORTS
|
|
324
|
-
? process.env.MCP_AUTOMATION_WS_PORTS.split(',').map((p) => parseInt(p.trim(), 10)).filter(Boolean)
|
|
325
|
-
: [8090, 8091];
|
|
326
|
-
const waitMs = 10000; // Hardcoded increased timeout
|
|
327
|
-
|
|
328
|
-
console.log(`Waiting up to ${waitMs}ms for automation bridge on ${bridgeHost}:${envPorts.join(',')}`);
|
|
329
|
-
|
|
330
|
-
async function waitForAnyPort(host, ports, timeoutMs = 10000) {
|
|
331
|
-
const start = Date.now();
|
|
332
|
-
while (Date.now() - start < timeoutMs) {
|
|
333
|
-
for (const port of ports) {
|
|
334
|
-
try {
|
|
335
|
-
await new Promise((resolve, reject) => {
|
|
336
|
-
const sock = new net.Socket();
|
|
337
|
-
let settled = false;
|
|
338
|
-
sock.setTimeout(1000);
|
|
339
|
-
sock.once('connect', () => { settled = true; sock.destroy(); resolve(true); });
|
|
340
|
-
sock.once('timeout', () => { if (!settled) { settled = true; sock.destroy(); reject(new Error('timeout')); } });
|
|
341
|
-
sock.once('error', () => { if (!settled) { settled = true; sock.destroy(); reject(new Error('error')); } });
|
|
342
|
-
sock.connect(port, host);
|
|
343
|
-
});
|
|
344
|
-
console.log(`✅ Automation bridge appears to be listening on ${host}:${port}`);
|
|
345
|
-
return port;
|
|
346
|
-
} catch {
|
|
347
|
-
// ignore and try next port
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
// Yield to the event loop once instead of sleeping.
|
|
351
|
-
await new Promise((r) => setImmediate(r));
|
|
352
|
-
}
|
|
353
|
-
throw new Error(`Timed out waiting for automation bridge on ports: ${ports.join(',')}`);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
try {
|
|
357
|
-
await waitForAnyPort(bridgeHost, envPorts, waitMs);
|
|
358
|
-
} catch (err) {
|
|
359
|
-
console.warn('Automation bridge did not become available before tests started:', err.message);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Decide whether to run the built server (dist/cli.js) or to run the
|
|
363
|
-
// TypeScript source directly. Prefer the built dist when it is up-to-date
|
|
364
|
-
// with the src tree. Fall back to running src with ts-node when dist is
|
|
365
|
-
// missing or older than the src modification time to avoid running stale code.
|
|
366
|
-
const distPath = path.join(repoRoot, 'dist', 'cli.js');
|
|
367
|
-
const srcDir = path.join(repoRoot, 'src');
|
|
368
|
-
|
|
369
|
-
async function getLatestMtime(dir) {
|
|
370
|
-
let latest = 0;
|
|
371
|
-
try {
|
|
372
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
373
|
-
for (const e of entries) {
|
|
374
|
-
const full = path.join(dir, e.name);
|
|
375
|
-
if (e.isDirectory()) {
|
|
376
|
-
const child = await getLatestMtime(full);
|
|
377
|
-
if (child > latest) latest = child;
|
|
378
|
-
} else {
|
|
379
|
-
try {
|
|
380
|
-
const st = await fs.stat(full);
|
|
381
|
-
const m = st.mtimeMs || 0;
|
|
382
|
-
if (m > latest) latest = m;
|
|
383
|
-
} catch (_) { }
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
} catch (_) {
|
|
387
|
-
// ignore
|
|
388
|
-
}
|
|
389
|
-
return latest;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Choose how to launch the server. Prefer using the built `dist/` executable so
|
|
393
|
-
// Node resolves ESM imports cleanly. If `dist/` is missing, attempt an automatic
|
|
394
|
-
// `npm run build` so users that run live tests don't hit ts-node resolution errors.
|
|
395
|
-
let useDist = false;
|
|
396
|
-
let distExists = false;
|
|
397
|
-
try {
|
|
398
|
-
await fs.access(distPath);
|
|
399
|
-
distExists = true;
|
|
400
|
-
} catch (e) {
|
|
401
|
-
distExists = false;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (process.env.UNREAL_MCP_FORCE_DIST === '1') {
|
|
405
|
-
useDist = true;
|
|
406
|
-
console.log('Forcing use of dist build via UNREAL_MCP_FORCE_DIST=1');
|
|
407
|
-
} else if (distExists) {
|
|
408
|
-
try {
|
|
409
|
-
const distStat = await fs.stat(distPath);
|
|
410
|
-
const srcLatest = await getLatestMtime(srcDir);
|
|
411
|
-
const srcIsNewer = srcLatest > (distStat.mtimeMs || 0);
|
|
412
|
-
const autoBuildEnabled = process.env.UNREAL_MCP_AUTO_BUILD === '1';
|
|
413
|
-
const autoBuildDisabled = process.env.UNREAL_MCP_NO_AUTO_BUILD === '1';
|
|
414
|
-
if (srcIsNewer) {
|
|
415
|
-
if (!autoBuildEnabled && !autoBuildDisabled) {
|
|
416
|
-
console.log('Detected newer source files than dist; attempting automatic build to refresh dist/ (set UNREAL_MCP_NO_AUTO_BUILD=1 to disable)');
|
|
417
|
-
}
|
|
418
|
-
if (autoBuildEnabled || !autoBuildDisabled) {
|
|
419
|
-
const { spawn } = await import('node:child_process');
|
|
420
|
-
try {
|
|
421
|
-
await new Promise((resolve, reject) => {
|
|
422
|
-
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
423
|
-
const ps = process.platform === 'win32'
|
|
424
|
-
? spawn(`${npmCmd} run build`, { cwd: repoRoot, stdio: 'inherit', shell: true })
|
|
425
|
-
: spawn(npmCmd, ['run', 'build'], { cwd: repoRoot, stdio: 'inherit' });
|
|
426
|
-
ps.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Build failed with code ${code}`))));
|
|
427
|
-
ps.on('error', (err) => reject(err));
|
|
428
|
-
});
|
|
429
|
-
console.log('Build succeeded — using dist/ for live tests');
|
|
430
|
-
useDist = true;
|
|
431
|
-
} catch (buildErr) {
|
|
432
|
-
console.warn('Automatic build failed or could not stat files — falling back to TypeScript source for live tests:', String(buildErr));
|
|
433
|
-
useDist = false;
|
|
434
|
-
}
|
|
435
|
-
} else {
|
|
436
|
-
console.log('Detected newer source files than dist but automatic build is disabled.');
|
|
437
|
-
console.log('Set UNREAL_MCP_AUTO_BUILD=1 to enable automatic builds, or run `npm run build` manually.');
|
|
438
|
-
useDist = false;
|
|
439
|
-
}
|
|
440
|
-
} else {
|
|
441
|
-
useDist = true;
|
|
442
|
-
console.log('Using built dist for live tests');
|
|
443
|
-
}
|
|
444
|
-
} catch (buildErr) {
|
|
445
|
-
console.warn('Automatic build failed or could not stat files — falling back to TypeScript source for live tests:', String(buildErr));
|
|
446
|
-
useDist = false;
|
|
447
|
-
console.log('Preferring TypeScript source for tests to pick up local changes (set UNREAL_MCP_FORCE_DIST=1 to force dist)');
|
|
448
|
-
}
|
|
449
|
-
} else {
|
|
450
|
-
console.log('dist not found — attempting to run `npm run build` to produce dist/ for live tests');
|
|
451
|
-
try {
|
|
452
|
-
const { spawn } = await import('node:child_process');
|
|
453
|
-
await new Promise((resolve, reject) => {
|
|
454
|
-
const ps = spawn(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build'], { cwd: repoRoot, stdio: 'inherit' });
|
|
455
|
-
ps.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Build failed with code ${code}`))));
|
|
456
|
-
ps.on('error', (err) => reject(err));
|
|
457
|
-
});
|
|
458
|
-
useDist = true;
|
|
459
|
-
console.log('Build succeeded — using dist/ for live tests');
|
|
460
|
-
} catch (buildErr) {
|
|
461
|
-
console.warn('Automatic build failed — falling back to running TypeScript source with ts-node-esm:', String(buildErr));
|
|
462
|
-
useDist = false;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (!useDist) {
|
|
467
|
-
serverCommand = process.env.UNREAL_MCP_SERVER_CMD ?? 'npx';
|
|
468
|
-
serverArgs = ['ts-node-esm', path.join(repoRoot, 'src', 'cli.ts')];
|
|
469
|
-
} else {
|
|
470
|
-
serverCommand = process.env.UNREAL_MCP_SERVER_CMD ?? serverCommand;
|
|
471
|
-
serverArgs = process.env.UNREAL_MCP_SERVER_ARGS?.split(',') ?? serverArgs;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
transport = new StdioClientTransport({
|
|
475
|
-
command: serverCommand,
|
|
476
|
-
args: serverArgs,
|
|
477
|
-
cwd: serverCwd,
|
|
478
|
-
stderr: 'inherit',
|
|
479
|
-
env: serverEnv
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
client = new Client({
|
|
483
|
-
name: 'unreal-mcp-test-runner',
|
|
484
|
-
version: '1.0.0'
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
await client.connect(transport);
|
|
488
|
-
await client.listTools({});
|
|
489
|
-
console.log('✅ Connected to Unreal MCP Server\n');
|
|
490
|
-
|
|
491
|
-
// Single-attempt call helper (no retries). This forwards a timeoutMs
|
|
492
|
-
// argument to the server so server-side automation calls use the same
|
|
493
|
-
// timeout the test harness expects.
|
|
494
|
-
callToolOnce = async function (callOptions, baseTimeoutMs) {
|
|
495
|
-
const envDefault = Number(process.env.UNREAL_MCP_TEST_CALL_TIMEOUT_MS ?? '60000') || 60000;
|
|
496
|
-
const perCall = Number(callOptions?.arguments?.timeoutMs) || undefined;
|
|
497
|
-
const base = typeof baseTimeoutMs === 'number' && baseTimeoutMs > 0 ? baseTimeoutMs : (perCall || envDefault);
|
|
498
|
-
const timeoutMs = base;
|
|
499
|
-
try {
|
|
500
|
-
console.log(`[CALL] ${callOptions.name} (timeout ${timeoutMs}ms)`);
|
|
501
|
-
const outgoing = Object.assign({}, callOptions, { arguments: { ...(callOptions.arguments || {}), timeoutMs } });
|
|
502
|
-
// Prefer instructing the MCP client to use a matching timeout if
|
|
503
|
-
// the client library supports per-call options; fall back to the
|
|
504
|
-
// plain call if not supported.
|
|
505
|
-
let callPromise;
|
|
506
|
-
try {
|
|
507
|
-
// Correct parameter order: (params, resultSchema?, options)
|
|
508
|
-
callPromise = client.callTool(outgoing, undefined, { timeout: timeoutMs });
|
|
509
|
-
} catch (err) {
|
|
510
|
-
// Fall back to calling the older signature where options might be second param
|
|
511
|
-
try {
|
|
512
|
-
callPromise = client.callTool(outgoing, { timeout: timeoutMs });
|
|
513
|
-
} catch (inner) {
|
|
514
|
-
try {
|
|
515
|
-
callPromise = client.callTool(outgoing);
|
|
516
|
-
} catch (inner2) {
|
|
517
|
-
throw inner2 || inner || err;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
let timeoutId;
|
|
523
|
-
const timeoutPromise = new Promise((_, rej) => {
|
|
524
|
-
timeoutId = setTimeout(() => rej(new Error(`Local test runner timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
525
|
-
if (timeoutId && typeof timeoutId.unref === 'function') {
|
|
526
|
-
timeoutId.unref();
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
try {
|
|
530
|
-
const timed = Promise.race([
|
|
531
|
-
callPromise,
|
|
532
|
-
timeoutPromise
|
|
533
|
-
]);
|
|
534
|
-
return await timed;
|
|
535
|
-
} finally {
|
|
536
|
-
if (timeoutId) {
|
|
537
|
-
clearTimeout(timeoutId);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
} catch (e) {
|
|
541
|
-
const msg = String(e?.message || e || '');
|
|
542
|
-
if (msg.includes('Unknown blueprint action')) {
|
|
543
|
-
return { structuredContent: { success: false, error: msg } };
|
|
544
|
-
}
|
|
545
|
-
throw e;
|
|
546
|
-
}
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
// Run each test case
|
|
550
|
-
for (let i = 0; i < testCases.length; i++) {
|
|
551
|
-
const testCase = testCases[i];
|
|
552
|
-
const testCaseTimeoutMs = Number(process.env.UNREAL_MCP_TEST_CASE_TIMEOUT_MS ?? testCase.arguments?.timeoutMs ?? '180000');
|
|
553
|
-
const startTime = performance.now();
|
|
554
|
-
|
|
555
|
-
try {
|
|
556
|
-
// Log test start to Unreal Engine console
|
|
557
|
-
const cleanScenario = (testCase.scenario || 'Unknown Test').replace(/"/g, "'");
|
|
558
|
-
await callToolOnce({
|
|
559
|
-
name: 'system_control',
|
|
560
|
-
arguments: { action: 'console_command', command: `Log "---- STARTING TEST: ${cleanScenario} ----"` }
|
|
561
|
-
}, 5000).catch(() => { });
|
|
562
|
-
} catch (e) { /* ignore */ }
|
|
563
|
-
|
|
564
|
-
try {
|
|
565
|
-
const response = await callToolOnce({ name: testCase.toolName, arguments: testCase.arguments }, testCaseTimeoutMs);
|
|
566
|
-
|
|
567
|
-
const endTime = performance.now();
|
|
568
|
-
const durationMs = endTime - startTime;
|
|
569
|
-
|
|
570
|
-
let structuredContent = response.structuredContent ?? null;
|
|
571
|
-
if (structuredContent === null && response.content?.length) {
|
|
572
|
-
for (const entry of response.content) {
|
|
573
|
-
if (entry?.type !== 'text' || typeof entry.text !== 'string') continue;
|
|
574
|
-
try { structuredContent = JSON.parse(entry.text); break; } catch { }
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
const normalizedResponse = { ...response, structuredContent };
|
|
578
|
-
if (RESPONSE_LOGGING_ENABLED) {
|
|
579
|
-
logMcpResponse(testCase.toolName + " :: " + testCase.scenario, normalizeMcpResponse(normalizedResponse));
|
|
580
|
-
}
|
|
581
|
-
const { passed, reason } = evaluateExpectation(testCase, normalizedResponse);
|
|
582
|
-
|
|
583
|
-
if (!passed) {
|
|
584
|
-
console.log(`[FAILED] ${testCase.scenario} (${durationMs.toFixed(1)} ms) => ${reason}`);
|
|
585
|
-
results.push({
|
|
586
|
-
scenario: testCase.scenario,
|
|
587
|
-
toolName: testCase.toolName,
|
|
588
|
-
arguments: testCase.arguments,
|
|
589
|
-
status: 'failed',
|
|
590
|
-
durationMs,
|
|
591
|
-
detail: reason,
|
|
592
|
-
response: normalizedResponse
|
|
593
|
-
});
|
|
594
|
-
} else {
|
|
595
|
-
console.log(`[PASSED] ${testCase.scenario} (${durationMs.toFixed(1)} ms)`);
|
|
596
|
-
results.push({
|
|
597
|
-
scenario: testCase.scenario,
|
|
598
|
-
toolName: testCase.toolName,
|
|
599
|
-
arguments: testCase.arguments,
|
|
600
|
-
status: 'passed',
|
|
601
|
-
durationMs,
|
|
602
|
-
detail: reason
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
} catch (error) {
|
|
607
|
-
const endTime = performance.now();
|
|
608
|
-
const durationMs = endTime - startTime;
|
|
609
|
-
const errorMessage = String(error?.message || error || '');
|
|
610
|
-
const lowerExpected = (testCase.expected || '').toString().toLowerCase();
|
|
611
|
-
const lowerError = errorMessage.toLowerCase();
|
|
612
|
-
|
|
613
|
-
// If the test explicitly expects a timeout (e.g. "timeout|error"), then
|
|
614
|
-
// an MCP/client timeout should be treated as the expected outcome rather
|
|
615
|
-
// than as a hard harness failure. Accept both "timeout" and "timed out"
|
|
616
|
-
// phrasing from different MCP client implementations.
|
|
617
|
-
if (lowerExpected.includes('timeout') && (lowerError.includes('timeout') || lowerError.includes('timed out'))) {
|
|
618
|
-
console.log(`[PASSED] ${testCase.scenario} (${durationMs.toFixed(1)} ms)`);
|
|
619
|
-
results.push({
|
|
620
|
-
scenario: testCase.scenario,
|
|
621
|
-
toolName: testCase.toolName,
|
|
622
|
-
arguments: testCase.arguments,
|
|
623
|
-
status: 'passed',
|
|
624
|
-
durationMs,
|
|
625
|
-
detail: errorMessage
|
|
626
|
-
});
|
|
627
|
-
continue;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
console.log(`[FAILED] ${testCase.scenario} (${durationMs.toFixed(1)} ms) => Error: ${errorMessage}`);
|
|
631
|
-
results.push({
|
|
632
|
-
scenario: testCase.scenario,
|
|
633
|
-
toolName: testCase.toolName,
|
|
634
|
-
arguments: testCase.arguments,
|
|
635
|
-
status: 'failed',
|
|
636
|
-
durationMs,
|
|
637
|
-
detail: errorMessage
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const resultsPath = await persistResults(toolName, results);
|
|
643
|
-
summarize(toolName, results, resultsPath);
|
|
644
|
-
|
|
645
|
-
const hasFailures = results.some((result) => result.status === 'failed');
|
|
646
|
-
process.exitCode = hasFailures ? 1 : 0;
|
|
647
|
-
|
|
648
|
-
} catch (error) {
|
|
649
|
-
console.error('Test runner failed:', error);
|
|
650
|
-
process.exit(1);
|
|
651
|
-
} finally {
|
|
652
|
-
if (client) {
|
|
653
|
-
try {
|
|
654
|
-
await client.close();
|
|
655
|
-
} catch {
|
|
656
|
-
// ignore
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
if (transport) {
|
|
660
|
-
try {
|
|
661
|
-
await transport.close();
|
|
662
|
-
} catch {
|
|
663
|
-
// ignore
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
export class TestRunner {
|
|
670
|
-
constructor(suiteName) {
|
|
671
|
-
this.suiteName = suiteName || 'Test Suite';
|
|
672
|
-
this.steps = [];
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
addStep(name, fn) {
|
|
676
|
-
this.steps.push({ name, fn });
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
async run() {
|
|
680
|
-
if (this.steps.length === 0) {
|
|
681
|
-
console.warn(`No steps registered for ${this.suiteName}`);
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
console.log('\n' + '='.repeat(60));
|
|
686
|
-
console.log(`${this.suiteName}`);
|
|
687
|
-
console.log('='.repeat(60));
|
|
688
|
-
console.log(`Total steps: ${this.steps.length}`);
|
|
689
|
-
console.log('');
|
|
690
|
-
|
|
691
|
-
let transport;
|
|
692
|
-
let client;
|
|
693
|
-
const results = [];
|
|
694
|
-
|
|
695
|
-
try {
|
|
696
|
-
const bridgeHost = process.env.MCP_AUTOMATION_WS_HOST ?? '127.0.0.1';
|
|
697
|
-
const envPorts = process.env.MCP_AUTOMATION_WS_PORTS
|
|
698
|
-
? process.env.MCP_AUTOMATION_WS_PORTS.split(',').map((p) => parseInt(p.trim(), 10)).filter(Boolean)
|
|
699
|
-
: [8090, 8091];
|
|
700
|
-
const waitMs = parseInt(process.env.UNREAL_MCP_WAIT_PORT_MS ?? '5000', 10);
|
|
701
|
-
|
|
702
|
-
async function waitForAnyPort(host, ports, timeoutMs = 10000) {
|
|
703
|
-
const start = Date.now();
|
|
704
|
-
while (Date.now() - start < timeoutMs) {
|
|
705
|
-
for (const port of ports) {
|
|
706
|
-
try {
|
|
707
|
-
await new Promise((resolve, reject) => {
|
|
708
|
-
const sock = new net.Socket();
|
|
709
|
-
let settled = false;
|
|
710
|
-
sock.setTimeout(1000);
|
|
711
|
-
sock.once('connect', () => { settled = true; sock.destroy(); resolve(true); });
|
|
712
|
-
sock.once('timeout', () => { if (!settled) { settled = true; sock.destroy(); reject(new Error('timeout')); } });
|
|
713
|
-
sock.once('error', () => { if (!settled) { settled = true; sock.destroy(); reject(new Error('error')); } });
|
|
714
|
-
sock.connect(port, host);
|
|
715
|
-
});
|
|
716
|
-
console.log(`✅ Automation bridge appears to be listening on ${host}:${port}`);
|
|
717
|
-
return port;
|
|
718
|
-
} catch {
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
await new Promise((r) => setImmediate(r));
|
|
722
|
-
}
|
|
723
|
-
throw new Error(`Timed out waiting for automation bridge on ports: ${ports.join(',')}`);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
try {
|
|
727
|
-
await waitForAnyPort(bridgeHost, envPorts, waitMs);
|
|
728
|
-
} catch (err) {
|
|
729
|
-
console.warn('Automation bridge did not become available before tests started:', err.message);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
const distPath = path.join(repoRoot, 'dist', 'cli.js');
|
|
733
|
-
const srcDir = path.join(repoRoot, 'src');
|
|
734
|
-
|
|
735
|
-
async function getLatestMtime(dir) {
|
|
736
|
-
let latest = 0;
|
|
737
|
-
try {
|
|
738
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
739
|
-
for (const e of entries) {
|
|
740
|
-
const full = path.join(dir, e.name);
|
|
741
|
-
if (e.isDirectory()) {
|
|
742
|
-
const child = await getLatestMtime(full);
|
|
743
|
-
if (child > latest) latest = child;
|
|
744
|
-
} else {
|
|
745
|
-
try {
|
|
746
|
-
const st = await fs.stat(full);
|
|
747
|
-
const m = st.mtimeMs || 0;
|
|
748
|
-
if (m > latest) latest = m;
|
|
749
|
-
} catch (_) { }
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
} catch (_) {
|
|
753
|
-
}
|
|
754
|
-
return latest;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
let useDist = false;
|
|
758
|
-
let distExists = false;
|
|
759
|
-
try {
|
|
760
|
-
await fs.access(distPath);
|
|
761
|
-
distExists = true;
|
|
762
|
-
} catch (e) {
|
|
763
|
-
distExists = false;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
if (process.env.UNREAL_MCP_FORCE_DIST === '1') {
|
|
767
|
-
useDist = true;
|
|
768
|
-
console.log('Forcing use of dist build via UNREAL_MCP_FORCE_DIST=1');
|
|
769
|
-
} else if (distExists) {
|
|
770
|
-
try {
|
|
771
|
-
const distStat = await fs.stat(distPath);
|
|
772
|
-
const srcLatest = await getLatestMtime(srcDir);
|
|
773
|
-
const srcIsNewer = srcLatest > (distStat.mtimeMs || 0);
|
|
774
|
-
const autoBuildEnabled = process.env.UNREAL_MCP_AUTO_BUILD === '1';
|
|
775
|
-
const autoBuildDisabled = process.env.UNREAL_MCP_NO_AUTO_BUILD === '1';
|
|
776
|
-
if (srcIsNewer) {
|
|
777
|
-
if (!autoBuildEnabled && !autoBuildDisabled) {
|
|
778
|
-
console.log('Detected newer source files than dist; attempting automatic build to refresh dist/ (set UNREAL_MCP_NO_AUTO_BUILD=1 to disable)');
|
|
779
|
-
}
|
|
780
|
-
if (autoBuildEnabled || !autoBuildDisabled) {
|
|
781
|
-
const { spawn } = await import('node:child_process');
|
|
782
|
-
try {
|
|
783
|
-
await new Promise((resolve, reject) => {
|
|
784
|
-
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
785
|
-
const ps = spawn(npmCmd, ['run', 'build'], { cwd: repoRoot, stdio: 'inherit', shell: process.platform === 'win32' });
|
|
786
|
-
ps.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Build failed with code ${code}`))));
|
|
787
|
-
ps.on('error', (err) => reject(err));
|
|
788
|
-
});
|
|
789
|
-
console.log('Build succeeded — using dist/ for live tests');
|
|
790
|
-
useDist = true;
|
|
791
|
-
} catch (buildErr) {
|
|
792
|
-
console.warn('Automatic build failed or could not stat files — falling back to TypeScript source for live tests:', String(buildErr));
|
|
793
|
-
useDist = false;
|
|
794
|
-
}
|
|
795
|
-
} else {
|
|
796
|
-
console.log('Detected newer source files than dist but automatic build is disabled.');
|
|
797
|
-
console.log('Set UNREAL_MCP_AUTO_BUILD=1 to enable automatic builds, or run `npm run build` manually.');
|
|
798
|
-
useDist = false;
|
|
799
|
-
}
|
|
800
|
-
} else {
|
|
801
|
-
useDist = true;
|
|
802
|
-
console.log('Using built dist for live tests');
|
|
803
|
-
}
|
|
804
|
-
} catch (buildErr) {
|
|
805
|
-
console.warn('Automatic build failed or could not stat files — falling back to TypeScript source for live tests:', String(buildErr));
|
|
806
|
-
useDist = false;
|
|
807
|
-
console.log('Preferring TypeScript source for tests to pick up local changes (set UNREAL_MCP_FORCE_DIST=1 to force dist)');
|
|
808
|
-
}
|
|
809
|
-
} else {
|
|
810
|
-
console.log('dist not found — attempting to run `npm run build` to produce dist/ for live tests');
|
|
811
|
-
try {
|
|
812
|
-
const { spawn } = await import('node:child_process');
|
|
813
|
-
await new Promise((resolve, reject) => {
|
|
814
|
-
const ps = spawn(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build'], { cwd: repoRoot, stdio: 'inherit' });
|
|
815
|
-
ps.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Build failed with code ${code}`))));
|
|
816
|
-
ps.on('error', (err) => reject(err));
|
|
817
|
-
});
|
|
818
|
-
useDist = true;
|
|
819
|
-
console.log('Build succeeded — using dist/ for live tests');
|
|
820
|
-
} catch (buildErr) {
|
|
821
|
-
console.warn('Automatic build failed — falling back to running TypeScript source with ts-node-esm:', String(buildErr));
|
|
822
|
-
useDist = false;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
if (!useDist) {
|
|
827
|
-
serverCommand = process.env.UNREAL_MCP_SERVER_CMD ?? 'npx';
|
|
828
|
-
serverArgs = ['ts-node-esm', path.join(repoRoot, 'src', 'cli.ts')];
|
|
829
|
-
} else {
|
|
830
|
-
serverCommand = process.env.UNREAL_MCP_SERVER_CMD ?? serverCommand;
|
|
831
|
-
serverArgs = process.env.UNREAL_MCP_SERVER_ARGS?.split(',') ?? serverArgs;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
transport = new StdioClientTransport({
|
|
835
|
-
command: serverCommand,
|
|
836
|
-
args: serverArgs,
|
|
837
|
-
cwd: serverCwd,
|
|
838
|
-
stderr: 'inherit',
|
|
839
|
-
env: serverEnv
|
|
840
|
-
});
|
|
841
|
-
|
|
842
|
-
client = new Client({
|
|
843
|
-
name: 'unreal-mcp-step-runner',
|
|
844
|
-
version: '1.0.0'
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
await client.connect(transport);
|
|
848
|
-
await client.listTools({});
|
|
849
|
-
console.log('✅ Connected to Unreal MCP Server\n');
|
|
850
|
-
|
|
851
|
-
const callToolOnce = async function (callOptions, baseTimeoutMs) {
|
|
852
|
-
const envDefault = Number(process.env.UNREAL_MCP_TEST_CALL_TIMEOUT_MS ?? '60000') || 60000;
|
|
853
|
-
const perCall = Number(callOptions?.arguments?.timeoutMs) || undefined;
|
|
854
|
-
const base = typeof baseTimeoutMs === 'number' && baseTimeoutMs > 0 ? baseTimeoutMs : (perCall || envDefault);
|
|
855
|
-
const timeoutMs = base;
|
|
856
|
-
try {
|
|
857
|
-
console.log(`[CALL] ${callOptions.name} (timeout ${timeoutMs}ms)`);
|
|
858
|
-
const outgoing = Object.assign({}, callOptions, { arguments: { ...(callOptions.arguments || {}), timeoutMs } });
|
|
859
|
-
let callPromise;
|
|
860
|
-
try {
|
|
861
|
-
callPromise = client.callTool(outgoing, undefined, { timeout: timeoutMs });
|
|
862
|
-
} catch (err) {
|
|
863
|
-
try {
|
|
864
|
-
callPromise = client.callTool(outgoing, { timeout: timeoutMs });
|
|
865
|
-
} catch (inner) {
|
|
866
|
-
try {
|
|
867
|
-
callPromise = client.callTool(outgoing);
|
|
868
|
-
} catch (inner2) {
|
|
869
|
-
throw inner2 || inner || err;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
let timeoutId;
|
|
875
|
-
const timeoutPromise = new Promise((_, rej) => {
|
|
876
|
-
timeoutId = setTimeout(() => rej(new Error(`Local test runner timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
877
|
-
if (timeoutId && typeof timeoutId.unref === 'function') {
|
|
878
|
-
timeoutId.unref();
|
|
879
|
-
}
|
|
880
|
-
});
|
|
881
|
-
try {
|
|
882
|
-
const timed = Promise.race([
|
|
883
|
-
callPromise,
|
|
884
|
-
timeoutPromise
|
|
885
|
-
]);
|
|
886
|
-
return await timed;
|
|
887
|
-
} finally {
|
|
888
|
-
if (timeoutId) {
|
|
889
|
-
clearTimeout(timeoutId);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
} catch (e) {
|
|
893
|
-
const msg = String(e?.message || e || '');
|
|
894
|
-
if (msg.includes('Unknown blueprint action')) {
|
|
895
|
-
return { structuredContent: { success: false, error: msg } };
|
|
896
|
-
}
|
|
897
|
-
throw e;
|
|
898
|
-
}
|
|
899
|
-
};
|
|
900
|
-
|
|
901
|
-
const tools = {
|
|
902
|
-
async executeTool(toolName, args, options = {}) {
|
|
903
|
-
const timeoutMs = typeof options.timeoutMs === 'number' ? options.timeoutMs : undefined;
|
|
904
|
-
const response = await callToolOnce({ name: toolName, arguments: args }, timeoutMs);
|
|
905
|
-
let structuredContent = response.structuredContent ?? null;
|
|
906
|
-
if (structuredContent === null && response.content?.length) {
|
|
907
|
-
for (const entry of response.content) {
|
|
908
|
-
if (entry?.type !== 'text' || typeof entry.text !== 'string') continue;
|
|
909
|
-
try {
|
|
910
|
-
structuredContent = JSON.parse(entry.text);
|
|
911
|
-
break;
|
|
912
|
-
} catch {
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
if (structuredContent && typeof structuredContent === 'object') {
|
|
918
|
-
return structuredContent;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
return {
|
|
922
|
-
success: !response.isError,
|
|
923
|
-
message: undefined,
|
|
924
|
-
error: undefined
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
};
|
|
928
|
-
|
|
929
|
-
for (const step of this.steps) {
|
|
930
|
-
const startTime = performance.now();
|
|
931
|
-
|
|
932
|
-
try {
|
|
933
|
-
// Log step start to Unreal Engine console
|
|
934
|
-
const cleanName = (step.name || 'Unknown Step').replace(/"/g, "'");
|
|
935
|
-
await callToolOnce({
|
|
936
|
-
name: 'system_control',
|
|
937
|
-
arguments: { action: 'console_command', command: `Log "---- STARTING STEP: ${cleanName} ----"` }
|
|
938
|
-
}, 5000).catch(() => { });
|
|
939
|
-
} catch (e) { /* ignore */ }
|
|
940
|
-
|
|
941
|
-
try {
|
|
942
|
-
const ok = await step.fn(tools);
|
|
943
|
-
const durationMs = performance.now() - startTime;
|
|
944
|
-
const status = ok ? 'passed' : 'failed';
|
|
945
|
-
console.log(formatResultLine({ scenario: step.name }, status, ok ? '' : 'Step returned false', durationMs));
|
|
946
|
-
results.push({
|
|
947
|
-
scenario: step.name,
|
|
948
|
-
toolName: null,
|
|
949
|
-
arguments: null,
|
|
950
|
-
status,
|
|
951
|
-
durationMs,
|
|
952
|
-
detail: ok ? undefined : 'Step returned false'
|
|
953
|
-
});
|
|
954
|
-
} catch (err) {
|
|
955
|
-
const durationMs = performance.now() - startTime;
|
|
956
|
-
const detail = err?.message || String(err);
|
|
957
|
-
console.log(formatResultLine({ scenario: step.name }, 'failed', detail, durationMs));
|
|
958
|
-
results.push({
|
|
959
|
-
scenario: step.name,
|
|
960
|
-
toolName: null,
|
|
961
|
-
arguments: null,
|
|
962
|
-
status: 'failed',
|
|
963
|
-
durationMs,
|
|
964
|
-
detail
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
const resultsPath = await persistResults(this.suiteName, results);
|
|
970
|
-
summarize(this.suiteName, results, resultsPath);
|
|
971
|
-
|
|
972
|
-
const hasFailures = results.some((result) => result.status === 'failed');
|
|
973
|
-
process.exitCode = hasFailures ? 1 : 0;
|
|
974
|
-
} catch (error) {
|
|
975
|
-
console.error('Step-based test runner failed:', error);
|
|
976
|
-
process.exit(1);
|
|
977
|
-
} finally {
|
|
978
|
-
if (client) {
|
|
979
|
-
try {
|
|
980
|
-
await client.close();
|
|
981
|
-
} catch {
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
if (transport) {
|
|
985
|
-
try {
|
|
986
|
-
await transport.close();
|
|
987
|
-
} catch {
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
|