ue-mcp 1.1.19 → 1.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/bridge.d.ts +18 -5
  3. package/dist/bridge.js +57 -14
  4. package/dist/bridge.js.map +1 -1
  5. package/dist/errors.d.ts +4 -1
  6. package/dist/errors.js +3 -0
  7. package/dist/errors.js.map +1 -1
  8. package/dist/global-config.d.ts +2 -2
  9. package/dist/global-config.js +2 -2
  10. package/dist/index.js +13 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/init.js +7 -0
  13. package/dist/init.js.map +1 -1
  14. package/dist/locking.d.ts +29 -0
  15. package/dist/locking.js +136 -0
  16. package/dist/locking.js.map +1 -0
  17. package/dist/port.d.ts +16 -0
  18. package/dist/port.js +42 -0
  19. package/dist/port.js.map +1 -0
  20. package/dist/project.d.ts +11 -0
  21. package/dist/project.js +1 -1
  22. package/dist/project.js.map +1 -1
  23. package/dist/schemas.d.ts +51 -0
  24. package/dist/schemas.js +19 -0
  25. package/dist/schemas.js.map +1 -1
  26. package/dist/tool-counts.json +6 -6
  27. package/dist/tools/asset.js +8 -0
  28. package/dist/tools/asset.js.map +1 -1
  29. package/dist/tools/blueprint.js +1 -0
  30. package/dist/tools/blueprint.js.map +1 -1
  31. package/dist/tools/feedback.d.ts +1 -1
  32. package/dist/tools/feedback.js +17 -14
  33. package/dist/tools/feedback.js.map +1 -1
  34. package/dist/ue-mcp.default.yml +25 -1
  35. package/package.json +2 -2
  36. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +65 -0
  37. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.h +11 -0
  38. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +328 -0
  39. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +36 -0
  40. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LockHandlers.cpp +107 -0
  41. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LockHandlers.h +25 -0
  42. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +66 -2
  43. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/LockRegistry.cpp +121 -0
  44. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/LockRegistry.h +67 -0
  45. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +8 -3
