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.
- package/README.md +1 -1
- package/dist/bridge.d.ts +18 -5
- package/dist/bridge.js +57 -14
- package/dist/bridge.js.map +1 -1
- package/dist/errors.d.ts +4 -1
- package/dist/errors.js +3 -0
- package/dist/errors.js.map +1 -1
- package/dist/global-config.d.ts +2 -2
- package/dist/global-config.js +2 -2
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/init.js +7 -0
- package/dist/init.js.map +1 -1
- package/dist/locking.d.ts +29 -0
- package/dist/locking.js +136 -0
- package/dist/locking.js.map +1 -0
- package/dist/port.d.ts +16 -0
- package/dist/port.js +42 -0
- package/dist/port.js.map +1 -0
- package/dist/project.d.ts +11 -0
- package/dist/project.js +1 -1
- package/dist/project.js.map +1 -1
- package/dist/schemas.d.ts +51 -0
- package/dist/schemas.js +19 -0
- package/dist/schemas.js.map +1 -1
- package/dist/tool-counts.json +6 -6
- package/dist/tools/asset.js +8 -0
- package/dist/tools/asset.js.map +1 -1
- package/dist/tools/blueprint.js +1 -0
- package/dist/tools/blueprint.js.map +1 -1
- package/dist/tools/feedback.d.ts +1 -1
- package/dist/tools/feedback.js +17 -14
- package/dist/tools/feedback.js.map +1 -1
- package/dist/ue-mcp.default.yml +25 -1
- package/package.json +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +65 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.h +11 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +328 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +36 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LockHandlers.cpp +107 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LockHandlers.h +25 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +66 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/LockRegistry.cpp +121 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/LockRegistry.h +67 -0
- 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
|
-
|
|
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
|
|
45
|
+
UE_LOG(LogMCPBridge, Log, TEXT("[UE-MCP] Bridge server starting on base port %d"), BasePort);
|
|
41
46
|
}
|
|
42
47
|
else
|
|
43
48
|
{
|