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,76 @@
|
|
|
1
|
+
export class CommandValidator {
|
|
2
|
+
private static readonly DANGEROUS_COMMANDS = [
|
|
3
|
+
'quit', 'exit', 'delete', 'destroy', 'kill', 'crash',
|
|
4
|
+
'viewmode visualizebuffer basecolor',
|
|
5
|
+
'viewmode visualizebuffer worldnormal',
|
|
6
|
+
'r.gpucrash',
|
|
7
|
+
'buildpaths', // Can cause access violation if nav system not initialized
|
|
8
|
+
'rebuildnavigation', // Can also crash without proper nav setup
|
|
9
|
+
'obj garbage', 'obj list', 'memreport' // Heavy debug commands that can stall
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
private static readonly FORBIDDEN_TOKENS = [
|
|
13
|
+
'rm ', 'rm-', 'del ', 'format ', 'shutdown', 'reboot',
|
|
14
|
+
'rmdir', 'mklink', 'copy ', 'move ', 'start "', 'system(',
|
|
15
|
+
'import os', 'import subprocess', 'subprocess.', 'os.system',
|
|
16
|
+
'exec(', 'eval(', '__import__', 'import sys', 'import importlib',
|
|
17
|
+
'with open', 'open(', 'write(', 'read('
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
private static readonly INVALID_PATTERNS = [
|
|
21
|
+
/^\d+$/, // Just numbers
|
|
22
|
+
/^invalid_command/i,
|
|
23
|
+
/^this_is_not_a_valid/i,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
static validate(command: string): void {
|
|
27
|
+
if (!command || typeof command !== 'string') {
|
|
28
|
+
throw new Error('Invalid command: must be a non-empty string');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const cmdTrimmed = command.trim();
|
|
32
|
+
if (cmdTrimmed.length === 0) {
|
|
33
|
+
return; // Empty commands are technically valid (no-op)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (cmdTrimmed.includes('\n') || cmdTrimmed.includes('\r')) {
|
|
37
|
+
throw new Error('Multi-line console commands are not allowed. Send one command per call.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const cmdLower = cmdTrimmed.toLowerCase();
|
|
41
|
+
|
|
42
|
+
if (cmdLower === 'py' || cmdLower.startsWith('py ')) {
|
|
43
|
+
throw new Error('Python console commands are blocked from external calls for safety.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this.DANGEROUS_COMMANDS.some(dangerous => cmdLower.includes(dangerous))) {
|
|
47
|
+
throw new Error(`Dangerous command blocked: ${command}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (cmdLower.includes('&&') || cmdLower.includes('||')) {
|
|
51
|
+
throw new Error('Command chaining with && or || is blocked for safety.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (this.FORBIDDEN_TOKENS.some(token => cmdLower.includes(token))) {
|
|
55
|
+
throw new Error(`Command contains unsafe token and was blocked: ${command}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static isLikelyInvalid(command: string): boolean {
|
|
60
|
+
const cmdTrimmed = command.trim();
|
|
61
|
+
return this.INVALID_PATTERNS.some(pattern => pattern.test(cmdTrimmed));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static getPriority(command: string): number {
|
|
65
|
+
if (command.includes('BuildLighting') || command.includes('BuildPaths')) {
|
|
66
|
+
return 1; // Heavy operation
|
|
67
|
+
} else if (command.includes('summon') || command.includes('spawn')) {
|
|
68
|
+
return 5; // Medium operation
|
|
69
|
+
} else if (command.startsWith('stat')) {
|
|
70
|
+
return 8; // Dedicated throttling for stat commands
|
|
71
|
+
} else if (command.startsWith('show')) {
|
|
72
|
+
return 9; // Light operation
|
|
73
|
+
}
|
|
74
|
+
return 7; // Default priority
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/utils/elicitation.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
2
2
|
import { Logger } from './logger.js';
|
|
3
3
|
|
|
4
4
|
// Minimal helper to opportunistically use MCP Elicitation when available.
|
|
5
|
-
// Safe across clients: validates schema shape
|
|
5
|
+
// Safe across clients: validates schema shape and handles timeouts and -32601 errors.
|
|
6
6
|
export type PrimitiveSchema =
|
|
7
7
|
| { type: 'string'; title?: string; description?: string; minLength?: number; maxLength?: number; pattern?: string; format?: 'email'|'uri'|'date'|'date-time'; default?: string }
|
|
8
8
|
| { type: 'number'|'integer'; title?: string; description?: string; minimum?: number; maximum?: number; default?: number }
|
|
@@ -17,7 +17,9 @@ export interface ElicitSchema {
|
|
|
17
17
|
|
|
18
18
|
export interface ElicitOptions {
|
|
19
19
|
timeoutMs?: number;
|
|
20
|
-
|
|
20
|
+
// Handler invoked when elicitation cannot be performed; previously named
|
|
21
|
+
// Handler invoked when elicitation cannot be performed.
|
|
22
|
+
alternate?: () => Promise<{ ok: boolean; value?: any; error?: string }>;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export function createElicitationHelper(server: Server, log: Logger) {
|
|
@@ -77,7 +79,7 @@ export function createElicitationHelper(server: Server, log: Logger) {
|
|
|
77
79
|
|
|
78
80
|
async function elicit(message: string, requestedSchema: ElicitSchema, opts: ElicitOptions = {}) {
|
|
79
81
|
if (!supported || !isSafeSchema(requestedSchema)) {
|
|
80
|
-
if (opts.
|
|
82
|
+
if (opts.alternate) return opts.alternate();
|
|
81
83
|
return { ok: false, error: 'elicitation-unsupported' };
|
|
82
84
|
}
|
|
83
85
|
|
|
@@ -98,10 +100,10 @@ export function createElicitationHelper(server: Server, log: Logger) {
|
|
|
98
100
|
|
|
99
101
|
if (action === 'accept') return { ok: true, value: content };
|
|
100
102
|
if (action === 'decline' || action === 'cancel') {
|
|
101
|
-
if (opts.
|
|
103
|
+
if (opts.alternate) return opts.alternate();
|
|
102
104
|
return { ok: false, error: action };
|
|
103
105
|
}
|
|
104
|
-
if (opts.
|
|
106
|
+
if (opts.alternate) return opts.alternate();
|
|
105
107
|
return { ok: false, error: 'unexpected-response' };
|
|
106
108
|
} catch (e: any) {
|
|
107
109
|
const msg = String(e?.message || e);
|
|
@@ -115,8 +117,9 @@ export function createElicitationHelper(server: Server, log: Logger) {
|
|
|
115
117
|
) {
|
|
116
118
|
supported = false;
|
|
117
119
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
// Use an alternate handler if provided when elicitation fails.
|
|
121
|
+
log.debug('Elicitation failed; using alternate handler', { error: msg, code });
|
|
122
|
+
if (opts.alternate) return opts.alternate();
|
|
120
123
|
return { ok: false, error: msg.includes('timeout') ? 'timeout' : 'rpc-failed' };
|
|
121
124
|
}
|
|
122
125
|
}
|
|
@@ -95,7 +95,7 @@ export class ErrorHandler {
|
|
|
95
95
|
// Unreal Engine specific errors
|
|
96
96
|
if (
|
|
97
97
|
errorMessage.includes('unreal') ||
|
|
98
|
-
errorMessage.includes('
|
|
98
|
+
errorMessage.includes('connection failed') ||
|
|
99
99
|
errorMessage.includes('blueprint') ||
|
|
100
100
|
errorMessage.includes('actor') ||
|
|
101
101
|
errorMessage.includes('asset')
|
|
@@ -128,7 +128,7 @@ export class ErrorHandler {
|
|
|
128
128
|
|
|
129
129
|
switch (type) {
|
|
130
130
|
case ErrorType.CONNECTION:
|
|
131
|
-
return 'Failed to connect to Unreal Engine. Please ensure
|
|
131
|
+
return 'Failed to connect to Unreal Engine. Please ensure the Automation Bridge plugin is active and the editor is running.';
|
|
132
132
|
|
|
133
133
|
case ErrorType.VALIDATION:
|
|
134
134
|
return `Invalid input: ${originalMessage}`;
|
|
@@ -164,114 +164,38 @@ export class ErrorHandler {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/**
|
|
167
|
-
* Retry
|
|
168
|
-
* Best practice from TypeScript async programming patterns
|
|
169
|
-
* @param operation - Async operation to retry
|
|
170
|
-
* @param options - Retry configuration
|
|
171
|
-
* @returns Result of the operation
|
|
167
|
+
* Retry a function with exponential backoff
|
|
172
168
|
*/
|
|
173
169
|
static async retryWithBackoff<T>(
|
|
174
|
-
|
|
170
|
+
fn: () => Promise<T>,
|
|
175
171
|
options: {
|
|
176
172
|
maxRetries?: number;
|
|
177
173
|
initialDelay?: number;
|
|
178
174
|
maxDelay?: number;
|
|
179
175
|
backoffMultiplier?: number;
|
|
180
|
-
shouldRetry?: (error:
|
|
176
|
+
shouldRetry?: (error: any) => boolean;
|
|
181
177
|
} = {}
|
|
182
178
|
): Promise<T> {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
shouldRetry = (error) => this.isRetriable(error)
|
|
189
|
-
} = options;
|
|
179
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
180
|
+
const initialDelay = options.initialDelay ?? 1000;
|
|
181
|
+
const maxDelay = options.maxDelay ?? 10000;
|
|
182
|
+
const multiplier = options.backoffMultiplier ?? 2;
|
|
183
|
+
const shouldRetry = options.shouldRetry ?? ((err) => this.isRetriable(err));
|
|
190
184
|
|
|
191
|
-
let lastError: unknown;
|
|
192
185
|
let delay = initialDelay;
|
|
193
186
|
|
|
194
187
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
195
188
|
try {
|
|
196
|
-
return await
|
|
189
|
+
return await fn();
|
|
197
190
|
} catch (error) {
|
|
198
|
-
lastError = error;
|
|
199
|
-
|
|
200
191
|
if (attempt === maxRetries || !shouldRetry(error)) {
|
|
201
192
|
throw error;
|
|
202
193
|
}
|
|
203
|
-
|
|
204
|
-
log.debug(`Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`);
|
|
194
|
+
|
|
205
195
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
206
|
-
|
|
207
|
-
delay = Math.min(delay * backoffMultiplier, maxDelay);
|
|
196
|
+
delay = Math.min(delay * multiplier, maxDelay);
|
|
208
197
|
}
|
|
209
198
|
}
|
|
210
|
-
|
|
211
|
-
throw lastError;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Add timeout to any promise
|
|
216
|
-
* @param promise - Promise to add timeout to
|
|
217
|
-
* @param timeoutMs - Timeout in milliseconds
|
|
218
|
-
* @param errorMessage - Custom error message for timeout
|
|
219
|
-
* @returns Promise that rejects on timeout
|
|
220
|
-
*/
|
|
221
|
-
static async withTimeout<T>(
|
|
222
|
-
promise: Promise<T>,
|
|
223
|
-
timeoutMs: number,
|
|
224
|
-
errorMessage = 'Operation timed out'
|
|
225
|
-
): Promise<T> {
|
|
226
|
-
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
227
|
-
|
|
228
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
229
|
-
timeoutHandle = setTimeout(() => {
|
|
230
|
-
reject(new Error(errorMessage));
|
|
231
|
-
}, timeoutMs);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
return await Promise.race([promise, timeoutPromise]);
|
|
236
|
-
} finally {
|
|
237
|
-
if (timeoutHandle !== undefined) {
|
|
238
|
-
clearTimeout(timeoutHandle);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Execute multiple operations with Promise.allSettled for better error handling
|
|
245
|
-
* Returns detailed results for each operation, including failures
|
|
246
|
-
* @param operations - Array of async operations to execute
|
|
247
|
-
* @returns Object with successful and failed operations separated
|
|
248
|
-
*/
|
|
249
|
-
static async batchExecute<T>(
|
|
250
|
-
operations: Array<() => Promise<T>>
|
|
251
|
-
): Promise<{
|
|
252
|
-
successful: Array<{ index: number; value: T }>;
|
|
253
|
-
failed: Array<{ index: number; reason: unknown }>;
|
|
254
|
-
successCount: number;
|
|
255
|
-
failureCount: number;
|
|
256
|
-
}> {
|
|
257
|
-
const results = await Promise.allSettled(operations.map(op => op()));
|
|
258
|
-
|
|
259
|
-
const successful: Array<{ index: number; value: T }> = [];
|
|
260
|
-
const failed: Array<{ index: number; reason: unknown }> = [];
|
|
261
|
-
|
|
262
|
-
results.forEach((result, index) => {
|
|
263
|
-
if (result.status === 'fulfilled') {
|
|
264
|
-
successful.push({ index, value: result.value });
|
|
265
|
-
} else {
|
|
266
|
-
failed.push({ index, reason: result.reason });
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
successful,
|
|
272
|
-
failed,
|
|
273
|
-
successCount: successful.length,
|
|
274
|
-
failureCount: failed.length
|
|
275
|
-
};
|
|
199
|
+
throw new Error('Max retries exceeded');
|
|
276
200
|
}
|
|
277
201
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function readIniFile(filePath: string): Promise<Record<string, Record<string, string>>> {
|
|
5
|
+
try {
|
|
6
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
7
|
+
const result: Record<string, Record<string, string>> = {};
|
|
8
|
+
let currentSection = '';
|
|
9
|
+
|
|
10
|
+
const lines = content.split(/\r?\n/);
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
const trimmed = line.trim();
|
|
13
|
+
if (!trimmed || trimmed.startsWith(';') || trimmed.startsWith('#')) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
18
|
+
currentSection = trimmed.substring(1, trimmed.length - 1);
|
|
19
|
+
result[currentSection] = {};
|
|
20
|
+
} else if (currentSection) {
|
|
21
|
+
const parts = trimmed.split('=');
|
|
22
|
+
if (parts.length >= 2) {
|
|
23
|
+
const key = parts[0].trim();
|
|
24
|
+
const value = parts.slice(1).join('=').trim();
|
|
25
|
+
result[currentSection][key] = value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error(`Failed to read INI file at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function getProjectSetting(projectPath: string, category: string, sectionName: string, key?: string): Promise<any> {
|
|
37
|
+
// Normalize project path to directory
|
|
38
|
+
let dirPath = projectPath;
|
|
39
|
+
if (dirPath.toLowerCase().endsWith('.uproject')) {
|
|
40
|
+
dirPath = path.dirname(dirPath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Possible file names/locations in order of preference (Config/DefaultX.ini, Saved/Config/WindowsEditor/X.ini)
|
|
44
|
+
// category is usually 'Project', 'Engine', 'Game', 'Input', etc.
|
|
45
|
+
const cleanCategory = category.replace(/^Default/, ''); // If caller passed 'DefaultEngine', normalize to 'Engine'
|
|
46
|
+
|
|
47
|
+
const candidates = [
|
|
48
|
+
path.join(dirPath, 'Config', `Default${cleanCategory}.ini`),
|
|
49
|
+
path.join(dirPath, 'Saved', 'Config', 'WindowsEditor', `${cleanCategory}.ini`),
|
|
50
|
+
path.join(dirPath, 'Saved', 'Config', 'Windows', `${cleanCategory}.ini`),
|
|
51
|
+
path.join(dirPath, 'Saved', 'Config', 'Mac', `${cleanCategory}.ini`),
|
|
52
|
+
path.join(dirPath, 'Saved', 'Config', 'Linux', `${cleanCategory}.ini`)
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
for (const configPath of candidates) {
|
|
56
|
+
try {
|
|
57
|
+
const iniData = await readIniFile(configPath);
|
|
58
|
+
// If we successfully read the file, check for the section
|
|
59
|
+
if (sectionName) {
|
|
60
|
+
const section = iniData[sectionName];
|
|
61
|
+
if (section) {
|
|
62
|
+
if (key) {
|
|
63
|
+
return section[key];
|
|
64
|
+
}
|
|
65
|
+
return section;
|
|
66
|
+
}
|
|
67
|
+
// If section not found in this file, continue to next candidate?
|
|
68
|
+
// Usually we want the most authoritative, but if it's missing the section, maybe it's in another?
|
|
69
|
+
// For now, if we find the file, we return the data from it or null if section missing.
|
|
70
|
+
// Merging is complex without a proper config hierarchy implementation.
|
|
71
|
+
// We will assume if the file exists, it's the one we want, or if section is missing, we fail for this file.
|
|
72
|
+
// But 'Default' might lack user overrides.
|
|
73
|
+
// Given this is a simple reader, we'll return the first match that contains the section,
|
|
74
|
+
// or if sectionName is empty, the first file found.
|
|
75
|
+
} else {
|
|
76
|
+
if (Object.keys(iniData).length > 0) {
|
|
77
|
+
return iniData;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (_e) {
|
|
81
|
+
// Continue to next candidate
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -16,13 +16,18 @@ export class Logger {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
debug(...args: any[]) {
|
|
19
|
-
if (this.shouldLog('debug'))
|
|
19
|
+
if (!this.shouldLog('debug')) return;
|
|
20
|
+
// Write to stderr to avoid corrupting MCP stdout stream
|
|
21
|
+
console.error(`[${this.scope}]`, ...args);
|
|
20
22
|
}
|
|
21
23
|
info(...args: any[]) {
|
|
22
|
-
if (this.shouldLog('info'))
|
|
24
|
+
if (!this.shouldLog('info')) return;
|
|
25
|
+
// Write to stderr to avoid corrupting MCP stdout stream
|
|
26
|
+
console.error(`[${this.scope}]`, ...args);
|
|
23
27
|
}
|
|
24
28
|
warn(...args: any[]) {
|
|
25
|
-
if (this.shouldLog('warn'))
|
|
29
|
+
if (!this.shouldLog('warn')) return;
|
|
30
|
+
console.warn(`[${this.scope}]`, ...args);
|
|
26
31
|
}
|
|
27
32
|
error(...args: any[]) {
|
|
28
33
|
if (this.shouldLog('error')) console.error(`[${this.scope}]`, ...args);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for normalize utility functions
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
toVec3Object,
|
|
7
|
+
toRotObject,
|
|
8
|
+
toVec3Tuple,
|
|
9
|
+
toRotTuple,
|
|
10
|
+
toFiniteNumber,
|
|
11
|
+
normalizePartialVector,
|
|
12
|
+
normalizeTransformInput
|
|
13
|
+
} from './normalize.js';
|
|
14
|
+
|
|
15
|
+
describe('toVec3Object', () => {
|
|
16
|
+
it('converts object input to Vector3', () => {
|
|
17
|
+
const result = toVec3Object({ x: 1, y: 2, z: 3 });
|
|
18
|
+
expect(result).toEqual({ x: 1, y: 2, z: 3 });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('converts array input to Vector3', () => {
|
|
22
|
+
const result = toVec3Object([1, 2, 3]);
|
|
23
|
+
expect(result).toEqual({ x: 1, y: 2, z: 3 });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns null for invalid input', () => {
|
|
27
|
+
expect(toVec3Object('invalid')).toBeNull();
|
|
28
|
+
expect(toVec3Object(null)).toBeNull();
|
|
29
|
+
expect(toVec3Object(undefined)).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns null for incomplete object', () => {
|
|
33
|
+
expect(toVec3Object({ x: 1, y: 2 })).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('handles zero values', () => {
|
|
37
|
+
const result = toVec3Object({ x: 0, y: 0, z: 0 });
|
|
38
|
+
expect(result).toEqual({ x: 0, y: 0, z: 0 });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('toRotObject', () => {
|
|
43
|
+
it('converts object input to Rotator', () => {
|
|
44
|
+
const result = toRotObject({ pitch: 10, yaw: 20, roll: 30 });
|
|
45
|
+
expect(result).toEqual({ pitch: 10, yaw: 20, roll: 30 });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('converts array input to Rotator', () => {
|
|
49
|
+
const result = toRotObject([10, 20, 30]);
|
|
50
|
+
expect(result).toEqual({ pitch: 10, yaw: 20, roll: 30 });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns null for invalid input', () => {
|
|
54
|
+
expect(toRotObject('invalid')).toBeNull();
|
|
55
|
+
expect(toRotObject(null)).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('toVec3Tuple', () => {
|
|
60
|
+
it('converts object to tuple', () => {
|
|
61
|
+
const result = toVec3Tuple({ x: 1, y: 2, z: 3 });
|
|
62
|
+
expect(result).toEqual([1, 2, 3]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('passes through valid array', () => {
|
|
66
|
+
const result = toVec3Tuple([1, 2, 3]);
|
|
67
|
+
expect(result).toEqual([1, 2, 3]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns null for invalid input', () => {
|
|
71
|
+
expect(toVec3Tuple([1, 2])).toBeNull();
|
|
72
|
+
expect(toVec3Tuple(null)).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('toRotTuple', () => {
|
|
77
|
+
it('converts object to tuple', () => {
|
|
78
|
+
const result = toRotTuple({ pitch: 10, yaw: 20, roll: 30 });
|
|
79
|
+
expect(result).toEqual([10, 20, 30]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('passes through valid array', () => {
|
|
83
|
+
const result = toRotTuple([10, 20, 30]);
|
|
84
|
+
expect(result).toEqual([10, 20, 30]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('toFiniteNumber', () => {
|
|
89
|
+
it('accepts valid numbers', () => {
|
|
90
|
+
expect(toFiniteNumber(42)).toBe(42);
|
|
91
|
+
expect(toFiniteNumber(0)).toBe(0);
|
|
92
|
+
expect(toFiniteNumber(-5)).toBe(-5);
|
|
93
|
+
expect(toFiniteNumber(3.14)).toBe(3.14);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('parses string numbers', () => {
|
|
97
|
+
expect(toFiniteNumber('42')).toBe(42);
|
|
98
|
+
expect(toFiniteNumber('3.14')).toBe(3.14);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('returns undefined for invalid input', () => {
|
|
102
|
+
expect(toFiniteNumber('not a number')).toBeUndefined();
|
|
103
|
+
expect(toFiniteNumber(NaN)).toBeUndefined();
|
|
104
|
+
expect(toFiniteNumber(Infinity)).toBeUndefined();
|
|
105
|
+
expect(toFiniteNumber(null)).toBeUndefined();
|
|
106
|
+
expect(toFiniteNumber(undefined)).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('normalizePartialVector', () => {
|
|
111
|
+
it('normalizes complete vectors', () => {
|
|
112
|
+
const result = normalizePartialVector({ x: 1, y: 2, z: 3 });
|
|
113
|
+
expect(result).toEqual({ x: 1, y: 2, z: 3 });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('handles partial vectors', () => {
|
|
117
|
+
const result = normalizePartialVector({ x: 1 });
|
|
118
|
+
expect(result).toBeDefined();
|
|
119
|
+
expect(result?.x).toBe(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('uses alternate keys for reading input', () => {
|
|
123
|
+
const result = normalizePartialVector(
|
|
124
|
+
{ pitch: 10, yaw: 20, roll: 30 },
|
|
125
|
+
['pitch', 'yaw', 'roll']
|
|
126
|
+
);
|
|
127
|
+
// alternateKeys are used to READ from input, but output is still x/y/z
|
|
128
|
+
expect(result).toEqual({ x: 10, y: 20, z: 30 });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('returns undefined for invalid input', () => {
|
|
132
|
+
expect(normalizePartialVector(null)).toBeUndefined();
|
|
133
|
+
expect(normalizePartialVector('string')).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('normalizeTransformInput', () => {
|
|
138
|
+
it('normalizes complete transform', () => {
|
|
139
|
+
const result = normalizeTransformInput({
|
|
140
|
+
location: { x: 0, y: 0, z: 100 },
|
|
141
|
+
rotation: { pitch: 0, yaw: 90, roll: 0 },
|
|
142
|
+
scale: { x: 1, y: 1, z: 1 }
|
|
143
|
+
});
|
|
144
|
+
expect(result).toBeDefined();
|
|
145
|
+
expect(result?.location).toBeDefined();
|
|
146
|
+
expect(result?.rotation).toBeDefined();
|
|
147
|
+
expect(result?.scale).toBeDefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('handles partial transforms', () => {
|
|
151
|
+
const result = normalizeTransformInput({
|
|
152
|
+
location: { x: 100, y: 200, z: 300 }
|
|
153
|
+
});
|
|
154
|
+
expect(result).toBeDefined();
|
|
155
|
+
expect(result?.location).toBeDefined();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('returns undefined for invalid input', () => {
|
|
159
|
+
expect(normalizeTransformInput(null)).toBeUndefined();
|
|
160
|
+
expect(normalizeTransformInput('invalid')).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
});
|
package/src/utils/normalize.ts
CHANGED
|
@@ -62,3 +62,63 @@ export function toRotTuple(input: any): Rot3Tuple | null {
|
|
|
62
62
|
return [pitch, yaw, roll];
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Parse a raw value into a finite number when possible.
|
|
67
|
+
* Accepts strings like "1.0" and returns number or undefined when invalid.
|
|
68
|
+
*/
|
|
69
|
+
export function toFiniteNumber(raw: unknown): number | undefined {
|
|
70
|
+
if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
|
|
71
|
+
if (typeof raw === 'string') {
|
|
72
|
+
const trimmed = raw.trim();
|
|
73
|
+
if (trimmed.length === 0) return undefined;
|
|
74
|
+
const parsed = Number(trimmed);
|
|
75
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Normalize a partial vector input. Unlike toVec3Object, this accepts
|
|
82
|
+
* partial specifications and returns an object containing only present
|
|
83
|
+
* components (x/y/z) when any are provided; otherwise returns undefined.
|
|
84
|
+
*/
|
|
85
|
+
export function normalizePartialVector(value: any, alternateKeys: string[] = ['x', 'y', 'z']): Record<string, number> | undefined {
|
|
86
|
+
if (value === undefined || value === null) return undefined;
|
|
87
|
+
const result: Record<string, number> = {};
|
|
88
|
+
const assignIfPresent = (component: 'x' | 'y' | 'z', raw: unknown) => {
|
|
89
|
+
const num = toFiniteNumber(raw);
|
|
90
|
+
if (num !== undefined) result[component] = num;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
if (value.length > 0) assignIfPresent('x', value[0]);
|
|
95
|
+
if (value.length > 1) assignIfPresent('y', value[1]);
|
|
96
|
+
if (value.length > 2) assignIfPresent('z', value[2]);
|
|
97
|
+
} else if (typeof value === 'object') {
|
|
98
|
+
const obj = value as Record<string, unknown>;
|
|
99
|
+
assignIfPresent('x', obj.x ?? obj[alternateKeys[0]]);
|
|
100
|
+
assignIfPresent('y', obj.y ?? obj[alternateKeys[1]]);
|
|
101
|
+
assignIfPresent('z', obj.z ?? obj[alternateKeys[2]]);
|
|
102
|
+
} else {
|
|
103
|
+
assignIfPresent('x', value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Normalize a transform-like input into a minimal object containing
|
|
111
|
+
* location/rotation/scale partial descriptors when present.
|
|
112
|
+
*/
|
|
113
|
+
export function normalizeTransformInput(transform: any): Record<string, unknown> | undefined {
|
|
114
|
+
if (!transform || typeof transform !== 'object') return undefined;
|
|
115
|
+
const result: Record<string, unknown> = {};
|
|
116
|
+
const location = normalizePartialVector(transform.location);
|
|
117
|
+
if (location) result.location = location;
|
|
118
|
+
const rotation = normalizePartialVector(transform.rotation, ['pitch', 'yaw', 'roll']);
|
|
119
|
+
if (rotation) result.rotation = rotation;
|
|
120
|
+
const scale = normalizePartialVector(transform.scale);
|
|
121
|
+
if (scale) result.scale = scale;
|
|
122
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function sanitizePath(path: string, allowedRoots: string[] = ['/Game', '/Engine']): string {
|
|
2
|
+
if (!path || typeof path !== 'string') {
|
|
3
|
+
throw new Error('Invalid path: must be a non-empty string');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const trimmed = path.trim();
|
|
7
|
+
if (trimmed.length === 0) {
|
|
8
|
+
throw new Error('Invalid path: cannot be empty');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Normalize separators
|
|
12
|
+
const normalized = trimmed.replace(/\\/g, '/');
|
|
13
|
+
|
|
14
|
+
// Prevent directory traversal
|
|
15
|
+
if (normalized.includes('..')) {
|
|
16
|
+
throw new Error('Invalid path: directory traversal (..) is not allowed');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Ensure path starts with a valid root
|
|
20
|
+
// We check case-insensitive for the root prefix to be user-friendly,
|
|
21
|
+
// but Unreal paths are typically case-insensitive anyway.
|
|
22
|
+
const isAllowed = allowedRoots.some(root =>
|
|
23
|
+
normalized.toLowerCase() === root.toLowerCase() ||
|
|
24
|
+
normalized.toLowerCase().startsWith(`${root.toLowerCase()}/`)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!isAllowed) {
|
|
28
|
+
throw new Error(`Invalid path: must start with one of [${allowedRoots.join(', ')}]`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Basic character validation (Unreal strictness)
|
|
32
|
+
// Blocks: < > : " | ? * (Windows reserved) and control characters
|
|
33
|
+
// allowing spaces, dots, underscores, dashes, slashes
|
|
34
|
+
// Note: Unreal allows spaces in some contexts but it's often safer to restrict them if strict mode is desired.
|
|
35
|
+
// For now, we block the definitely invalid ones.
|
|
36
|
+
// eslint-disable-next-line no-control-regex
|
|
37
|
+
const invalidChars = /[<>:"|?*\x00-\x1f]/;
|
|
38
|
+
if (invalidChars.test(normalized)) {
|
|
39
|
+
throw new Error('Invalid path: contains illegal characters');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|