@@ -0,0 +1,121 @@
1
+ #include "LockRegistry.h"
2
+ #include "HAL/PlatformTime.h"
3
+ #include "HAL/CriticalSection.h"
4
+ #include "Misc/ScopeLock.h"
5
+
6
+ FMCPLockRegistry& FMCPLockRegistry::Get()
7
+ {
8
+ static FMCPLockRegistry Instance;
9
+ return Instance;
10
+ }
11
+
12
+ FString FMCPLockRegistry::NormalizeKey(const FString& AssetPath)
13
+ {
14
+ FString Key = AssetPath;
15
+ Key.TrimStartAndEndInline();
16
+ Key.ReplaceInline(TEXT("\\"), TEXT("/"));
17
+
18
+ // Collapse ObjectPath ("/Game/Foo.Foo") to PackagePath ("/Game/Foo"): if the
19
+ // last '.' comes after the last '/', drop from that '.' onward so both
20
+ // spellings key the same lock. Paths without a dotted object suffix are left
21
+ // as-is.
22
+ int32 SlashIdx = INDEX_NONE;
23
+ int32 DotIdx = INDEX_NONE;
24
+ Key.FindLastChar(TEXT('/'), SlashIdx);
25
+ Key.FindLastChar(TEXT('.'), DotIdx);
26
+ if (DotIdx != INDEX_NONE && DotIdx > SlashIdx)
27
+ {
28
+ Key = Key.Left(DotIdx);
29
+ }
30
+
31
+ return Key.ToLower();
32
+ }
33
+
34
+ void FMCPLockRegistry::PurgeExpired(double Now)
35
+ {
36
+ for (auto It = Locks.CreateIterator(); It; ++It)
37
+ {
38
+ if (It->Value.ExpiresMono <= Now)
39
+ {
40
+ It.RemoveCurrent();
41
+ }
42
+ }
43
+ }
44
+
45
+ bool FMCPLockRegistry::Acquire(const FString& AssetPath, const FString& SessionId, double TtlSeconds, FMCPAssetLock& OutHolder)
46
+ {
47
+ FScopeLock ScopeLock(&Mutex);
48
+ const double Now = FPlatformTime::Seconds();
49
+ PurgeExpired(Now);
50
+
51
+ const FString Key = NormalizeKey(AssetPath);
52
+ if (FMCPAssetLock* Existing = Locks.Find(Key))
53
+ {
54
+ if (Existing->SessionId == SessionId)
55
+ {
56
+ // Re-entrant: same session refreshes its own lease rather than
57
+ // deadlocking on a lock it already holds.
58
+ Existing->ExpiresMono = Now + TtlSeconds;
59
+ Existing->TtlSeconds = TtlSeconds;
60
+ OutHolder = *Existing;
61
+ return true;
62
+ }
63
+ // Held by a different, still-live session.
64
+ OutHolder = *Existing;
65
+ return false;
66
+ }
67
+
68
+ FMCPAssetLock New;
69
+ New.AssetPath = AssetPath;
70
+ New.SessionId = SessionId;
71
+ New.AcquiredAtUtc = FDateTime::UtcNow();
72
+ New.ExpiresMono = Now + TtlSeconds;
73
+ New.TtlSeconds = TtlSeconds;
74
+ Locks.Add(Key, New);
75
+ OutHolder = New;
76
+ return true;
77
+ }
78
+
79
+ bool FMCPLockRegistry::Release(const FString& AssetPath, const FString& SessionId, bool bForce, FMCPAssetLock& OutReleased)
80
+ {
81
+ FScopeLock ScopeLock(&Mutex);
82
+ const FString Key = NormalizeKey(AssetPath);
83
+ if (FMCPAssetLock* Existing = Locks.Find(Key))
84
+ {
85
+ if (bForce || Existing->SessionId == SessionId)
86
+ {
87
+ OutReleased = *Existing;
88
+ Locks.Remove(Key);
89
+ return true;
90
+ }
91
+ }
92
+ return false;
93
+ }
94
+
95
+ int32 FMCPLockRegistry::ReleaseSession(const FString& SessionId)
96
+ {
97
+ FScopeLock ScopeLock(&Mutex);
98
+ int32 Count = 0;
99
+ for (auto It = Locks.CreateIterator(); It; ++It)
100
+ {
101
+ if (It->Value.SessionId == SessionId)
102
+ {
103
+ It.RemoveCurrent();
104
+ ++Count;
105
+ }
106
+ }
107
+ return Count;
108
+ }
109
+
110
+ TArray<FMCPAssetLock> FMCPLockRegistry::List()
111
+ {
112
+ FScopeLock ScopeLock(&Mutex);
113
+ PurgeExpired(FPlatformTime::Seconds());
114
+ TArray<FMCPAssetLock> Out;
115
+ Out.Reserve(Locks.Num());
116
+ for (const TPair<FString, FMCPAssetLock>& Pair : Locks)
117
+ {
118
+ Out.Add(Pair.Value);
119
+ }
120
+ return Out;
121
+ }
@@ -0,0 +1,67 @@
1
+ #pragma once
2
+
3
+ #include "CoreMinimal.h"
4
+ #include "Misc/DateTime.h"
5
+ #include "HAL/CriticalSection.h"
6
+
7
+ /**
8
+ * A single held asset lock. AcquiredAtUtc is for display; ExpiresMono is the
9
+ * monotonic deadline used for TTL math (wall-clock is unsuitable because it can
10
+ * jump). TtlSeconds is the lease length the holder requested.
11
+ */
12
+ struct FMCPAssetLock
13
+ {
14
+ FString AssetPath;
15
+ FString SessionId;
16
+ FDateTime AcquiredAtUtc;
17
+ double ExpiresMono = 0.0;
18
+ double TtlSeconds = 0.0;
19
+ };
20
+
21
+ /**
22
+ * Per-asset exclusive lock registry, living in the editor bridge — the single
23
+ * shared resource every agent connects to. A lock registry on the Node side
24
+ * would only serialize one agent process; because every agent's Node server
25
+ * talks to this one bridge, the registry has to live here to serialize across
26
+ * agents (or an agent plus a human).
27
+ *
28
+ * Locks carry a session id and a TTL. A crashed or disconnected agent never
29
+ * wedges an asset: the lease simply expires and the next acquire reclaims it.
30
+ * Re-acquiring a lock you already hold (same session) just refreshes the lease,
31
+ * so a single agent never blocks itself across a multi-step operation.
32
+ *
33
+ * All handler execution is marshalled onto the game thread, so these operations
34
+ * are already serialized; the critical section is defence-in-depth for the
35
+ * List()/expiry paths.
36
+ */
37
+ class FMCPLockRegistry
38
+ {
39
+ public:
40
+ static FMCPLockRegistry& Get();
41
+
42
+ /** Canonical key: trimmed, forward-slashed, ObjectPath ("/Game/Foo.Foo")
43
+ * collapsed to PackagePath ("/Game/Foo"), lowercased — so every spelling
44
+ * of the same asset contends for one lock. */
45
+ static FString NormalizeKey(const FString& AssetPath);
46
+
47
+ /** Acquire or refresh. Returns true when the caller holds the lock after the
48
+ * call (fresh acquire or same-session refresh). On false the asset is held
49
+ * by another live session; OutHolder describes it. */
50
+ bool Acquire(const FString& AssetPath, const FString& SessionId, double TtlSeconds, FMCPAssetLock& OutHolder);
51
+
52
+ /** Release a lock. Succeeds when held by SessionId, or unconditionally when
53
+ * bForce. OutReleased describes what was removed. */
54
+ bool Release(const FString& AssetPath, const FString& SessionId, bool bForce, FMCPAssetLock& OutReleased);
55
+
56
+ /** Release every lock held by a session (agent shutdown). Returns the count. */
57
+ int32 ReleaseSession(const FString& SessionId);
58
+
59
+ /** Snapshot of all currently-held (non-expired) locks. */
60
+ TArray<FMCPAssetLock> List();
61
+
62
+ private:
63
+ void PurgeExpired(double Now);
64
+
65
+ FCriticalSection Mutex;
66
+ TMap<FString, FMCPAssetLock> Locks; // key = NormalizeKey(AssetPath)
67
+ };
@@ -15,8 +15,13 @@ static TSharedPtr<FMCPBridgeServer> G_BridgeServer;
15
15
 
