ue-mcp 1.0.69 → 1.0.70

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # UE-MCP
2
2
 
3
- **Unreal Engine Model Context Protocol Server** - gives AI assistants deep read/write access to the Unreal Editor through <!-- count:tools -->21<!-- /count --> category tools covering <!-- count:actions -->542+<!-- /count --> actions, plus a YAML flow engine for multi-step workflows.
3
+ **Unreal Engine Model Context Protocol Server** - gives AI assistants deep read/write access to the Unreal Editor through <!-- count:tools -->21<!-- /count --> category tools covering <!-- count:actions -->545+<!-- /count --> actions, plus a YAML flow engine for multi-step workflows.
4
4
 
5
5
  ```mermaid
6
6
  flowchart LR
@@ -57,7 +57,7 @@ If you prefer to configure manually, add to your MCP client config:
57
57
 
58
58
  - [Getting Started](https://db-lyon.github.io/ue-mcp/getting-started/) — Installation, configuration, first run
59
59
  - [Architecture](https://db-lyon.github.io/ue-mcp/architecture/) — How the pieces fit together
60
- - [Tool Reference](https://db-lyon.github.io/ue-mcp/tool-reference/) - All <!-- count:tools -->21<!-- /count --> tools with <!-- count:actions -->542+<!-- /count --> actions
60
+ - [Tool Reference](https://db-lyon.github.io/ue-mcp/tool-reference/) - All <!-- count:tools -->21<!-- /count --> tools with <!-- count:actions -->545+<!-- /count --> actions
61
61
  - [Flows](https://db-lyon.github.io/ue-mcp/flows/) - YAML flow engine, custom tasks, rollback, hooks
62
62
  - [Configuration](https://db-lyon.github.io/ue-mcp/configuration/) — `.ue-mcp.json` and MCP client config
63
63
  - [Neon Shrine Demo](https://db-lyon.github.io/ue-mcp/neon-shrine-demo/) — Interactive guided demo
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "tools": 21,
3
- "actions": 542,
4
- "bridgeActions": 514,
3
+ "actions": 545,
4
+ "bridgeActions": 517,
5
5
  "localActions": 28,
6
6
  "perTool": {
7
7
  "project": 25,
@@ -19,13 +19,13 @@
19
19
  "editor": 54,
20
20
  "reflection": 8,
21
21
  "gameplay": 53,
22
- "gas": 9,
22
+ "gas": 12,
23
23
  "networking": 11,
24
24
  "demo": 3,
25
25
  "feedback": 1,
26
26
  "statetree": 35,
27
27
  "plugins": 2
28
28
  },
29
- "generatedAt": "2026-05-29T20:17:18.759Z",
30
- "version": "1.0.69"
29
+ "generatedAt": "2026-05-29T20:36:26.666Z",
30
+ "version": "1.0.70"
31
31
  }
package/dist/tools/gas.js CHANGED
@@ -10,6 +10,9 @@ export const gasTool = categoryTool("gas", "Gameplay Ability System: abilities,
10
10
  set_effect_modifier: bp("Add modifier. Params: effectPath, attribute, operation?, magnitude?", "set_effect_modifier"),
11
11
  create_cue: bp("Create GameplayCue. Params: name, packagePath?, cueType?", "create_gameplay_cue"),
12
12
  get_info: bp("Inspect GAS setup. Params: blueprintPath", "get_gas_info"),
13
+ apply_effect: bp("Apply a GameplayEffect to a live actor's ASC (agnostic stat/damage stimulus - uses the game's own effect). Params: actorLabel, effectClass (content path or class name), level?, setByCaller? ({tag-or-name: magnitude}), world? (auto|pie|editor, default auto)", "apply_effect"),
14
+ set_attribute: bp("Set a gameplay attribute's base value on a live actor's ASC (recalculates CurrentValue through the aggregator). Params: actorLabel, attribute (Health | SetName.Health), value, world?", "set_attribute"),
15
+ get_attribute: bp("Read gameplay attribute base + current values on a live actor's ASC. Omit attribute to list all. Params: actorLabel, attribute?, world?", "get_attribute"),
13
16
  }, undefined, {
14
17
  blueprintPath: z.string().optional(),
15
18
  name: z.string().optional(),
@@ -31,5 +34,12 @@ export const gasTool = categoryTool("gas", "Gameplay Ability System: abilities,
31
34
  magnitude: z.number().optional(),
32
35
  durationPolicy: z.string().optional(),
33
36
  cueType: z.string().optional(),
37
+ // Runtime GAS control (apply_effect / set_attribute / get_attribute)
38
+ actorLabel: z.string().optional().describe("Live actor label/name for runtime GAS actions"),
39
+ effectClass: z.string().optional().describe("apply_effect: GameplayEffect content path or class name"),
40
+ level: z.number().optional().describe("apply_effect: effect level (default 1)"),
41
+ setByCaller: z.record(z.number()).optional().describe("apply_effect: SetByCaller magnitudes keyed by gameplay tag or name"),
42
+ value: z.number().optional().describe("set_attribute: new base value"),
43
+ world: z.string().optional().describe("Runtime world scope: auto (default) | pie | editor"),
34
44
  });
35
45
  //# sourceMappingURL=gas.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gas.js","sourceRoot":"","sources":["../../src/tools/gas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,EAAE,EAAgB,MAAM,aAAa,CAAC;AAE7D,MAAM,CAAC,MAAM,OAAO,GAAY,YAAY,CAC1C,KAAK,EACL,oEAAoE,EACpE;IACE,OAAO,EAAc,EAAE,CAAC,mEAAmE,EAAE,8BAA8B,CAAC;IAC5H,oBAAoB,EAAE,EAAE,CAAC,oDAAoD,EAAE,sBAAsB,CAAC;IACtG,aAAa,EAAQ,EAAE,CAAC,8EAA8E,EAAE,eAAe,CAAC;IACxH,cAAc,EAAO,EAAE,CAAC,qEAAqE,EAAE,yBAAyB,CAAC;IACzH,gBAAgB,EAAK,EAAE,CAAC,0IAA0I,EAAE,kBAAkB,CAAC;IACvL,aAAa,EAAQ,EAAE,CAAC,uEAAuE,EAAE,wBAAwB,CAAC;IAC1H,mBAAmB,EAAE,EAAE,CAAC,qEAAqE,EAAE,qBAAqB,CAAC;IACrH,UAAU,EAAW,EAAE,CAAC,0DAA0D,EAAE,qBAAqB,CAAC;IAC1G,QAAQ,EAAa,EAAE,CAAC,0CAA0C,EAAE,cAAc,CAAC;CACpF,EACD,SAAS,EACT;IACE,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACpC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACpC,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACvC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACpC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACnC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC5C,yBAAyB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACzD,wBAAwB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACxD,wBAAwB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACxD,uBAAuB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACvD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACrC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC/B,CACF,CAAC"}
1
+ {"version":3,"file":"gas.js","sourceRoot":"","sources":["../../src/tools/gas.ts"],"names":[],"mappings":"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"}
@@ -2927,6 +2927,24 @@ tasks:
2927
2927
  description: "Inspect GAS setup. Params: blueprintPath"
2928
2928
  options:
2929
2929
  method: get_gas_info
2930
+ gas.apply_effect:
2931
+ class_path: ue-mcp.bridge
2932
+ group: gas
2933
+ description: "Apply a GameplayEffect to a live actor's ASC (agnostic stat/damage stimulus - uses the game's own effect). Params: actorLabel, effectClass (content path or class name), level?, setByCaller? ({tag-or-name: magnitude}), world? (auto|pie|editor, default auto)"
2934
+ options:
2935
+ method: apply_effect
2936
+ gas.set_attribute:
2937
+ class_path: ue-mcp.bridge
2938
+ group: gas
2939
+ description: "Set a gameplay attribute's base value on a live actor's ASC (recalculates CurrentValue through the aggregator). Params: actorLabel, attribute (Health | SetName.Health), value, world?"
2940
+ options:
2941
+ method: set_attribute
2942
+ gas.get_attribute:
2943
+ class_path: ue-mcp.bridge
2944
+ group: gas
2945
+ description: "Read gameplay attribute base + current values on a live actor's ASC. Omit attribute to list all. Params: actorLabel, attribute?, world?"
2946
+ options:
2947
+ method: get_attribute
2930
2948
 
2931
2949
  # ── networking ──
2932
2950
  networking.set_replicates:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ue-mcp",
3
- "version": "1.0.69",
4
- "description": "Unreal Engine MCP server - 21 tools, 542+ actions for AI-driven editor control",
3
+ "version": "1.0.70",
4
+ "description": "Unreal Engine MCP server - 21 tools, 545+ actions for AI-driven editor control",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "exports": {
@@ -33,6 +33,9 @@ void FGasHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
33
33
  Registry.RegisterHandler(TEXT("add_attribute"), &AddAttribute);
34
34
  Registry.RegisterHandler(TEXT("set_ability_tags"), &SetAbilityTags);
35
35
  Registry.RegisterHandler(TEXT("set_effect_modifier"), &SetEffectModifier);
36
+ Registry.RegisterHandler(TEXT("apply_effect"), &ApplyEffect);
37
+ Registry.RegisterHandler(TEXT("set_attribute"), &SetAttribute);
38
+ Registry.RegisterHandler(TEXT("get_attribute"), &GetAttribute);
36
39
  }
37
40
 
38
41
  TSharedPtr<FJsonValue> FGasHandlers::CreateGasBlueprint(
@@ -32,4 +32,10 @@ private:
32
32
  static TSharedPtr<FJsonValue> AddAttribute(const TSharedPtr<FJsonObject>& Params);
33
33
  static TSharedPtr<FJsonValue> SetAbilityTags(const TSharedPtr<FJsonObject>& Params);
34
34
  static TSharedPtr<FJsonValue> SetEffectModifier(const TSharedPtr<FJsonObject>& Params);
35
+
36
+ // Runtime GAS control (operates on a live actor's AbilitySystemComponent,
37
+ // PIE by default). Implemented in GasHandlers_Runtime.cpp.
38
+ static TSharedPtr<FJsonValue> ApplyEffect(const TSharedPtr<FJsonObject>& Params);
39
+ static TSharedPtr<FJsonValue> SetAttribute(const TSharedPtr<FJsonObject>& Params);
40
+ static TSharedPtr<FJsonValue> GetAttribute(const TSharedPtr<FJsonObject>& Params);
35
41
  };
@@ -0,0 +1,300 @@
1
+ // Runtime GAS control: apply a GameplayEffect, and get/set attributes on a
2
+ // live actor's AbilitySystemComponent. These are the agnostic "affect a stat"
3
+ // test stimuli - they drive the game's OWN effects and attributes rather than
4
+ // assuming a damage pipeline, so they work for any GAS game. Non-GAS games set
5
+ // reflection-exposed stats via level.set_actor_property or call their own
6
+ // functions via editor.invoke_function instead.
7
+
8
+ #include "GasHandlers.h"
9
+ #include "HandlerUtils.h"
10
+ #include "Dom/JsonObject.h"
11
+ #include "Dom/JsonValue.h"
12
+ #include "GameFramework/Actor.h"
13
+ #include "AbilitySystemComponent.h"
14
+ #include "AbilitySystemBlueprintLibrary.h"
15
+ #include "AttributeSet.h"
16
+ #include "GameplayEffect.h"
17
+ #include "GameplayEffectTypes.h"
18
+ #include "GameplayTagContainer.h"
19
+
20
+ namespace
21
+ {
22
+ // Resolve the world for this call. Defaults to "auto" (prefer PIE), since
23
+ // runtime GAS control is almost always exercised during Play-In-Editor.
24
+ UWorld* ResolveRuntimeWorld(const TSharedPtr<FJsonObject>& Params)
25
+ {
26
+ return ResolveWorldScope(OptionalString(Params, TEXT("world"), TEXT("auto")));
27
+ }
28
+
29
+ // Find the actor (by label or name) in the resolved world and return its
30
+ // AbilitySystemComponent. On any failure writes a structured error to
31
+ // OutError and returns nullptr.
32
+ UAbilitySystemComponent* ResolveASC(
33
+ const TSharedPtr<FJsonObject>& Params,
34
+ AActor*& OutActor,
35
+ TSharedPtr<FJsonValue>& OutError)
36
+ {
37
+ FString ActorLabel;
38
+ if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel))
39
+ {
40
+ OutError = Err;
41
+ return nullptr;
42
+ }
43
+
44
+ UWorld* World = ResolveRuntimeWorld(Params);
45
+ if (!World)
46
+ {
47
+ OutError = MCPError(TEXT("No world available. For PIE actors, start Play-In-Editor first."));
48
+ return nullptr;
49
+ }
50
+
51
+ AActor* Actor = FindActorByLabelOrName(World, ActorLabel);
52
+ if (!Actor)
53
+ {
54
+ OutError = MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
55
+ return nullptr;
56
+ }
57
+ OutActor = Actor;
58
+
59
+ UAbilitySystemComponent* ASC =
60
+ UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Actor);
61
+ if (!ASC)
62
+ {
63
+ OutError = MCPError(FString::Printf(
64
+ TEXT("Actor '%s' has no AbilitySystemComponent (not a GAS actor)"), *ActorLabel));
65
+ return nullptr;
66
+ }
67
+ return ASC;
68
+ }
69
+
70
+ // Resolve a FGameplayAttribute by name against the ASC's spawned attribute
71
+ // sets. Accepts a bare property name ("Health") or qualified forms
72
+ // ("HealthSet.Health" / "HealthSet:Health"). Returns an invalid attribute
73
+ // on miss; writes the matched set name to OutSetName on hit.
74
+ FGameplayAttribute FindAttributeByName(
75
+ UAbilitySystemComponent* ASC,
76
+ const FString& Name,
77
+ FString& OutSetName)
78
+ {
79
+ for (const UAttributeSet* Set : ASC->GetSpawnedAttributes())
80
+ {
81
+ if (!Set) continue;
82
+ UClass* SetClass = Set->GetClass();
83
+ const FString SetName = SetClass->GetName();
84
+ for (TFieldIterator<FProperty> It(SetClass); It; ++It)
85
+ {
86
+ FStructProperty* SProp = CastField<FStructProperty>(*It);
87
+ if (!SProp || SProp->Struct != FGameplayAttributeData::StaticStruct()) continue;
88
+ const FString PropName = SProp->GetName();
89
+ if (PropName == Name
90
+ || (SetName + TEXT(".") + PropName) == Name
91
+ || (SetName + TEXT(":") + PropName) == Name)
92
+ {
93
+ OutSetName = SetName;
94
+ return FGameplayAttribute(SProp);
95
+ }
96
+ }
97
+ }
98
+ return FGameplayAttribute();
99
+ }
100
+
101
+ // Append one attribute's name/base/current to a JSON object.
102
+ void WriteAttributeRow(
103
+ TSharedPtr<FJsonObject> Obj,
104
+ UAbilitySystemComponent* ASC,
105
+ const FGameplayAttribute& Attr)
106
+ {
107
+ Obj->SetStringField(TEXT("attribute"), Attr.GetName());
108
+ Obj->SetNumberField(TEXT("baseValue"), ASC->GetNumericAttributeBase(Attr));
109
+ Obj->SetNumberField(TEXT("currentValue"), ASC->GetNumericAttribute(Attr));
110
+ }
111
+
112
+ // Resolve a UGameplayEffect subclass from a content path or short class name.
113
+ UClass* ResolveEffectClass(const FString& Spec)
114
+ {
115
+ auto IsEffect = [](UClass* C) { return C && C->IsChildOf(UGameplayEffect::StaticClass()); };
116
+
117
+ if (Spec.Contains(TEXT("/")))
118
+ {
119
+ // Direct class load (native or already-_C class path).
120
+ if (UClass* C = LoadObject<UClass>(nullptr, *Spec); IsEffect(C)) return C;
121
+ // Blueprint generated class: "/Game/Foo/GE_Bar" -> ".../GE_Bar.GE_Bar_C".
122
+ FString AssetName;
123
+ Spec.Split(TEXT("/"), nullptr, &AssetName, ESearchCase::CaseSensitive, ESearchDir::FromEnd);
124
+ const FString ClassPath = Spec + TEXT(".") + AssetName + TEXT("_C");
125
+ if (UClass* C = LoadObject<UClass>(nullptr, *ClassPath); IsEffect(C)) return C;
126
+ // Fall back to loading the Blueprint and taking its generated class.
127
+ if (UBlueprint* BP = LoadAssetByPath<UBlueprint>(Spec))
128
+ {
129
+ if (IsEffect(BP->GeneratedClass)) return BP->GeneratedClass;
130
+ }
131
+ return nullptr;
132
+ }
133
+
134
+ UClass* C = FindClassByShortName(Spec);
135
+ return IsEffect(C) ? C : nullptr;
136
+ }
137
+ }
138
+
139
+ TSharedPtr<FJsonValue> FGasHandlers::ApplyEffect(const TSharedPtr<FJsonObject>& Params)
140
+ {
141
+ MCP_CHECK_GAME_THREAD();
142
+
143
+ FString EffectSpec;
144
+ if (auto Err = RequireStringAlt(Params, TEXT("effectClass"), TEXT("effectPath"), EffectSpec)) return Err;
145
+
146
+ AActor* Actor = nullptr;
147
+ TSharedPtr<FJsonValue> Err;
148
+ UAbilitySystemComponent* ASC = ResolveASC(Params, Actor, Err);
149
+ if (!ASC) return Err;
150
+
151
+ UClass* EffectClass = ResolveEffectClass(EffectSpec);
152
+ if (!EffectClass)
153
+ {
154
+ return MCPError(FString::Printf(
155
+ TEXT("GameplayEffect class not found: %s (pass a content path or class name)"), *EffectSpec));
156
+ }
157
+
158
+ const float Level = static_cast<float>(OptionalNumber(Params, TEXT("level"), 1.0));
159
+
160
+ FGameplayEffectContextHandle Context = ASC->MakeEffectContext();
161
+ Context.AddInstigator(Actor, Actor);
162
+ FGameplayEffectSpecHandle SpecHandle = ASC->MakeOutgoingSpec(EffectClass, Level, Context);
163
+ if (!SpecHandle.IsValid() || !SpecHandle.Data.IsValid())
164
+ {
165
+ return MCPError(TEXT("Failed to build a GameplayEffectSpec for the effect"));
166
+ }
167
+
168
+ // SetByCaller magnitudes: { "<tag-or-name>": <number> }. Prefer a gameplay
169
+ // tag when the key resolves to one; otherwise use the FName overload.
170
+ const TSharedPtr<FJsonObject>* SetByCaller = nullptr;
171
+ TArray<FString> AppliedKeys;
172
+ if (Params->TryGetObjectField(TEXT("setByCaller"), SetByCaller) && SetByCaller && (*SetByCaller).IsValid())
173
+ {
174
+ for (const auto& KV : (*SetByCaller)->Values)
175
+ {
176
+ double Mag = 0.0;
177
+ if (!KV.Value.IsValid() || !KV.Value->TryGetNumber(Mag)) continue;
178
+ const FGameplayTag Tag = FGameplayTag::RequestGameplayTag(FName(*KV.Key), /*ErrorIfNotFound*/ false);
179
+ if (Tag.IsValid())
180
+ {
181
+ SpecHandle.Data->SetSetByCallerMagnitude(Tag, static_cast<float>(Mag));
182
+ }
183
+ else
184
+ {
185
+ SpecHandle.Data->SetSetByCallerMagnitude(FName(*KV.Key), static_cast<float>(Mag));
186
+ }
187
+ AppliedKeys.Add(KV.Key);
188
+ }
189
+ }
190
+
191
+ const FActiveGameplayEffectHandle Active = ASC->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data);
192
+
193
+ auto Result = MCPSuccess();
194
+ Result->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
195
+ Result->SetStringField(TEXT("effect"), EffectClass->GetPathName());
196
+ Result->SetNumberField(TEXT("level"), Level);
197
+ Result->SetBoolField(TEXT("applied"), Active.WasSuccessfullyApplied());
198
+ // Duration/Infinite effects produce a live handle; instant effects don't.
199
+ Result->SetBoolField(TEXT("durationActive"), Active.IsValid());
200
+ if (AppliedKeys.Num() > 0)
201
+ {
202
+ TArray<TSharedPtr<FJsonValue>> Keys;
203
+ for (const FString& K : AppliedKeys) Keys.Add(MakeShared<FJsonValueString>(K));
204
+ Result->SetArrayField(TEXT("setByCaller"), Keys);
205
+ }
206
+ return MCPResult(Result);
207
+ }
208
+
209
+ TSharedPtr<FJsonValue> FGasHandlers::SetAttribute(const TSharedPtr<FJsonObject>& Params)
210
+ {
211
+ MCP_CHECK_GAME_THREAD();
212
+
213
+ FString AttrName;
214
+ if (auto Err = RequireString(Params, TEXT("attribute"), AttrName)) return Err;
215
+
216
+ double NewValue = 0.0;
217
+ if (!Params->TryGetNumberField(TEXT("value"), NewValue))
218
+ {
219
+ return MCPError(TEXT("Missing required parameter 'value'"));
220
+ }
221
+
222
+ AActor* Actor = nullptr;
223
+ TSharedPtr<FJsonValue> Err;
224
+ UAbilitySystemComponent* ASC = ResolveASC(Params, Actor, Err);
225
+ if (!ASC) return Err;
226
+
227
+ FString SetName;
228
+ const FGameplayAttribute Attr = FindAttributeByName(ASC, AttrName, SetName);
229
+ if (!Attr.IsValid())
230
+ {
231
+ return MCPError(FString::Printf(
232
+ TEXT("Attribute '%s' not found on '%s'. Use get_attribute with no 'attribute' to list available ones."),
233
+ *AttrName, *Actor->GetActorLabel()));
234
+ }
235
+
236
+ const float OldBase = ASC->GetNumericAttributeBase(Attr);
237
+ // SetNumericAttributeBase recalculates CurrentValue through the aggregator,
238
+ // so dependent modifiers stay consistent (unlike a raw property write).
239
+ ASC->SetNumericAttributeBase(Attr, static_cast<float>(NewValue));
240
+
241
+ auto Result = MCPSuccess();
242
+ MCPSetUpdated(Result);
243
+ Result->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
244
+ Result->SetStringField(TEXT("attributeSet"), SetName);
245
+ Result->SetStringField(TEXT("attribute"), Attr.GetName());
246
+ Result->SetNumberField(TEXT("previousBaseValue"), OldBase);
247
+ Result->SetNumberField(TEXT("baseValue"), ASC->GetNumericAttributeBase(Attr));
248
+ Result->SetNumberField(TEXT("currentValue"), ASC->GetNumericAttribute(Attr));
249
+ return MCPResult(Result);
250
+ }
251
+
252
+ TSharedPtr<FJsonValue> FGasHandlers::GetAttribute(const TSharedPtr<FJsonObject>& Params)
253
+ {
254
+ MCP_CHECK_GAME_THREAD();
255
+
256
+ AActor* Actor = nullptr;
257
+ TSharedPtr<FJsonValue> Err;
258
+ UAbilitySystemComponent* ASC = ResolveASC(Params, Actor, Err);
259
+ if (!ASC) return Err;
260
+
261
+ auto Result = MCPSuccess();
262
+ Result->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
263
+
264
+ const FString AttrName = OptionalString(Params, TEXT("attribute"));
265
+ if (!AttrName.IsEmpty())
266
+ {
267
+ FString SetName;
268
+ const FGameplayAttribute Attr = FindAttributeByName(ASC, AttrName, SetName);
269
+ if (!Attr.IsValid())
270
+ {
271
+ return MCPError(FString::Printf(
272
+ TEXT("Attribute '%s' not found on '%s'"), *AttrName, *Actor->GetActorLabel()));
273
+ }
274
+ Result->SetStringField(TEXT("attributeSet"), SetName);
275
+ WriteAttributeRow(Result, ASC, Attr);
276
+ return MCPResult(Result);
277
+ }
278
+
279
+ // No attribute named: enumerate every attribute across all spawned sets.
280
+ TArray<TSharedPtr<FJsonValue>> Rows;
281
+ for (const UAttributeSet* Set : ASC->GetSpawnedAttributes())
282
+ {
283
+ if (!Set) continue;
284
+ UClass* SetClass = Set->GetClass();
285
+ const FString SetName = SetClass->GetName();
286
+ for (TFieldIterator<FProperty> It(SetClass); It; ++It)
287
+ {
288
+ FStructProperty* SProp = CastField<FStructProperty>(*It);
289
+ if (!SProp || SProp->Struct != FGameplayAttributeData::StaticStruct()) continue;
290
+ const FGameplayAttribute Attr(SProp);
291
+ TSharedPtr<FJsonObject> Row = MakeShared<FJsonObject>();
292
+ Row->SetStringField(TEXT("attributeSet"), SetName);
293
+ WriteAttributeRow(Row, ASC, Attr);
294
+ Rows.Add(MakeShared<FJsonValueObject>(Row));
295
+ }
296
+ }
297
+ Result->SetArrayField(TEXT("attributes"), Rows);
298
+ Result->SetNumberField(TEXT("count"), Rows.Num());
299
+ return MCPResult(Result);
300
+ }