16
16
  void FUE_MCP_BridgeModule::StartupModule()
17
17
  {
18
- // Create and start bridge server
19
- G_BridgeServer = MakeShared<FMCPBridgeServer>(9877);
18
+ // Create and start bridge server. The base port is derived per-worktree
19
+ // from the project root path (or an explicit -MCPPort / UE_MCP_PORT
20
+ // override) so multiple checkouts can run side-by-side without colliding;
21
+ // the probe loop in Run() resolves the rare clash and publishes the actual
22
+ // bound port to the per-project lockfile.
23
+ const int32 BasePort = FMCPBridgeServer::ResolveConfiguredPort();
24
+ G_BridgeServer = MakeShared<FMCPBridgeServer>(BasePort);
20
25
  FDialogHandlers::InstallDialogHook();
21
26
  // Safety net: auto-decline overwrite dialogs to prevent game thread blocking.
22
27
  // Handlers should check for existing assets before creating, but if a dialog
@@ -37,7 +42,7 @@ void FUE_MCP_BridgeModule::StartupModule()
37
42
 
38
43
  if (G_BridgeServer->Start())
39
44
  {
40
- UE_LOG(LogMCPBridge, Log, TEXT("[UE-MCP] Bridge server started on port 9877"));
45
+ UE_LOG(LogMCPBridge, Log, TEXT("[UE-MCP] Bridge server starting on base port %d"), BasePort);
41
46
  }
42
47
  else
43
48
  {