Rename MCP endpoints to snake_case. Eliminate javascript wrapper.

This commit is contained in:
2026-03-06 04:53:33 -05:00
parent 3ed7a33f69
commit 6762434b43
4478 changed files with 273 additions and 1120130 deletions

View File

@@ -1,15 +1,4 @@
{
"mcpServers": {
"blueprint-mcp": {
"type": "stdio",
"command": "node",
"args": [
"/home/jyelon/integration/tools/blueprint-mcp/dist/index.js"
],
"env": {
"UE_PROJECT_DIR": "/home/jyelon/integration",
"UE_PORT": "9847"
}
}
}
}

View File

@@ -1727,11 +1727,11 @@ struct FNodeActionSearch
};
// ============================================================
// SearchNodeActions — search the blueprint action database
// SearchNodeTypes — search the blueprint action database
// for spawners matching a query string (same pool as the right-click menu)
// ============================================================
void UMCPHandler_SearchNodeActions::Handle(const FJsonObject* Json, FJsonObject* Result)
void UMCPHandler_SearchNodeTypes::Handle(const FJsonObject* Json, FJsonObject* Result)
{
int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500);
@@ -1746,7 +1746,7 @@ void UMCPHandler_SearchNodeActions::Handle(const FJsonObject* Json, FJsonObject*
Result->SetBoolField(TEXT("success"), true);
Result->SetNumberField(TEXT("count"), ResultArray.Num());
Result->SetArrayField(TEXT("actions"), ResultArray);
Result->SetArrayField(TEXT("results"), ResultArray);
}
// ============================================================
@@ -1816,7 +1816,7 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
if (Matches.Num() == 0)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(
TEXT("No action found matching '%s'. Use search_node_actions to find available actions."),
TEXT("No action found matching '%s'. Use search_node_types to find available actions."),
*Entry.ActionName));
continue;
}

View File

@@ -572,137 +572,85 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode)
return true;
}));
// Queued (need main thread for LoadObject)
// Old-style handlers (queued, not yet ported to UMCPHandler)
Router->BindRoute(FHttpPath(TEXT("/api/blueprint")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("blueprint")));
Router->BindRoute(FHttpPath(TEXT("/api/graph")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("graph")));
Router->BindRoute(FHttpPath(TEXT("/api/search")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("search")));
// Reference finder + write tools
Router->BindRoute(FHttpPath(TEXT("/api/references")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("references")));
Router->BindRoute(FHttpPath(TEXT("/api/replace-function-calls")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("replace_function_calls")));
Router->BindRoute(FHttpPath(TEXT("/api/change-variable-type")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("changeVariableType")));
Router->BindRoute(FHttpPath(TEXT("/api/change-function-param-type")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("changeFunctionParamType")));
Router->BindRoute(FHttpPath(TEXT("/api/delete-asset")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("delete_asset")));
Router->BindRoute(FHttpPath(TEXT("/api/test-save")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("testSave")));
Router->BindRoute(FHttpPath(TEXT("/api/connect-pins")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("connect_pins")));
Router->BindRoute(FHttpPath(TEXT("/api/disconnect-pin")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("disconnect_pin")));
Router->BindRoute(FHttpPath(TEXT("/api/refresh-all-nodes")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("refresh_all_nodes")));
Router->BindRoute(FHttpPath(TEXT("/api/set-pin-default")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("set_pin_default")));
Router->BindRoute(FHttpPath(TEXT("/api/move-node")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("move_node")));
Router->BindRoute(FHttpPath(TEXT("/api/get-node-comment")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("get_node_comment")));
Router->BindRoute(FHttpPath(TEXT("/api/set-node-comment")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("set_node_comment")));
Router->BindRoute(FHttpPath(TEXT("/api/get-pin-info")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("getPinInfo")));
Router->BindRoute(FHttpPath(TEXT("/api/check-pin-compatibility")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("checkPinCompatibility")));
Router->BindRoute(FHttpPath(TEXT("/api/list-classes")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("listClasses")));
Router->BindRoute(FHttpPath(TEXT("/api/list-functions")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("listFunctions")));
Router->BindRoute(FHttpPath(TEXT("/api/list-properties")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("listProperties")));
Router->BindRoute(FHttpPath(TEXT("/api/change-struct-node-type")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("change_struct_node_type")));
Router->BindRoute(FHttpPath(TEXT("/api/remove-function-parameter")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("removeFunctionParameter")));
Router->BindRoute(FHttpPath(TEXT("/api/delete-node")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("delete_node")));
Router->BindRoute(FHttpPath(TEXT("/api/duplicate-nodes")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("duplicate_nodes")));
Router->BindRoute(FHttpPath(TEXT("/api/search-by-type")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("searchByType")));
Router->BindRoute(FHttpPath(TEXT("/api/validate-blueprint")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("validateBlueprint")));
Router->BindRoute(FHttpPath(TEXT("/api/validate-all-blueprints")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("validateAllBlueprints")));
Router->BindRoute(FHttpPath(TEXT("/api/search-node-actions")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("search_node_actions")));
Router->BindRoute(FHttpPath(TEXT("/api/spawn-node")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("spawn_node")));
Router->BindRoute(FHttpPath(TEXT("/api/rename-asset")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("rename_asset")));
Router->BindRoute(FHttpPath(TEXT("/api/reparent-blueprint")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("reparentBlueprint")));
Router->BindRoute(FHttpPath(TEXT("/api/set-blueprint-default")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("set_blueprint_default")));
Router->BindRoute(FHttpPath(TEXT("/api/create-blueprint")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("createBlueprint")));
Router->BindRoute(FHttpPath(TEXT("/api/create-graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("createGraph")));
Router->BindRoute(FHttpPath(TEXT("/api/create-struct")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("createStruct")));
Router->BindRoute(FHttpPath(TEXT("/api/create-enum")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("createEnum")));
Router->BindRoute(FHttpPath(TEXT("/api/add-struct-property")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("addStructProperty")));
Router->BindRoute(FHttpPath(TEXT("/api/remove-struct-property")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("removeStructProperty")));
Router->BindRoute(FHttpPath(TEXT("/api/delete-graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("deleteGraph")));
Router->BindRoute(FHttpPath(TEXT("/api/rename-graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("renameGraph")));
Router->BindRoute(FHttpPath(TEXT("/api/add-variable")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("addVariable")));
Router->BindRoute(FHttpPath(TEXT("/api/remove-variable")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("removeVariable")));
Router->BindRoute(FHttpPath(TEXT("/api/set-variable-metadata")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("setVariableMetadata")));
// Interface tools
Router->BindRoute(FHttpPath(TEXT("/api/add-interface")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("add_interface")));
Router->BindRoute(FHttpPath(TEXT("/api/remove-interface")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("remove_interface")));
Router->BindRoute(FHttpPath(TEXT("/api/list-interfaces")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("list_interfaces")));
// Event Dispatcher tools
Router->BindRoute(FHttpPath(TEXT("/api/add-event-dispatcher")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("addEventDispatcher")));
Router->BindRoute(FHttpPath(TEXT("/api/list-event-dispatchers")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("listEventDispatchers")));
// Function parameter tools
Router->BindRoute(FHttpPath(TEXT("/api/add-function-parameter")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("addFunctionParameter")));
// Component tools
Router->BindRoute(FHttpPath(TEXT("/api/add-component")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("addComponent")));
Router->BindRoute(FHttpPath(TEXT("/api/remove-component")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("removeComponent")));
Router->BindRoute(FHttpPath(TEXT("/api/list-components")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("listComponents")));
// Snapshot / Safety tools
Router->BindRoute(FHttpPath(TEXT("/api/snapshot-graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("snapshotGraph")));
Router->BindRoute(FHttpPath(TEXT("/api/diff-graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("diffGraph")));
Router->BindRoute(FHttpPath(TEXT("/api/restore-graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("restoreGraph")));
Router->BindRoute(FHttpPath(TEXT("/api/find-disconnected-pins")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("findDisconnectedPins")));
Router->BindRoute(FHttpPath(TEXT("/api/analyze-rebuild-impact")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("analyzeRebuildImpact")));
Router->BindRoute(FHttpPath(TEXT("/api/diff-blueprints")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("diff_blueprints")));
Router->BindRoute(FHttpPath(TEXT("/api/change_variable_type")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("change_variable_type")));
Router->BindRoute(FHttpPath(TEXT("/api/change_function_param_type")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("change_function_param_type")));
Router->BindRoute(FHttpPath(TEXT("/api/test_save")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("test_save")));
Router->BindRoute(FHttpPath(TEXT("/api/get_pin_info")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("get_pin_info")));
Router->BindRoute(FHttpPath(TEXT("/api/check_pin_compatibility")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("check_pin_compatibility")));
Router->BindRoute(FHttpPath(TEXT("/api/list_classes")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("list_classes")));
Router->BindRoute(FHttpPath(TEXT("/api/list_functions")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("list_functions")));
Router->BindRoute(FHttpPath(TEXT("/api/list_properties")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("list_properties")));
Router->BindRoute(FHttpPath(TEXT("/api/remove_function_parameter")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("remove_function_parameter")));
Router->BindRoute(FHttpPath(TEXT("/api/search_by_type")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("search_by_type")));
Router->BindRoute(FHttpPath(TEXT("/api/validate_blueprint")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("validate_blueprint")));
Router->BindRoute(FHttpPath(TEXT("/api/validate_all_blueprints")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("validate_all_blueprints")));
Router->BindRoute(FHttpPath(TEXT("/api/reparent_blueprint")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("reparent_blueprint")));
Router->BindRoute(FHttpPath(TEXT("/api/create_blueprint")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("create_blueprint")));
Router->BindRoute(FHttpPath(TEXT("/api/create_graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("create_graph")));
Router->BindRoute(FHttpPath(TEXT("/api/create_struct")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("create_struct")));
Router->BindRoute(FHttpPath(TEXT("/api/create_enum")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("create_enum")));
Router->BindRoute(FHttpPath(TEXT("/api/add_struct_property")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("add_struct_property")));
Router->BindRoute(FHttpPath(TEXT("/api/remove_struct_property")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("remove_struct_property")));
Router->BindRoute(FHttpPath(TEXT("/api/delete_graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("delete_graph")));
Router->BindRoute(FHttpPath(TEXT("/api/rename_graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("rename_graph")));
Router->BindRoute(FHttpPath(TEXT("/api/add_variable")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("add_variable")));
Router->BindRoute(FHttpPath(TEXT("/api/remove_variable")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("remove_variable")));
Router->BindRoute(FHttpPath(TEXT("/api/set_variable_metadata")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("set_variable_metadata")));
Router->BindRoute(FHttpPath(TEXT("/api/add_event_dispatcher")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("add_event_dispatcher")));
Router->BindRoute(FHttpPath(TEXT("/api/list_event_dispatchers")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("list_event_dispatchers")));
Router->BindRoute(FHttpPath(TEXT("/api/add_function_parameter")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("add_function_parameter")));
Router->BindRoute(FHttpPath(TEXT("/api/add_component")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("add_component")));
Router->BindRoute(FHttpPath(TEXT("/api/remove_component")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("remove_component")));
Router->BindRoute(FHttpPath(TEXT("/api/list_components")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("list_components")));
Router->BindRoute(FHttpPath(TEXT("/api/snapshot_graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("snapshot_graph")));
Router->BindRoute(FHttpPath(TEXT("/api/diff_graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("diff_graph")));
Router->BindRoute(FHttpPath(TEXT("/api/restore_graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("restore_graph")));
Router->BindRoute(FHttpPath(TEXT("/api/find_disconnected_pins")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("find_disconnected_pins")));
Router->BindRoute(FHttpPath(TEXT("/api/analyze_rebuild_impact")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("analyze_rebuild_impact")));
// Material read-only tools (Phase 1)
Router->BindRoute(FHttpPath(TEXT("/api/materials")), EHttpServerRequestVerbs::VERB_GET,
@@ -722,46 +670,46 @@ Router->BindRoute(FHttpPath(TEXT("/api/search-node-actions")), EHttpServerReques
return true;
}));
Router->BindRoute(FHttpPath(TEXT("/api/material")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("getMaterial")));
Router->BindRoute(FHttpPath(TEXT("/api/material-graph")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("getMaterialGraph")));
Router->BindRoute(FHttpPath(TEXT("/api/describe-material")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("describeMaterial")));
Router->BindRoute(FHttpPath(TEXT("/api/search-materials")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("searchMaterials")));
Router->BindRoute(FHttpPath(TEXT("/api/material-references")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("findMaterialReferences")));
QueuedHandler(TEXT("get_material")));
Router->BindRoute(FHttpPath(TEXT("/api/material_graph")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("get_material_graph")));
Router->BindRoute(FHttpPath(TEXT("/api/describe_material")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("describe_material")));
Router->BindRoute(FHttpPath(TEXT("/api/search_materials")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("search_materials")));
Router->BindRoute(FHttpPath(TEXT("/api/material_references")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("find_material_references")));
// Material mutation tools (Phase 2)
Router->BindRoute(FHttpPath(TEXT("/api/create-material")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("createMaterial")));
Router->BindRoute(FHttpPath(TEXT("/api/set-material-property")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("setMaterialProperty")));
Router->BindRoute(FHttpPath(TEXT("/api/add-material-expression")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("addMaterialExpression")));
Router->BindRoute(FHttpPath(TEXT("/api/delete-material-expression")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("deleteMaterialExpression")));
Router->BindRoute(FHttpPath(TEXT("/api/connect-material-pins")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("connectMaterialPins")));
Router->BindRoute(FHttpPath(TEXT("/api/disconnect-material-pin")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("disconnectMaterialPin")));
Router->BindRoute(FHttpPath(TEXT("/api/set-expression-value")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("setExpressionValue")));
Router->BindRoute(FHttpPath(TEXT("/api/move-material-expression")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("moveMaterialExpression")));
Router->BindRoute(FHttpPath(TEXT("/api/create_material")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("create_material")));
Router->BindRoute(FHttpPath(TEXT("/api/set_material_property")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("set_material_property")));
Router->BindRoute(FHttpPath(TEXT("/api/add_material_expression")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("add_material_expression")));
Router->BindRoute(FHttpPath(TEXT("/api/delete_material_expression")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("delete_material_expression")));
Router->BindRoute(FHttpPath(TEXT("/api/connect_material_pins")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("connect_material_pins")));
Router->BindRoute(FHttpPath(TEXT("/api/disconnect_material_pin")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("disconnect_material_pin")));
Router->BindRoute(FHttpPath(TEXT("/api/set_expression_value")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("set_expression_value")));
Router->BindRoute(FHttpPath(TEXT("/api/move_material_expression")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("move_material_expression")));
// Material instance tools (Phase 3)
Router->BindRoute(FHttpPath(TEXT("/api/create-material-instance")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("createMaterialInstance")));
Router->BindRoute(FHttpPath(TEXT("/api/set-material-instance-parameter")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("setMaterialInstanceParameter")));
Router->BindRoute(FHttpPath(TEXT("/api/material-instance-params")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("getMaterialInstanceParams")));
Router->BindRoute(FHttpPath(TEXT("/api/reparent-material-instance")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("reparentMaterialInstance")));
Router->BindRoute(FHttpPath(TEXT("/api/create_material_instance")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("create_material_instance")));
Router->BindRoute(FHttpPath(TEXT("/api/set_material_instance_parameter")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("set_material_instance_parameter")));
Router->BindRoute(FHttpPath(TEXT("/api/material_instance_params")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("get_material_instance_params")));
Router->BindRoute(FHttpPath(TEXT("/api/reparent_material_instance")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("reparent_material_instance")));
// Material function tools (Phase 4)
Router->BindRoute(FHttpPath(TEXT("/api/material-functions")), EHttpServerRequestVerbs::VERB_GET,
Router->BindRoute(FHttpPath(TEXT("/api/material_functions")), EHttpServerRequestVerbs::VERB_GET,
FHttpRequestHandler::CreateLambda(
[this](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
@@ -777,54 +725,60 @@ Router->BindRoute(FHttpPath(TEXT("/api/search-node-actions")), EHttpServerReques
OnComplete(MoveTemp(R));
return true;
}));
Router->BindRoute(FHttpPath(TEXT("/api/material-function")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("getMaterialFunction")));
Router->BindRoute(FHttpPath(TEXT("/api/create-material-function")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("createMaterialFunction")));
Router->BindRoute(FHttpPath(TEXT("/api/material_function")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("get_material_function")));
Router->BindRoute(FHttpPath(TEXT("/api/create_material_function")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("create_material_function")));
// Material snapshot/diff/restore (Phase 5)
Router->BindRoute(FHttpPath(TEXT("/api/snapshot-material-graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("snapshotMaterialGraph")));
Router->BindRoute(FHttpPath(TEXT("/api/diff-material-graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("diffMaterialGraph")));
Router->BindRoute(FHttpPath(TEXT("/api/restore-material-graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("restoreMaterialGraph")));
Router->BindRoute(FHttpPath(TEXT("/api/validate-material")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("validateMaterial")));
Router->BindRoute(FHttpPath(TEXT("/api/snapshot_material_graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("snapshot_material_graph")));
Router->BindRoute(FHttpPath(TEXT("/api/diff_material_graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("diff_material_graph")));
Router->BindRoute(FHttpPath(TEXT("/api/restore_material_graph")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("restore_material_graph")));
Router->BindRoute(FHttpPath(TEXT("/api/validate_material")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("validate_material")));
// Animation Blueprint tools
Router->BindRoute(FHttpPath(TEXT("/api/create-anim-blueprint")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("createAnimBlueprint")));
Router->BindRoute(FHttpPath(TEXT("/api/add-anim-state")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("addAnimState")));
Router->BindRoute(FHttpPath(TEXT("/api/remove-anim-state")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("removeAnimState")));
Router->BindRoute(FHttpPath(TEXT("/api/add-anim-transition")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("addAnimTransition")));
Router->BindRoute(FHttpPath(TEXT("/api/set-transition-rule")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("setTransitionRule")));
Router->BindRoute(FHttpPath(TEXT("/api/add-anim-node")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("addAnimNode")));
Router->BindRoute(FHttpPath(TEXT("/api/add-state-machine")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("addStateMachine")));
Router->BindRoute(FHttpPath(TEXT("/api/set-state-animation")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("setStateAnimation")));
Router->BindRoute(FHttpPath(TEXT("/api/list-anim-slots")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("listAnimSlots")));
Router->BindRoute(FHttpPath(TEXT("/api/list-sync-groups")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("listSyncGroups")));
Router->BindRoute(FHttpPath(TEXT("/api/create-blend-space")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("createBlendSpace")));
Router->BindRoute(FHttpPath(TEXT("/api/set-blend-space-samples")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("setBlendSpaceSamples")));
Router->BindRoute(FHttpPath(TEXT("/api/set-state-blend-space")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("setStateBlendSpace")));
Router->BindRoute(FHttpPath(TEXT("/api/create_anim_blueprint")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("create_anim_blueprint")));
Router->BindRoute(FHttpPath(TEXT("/api/add_anim_state")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("add_anim_state")));
Router->BindRoute(FHttpPath(TEXT("/api/remove_anim_state")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("remove_anim_state")));
Router->BindRoute(FHttpPath(TEXT("/api/add_anim_transition")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("add_anim_transition")));
Router->BindRoute(FHttpPath(TEXT("/api/set_transition_rule")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("set_transition_rule")));
Router->BindRoute(FHttpPath(TEXT("/api/add_anim_node")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("add_anim_node")));
Router->BindRoute(FHttpPath(TEXT("/api/add_state_machine")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("add_state_machine")));
Router->BindRoute(FHttpPath(TEXT("/api/set_state_animation")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("set_state_animation")));
Router->BindRoute(FHttpPath(TEXT("/api/list_anim_slots")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("list_anim_slots")));
Router->BindRoute(FHttpPath(TEXT("/api/list_sync_groups")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("list_sync_groups")));
Router->BindRoute(FHttpPath(TEXT("/api/create_blend_space")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("create_blend_space")));
Router->BindRoute(FHttpPath(TEXT("/api/set_blend_space_samples")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("set_blend_space_samples")));
Router->BindRoute(FHttpPath(TEXT("/api/set_state_blend_space")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("set_state_blend_space")));
// Register TMap dispatch handlers
RegisterHandlers();
// Build new-style handler registry from UMCPHandler subclasses
// Build new-style handler registry from UMCPHandler subclasses and bind routes for each
BuildMCPHandlerRegistry();
for (const auto& KV : MCPHandlerRegistry)
{
FString Path = FString::Printf(TEXT("/api/%s"), *KV.Key);
Router->BindRoute(FHttpPath(Path), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(KV.Key));
}
// Register old-style TMap dispatch handlers
RegisterHandlers();
HttpModule.StartAllListeners();
@@ -959,9 +913,9 @@ void FBlueprintMCPServer::RegisterHandlers()
// Mutation endpoints — wrapped in undo transactions by ProcessOneRequest()
MutationEndpoints = {
TEXT("replace_function_calls"),
TEXT("changeVariableType"),
TEXT("changeFunctionParamType"),
TEXT("removeFunctionParameter"),
TEXT("change_variable_type"),
TEXT("change_function_param_type"),
TEXT("remove_function_parameter"),
TEXT("delete_asset"),
TEXT("connect_pins"),
TEXT("disconnect_pin"),
@@ -974,47 +928,47 @@ void FBlueprintMCPServer::RegisterHandlers()
TEXT("spawn_node"),
TEXT("set_node_comment"),
TEXT("rename_asset"),
TEXT("reparentBlueprint"),
TEXT("reparent_blueprint"),
TEXT("set_blueprint_default"),
TEXT("createBlueprint"),
TEXT("createGraph"),
TEXT("deleteGraph"),
TEXT("renameGraph"),
TEXT("addVariable"),
TEXT("removeVariable"),
TEXT("setVariableMetadata"),
TEXT("create_blueprint"),
TEXT("create_graph"),
TEXT("delete_graph"),
TEXT("rename_graph"),
TEXT("add_variable"),
TEXT("remove_variable"),
TEXT("set_variable_metadata"),
TEXT("add_interface"),
TEXT("remove_interface"),
TEXT("addEventDispatcher"),
TEXT("addFunctionParameter"),
TEXT("addComponent"),
TEXT("removeComponent"),
TEXT("restoreGraph"),
TEXT("createStruct"),
TEXT("createEnum"),
TEXT("addStructProperty"),
TEXT("removeStructProperty"),
TEXT("createMaterial"),
TEXT("setMaterialProperty"),
TEXT("addMaterialExpression"),
TEXT("deleteMaterialExpression"),
TEXT("connectMaterialPins"),
TEXT("disconnectMaterialPin"),
TEXT("setExpressionValue"),
TEXT("moveMaterialExpression"),
TEXT("createMaterialInstance"),
TEXT("setMaterialInstanceParameter"),
TEXT("reparentMaterialInstance"),
TEXT("createMaterialFunction"),
TEXT("restoreMaterialGraph"),
TEXT("createAnimBlueprint"),
TEXT("addAnimState"),
TEXT("removeAnimState"),
TEXT("addAnimTransition"),
TEXT("setTransitionRule"),
TEXT("addAnimNode"),
TEXT("addStateMachine"),
TEXT("setStateAnimation"),
TEXT("add_event_dispatcher"),
TEXT("add_function_parameter"),
TEXT("add_component"),
TEXT("remove_component"),
TEXT("restore_graph"),
TEXT("create_struct"),
TEXT("create_enum"),
TEXT("add_struct_property"),
TEXT("remove_struct_property"),
TEXT("create_material"),
TEXT("set_material_property"),
TEXT("add_material_expression"),
TEXT("delete_material_expression"),
TEXT("connect_material_pins"),
TEXT("disconnect_material_pin"),
TEXT("set_expression_value"),
TEXT("move_material_expression"),
TEXT("create_material_instance"),
TEXT("set_material_instance_parameter"),
TEXT("reparent_material_instance"),
TEXT("create_material_function"),
TEXT("restore_material_graph"),
TEXT("create_anim_blueprint"),
TEXT("add_anim_state"),
TEXT("remove_anim_state"),
TEXT("add_anim_transition"),
TEXT("set_transition_rule"),
TEXT("add_anim_node"),
TEXT("add_state_machine"),
TEXT("set_state_animation"),
};
// All handlers have uniform signature: void(const FJsonObject&, FJsonObject&)
@@ -1028,95 +982,77 @@ void FBlueprintMCPServer::RegisterHandlers()
H(TEXT("graph"), &FBlueprintMCPServer::HandleGetGraph);
H(TEXT("search"), &FBlueprintMCPServer::HandleSearch);
H(TEXT("references"), &FBlueprintMCPServer::HandleFindReferences);
H(TEXT("testSave"), &FBlueprintMCPServer::HandleTestSave);
H(TEXT("searchByType"), &FBlueprintMCPServer::HandleSearchByType);
// replace_function_calls is now handled by UMCPHandler_ReplaceFunctionCalls (new-style registry)
H(TEXT("changeVariableType"), &FBlueprintMCPServer::HandleChangeVariableType);
H(TEXT("changeFunctionParamType"), &FBlueprintMCPServer::HandleChangeFunctionParamType);
H(TEXT("removeFunctionParameter"), &FBlueprintMCPServer::HandleRemoveFunctionParameter);
// delete_asset is now handled by UMCPHandler_DeleteAsset (new-style registry)
// connect_pins is now handled by UMCPHandler_ConnectPins (new-style registry)
// disconnect_pin is now handled by UMCPHandler_DisconnectPin (new-style registry)
// refresh_all_nodes is now handled by UMCPHandler_RefreshAllNodes (new-style registry)
// set_pin_default is now handled by UMCPHandler_SetPinDefault (new-style registry)
// move_node is now handled by UMCPHandler_MoveNode (new-style registry)
// get_node_comment is now handled by UMCPHandler_GetNodeComment (new-style registry)
// set_node_comment is now handled by UMCPHandler_SetNodeComment (new-style registry)
H(TEXT("getPinInfo"), &FBlueprintMCPServer::HandleGetPinInfo);
H(TEXT("checkPinCompatibility"), &FBlueprintMCPServer::HandleCheckPinCompatibility);
H(TEXT("listClasses"), &FBlueprintMCPServer::HandleListClasses);
H(TEXT("listFunctions"), &FBlueprintMCPServer::HandleListFunctions);
H(TEXT("listProperties"), &FBlueprintMCPServer::HandleListProperties);
// change_struct_node_type is now handled by UMCPHandler_ChangeStructNodeType (new-style registry)
// delete_node is now handled by UMCPHandler_DeleteNode (new-style registry)
// duplicate_nodes is now handled by UMCPHandler_DuplicateNodes (new-style registry)
H(TEXT("validateBlueprint"), &FBlueprintMCPServer::HandleValidateBlueprint);
H(TEXT("validateAllBlueprints"), &FBlueprintMCPServer::HandleValidateAllBlueprints);
// search_node_actions is now handled by UMCPHandler_SearchNodeActions (new-style registry)
// spawn_node is now handled by UMCPHandler_SpawnNode (new-style registry)
// rename_asset is now handled by UMCPHandler_RenameAsset (new-style registry)
H(TEXT("reparentBlueprint"), &FBlueprintMCPServer::HandleReparentBlueprint);
// set_blueprint_default is now handled by UMCPHandler_SetBlueprintDefault (new-style registry)
H(TEXT("createBlueprint"), &FBlueprintMCPServer::HandleCreateBlueprint);
H(TEXT("createGraph"), &FBlueprintMCPServer::HandleCreateGraph);
H(TEXT("deleteGraph"), &FBlueprintMCPServer::HandleDeleteGraph);
H(TEXT("renameGraph"), &FBlueprintMCPServer::HandleRenameGraph);
H(TEXT("addVariable"), &FBlueprintMCPServer::HandleAddVariable);
H(TEXT("removeVariable"), &FBlueprintMCPServer::HandleRemoveVariable);
H(TEXT("setVariableMetadata"), &FBlueprintMCPServer::HandleSetVariableMetadata);
// add_interface, remove_interface, list_interfaces now handled by new-style registry
H(TEXT("addEventDispatcher"), &FBlueprintMCPServer::HandleAddEventDispatcher);
H(TEXT("listEventDispatchers"), &FBlueprintMCPServer::HandleListEventDispatchers);
H(TEXT("addFunctionParameter"), &FBlueprintMCPServer::HandleAddFunctionParameter);
H(TEXT("addComponent"), &FBlueprintMCPServer::HandleAddComponent);
H(TEXT("removeComponent"), &FBlueprintMCPServer::HandleRemoveComponent);
H(TEXT("listComponents"), &FBlueprintMCPServer::HandleListComponents);
H(TEXT("snapshotGraph"), &FBlueprintMCPServer::HandleSnapshotGraph);
H(TEXT("diffGraph"), &FBlueprintMCPServer::HandleDiffGraph);
H(TEXT("restoreGraph"), &FBlueprintMCPServer::HandleRestoreGraph);
H(TEXT("findDisconnectedPins"), &FBlueprintMCPServer::HandleFindDisconnectedPins);
H(TEXT("analyzeRebuildImpact"), &FBlueprintMCPServer::HandleAnalyzeRebuildImpact);
// diff_blueprints is now handled by UMCPHandler_DiffBlueprints (new-style registry)
H(TEXT("createStruct"), &FBlueprintMCPServer::HandleCreateStruct);
H(TEXT("createEnum"), &FBlueprintMCPServer::HandleCreateEnum);
H(TEXT("addStructProperty"), &FBlueprintMCPServer::HandleAddStructProperty);
H(TEXT("removeStructProperty"), &FBlueprintMCPServer::HandleRemoveStructProperty);
H(TEXT("getMaterial"), &FBlueprintMCPServer::HandleGetMaterial);
H(TEXT("getMaterialGraph"), &FBlueprintMCPServer::HandleGetMaterialGraph);
H(TEXT("searchMaterials"), &FBlueprintMCPServer::HandleSearchMaterials);
H(TEXT("getMaterialInstanceParams"),&FBlueprintMCPServer::HandleGetMaterialInstanceParameters);
H(TEXT("getMaterialFunction"), &FBlueprintMCPServer::HandleGetMaterialFunction);
H(TEXT("describeMaterial"), &FBlueprintMCPServer::HandleDescribeMaterial);
H(TEXT("findMaterialReferences"), &FBlueprintMCPServer::HandleFindMaterialReferences);
H(TEXT("createMaterial"), &FBlueprintMCPServer::HandleCreateMaterial);
H(TEXT("setMaterialProperty"), &FBlueprintMCPServer::HandleSetMaterialProperty);
H(TEXT("addMaterialExpression"), &FBlueprintMCPServer::HandleAddMaterialExpression);
H(TEXT("deleteMaterialExpression"), &FBlueprintMCPServer::HandleDeleteMaterialExpression);
H(TEXT("connectMaterialPins"), &FBlueprintMCPServer::HandleConnectMaterialPins);
H(TEXT("disconnectMaterialPin"), &FBlueprintMCPServer::HandleDisconnectMaterialPin);
H(TEXT("setExpressionValue"), &FBlueprintMCPServer::HandleSetExpressionValue);
H(TEXT("moveMaterialExpression"), &FBlueprintMCPServer::HandleMoveMaterialExpression);
H(TEXT("createMaterialInstance"), &FBlueprintMCPServer::HandleCreateMaterialInstance);
H(TEXT("setMaterialInstanceParameter"), &FBlueprintMCPServer::HandleSetMaterialInstanceParameter);
H(TEXT("reparentMaterialInstance"), &FBlueprintMCPServer::HandleReparentMaterialInstance);
H(TEXT("createMaterialFunction"), &FBlueprintMCPServer::HandleCreateMaterialFunction);
H(TEXT("snapshotMaterialGraph"), &FBlueprintMCPServer::HandleSnapshotMaterialGraph);
H(TEXT("diffMaterialGraph"), &FBlueprintMCPServer::HandleDiffMaterialGraph);
H(TEXT("restoreMaterialGraph"), &FBlueprintMCPServer::HandleRestoreMaterialGraph);
H(TEXT("validateMaterial"), &FBlueprintMCPServer::HandleValidateMaterial);
H(TEXT("createAnimBlueprint"), &FBlueprintMCPServer::HandleCreateAnimBlueprint);
H(TEXT("addAnimState"), &FBlueprintMCPServer::HandleAddAnimState);
H(TEXT("removeAnimState"), &FBlueprintMCPServer::HandleRemoveAnimState);
H(TEXT("addAnimTransition"), &FBlueprintMCPServer::HandleAddAnimTransition);
H(TEXT("setTransitionRule"), &FBlueprintMCPServer::HandleSetTransitionRule);
H(TEXT("addAnimNode"), &FBlueprintMCPServer::HandleAddAnimNode);
H(TEXT("addStateMachine"), &FBlueprintMCPServer::HandleAddStateMachine);
H(TEXT("setStateAnimation"), &FBlueprintMCPServer::HandleSetStateAnimation);
H(TEXT("listAnimSlots"), &FBlueprintMCPServer::HandleListAnimSlots);
H(TEXT("listSyncGroups"), &FBlueprintMCPServer::HandleListSyncGroups);
H(TEXT("createBlendSpace"), &FBlueprintMCPServer::HandleCreateBlendSpace);
H(TEXT("setBlendSpaceSamples"), &FBlueprintMCPServer::HandleSetBlendSpaceSamples);
H(TEXT("setStateBlendSpace"), &FBlueprintMCPServer::HandleSetStateBlendSpace);
H(TEXT("test_save"), &FBlueprintMCPServer::HandleTestSave);
H(TEXT("search_by_type"), &FBlueprintMCPServer::HandleSearchByType);
H(TEXT("change_variable_type"), &FBlueprintMCPServer::HandleChangeVariableType);
H(TEXT("change_function_param_type"), &FBlueprintMCPServer::HandleChangeFunctionParamType);
H(TEXT("remove_function_parameter"), &FBlueprintMCPServer::HandleRemoveFunctionParameter);
H(TEXT("get_pin_info"), &FBlueprintMCPServer::HandleGetPinInfo);
H(TEXT("check_pin_compatibility"), &FBlueprintMCPServer::HandleCheckPinCompatibility);
H(TEXT("list_classes"), &FBlueprintMCPServer::HandleListClasses);
H(TEXT("list_functions"), &FBlueprintMCPServer::HandleListFunctions);
H(TEXT("list_properties"), &FBlueprintMCPServer::HandleListProperties);
H(TEXT("validate_blueprint"), &FBlueprintMCPServer::HandleValidateBlueprint);
H(TEXT("validate_all_blueprints"), &FBlueprintMCPServer::HandleValidateAllBlueprints);
H(TEXT("reparent_blueprint"), &FBlueprintMCPServer::HandleReparentBlueprint);
H(TEXT("create_blueprint"), &FBlueprintMCPServer::HandleCreateBlueprint);
H(TEXT("create_graph"), &FBlueprintMCPServer::HandleCreateGraph);
H(TEXT("delete_graph"), &FBlueprintMCPServer::HandleDeleteGraph);
H(TEXT("rename_graph"), &FBlueprintMCPServer::HandleRenameGraph);
H(TEXT("add_variable"), &FBlueprintMCPServer::HandleAddVariable);
H(TEXT("remove_variable"), &FBlueprintMCPServer::HandleRemoveVariable);
H(TEXT("set_variable_metadata"), &FBlueprintMCPServer::HandleSetVariableMetadata);
H(TEXT("add_event_dispatcher"), &FBlueprintMCPServer::HandleAddEventDispatcher);
H(TEXT("list_event_dispatchers"), &FBlueprintMCPServer::HandleListEventDispatchers);
H(TEXT("add_function_parameter"), &FBlueprintMCPServer::HandleAddFunctionParameter);
H(TEXT("add_component"), &FBlueprintMCPServer::HandleAddComponent);
H(TEXT("remove_component"), &FBlueprintMCPServer::HandleRemoveComponent);
H(TEXT("list_components"), &FBlueprintMCPServer::HandleListComponents);
H(TEXT("snapshot_graph"), &FBlueprintMCPServer::HandleSnapshotGraph);
H(TEXT("diff_graph"), &FBlueprintMCPServer::HandleDiffGraph);
H(TEXT("restore_graph"), &FBlueprintMCPServer::HandleRestoreGraph);
H(TEXT("find_disconnected_pins"), &FBlueprintMCPServer::HandleFindDisconnectedPins);
H(TEXT("analyze_rebuild_impact"), &FBlueprintMCPServer::HandleAnalyzeRebuildImpact);
H(TEXT("create_struct"), &FBlueprintMCPServer::HandleCreateStruct);
H(TEXT("create_enum"), &FBlueprintMCPServer::HandleCreateEnum);
H(TEXT("add_struct_property"), &FBlueprintMCPServer::HandleAddStructProperty);
H(TEXT("remove_struct_property"), &FBlueprintMCPServer::HandleRemoveStructProperty);
H(TEXT("get_material"), &FBlueprintMCPServer::HandleGetMaterial);
H(TEXT("get_material_graph"), &FBlueprintMCPServer::HandleGetMaterialGraph);
H(TEXT("search_materials"), &FBlueprintMCPServer::HandleSearchMaterials);
H(TEXT("get_material_instance_params"),&FBlueprintMCPServer::HandleGetMaterialInstanceParameters);
H(TEXT("get_material_function"), &FBlueprintMCPServer::HandleGetMaterialFunction);
H(TEXT("describe_material"), &FBlueprintMCPServer::HandleDescribeMaterial);
H(TEXT("find_material_references"), &FBlueprintMCPServer::HandleFindMaterialReferences);
H(TEXT("create_material"), &FBlueprintMCPServer::HandleCreateMaterial);
H(TEXT("set_material_property"), &FBlueprintMCPServer::HandleSetMaterialProperty);
H(TEXT("add_material_expression"), &FBlueprintMCPServer::HandleAddMaterialExpression);
H(TEXT("delete_material_expression"), &FBlueprintMCPServer::HandleDeleteMaterialExpression);
H(TEXT("connect_material_pins"), &FBlueprintMCPServer::HandleConnectMaterialPins);
H(TEXT("disconnect_material_pin"), &FBlueprintMCPServer::HandleDisconnectMaterialPin);
H(TEXT("set_expression_value"), &FBlueprintMCPServer::HandleSetExpressionValue);
H(TEXT("move_material_expression"), &FBlueprintMCPServer::HandleMoveMaterialExpression);
H(TEXT("create_material_instance"), &FBlueprintMCPServer::HandleCreateMaterialInstance);
H(TEXT("set_material_instance_parameter"), &FBlueprintMCPServer::HandleSetMaterialInstanceParameter);
H(TEXT("reparent_material_instance"), &FBlueprintMCPServer::HandleReparentMaterialInstance);
H(TEXT("create_material_function"), &FBlueprintMCPServer::HandleCreateMaterialFunction);
H(TEXT("snapshot_material_graph"), &FBlueprintMCPServer::HandleSnapshotMaterialGraph);
H(TEXT("diff_material_graph"), &FBlueprintMCPServer::HandleDiffMaterialGraph);
H(TEXT("restore_material_graph"), &FBlueprintMCPServer::HandleRestoreMaterialGraph);
H(TEXT("validate_material"), &FBlueprintMCPServer::HandleValidateMaterial);
H(TEXT("create_anim_blueprint"), &FBlueprintMCPServer::HandleCreateAnimBlueprint);
H(TEXT("add_anim_state"), &FBlueprintMCPServer::HandleAddAnimState);
H(TEXT("remove_anim_state"), &FBlueprintMCPServer::HandleRemoveAnimState);
H(TEXT("add_anim_transition"), &FBlueprintMCPServer::HandleAddAnimTransition);
H(TEXT("set_transition_rule"), &FBlueprintMCPServer::HandleSetTransitionRule);
H(TEXT("add_anim_node"), &FBlueprintMCPServer::HandleAddAnimNode);
H(TEXT("add_state_machine"), &FBlueprintMCPServer::HandleAddStateMachine);
H(TEXT("set_state_animation"), &FBlueprintMCPServer::HandleSetStateAnimation);
H(TEXT("list_anim_slots"), &FBlueprintMCPServer::HandleListAnimSlots);
H(TEXT("list_sync_groups"), &FBlueprintMCPServer::HandleListSyncGroups);
H(TEXT("create_blend_space"), &FBlueprintMCPServer::HandleCreateBlendSpace);
H(TEXT("set_blend_space_samples"), &FBlueprintMCPServer::HandleSetBlendSpaceSamples);
H(TEXT("set_state_blend_space"), &FBlueprintMCPServer::HandleSetStateBlendSpace);
}
void FBlueprintMCPServer::BuildMCPHandlerRegistry()

View File

@@ -132,14 +132,14 @@ public:
UPROPERTY(meta=(Description="Graph name (e.g. 'EventGraph')"))
FString Graph;
UPROPERTY(meta=(Description="Array of {actionName, posX, posY} objects. Use search_node_actions to find action names."))
UPROPERTY(meta=(Description="Array of {actionName, posX, posY} objects. Use search_node_types to find action names."))
FMCPJsonArray Nodes;
virtual FString GetDescription() const override
{
return TEXT("Create nodes in a Blueprint graph using the editor's action database. "
"Can create ANY node type that appears in the editor's right-click menu, including custom K2 nodes. "
"Use search_node_actions first to find the exact action name.");
"Use search_node_types first to find the exact action name.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override;
@@ -420,8 +420,8 @@ public:
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override;
};
UCLASS(meta=(ToolName="search_node_actions"))
class UMCPHandler_SearchNodeActions : public UMCPHandler
UCLASS(meta=(ToolName="search_node_types"))
class UMCPHandler_SearchNodeTypes : public UMCPHandler
{
GENERATED_BODY()

View File

@@ -1,75 +0,0 @@
# Blueprint MCP TODO
> **Note:** The original feature roadmap (force delete, delete_node, search_by_type, add_node, rename_asset, validate_blueprint, batch operations, dry run, URL encoding fix, workflow recipes, tool chaining hints, summary parameter types, describe_graph data flow, write tool updated state, actionable error messages, type name docs) has been fully implemented.
---
## Implemented: Blueprint Safety Tools (Feb 2025)
The following features from the incident post-mortem have been implemented as 5 new MCP tools:
### `snapshot_graph` / `diff_graph` / `restore_graph` — Graph State Backup & Restore
Snapshot a Blueprint's full graph state before destructive operations, diff against the snapshot after, and bulk-restore severed connections. Snapshots persist to disk at `Saved/BlueprintMCP/Snapshots/`. `restore_graph` also absorbs the originally-planned `reconnect_break_node` feature via optional `nodeId` and `pinMap` parameters.
### `find_disconnected_pins` — Detect Broken Wiring
Scans Blueprints for Break/Make struct nodes with broken types (HIGH confidence) or zero data connections (MEDIUM confidence). Also supports snapshot-based definite-break detection. Catches the "compile clean but broken data flow" problem that `validate_blueprint` misses.
### `analyze_rebuild_impact` — Pre-Rebuild Impact Analysis
Given a C++ module name, enumerates all USTRUCTs/UENUMs via UE5 reflection, scans all Blueprints for Break/Make nodes, variables, and function parameters referencing those types, and classifies risk as HIGH/MEDIUM/LOW with connection-at-risk counts.
See **Recipe 3: C++ Rebuild Safety** in the workflow-recipes resource for the full before/after rebuild workflow.
---
---
# Incident Report: C++ USTRUCT Rebuild Broke Break Nodes (2025-02)
## What Happened
The `StateParsers` C++ function library was rewritten to accept `FJsonLibraryObject` (from the JsonLibrary marketplace plugin) instead of `FJsonObjectWrapper` (UE5 built-in). After the C++ module was rebuilt and hot-reloaded in the editor, **13 `Break <struct>` nodes across 6 Blueprints** lost their struct type association and became `Break <unknown struct>`. This silently severed **all output data pin connections** on those nodes — roughly **80+ data wires** were destroyed.
### Affected Blueprints and Node Counts
| Blueprint | Break Nodes Affected | Data Connections Lost |
|-----------|---------------------|----------------------|
| BP_Patient_Base | 5 (FSkinState, FDisabilityState, FExposureState, FExaminationState, FCirculationState) | ~45 |
| BP_PatientManager | 3 (FVitals x2, FCirculationState) | ~9 |
| BPC_BreathingController | 3 (FBreathingSoundState, FWorkOfBreathingState, FLungSoundState) | ~16 |
| BPC_HybridTheory | 1 (FHybridControlState) | ~3 |
| BPC_DeviceController | 1 (FDeviceState — type survived but pins disconnected) | ~12 |
### Root Cause
When the C++ module containing the USTRUCT definitions is recompiled, UE5 invalidates and reloads the struct metadata. Break/Make struct nodes that reference those structs lose their type binding. The node reverts to `<unknown struct>` and all output pins (which are dynamically generated from the struct's UPROPERTY fields) are destroyed along with their connections.
This is a known UE5 behavior — the struct definitions themselves were unchanged, but the module reload triggered the invalidation anyway.
### Why It Was Hard to Detect
1. **No compiler errors initially** — Blueprints with orphaned Break nodes don't always produce compile errors if the exec chain still flows (the Break node just becomes a no-op with no outputs).
2. **Silent data loss** — The data wires were destroyed, not just disconnected. There's no "undo" and no log of what was connected before.
3. **Scattered across many BPs** — The Break nodes were in 6 different Blueprints across different subsystems.
4. **BPC_DeviceController was missed** — The initial search found 4 affected BPs. BPC_DeviceController wasn't caught until a second pass because its Break node retained the correct type but had disconnected pins (a subtler failure mode).
### How It Was Fixed
1. `search_by_type` (if it had existed) or manual `get_blueprint_graph` calls to find all Break nodes with `<unknown struct>` types
2. `change_struct_node_type` to reassign the correct struct type to each broken Break node
3. `refresh_all_nodes` on each affected Blueprint
4. Manual analysis of each graph to determine what each Break output pin should connect to
5. ~80 individual `connect_pins` calls to rewire all data connections
6. `validate_blueprint` on each BP to confirm clean compilation
Total effort: ~3 hours of tedious graph inspection and pin-by-pin reconnection.
## Prevention Checklist for Future C++ Rebuilds
When modifying C++ USTRUCT definitions or the module containing them:
1. **Before rebuild**: Use `get_blueprint_graph` on all Blueprints that use Break/Make nodes for structs in the module. Save the JSON output as a reference for reconnection.
2. **After rebuild**: Immediately check all Break/Make nodes — run `validate_blueprint` on every affected BP.
3. **If nodes are broken**: Use `change_struct_node_type` to restore types, then manually reconnect pins using the saved graph state as reference.
4. **Don't trust "compiles clean"**: A Blueprint can compile cleanly while having completely disconnected data flow. Always verify data pin connections, not just compilation status.

View File

@@ -1,11 +0,0 @@
export interface NodeMap {
[id: string]: any;
}
export declare function describeNode(node: any): string;
/** Annotate a node description with data pin input connections (#10) */
export declare function annotateDataFlow(node: any, nodeMap: NodeMap): string;
/** Annotate output data pins that feed into other nodes (#10) */
export declare function annotateDataOutputs(node: any, nodeMap: NodeMap): string[];
export declare function walkExecChain(startNodeId: string, nodeMap: NodeMap, visited: Set<string>, depth?: number): string[];
export declare function describeGraph(graphData: any): string;
export declare function summarizeBlueprint(data: any): string;

View File

@@ -1,323 +0,0 @@
import { flagType, formatVarType, formatParams } from "./helpers.js";
export function describeNode(node) {
const cls = node.class || "";
if (node.nodeType === "CallParentFunction") {
return `CALL PARENT ${node.functionName || node.title}`;
}
if (node.nodeType === "OverrideEvent") {
return `OVERRIDE ${node.eventName || node.title}`;
}
if (cls.includes("CallFunction")) {
const target = node.targetClass ? `${node.targetClass}.` : "";
return `CALL ${target}${node.functionName || node.title}`;
}
if (node.nodeType === "VariableSet")
return `SET ${node.variableName || node.title}`;
if (node.nodeType === "VariableGet")
return `GET ${node.variableName || node.title}`;
if (node.nodeType === "Branch")
return "IF";
if (node.nodeType === "DynamicCast")
return `CAST to ${node.castTarget || "?"}`;
if (node.nodeType === "MacroInstance")
return `MACRO ${node.macroName || node.title}`;
if (cls.includes("AssignmentStatement"))
return "ASSIGN";
if (cls.includes("K2Node_Select"))
return "SELECT";
if (cls.includes("SwitchEnum") || cls.includes("SwitchInteger") || cls.includes("SwitchString") || cls.includes("Switch"))
return `SWITCH`;
if (cls.includes("ForEachLoop") || cls.includes("ForLoop"))
return `FOR LOOP`;
if (cls.includes("Sequence"))
return "SEQUENCE";
if (cls.includes("SpawnActor"))
return "SPAWN ACTOR";
if (cls.includes("CreateWidget"))
return "CREATE WIDGET";
if (cls.includes("Knot"))
return null; // skip reroute nodes
return node.title || cls;
}
/** Annotate a node description with data pin input connections (#10) */
export function annotateDataFlow(node, nodeMap) {
const dataInputs = (node.pins || []).filter((p) => p.type !== "exec" && p.direction === "Input" && p.connections?.length > 0);
if (dataInputs.length === 0)
return "";
const parts = [];
for (const pin of dataInputs) {
for (const conn of pin.connections) {
const sourceNode = nodeMap[conn.nodeId];
if (!sourceNode)
continue;
const sourceName = sourceNode.variableName || sourceNode.functionName || sourceNode.title || sourceNode.class || "?";
const sourcePin = conn.pinName || "?";
parts.push(`${pin.name}=${sourceName}.${sourcePin}`);
}
}
if (parts.length === 0)
return "";
return `(${parts.join(", ")})`;
}
/** Annotate output data pins that feed into other nodes (#10) */
export function annotateDataOutputs(node, nodeMap) {
const lines = [];
const dataOutputs = (node.pins || []).filter((p) => p.type !== "exec" && p.direction === "Output" && p.connections?.length > 0);
for (const pin of dataOutputs) {
for (const conn of pin.connections) {
const targetNode = nodeMap[conn.nodeId];
if (!targetNode)
continue;
const targetName = targetNode.variableName || targetNode.functionName || targetNode.title || targetNode.class || "?";
lines.push(`\u2192 ${pin.name} \u2192 [${targetName}.${conn.pinName || "?"}]`);
}
}
return lines;
}
export function walkExecChain(startNodeId, nodeMap, visited, depth = 0) {
if (depth > 50 || visited.has(startNodeId))
return [];
visited.add(startNodeId);
const node = nodeMap[startNodeId];
if (!node)
return [];
const lines = [];
const indent = " ".repeat(depth + 1);
const desc = describeNode(node);
const dataFlow = annotateDataFlow(node, nodeMap);
// Find exec output pins (pins with type "exec" and direction "Output")
const execOutPins = (node.pins || []).filter((p) => p.type === "exec" && p.direction === "Output");
if (node.nodeType === "Branch") {
// Special handling for branch: show IF with True/False paths
lines.push(`${indent}IF:${dataFlow ? ` ${dataFlow}` : ""}`);
for (const pin of execOutPins) {
const label = pin.name || "?";
if (pin.connections?.length) {
lines.push(`${indent} [${label}]:`);
for (const conn of pin.connections) {
lines.push(...walkExecChain(conn.nodeId, nodeMap, visited, depth + 2));
}
}
}
}
else if (node.class?.includes("Sequence")) {
lines.push(`${indent}SEQUENCE:`);
for (let i = 0; i < execOutPins.length; i++) {
const pin = execOutPins[i];
if (pin.connections?.length) {
lines.push(`${indent} [${i}]:`);
for (const conn of pin.connections) {
lines.push(...walkExecChain(conn.nodeId, nodeMap, visited, depth + 2));
}
}
}
}
else if (node.class?.includes("ForEachLoop") || node.class?.includes("ForLoop")) {
if (desc)
lines.push(`${indent}${desc}:${dataFlow ? ` ${dataFlow}` : ""}`);
for (const pin of execOutPins) {
const label = pin.name || "?";
if (pin.connections?.length) {
lines.push(`${indent} [${label}]:`);
for (const conn of pin.connections) {
lines.push(...walkExecChain(conn.nodeId, nodeMap, visited, depth + 2));
}
}
}
}
else if (node.class?.includes("Switch")) {
if (desc)
lines.push(`${indent}${desc}:${dataFlow ? ` ${dataFlow}` : ""}`);
for (const pin of execOutPins) {
if (pin.connections?.length) {
lines.push(`${indent} [${pin.name}]:`);
for (const conn of pin.connections) {
lines.push(...walkExecChain(conn.nodeId, nodeMap, visited, depth + 2));
}
}
}
}
else {
// Normal linear node: describe it and follow the first "then" exec pin
if (desc) {
lines.push(`${indent}${desc}${dataFlow ? ` ${dataFlow}` : ""}`);
// Show data output connections (#10)
const dataOuts = annotateDataOutputs(node, nodeMap);
for (const dout of dataOuts) {
lines.push(`${indent} ${dout}`);
}
}
// Follow exec chain: look for "then" pin or first exec output with connections
const thenPin = execOutPins.find((p) => p.name === "then" || p.name === "execute" || p.name === "output") || execOutPins[0];
if (thenPin?.connections?.length) {
for (const conn of thenPin.connections) {
lines.push(...walkExecChain(conn.nodeId, nodeMap, visited, depth));
}
}
}
return lines;
}
export function describeGraph(graphData) {
const lines = [];
const nodes = graphData.nodes || [];
lines.push(`# ${graphData.name} (${nodes.length} nodes)`);
// State machine description mode
if (graphData.graphType === "StateMachine") {
if (graphData.entryState) {
lines.push(`Entry → ${graphData.entryState}`);
}
lines.push("");
// Collect states and transitions
const states = nodes.filter((n) => n.nodeType === "AnimState");
const transitions = nodes.filter((n) => n.nodeType === "AnimTransition");
if (states.length > 0) {
lines.push("States:");
for (const s of states) {
const animInfo = s.animationAsset ? ` [AnimSequence: ${s.animationAsset}]` :
s.blendSpaceAsset ? ` [BlendSpace: ${s.blendSpaceAsset}]` : "";
lines.push(` ${s.stateName || s.title}${animInfo}`);
}
}
if (transitions.length > 0) {
lines.push("");
lines.push("Transitions:");
for (const t of transitions) {
const from = t.fromState || "?";
const to = t.toState || "?";
const dur = t.crossfadeDuration !== undefined ? `${t.crossfadeDuration}s` : "?";
const pri = t.priorityOrder !== undefined ? `priority ${t.priorityOrder}` : "";
const bidir = t.bBidirectional ? ", bidirectional" : "";
lines.push(` ${from}${to} (${dur}${pri ? `, ${pri}` : ""}${bidir})`);
}
}
return lines.join("\n");
}
// AnimGraph — identify anim node types
if (graphData.graphType === "AnimGraph") {
lines.push(`(Animation Graph)`);
}
else if (graphData.graphType === "TransitionRule") {
lines.push(`(Transition Rule)`);
}
// Build node lookup
const nodeMap = {};
for (const n of nodes) {
nodeMap[n.id] = n;
}
// Find entry points: Event nodes, CustomEvent nodes, FunctionEntry nodes
const entryNodes = nodes.filter((n) => n.nodeType === "Event" ||
n.nodeType === "CustomEvent" ||
n.class?.includes("FunctionEntry") ||
n.class?.includes("K2Node_Tunnel") && n.pins?.some((p) => p.type === "exec" && p.direction === "Output" && p.connections?.length));
if (entryNodes.length === 0) {
// No entry points found - list all nodes as a fallback
lines.push("\n(No event/entry nodes found)");
lines.push("Nodes:");
for (const n of nodes) {
const desc = describeNode(n);
if (desc)
lines.push(` ${desc}`);
}
return lines.join("\n");
}
for (const entry of entryNodes) {
const label = entry.eventName || entry.title || entry.class;
lines.push(`\n## on ${label}:`);
// Find exec output pins to start walking
const execOuts = (entry.pins || []).filter((p) => p.type === "exec" && p.direction === "Output");
const visited = new Set();
visited.add(entry.id);
for (const pin of execOuts) {
if (pin.connections?.length) {
for (const conn of pin.connections) {
lines.push(...walkExecChain(conn.nodeId, nodeMap, visited, 0));
}
}
}
}
return lines.join("\n");
}
export function summarizeBlueprint(data) {
const lines = [];
lines.push(`# ${data.name}`);
lines.push(`Parent: ${data.parentClass || "?"} | Path: ${data.path}`);
if (data.blueprintType)
lines.push(`Type: ${data.blueprintType}`);
if (data.isAnimBlueprint) {
lines.push(`Animation Blueprint: yes`);
if (data.targetSkeleton)
lines.push(`Target Skeleton: ${data.targetSkeleton}`);
}
if (data.interfaces?.length) {
lines.push(`\n## Interfaces (${data.interfaces.length})`);
for (const iface of data.interfaces)
lines.push(` ${iface}`);
}
if (data.variables?.length) {
lines.push(`\n## Variables (${data.variables.length})`);
for (const v of data.variables) {
const defVal = v.defaultValue ? ` = ${v.defaultValue}` : "";
const cat = v.category ? ` [${v.category}]` : "";
const typeStr = flagType(formatVarType(v));
lines.push(` ${v.name}: ${typeStr}${defVal}${cat}`);
}
}
if (data.graphs?.length) {
lines.push(`\n## Graphs (${data.graphs.length})`);
for (const g of data.graphs) {
const nodes = g.nodes || [];
const nodeCount = nodes.length;
// Collect events with their parameters (#9)
const events = nodes
.filter((n) => n.nodeType === "Event" || n.nodeType === "CustomEvent")
.map((n) => {
const name = n.eventName || n.title;
const params = (n.pins || [])
.filter((p) => p.direction === "Output" && p.type !== "exec" && p.name !== "")
.map((p) => ({ name: p.name, type: p.subtype || p.type || "" }));
return `${name}${formatParams(params.length > 0 ? params : n.parameters)}`;
});
// Collect unique function calls
const calls = [
...new Set(nodes
.filter((n) => n.class?.includes("CallFunction") && n.functionName)
.map((n) => n.functionName)),
];
// Collect variable writes
const varSets = [
...new Set(nodes
.filter((n) => n.nodeType === "VariableSet" && n.variableName)
.map((n) => n.variableName)),
];
// Collect delegates with parameters (#9)
const delegates = nodes
.filter((n) => n.class?.includes("CreateDelegate") || n.class?.includes("DelegateFunction") || n.nodeType === "EventDispatcher")
.map((n) => {
const name = n.delegateName || n.functionName || n.title;
return `${name}${formatParams(n.parameters)}`;
});
// Collect function entries with parameters (#9 - for function graphs)
const funcEntries = nodes
.filter((n) => n.class?.includes("FunctionEntry"))
.map((n) => {
const params = (n.pins || [])
.filter((p) => p.direction === "Output" && p.type !== "exec" && p.name !== "")
.map((p) => ({ name: p.name, type: p.subtype || p.type || "" }));
return params.length > 0 ? formatParams(params) : "";
});
const funcParamStr = funcEntries.length === 1 ? funcEntries[0] : "";
const graphTypeStr = g.graphType ? ` [${g.graphType}]` : "";
lines.push(` ${g.name} (${nodeCount} nodes)${funcParamStr}${graphTypeStr}`);
if (events.length)
lines.push(` Events: ${events.join(", ")}`);
if (delegates.length)
lines.push(` Delegates: ${delegates.join(", ")}`);
if (calls.length)
lines.push(` Calls: ${calls.join(", ")}`);
if (varSets.length)
lines.push(` Sets: ${varSets.join(", ")}`);
}
}
return lines.join("\n");
}
//# sourceMappingURL=graph-describe.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +0,0 @@
export declare const TYPE_NAME_DOCS = "Type name formats: C++ USTRUCTs use F-prefixed name (e.g. 'FVitals', 'FDeviceState'), BP structs (UserDefinedStruct) use asset name (e.g. 'S_Vitals'), enums use enum name (e.g. 'ELungSound'). Object references use colon syntax: 'object:Actor', 'softobject:Actor', 'class:Actor' (TSubclassOf), 'softclass:Actor', 'interface:MyInterface'.";
export declare const UNRESOLVED_TYPE_PATTERNS: string[];
export declare function flagType(typeName: string): string;
export declare function formatVarType(v: any): string;
/** Format parameter list for functions/events/delegates with type flagging (#9) */
export declare function formatParams(params: any[] | undefined): string;
/** Format updated state returned by mutation tools (#11) */
export declare function formatUpdatedState(data: any): string[];

View File

@@ -1,62 +0,0 @@
// --- Type name format documentation (shared across tool descriptions) ---
export const TYPE_NAME_DOCS = `Type name formats: C++ USTRUCTs use F-prefixed name (e.g. 'FVitals', 'FDeviceState'), BP structs (UserDefinedStruct) use asset name (e.g. 'S_Vitals'), enums use enum name (e.g. 'ELungSound'). Object references use colon syntax: 'object:Actor', 'softobject:Actor', 'class:Actor' (TSubclassOf), 'softclass:Actor', 'interface:MyInterface'.`;
// --- Warning marker for unresolved types ---
export const UNRESOLVED_TYPE_PATTERNS = ["<None>", "<unknown>", "None", "NONE"];
export function flagType(typeName) {
if (!typeName)
return "\u26A0 <None>";
for (const pat of UNRESOLVED_TYPE_PATTERNS) {
if (typeName === pat || typeName.includes(pat)) {
return `\u26A0 ${typeName}`;
}
}
return typeName;
}
export function formatVarType(v) {
let t = v.type || "unknown";
if (v.subtype)
t = v.subtype;
if (v.isMap)
return `Map<${v.type}, ${v.subtype || "?"}>`;
if (v.isSet)
return `Set<${t}>`;
if (v.isArray)
return `${t}[]`;
return t;
}
/** Format parameter list for functions/events/delegates with type flagging (#9) */
export function formatParams(params) {
if (!params || params.length === 0)
return "";
const parts = params.map((p) => {
const name = p.name || "?";
const type = p.type || p.pinType || "";
return `${name}: ${flagType(type)}`;
});
return ` \u2014 Params: ${parts.join(", ")}`;
}
/** Format updated state returned by mutation tools (#11) */
export function formatUpdatedState(data) {
const lines = [];
if (data.updatedState) {
lines.push(`\nUpdated state:`);
const state = data.updatedState;
if (state.variables?.length) {
lines.push(` Variables: ${state.variables.map((v) => `${v.name}: ${v.type}`).join(", ")}`);
}
if (state.pins?.length) {
lines.push(` Pins:`);
for (const pin of state.pins) {
lines.push(` ${pin.direction === "Output" ? "\u2192" : "\u2190"} ${pin.name}: ${pin.type}${pin.subtype ? ` (${pin.subtype})` : ""}`);
}
}
if (state.nodeCount !== undefined) {
lines.push(` Nodes: ${state.nodeCount}`);
}
if (state.graphCount !== undefined) {
lines.push(` Graphs: ${state.graphCount}`);
}
}
return lines;
}
//# sourceMappingURL=helpers.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"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"}

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1,78 +0,0 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { getUEHealth, gracefulShutdown, state } from "./ue-bridge.js";
// Tool registrations
import { registerReadTools } from "./tools/read.js";
import { registerMutationTools } from "./tools/mutation.js";
import { registerVariableTools } from "./tools/variables.js";
import { registerParamTools } from "./tools/params.js";
import { registerGraphTools } from "./tools/graphs.js";
import { registerInterfaceTools } from "./tools/interfaces.js";
import { registerDispatcherTools } from "./tools/dispatchers.js";
import { registerComponentTools } from "./tools/components.js";
import { registerSnapshotTools } from "./tools/snapshot.js";
import { registerValidationTools } from "./tools/validation.js";
import { registerUtilityTools } from "./tools/utility.js";
import { registerDiscoveryTools } from "./tools/discovery.js";
import { registerDiffBlueprintsTools } from "./tools/diff-blueprints.js";
import { registerUserTypeTools } from "./tools/user-types.js";
import { registerMaterialReadTools } from "./tools/material-read.js";
import { registerMaterialMutationTools } from "./tools/material-mutation.js";
import { registerAnimationTools } from "./tools/animation-mutation.js";
// Resource registrations
import { registerBlueprintListResource } from "./resources/blueprint-list.js";
import { registerWorkflowRecipesResource } from "./resources/workflow-recipes.js";
const server = new McpServer({
name: "blueprint-mcp",
version: "1.0.0",
});
// Register all tools
registerReadTools(server);
registerMutationTools(server);
registerVariableTools(server);
registerParamTools(server);
registerGraphTools(server);
registerInterfaceTools(server);
registerDispatcherTools(server);
registerComponentTools(server);
registerSnapshotTools(server);
registerValidationTools(server);
registerUtilityTools(server);
registerDiscoveryTools(server);
registerDiffBlueprintsTools(server);
registerUserTypeTools(server);
registerMaterialReadTools(server);
registerMaterialMutationTools(server);
registerAnimationTools(server);
// Register resources
registerBlueprintListResource(server);
registerWorkflowRecipesResource(server);
// Cleanup on exit — only kill the commandlet if we spawned it (don't kill the editor).
process.on("exit", () => { if (!state.editorMode)
state.ueProcess?.kill(); });
for (const sig of ["SIGINT", "SIGTERM"]) {
process.on(sig, async () => {
if (!state.editorMode && state.ueProcess) {
await gracefulShutdown();
}
process.exit();
});
}
async function main() {
const health = await getUEHealth();
if (health) {
state.editorMode = health.mode === "editor";
console.error(`Connected to UE5 ${health.mode} \u2014 MCP server already running.`);
}
else {
state.editorMode = false;
console.error("UE5 server not detected. Commandlet will be spawned on first tool call.");
}
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});
//# sourceMappingURL=index.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}

View File

@@ -1 +0,0 @@
export declare function describeMaterial(data: any): string;

View File

@@ -1,22 +0,0 @@
export function describeMaterial(data) {
const lines = [];
if (data.name) {
lines.push(`# Material: ${data.name}`);
}
if (data.description) {
lines.push(data.description);
return lines.join("\n");
}
// Format inputs array if present
if (data.inputs && Array.isArray(data.inputs)) {
for (const input of data.inputs) {
const connected = input.connected ? "" : " (disconnected)";
lines.push(`${input.input}: ${input.chain || "empty"}${connected}`);
}
}
if (lines.length <= 1) {
lines.push("No material input data available.");
}
return lines.join("\n");
}
//# sourceMappingURL=material-describe.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"material-describe.js","sourceRoot":"","sources":["../src/material-describe.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB,CAAC,IAAS;IACxC,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,KAAK,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC7B,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,iCAAiC;IACjC,IAAI,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,iBAAiB,CAAC;YAC3D,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,IAAI,OAAO,GAAG,SAAS,EAAE,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerBlueprintListResource(server: McpServer): void;

View File

@@ -1,11 +0,0 @@
import { isUEHealthy, ueGet } from "../ue-bridge.js";
export function registerBlueprintListResource(server) {
server.resource("blueprint-list", "blueprint:///list", { description: "List of all exported Blueprints", mimeType: "application/json" }, async (uri) => {
if (!(await isUEHealthy())) {
return { contents: [{ uri: uri.href, text: "[]", mimeType: "application/json" }] };
}
const data = await ueGet("/api/list");
return { contents: [{ uri: uri.href, text: JSON.stringify(data.blueprints, null, 2), mimeType: "application/json" }] };
});
}
//# sourceMappingURL=blueprint-list.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"blueprint-list.js","sourceRoot":"","sources":["../../src/resources/blueprint-list.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAErD,MAAM,UAAU,6BAA6B,CAAC,MAAiB;IAC7D,MAAM,CAAC,QAAQ,CACb,gBAAgB,EAChB,mBAAmB,EACnB,EAAE,WAAW,EAAE,iCAAiC,EAAE,QAAQ,EAAE,kBAAkB,EAAE,EAChF,KAAK,EAAE,GAAG,EAAE,EAAE;QACZ,IAAI,CAAC,CAAC,MAAM,WAAW,EAAE,CAAC,EAAE,CAAC;YAC3B,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,kBAAkB,EAAE,CAAC,EAAE,CAAC;QACrF,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,CAAC;QACtC,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,kBAAkB,EAAE,CAAC,EAAE,CAAC;IACzH,CAAC,CACF,CAAC;AACJ,CAAC"}

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerWorkflowRecipesResource(server: McpServer): void;

View File

@@ -1,131 +0,0 @@
export function registerWorkflowRecipesResource(server) {
server.resource("workflow-recipes", "blueprint:///recipes", { description: "Workflow recipes for common Blueprint migration tasks", mimeType: "text/markdown" }, async (uri) => {
const recipes = `# Blueprint MCP Workflow Recipes
## Recipe 1: Migrate BP Struct to C++ USTRUCT
When replacing a Blueprint UserDefinedStruct (e.g. \`S_Vitals\`) with a C++ USTRUCT (e.g. \`FVitals\`):
### Steps
1. **Identify all usages** of the old struct:
\`\`\`
search_by_type(typeName="S_Vitals")
find_asset_references(assetPath="/Game/Blueprints/WebUI/S_Vitals")
\`\`\`
2. **Change variable types** in each Blueprint that declares a variable of this type:
\`\`\`
change_variable_type(blueprint="BP_PatientManager", variable="CurrentVitals", newType="FVitals", typeCategory="struct")
\`\`\`
3. **Change function/event parameter types** where the struct is used as a parameter:
\`\`\`
change_function_parameter_type(blueprint="BP_PatientManager", functionName="UpdateVitals", paramName="Vitals", newType="FVitals")
\`\`\`
4. **Update Break/Make struct nodes** to use the new type:
\`\`\`
change_struct_node_type(blueprint="BP_PatientJson", nodeId="<guid>", newType="FVitals")
\`\`\`
5. **Refresh all nodes** in each modified Blueprint:
\`\`\`
refresh_all_nodes(blueprint="BP_PatientManager")
\`\`\`
6. **Validate** each Blueprint compiles cleanly:
\`\`\`
validate_blueprint(blueprint="BP_PatientManager")
\`\`\`
7. **Delete the old BP struct** once all references are removed:
\`\`\`
find_asset_references(assetPath="/Game/Blueprints/WebUI/S_Vitals")
delete_asset(assetPath="/Game/Blueprints/WebUI/S_Vitals")
\`\`\`
### Tips
- Use \`dryRun=true\` on mutation tools to preview changes first
- Use batch mode to change multiple variables/parameters at once
- The \`search_by_type\` tool is more granular than \`find_asset_references\` for finding specific usages
- After changing parameter types, check delegate graphs that bind to those functions
---
## Recipe 2: Convert BP Function Library to C++
When replacing a Blueprint Function Library (e.g. \`FL_StateParsers\`) with a C++ equivalent (e.g. \`UStateParsersLibrary\`):
### Steps
1. **Identify all Blueprints** that call functions from the library:
\`\`\`
find_asset_references(assetPath="/Game/Blueprints/WebUI/FL_StateParsers")
\`\`\`
2. **For each referencing Blueprint**, redirect function calls:
\`\`\`
replace_function_calls(blueprint="BP_PatientJson", oldClass="FL_StateParsers", newClass="StateParsersLibrary")
\`\`\`
3. **Refresh nodes** to update pin types:
\`\`\`
refresh_all_nodes(blueprint="BP_PatientJson")
\`\`\`
4. **Fix broken connections** reported by replace_function_calls:
- Use \`get_blueprint_graph\` to inspect the affected graph
- Use \`connect_pins\` to rewire broken data connections
- If pin types changed, use \`change_struct_node_type\` for Break/Make nodes
5. **Validate** each Blueprint:
\`\`\`
validate_blueprint(blueprint="BP_PatientJson")
\`\`\`
6. **Delete the old BP function library**:
\`\`\`
delete_asset(assetPath="/Game/Blueprints/WebUI/FL_StateParsers")
\`\`\`
### Tips
- Preview with \`dryRun=true\` on \`replace_function_calls\` first
- If function signatures changed (different parameter types), connections will break and need manual rewiring
- The \`brokenConnections\` array in the response tells you exactly which pins lost their wires
---
## Recipe 3: C++ Rebuild Safety
When rebuilding a C++ module containing USTRUCT/UENUM definitions:
### Before Rebuild
1. Analyze impact:
\`analyze_rebuild_impact(moduleName="YourModule")\`
2. Snapshot HIGH-risk Blueprints:
\`snapshot_graph(blueprint="BP_Affected1")\`
\`snapshot_graph(blueprint="BP_Affected2")\`
### After Rebuild
3. Assess damage:
\`find_disconnected_pins(filter="/Game/Blueprints/")\`
4. Diff each snapshot:
\`diff_graph(blueprint="BP_Affected1", snapshotId="snap_...")\`
5. Fix broken struct types:
\`change_struct_node_type(blueprint="BP_Affected1", nodeId="...", newType="FYourStruct")\`
6. Restore connections:
\`restore_graph(blueprint="BP_Affected1", snapshotId="snap_...")\`
7. Verify:
\`validate_blueprint(blueprint="BP_Affected1")\`
\`find_disconnected_pins(blueprint="BP_Affected1")\`
`;
return { contents: [{ uri: uri.href, text: recipes, mimeType: "text/markdown" }] };
});
}
//# sourceMappingURL=workflow-recipes.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"workflow-recipes.js","sourceRoot":"","sources":["../../src/resources/workflow-recipes.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,+BAA+B,CAAC,MAAiB;IAC/D,MAAM,CAAC,QAAQ,CACb,kBAAkB,EAClB,sBAAsB,EACtB,EAAE,WAAW,EAAE,uDAAuD,EAAE,QAAQ,EAAE,eAAe,EAAE,EACnG,KAAK,EAAE,GAAG,EAAE,EAAE;QACZ,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4HrB,CAAC;QACI,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC;IACrF,CAAC,CACF,CAAC;AACJ,CAAC"}

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerAnimationTools(server: McpServer): void;

View File

@@ -1,412 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost } from "../ue-bridge.js";
export function registerAnimationTools(server) {
// ---------------------------------------------------------------------------
// create_anim_blueprint
// ---------------------------------------------------------------------------
server.tool("create_anim_blueprint", "Create a new Animation Blueprint asset with a target skeleton.", {
name: z.string().describe("Animation Blueprint name (e.g. 'ABP_MyCharacter')"),
packagePath: z.string().default("/Game").describe("Package path (e.g. '/Game/Animations')"),
skeleton: z.string().describe("Skeleton asset name or path. Use '__create_test_skeleton__' for testing."),
parentClass: z.string().optional().describe("Parent class (default: AnimInstance)"),
}, async ({ name, packagePath, skeleton, parentClass }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { name, packagePath, skeleton };
if (parentClass)
body.parentClass = parentClass;
const data = await uePost("/api/create-anim-blueprint", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Created Animation Blueprint: ${data.blueprintName || name}`);
lines.push(`Path: ${data.assetPath || packagePath}`);
lines.push(`Skeleton: ${data.targetSkeleton || skeleton}`);
lines.push(`Parent: ${data.parentClass || "AnimInstance"}`);
if (data.graphs?.length)
lines.push(`Graphs: ${data.graphs.join(", ")}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(`\nNext steps:`);
lines.push(` 1. Use add_state_machine to add state machines to the AnimGraph`);
lines.push(` 2. Use add_anim_state to add states to a state machine`);
lines.push(` 3. Use add_anim_transition to connect states with transitions`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// add_anim_state
// ---------------------------------------------------------------------------
server.tool("add_anim_state", "Add a state to a state machine graph in an Animation Blueprint.", {
blueprint: z.string().describe("Animation Blueprint name or path"),
graph: z.string().describe("State machine graph name"),
stateName: z.string().describe("Name for the new state"),
animationAsset: z.string().optional().describe("Animation sequence asset to assign to the state"),
posX: z.number().optional().describe("X position in graph"),
posY: z.number().optional().describe("Y position in graph"),
}, async ({ blueprint, graph, stateName, animationAsset, posX, posY }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, graph, stateName };
if (animationAsset)
body.animationAsset = animationAsset;
if (posX !== undefined)
body.posX = posX;
if (posY !== undefined)
body.posY = posY;
const data = await uePost("/api/add-anim-state", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Added state "${data.stateName || stateName}" to ${data.graph || graph}`);
lines.push(`Node ID: ${data.nodeId}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(`\nNext steps:`);
lines.push(` 1. Use add_anim_transition to connect this state to other states`);
lines.push(` 2. Use set_state_animation to assign an animation to this state`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// remove_anim_state
// ---------------------------------------------------------------------------
server.tool("remove_anim_state", "Remove a state and its connected transitions from a state machine graph.", {
blueprint: z.string().describe("Animation Blueprint name or path"),
graph: z.string().describe("State machine graph name"),
stateName: z.string().describe("Name of the state to remove"),
}, async ({ blueprint, graph, stateName }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/remove-anim-state", { blueprint, graph, stateName });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Removed state "${data.removedState || stateName}"`);
lines.push(`Removed transitions: ${data.removedTransitions ?? 0}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// add_anim_transition
// ---------------------------------------------------------------------------
server.tool("add_anim_transition", "Add a transition between two states in a state machine graph.", {
blueprint: z.string().describe("Animation Blueprint name or path"),
graph: z.string().describe("State machine graph name"),
fromState: z.string().describe("Source state name"),
toState: z.string().describe("Target state name"),
crossfadeDuration: z.number().optional().describe("Crossfade duration in seconds (default: 0.2)"),
priority: z.number().optional().describe("Transition priority order"),
bBidirectional: z.boolean().optional().describe("Whether the transition is bidirectional"),
}, async ({ blueprint, graph, fromState, toState, crossfadeDuration, priority, bBidirectional }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, graph, fromState, toState };
if (crossfadeDuration !== undefined)
body.crossfadeDuration = crossfadeDuration;
if (priority !== undefined)
body.priority = priority;
if (bBidirectional !== undefined)
body.bBidirectional = bBidirectional;
const data = await uePost("/api/add-anim-transition", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Added transition: ${data.fromState || fromState}${data.toState || toState}`);
lines.push(`Node ID: ${data.nodeId}`);
lines.push(`Crossfade: ${data.crossfadeDuration ?? 0.2}s`);
lines.push(`Priority: ${data.priorityOrder ?? 1}`);
if (data.bBidirectional)
lines.push(`Bidirectional: true`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(`\nNext steps:`);
lines.push(` 1. Use set_transition_rule to configure crossfade and priority`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// set_transition_rule
// ---------------------------------------------------------------------------
server.tool("set_transition_rule", "Update properties on an existing transition between two states.", {
blueprint: z.string().describe("Animation Blueprint name or path"),
graph: z.string().describe("State machine graph name"),
fromState: z.string().describe("Source state name"),
toState: z.string().describe("Target state name"),
crossfadeDuration: z.number().optional().describe("Crossfade duration in seconds"),
blendMode: z.number().optional().describe("Alpha blend option (0=Linear, 1=Cubic, 2=HermiteCubic, 3=Sinusoidal, 4=QuadraticInOut, 5=CubicInOut, 6=QuarticInOut, 7=QuinticInOut, 8=CircularIn, 9=CircularOut, 10=CircularInOut, 11=ExpIn, 12=ExpOut, 13=ExpInOut, 14=Custom)"),
priorityOrder: z.number().optional().describe("Transition priority order"),
logicType: z.number().optional().describe("Transition logic type (0=Standard, 1=Custom)"),
bBidirectional: z.boolean().optional().describe("Whether the transition is bidirectional"),
}, async ({ blueprint, graph, fromState, toState, crossfadeDuration, blendMode, priorityOrder, logicType, bBidirectional }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, graph, fromState, toState };
if (crossfadeDuration !== undefined)
body.crossfadeDuration = crossfadeDuration;
if (blendMode !== undefined)
body.blendMode = blendMode;
if (priorityOrder !== undefined)
body.priorityOrder = priorityOrder;
if (logicType !== undefined)
body.logicType = logicType;
if (bBidirectional !== undefined)
body.bBidirectional = bBidirectional;
const data = await uePost("/api/set-transition-rule", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Updated transition: ${data.fromState || fromState}${data.toState || toState}`);
lines.push(`Properties changed: ${data.propertiesChanged ?? 0}`);
lines.push(`Crossfade: ${data.crossfadeDuration}s (blendMode: ${data.blendMode})`);
lines.push(`Priority: ${data.priorityOrder}`);
lines.push(`Bidirectional: ${data.bBidirectional}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// add_anim_node
// ---------------------------------------------------------------------------
server.tool("add_anim_node", "Add an animation node (sequence player, blend space, state machine) to an AnimGraph.", {
blueprint: z.string().describe("Animation Blueprint name or path"),
graph: z.string().optional().describe("Target graph name (default: AnimGraph)"),
nodeType: z.enum(["SequencePlayer", "BlendSpacePlayer", "StateMachine"]).describe("Type of anim node to add"),
animationAsset: z.string().optional().describe("Animation/blend space asset name to assign"),
posX: z.number().optional().describe("X position in graph"),
posY: z.number().optional().describe("Y position in graph"),
}, async ({ blueprint, graph, nodeType, animationAsset, posX, posY }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, nodeType };
if (graph)
body.graph = graph;
if (animationAsset)
body.animationAsset = animationAsset;
if (posX !== undefined)
body.posX = posX;
if (posY !== undefined)
body.posY = posY;
const data = await uePost("/api/add-anim-node", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Added ${data.nodeType || nodeType} node to ${data.graph || graph || "AnimGraph"}`);
lines.push(`Node ID: ${data.nodeId}`);
if (data.stateMachineGraph)
lines.push(`State machine sub-graph: ${data.stateMachineGraph}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
if (nodeType === "StateMachine") {
lines.push(`\nNext steps:`);
lines.push(` 1. Use add_anim_state to add states to ${data.stateMachineGraph}`);
lines.push(` 2. Use add_anim_transition to connect states`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// add_state_machine
// ---------------------------------------------------------------------------
server.tool("add_state_machine", "Add a new state machine to the root AnimGraph of an Animation Blueprint.", {
blueprint: z.string().describe("Animation Blueprint name or path"),
name: z.string().optional().describe("State machine name (default: NewStateMachine)"),
posX: z.number().optional().describe("X position in graph"),
posY: z.number().optional().describe("Y position in graph"),
}, async ({ blueprint, name, posX, posY }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint };
if (name)
body.name = name;
if (posX !== undefined)
body.posX = posX;
if (posY !== undefined)
body.posY = posY;
const data = await uePost("/api/add-state-machine", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Added state machine to AnimGraph`);
lines.push(`Node ID: ${data.nodeId}`);
if (data.stateMachineGraph)
lines.push(`Sub-graph: ${data.stateMachineGraph}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(`\nNext steps:`);
lines.push(` 1. Use add_anim_state to add states to ${data.stateMachineGraph || "the state machine"}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// set_state_animation
// ---------------------------------------------------------------------------
server.tool("set_state_animation", "Set or replace the animation sequence played by a state in a state machine.", {
blueprint: z.string().describe("Animation Blueprint name or path"),
graph: z.string().describe("State machine graph name"),
stateName: z.string().describe("State name"),
animationAsset: z.string().describe("Animation sequence asset name or path"),
}, async ({ blueprint, graph, stateName, animationAsset }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/set-state-animation", { blueprint, graph, stateName, animationAsset });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Set animation for state "${data.stateName || stateName}"`);
lines.push(`Animation: ${data.animationAsset || animationAsset}`);
lines.push(`Created new node: ${data.createdNewNode ?? false}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// create_blend_space
// ---------------------------------------------------------------------------
server.tool("create_blend_space", "Create a new 2D Blend Space asset with a target skeleton.", {
name: z.string().describe("Blend Space name (e.g. 'BS_Locomotion')"),
packagePath: z.string().default("/Game").describe("Package path (e.g. '/Game/Animations')"),
skeleton: z.string().describe("Skeleton asset name or path. Use '__create_test_skeleton__' for testing."),
}, async ({ name, packagePath, skeleton }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/create-blend-space", { name, packagePath, skeleton });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Created Blend Space: ${data.assetPath || `${packagePath}/${name}`}`);
lines.push(`Skeleton: ${data.skeleton || skeleton}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(`\nNext steps:`);
lines.push(` 1. Use set_blend_space_samples to add animation samples at X/Y coordinates`);
lines.push(` 2. Use set_state_blend_space to wire it into an anim state`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// set_blend_space_samples
// ---------------------------------------------------------------------------
server.tool("set_blend_space_samples", "Add animation samples to a 2D Blend Space at specific X/Y coordinates. Replaces all existing samples.", {
blendSpace: z.string().describe("Blend Space asset name or path"),
axisXName: z.string().optional().describe("Display name for the X axis"),
axisXMin: z.number().optional().describe("Minimum value for X axis"),
axisXMax: z.number().optional().describe("Maximum value for X axis"),
axisYName: z.string().optional().describe("Display name for the Y axis"),
axisYMin: z.number().optional().describe("Minimum value for Y axis"),
axisYMax: z.number().optional().describe("Maximum value for Y axis"),
samples: z.array(z.object({
animationAsset: z.string().describe("Animation sequence asset name or path"),
x: z.number().describe("X coordinate in blend space"),
y: z.number().describe("Y coordinate in blend space"),
})).describe("Array of animation samples with X/Y positions"),
}, async ({ blendSpace, axisXName, axisXMin, axisXMax, axisYName, axisYMin, axisYMax, samples }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blendSpace, samples };
if (axisXName !== undefined)
body.axisXName = axisXName;
if (axisXMin !== undefined)
body.axisXMin = axisXMin;
if (axisXMax !== undefined)
body.axisXMax = axisXMax;
if (axisYName !== undefined)
body.axisYName = axisYName;
if (axisYMin !== undefined)
body.axisYMin = axisYMin;
if (axisYMax !== undefined)
body.axisYMax = axisYMax;
const data = await uePost("/api/set-blend-space-samples", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Set ${data.samplesSet ?? samples.length} samples on ${data.blendSpace || blendSpace}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// set_state_blend_space
// ---------------------------------------------------------------------------
server.tool("set_state_blend_space", "Place a BlendSpacePlayer node inside an anim state, connect it to the Output Animation Pose, and optionally wire X/Y input pins to named variables.", {
blueprint: z.string().describe("Animation Blueprint name or path"),
graph: z.string().describe("State machine graph name"),
stateName: z.string().describe("State name"),
blendSpace: z.string().describe("Blend Space asset name or path"),
xVariable: z.string().optional().describe("Blueprint float variable name to wire to X input"),
yVariable: z.string().optional().describe("Blueprint float variable name to wire to Y input"),
}, async ({ blueprint, graph, stateName, blendSpace, xVariable, yVariable }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, graph, stateName, blendSpace };
if (xVariable)
body.xVariable = xVariable;
if (yVariable)
body.yVariable = yVariable;
const data = await uePost("/api/set-state-blend-space", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Set blend space for state "${data.stateName || stateName}"`);
lines.push(`Blend Space: ${data.blendSpace || blendSpace}`);
lines.push(`Node ID: ${data.nodeId}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// list_anim_slots
// ---------------------------------------------------------------------------
server.tool("list_anim_slots", "List all montage slot names used in an Animation Blueprint.", {
blueprint: z.string().describe("Animation Blueprint name or path"),
}, async ({ blueprint }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/list-anim-slots", { blueprint });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Animation slots in ${data.blueprint || blueprint}: ${data.count ?? 0}`);
if (data.slots?.length) {
for (const slot of data.slots) {
lines.push(` ${slot}`);
}
}
else {
lines.push(` (no slots found)`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// list_sync_groups
// ---------------------------------------------------------------------------
server.tool("list_sync_groups", "List all sync group names used in an Animation Blueprint.", {
blueprint: z.string().describe("Animation Blueprint name or path"),
}, async ({ blueprint }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/list-sync-groups", { blueprint });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Sync groups in ${data.blueprint || blueprint}: ${data.count ?? 0}`);
if (data.syncGroups?.length) {
for (const group of data.syncGroups) {
lines.push(` ${group}`);
}
}
else {
lines.push(` (no sync groups found)`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=animation-mutation.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerComponentTools(server: McpServer): void;

View File

@@ -1,81 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost } from "../ue-bridge.js";
export function registerComponentTools(server) {
server.tool("list_components", "List all components in a Blueprint's component hierarchy (Simple Construction Script). Shows component class, name, and parent-child relationships. Only works on Actor-based Blueprints.", {
blueprint: z.string().describe("Blueprint name or package path (e.g. 'BP_Patient_Base')"),
}, async ({ blueprint }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/list-components", { blueprint });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Components (${data.count || 0}):`);
for (const c of data.components || []) {
const parent = c.parentComponent ? ` (parent: ${c.parentComponent})` : "";
const root = c.isSceneRoot ? " [Root]" : "";
const children = c.childCount > 0 ? ` [${c.childCount} children]` : "";
lines.push(` ${c.name}: ${c.componentClass}${parent}${root}${children}`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("add_component", "Add a component to a Blueprint's component hierarchy (Simple Construction Script). Only works on Actor-based Blueprints. Common component classes: StaticMeshComponent, SkeletalMeshComponent, AudioComponent, SceneComponent, BoxCollisionComponent, SphereCollisionComponent, CapsuleComponent, ArrowComponent, ChildActorComponent, SpotLightComponent, PointLightComponent, WidgetComponent, BillboardComponent.", {
blueprint: z.string().describe("Blueprint name or package path"),
componentClass: z.string().describe("Component class name (e.g. 'StaticMeshComponent', 'AudioComponent')"),
name: z.string().describe("Name for the new component (e.g. 'MyMesh')"),
parentComponent: z.string().optional().describe("Name of the parent component to attach to (optional, defaults to root set)"),
}, async ({ blueprint, componentClass, name, parentComponent }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, componentClass, name };
if (parentComponent)
body.parentComponent = parentComponent;
const data = await uePost("/api/add-component", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Component added successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Name: ${data.name}`);
lines.push(`Class: ${data.componentClass}`);
if (data.parentComponent)
lines.push(`Parent: ${data.parentComponent}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(``);
lines.push(`Next steps:`);
lines.push(` list_components(blueprint="${blueprint}") — verify the component hierarchy`);
lines.push(` set_blueprint_default(blueprint="${blueprint}", ...) — configure component properties`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("remove_component", "Remove a component from a Blueprint's component hierarchy (Simple Construction Script). Cannot remove a root component that has children — remove or re-parent children first. Children of non-root removed components are promoted to the removed component's parent.", {
blueprint: z.string().describe("Blueprint name or package path"),
name: z.string().describe("Name of the component to remove"),
}, async ({ blueprint, name }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/remove-component", { blueprint, name });
if (data.error) {
let msg = `Error: ${data.error}`;
if (data.existingComponents?.length) {
msg += `\nExisting components: ${data.existingComponents.join(", ")}`;
}
return { content: [{ type: "text", text: msg }] };
}
const lines = [];
lines.push(`Component removed successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Removed: ${data.name}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(``);
lines.push(`Next steps:`);
lines.push(` list_components(blueprint="${blueprint}") — verify the component was removed`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=components.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"components.js","sourceRoot":"","sources":["../../src/tools/components.ts"],"names":[],"mappings":"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"}

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerDiffBlueprintsTools(server: McpServer): void;

View File

@@ -1,69 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost } from "../ue-bridge.js";
export function registerDiffBlueprintsTools(server) {
server.tool("diff_blueprints", "Structural diff between two different Blueprints. Compares nodes, connections, and variables across graphs. Use for comparing patient variants, finding divergence after copy-paste, or auditing consistency.", {
blueprintA: z.string().describe("First Blueprint name or package path"),
blueprintB: z.string().describe("Second Blueprint name or package path"),
graph: z.string().optional().describe("Specific graph to compare (e.g. 'EventGraph'). If omitted, compares all graphs."),
}, async ({ blueprintA, blueprintB, graph }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprintA, blueprintB };
if (graph)
body.graph = graph;
const data = await uePost("/api/diff-blueprints", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Diff: ${data.blueprintA} vs ${data.blueprintB}`);
lines.push(`Total differences: ${data.totalDifferences}\n`);
for (const g of data.graphs || []) {
const status = g.status === "identical" ? "IDENTICAL" :
g.status === "onlyInA" ? `ONLY IN A (${g.nodeCountA} nodes)` :
g.status === "onlyInB" ? `ONLY IN B (${g.nodeCountB} nodes)` :
"DIFFERENT";
lines.push(`--- ${g.graph}: ${status} ---`);
if (g.nodeCountA !== undefined && g.nodeCountB !== undefined) {
lines.push(` Nodes: A=${g.nodeCountA}, B=${g.nodeCountB}`);
}
if (g.nodesOnlyInA?.length) {
lines.push(` Nodes only in A:`);
for (const n of g.nodesOnlyInA) {
lines.push(` - ${n.title} (${n.class}) x${n.extraCount}`);
}
}
if (g.nodesOnlyInB?.length) {
lines.push(` Nodes only in B:`);
for (const n of g.nodesOnlyInB) {
lines.push(` - ${n.title} (${n.class}) x${n.extraCount}`);
}
}
if (g.connectionsOnlyInA?.length) {
lines.push(` Connections only in A: ${g.connectionsOnlyInA.length}`);
for (const c of g.connectionsOnlyInA.slice(0, 10)) {
lines.push(` - ${c}`);
}
if (g.connectionsOnlyInA.length > 10)
lines.push(` ... and ${g.connectionsOnlyInA.length - 10} more`);
}
if (g.connectionsOnlyInB?.length) {
lines.push(` Connections only in B: ${g.connectionsOnlyInB.length}`);
for (const c of g.connectionsOnlyInB.slice(0, 10)) {
lines.push(` - ${c}`);
}
if (g.connectionsOnlyInB.length > 10)
lines.push(` ... and ${g.connectionsOnlyInB.length - 10} more`);
}
lines.push(``);
}
if (data.variablesOnlyInA?.length) {
lines.push(`Variables only in A: ${data.variablesOnlyInA.join(", ")}`);
}
if (data.variablesOnlyInB?.length) {
lines.push(`Variables only in B: ${data.variablesOnlyInB.join(", ")}`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=diff-blueprints.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"diff-blueprints.js","sourceRoot":"","sources":["../../src/tools/diff-blueprints.ts"],"names":[],"mappings":"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"}

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerDiscoveryTools(server: McpServer): void;

View File

@@ -1,192 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost } from "../ue-bridge.js";
export function registerDiscoveryTools(server) {
server.tool("get_pin_info", "Get detailed information about a specific pin on a Blueprint node, including type details, container type (array/set/map), default value, and current connections.", {
blueprint: z.string().describe("Blueprint name or package path"),
nodeId: z.string().describe("Node GUID"),
pinName: z.string().describe("Pin name"),
}, async ({ blueprint, nodeId, pinName }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/get-pin-info", { blueprint, nodeId, pinName });
if (data.error) {
let msg = `Error: ${data.error}`;
if (data.availablePins?.length) {
msg += `\n\nAvailable pins:`;
for (const p of data.availablePins) {
msg += `\n ${p.direction === "Output" ? "\u2192" : "\u2190"} ${p.name}: ${p.type}`;
}
}
return { content: [{ type: "text", text: msg }] };
}
const lines = [];
lines.push(`Pin: ${data.pinName}`);
lines.push(`Direction: ${data.direction}`);
lines.push(`Type: ${data.type}${data.subtype ? ` (${data.subtype})` : ""}${data.subCategory ? ` [${data.subCategory}]` : ""}`);
const containers = [];
if (data.isArray)
containers.push("Array");
if (data.isSet)
containers.push("Set");
if (data.isMap)
containers.push("Map");
if (containers.length)
lines.push(`Container: ${containers.join(", ")}`);
if (data.isReference)
lines.push(`Reference: true`);
if (data.isConst)
lines.push(`Const: true`);
if (data.defaultValue !== undefined)
lines.push(`Default value: ${data.defaultValue}`);
if (data.defaultTextValue !== undefined)
lines.push(`Default text: ${data.defaultTextValue}`);
if (data.defaultObject !== undefined)
lines.push(`Default object: ${data.defaultObject}`);
if (data.connectedTo?.length) {
lines.push(`\nConnections (${data.connectedTo.length}):`);
for (const c of data.connectedTo) {
lines.push(` ${c.nodeTitle} (${c.nodeId}).${c.pinName}`);
}
}
else {
lines.push(`\nConnections: none`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("check_pin_compatibility", "Check whether two pins can be connected before attempting connect_pins. Returns compatibility status, connection type (direct, requires conversion, etc.), and any UE5 schema messages.", {
blueprint: z.string().describe("Blueprint name or package path"),
sourceNodeId: z.string().describe("Source node GUID"),
sourcePinName: z.string().describe("Source pin name"),
targetNodeId: z.string().describe("Target node GUID"),
targetPinName: z.string().describe("Target pin name"),
}, async ({ blueprint, sourceNodeId, sourcePinName, targetNodeId, targetPinName }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/check-pin-compatibility", {
blueprint, sourceNodeId, sourcePinName, targetNodeId, targetPinName,
});
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
const icon = data.compatible ? "\u2705" : "\u274c";
lines.push(`${icon} Compatible: ${data.compatible}`);
lines.push(`Connection type: ${data.connectionType}`);
if (data.message)
lines.push(`Message: ${data.message}`);
lines.push(``);
lines.push(`Source pin type: ${data.sourcePinType}${data.sourcePinSubtype ? ` (${data.sourcePinSubtype})` : ""}`);
lines.push(`Target pin type: ${data.targetPinType}${data.targetPinSubtype ? ` (${data.targetPinSubtype})` : ""}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("list_classes", "List available UE5 classes. Filter by name substring and/or parent class. Useful for discovering class names to use with add_node(CallFunction), add_node(DynamicCast), add_node(SpawnActorFromClass), etc.", {
filter: z.string().optional().describe("Substring to match against class name (case-insensitive)"),
parentClass: z.string().optional().describe("Only show classes that inherit from this class (e.g. 'Actor', 'ActorComponent')"),
limit: z.number().optional().default(100).describe("Maximum number of results (default: 100, max: 500)"),
}, async ({ filter, parentClass, limit }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = {};
if (filter)
body.filter = filter;
if (parentClass)
body.parentClass = parentClass;
if (limit !== undefined)
body.limit = limit;
const data = await uePost("/api/list-classes", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (data.truncated) {
lines.push(`Showing ${data.count} of ${data.totalMatched} matching classes (limit: ${data.limit}).\n`);
}
else {
lines.push(`Found ${data.count} classes.\n`);
}
for (const cls of data.classes) {
const tags = [];
if (cls.isBlueprint)
tags.push("BP");
if (cls.flags?.length)
tags.push(...cls.flags);
const tagStr = tags.length ? ` [${tags.join(", ")}]` : "";
lines.push(`${cls.name}${tagStr} : ${cls.parentClass || "none"}`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("list_functions", "List Blueprint-callable functions on a UE5 class, including parameter signatures and return types. Use this to discover function names for add_node(CallFunction, functionName=...).", {
className: z.string().describe("Class name (e.g. 'KismetSystemLibrary', 'KismetMathLibrary', 'Actor')"),
filter: z.string().optional().describe("Substring to match against function name (case-insensitive)"),
}, async ({ className, filter }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { className };
if (filter)
body.filter = filter;
const data = await uePost("/api/list-functions", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`${data.className}: ${data.count} callable functions.\n`);
for (const fn of data.functions) {
const tags = [];
if (fn.isPure)
tags.push("pure");
if (fn.isStatic)
tags.push("static");
if (fn.isEvent)
tags.push("event");
if (fn.isConst)
tags.push("const");
const tagStr = tags.length ? ` [${tags.join(", ")}]` : "";
const params = fn.parameters
.filter((p) => !p.isOutput)
.map((p) => `${p.name}: ${p.type}`)
.join(", ");
const outParams = fn.parameters
.filter((p) => p.isOutput)
.map((p) => `${p.name}: ${p.type}`)
.join(", ");
let sig = `${fn.name}(${params})`;
if (fn.returnType)
sig += ` -> ${fn.returnType}`;
if (outParams)
sig += ` [out: ${outParams}]`;
sig += tagStr;
if (fn.definedIn && fn.definedIn !== data.className) {
sig += ` (from ${fn.definedIn})`;
}
lines.push(sig);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("list_properties", "List properties on a UE5 class, including types and property flags (BlueprintVisible, EditAnywhere, etc.).", {
className: z.string().describe("Class name (e.g. 'Actor', 'CharacterMovementComponent')"),
filter: z.string().optional().describe("Substring to match against property name (case-insensitive)"),
}, async ({ className, filter }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { className };
if (filter)
body.filter = filter;
const data = await uePost("/api/list-properties", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`${data.className}: ${data.count} properties.\n`);
for (const prop of data.properties) {
const flagStr = prop.flags?.length ? ` [${prop.flags.join(", ")}]` : "";
let line = `${prop.name}: ${prop.type}${flagStr}`;
if (prop.definedIn && prop.definedIn !== data.className) {
line += ` (from ${prop.definedIn})`;
}
lines.push(line);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=discovery.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerDispatcherTools(server: McpServer): void;

View File

@@ -1,71 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost } from "../ue-bridge.js";
import { TYPE_NAME_DOCS } from "../helpers.js";
export function registerDispatcherTools(server) {
server.tool("add_event_dispatcher", `Create an event dispatcher (multicast delegate) on a Blueprint. Optionally include typed parameters in the dispatcher signature. ${TYPE_NAME_DOCS}`, {
blueprint: z.string().describe("Blueprint name or package path"),
dispatcherName: z.string().describe("Name for the event dispatcher (e.g. 'OnHealthChanged')"),
parameters: z.array(z.object({
name: z.string().describe("Parameter name"),
type: z.string().describe("Parameter type (e.g. 'float', 'bool', 'string', 'FVector', 'object')"),
})).optional().describe("Optional array of typed parameters for the dispatcher signature"),
}, async ({ blueprint, dispatcherName, parameters }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, dispatcherName };
if (parameters?.length)
body.parameters = parameters;
const data = await uePost("/api/add-event-dispatcher", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Event dispatcher created successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Dispatcher: ${data.dispatcherName}`);
if (data.parameters?.length) {
lines.push(`Parameters:`);
for (const p of data.parameters) {
lines.push(` ${p.name}: ${p.type}`);
}
}
else {
lines.push(`Parameters: (none)`);
}
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(``);
lines.push(`Next steps:`);
lines.push(` list_event_dispatchers(blueprint="${blueprint}") — verify the dispatcher was created`);
lines.push(` add_function_parameter(blueprint="${blueprint}", functionName="${dispatcherName}", ...) — add more parameters`);
lines.push(` add_node(blueprint="${blueprint}", graph="EventGraph", nodeType="CallFunction", functionName="<dispatcherName>_Event") — bind to it`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("list_event_dispatchers", "List all event dispatchers (multicast delegates) on a Blueprint, including their parameter signatures.", {
blueprint: z.string().describe("Blueprint name or package path"),
}, async ({ blueprint }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/list-event-dispatchers", { blueprint });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Event dispatchers: ${data.count}`);
if (data.dispatchers?.length) {
lines.push(``);
for (const d of data.dispatchers) {
if (d.parameters?.length) {
const paramStr = d.parameters.map((p) => `${p.name}: ${p.type}`).join(", ");
lines.push(` ${d.name}(${paramStr})`);
}
else {
lines.push(` ${d.name}()`);
}
}
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=dispatchers.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"dispatchers.js","sourceRoot":"","sources":["../../src/tools/dispatchers.ts"],"names":[],"mappings":"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"}

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerGraphTools(server: McpServer): void;

View File

@@ -1,129 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost } from "../ue-bridge.js";
export function registerGraphTools(server) {
server.tool("reparent_blueprint", "Change a Blueprint's parent class. Can reparent to a C++ class (e.g. 'WebUIHUD') or another Blueprint. Compiles, refreshes all nodes, and saves.", {
blueprint: z.string().describe("Blueprint name or package path (e.g. 'HUD_WebUIInterface')"),
newParentClass: z.string().describe("New parent class name — C++ class (e.g. 'WebUIHUD') or Blueprint name"),
}, async ({ blueprint, newParentClass }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/reparent-blueprint", { blueprint, newParentClass });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Blueprint reparented successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Old parent: ${data.oldParentClass}`);
lines.push(`New parent: ${data.newParentClass}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("create_blueprint", "Create a new Blueprint asset. Specify a parent class (C++ or Blueprint) and package path.", {
blueprintName: z.string().describe("Name for the new Blueprint (e.g. 'BP_MyActor')"),
packagePath: z.string().describe("Package path (e.g. '/Game/Blueprints/Actors')"),
parentClass: z.string().describe("Parent class — C++ class (e.g. 'Actor', 'Pawn') or Blueprint name"),
blueprintType: z.enum(["Normal", "Interface", "FunctionLibrary", "MacroLibrary"])
.optional().default("Normal")
.describe("Blueprint type (default: Normal)"),
}, async ({ blueprintName, packagePath, parentClass, blueprintType }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/create-blueprint", { blueprintName, packagePath, parentClass, blueprintType });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Blueprint created successfully.`);
lines.push(`Name: ${data.blueprintName}`);
lines.push(`Path: ${data.assetPath}`);
lines.push(`Parent: ${data.parentClass}`);
lines.push(`Type: ${data.blueprintType}`);
if (data.graphs?.length)
lines.push(`Graphs: ${data.graphs.join(", ")}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(``);
lines.push(`Next steps:`);
lines.push(` get_blueprint(blueprint="${data.blueprintName}") — inspect the new Blueprint`);
lines.push(` add_node(blueprint="${data.blueprintName}", ...) — add logic`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("create_graph", "Create a new function graph, macro graph, or custom event in a Blueprint. For function/macro, creates a new named graph with entry/exit nodes. For customEvent, adds a CustomEvent node to the EventGraph.", {
blueprint: z.string().describe("Blueprint name or package path"),
graphName: z.string().describe("Name for the new graph or custom event"),
graphType: z.enum(["function", "macro", "customEvent"]).describe("Type of graph to create: 'function' (new function graph), 'macro' (new macro graph), 'customEvent' (CustomEvent node in EventGraph)"),
}, async ({ blueprint, graphName, graphType }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/create-graph", { blueprint, graphName, graphType });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Graph created successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Graph: ${data.graphName}`);
lines.push(`Type: ${data.graphType}`);
if (data.nodeId)
lines.push(`Node ID: ${data.nodeId}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(``);
lines.push(`Next steps:`);
if (graphType === "customEvent") {
lines.push(` add_node(blueprint="${blueprint}", graph="EventGraph", ...) — add logic after the event`);
}
else {
lines.push(` add_node(blueprint="${blueprint}", graph="${graphName}", ...) — add nodes to the new graph`);
}
lines.push(` get_blueprint_graph(blueprint="${blueprint}", graph="${graphName}") — inspect the graph`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("delete_graph", "Delete an entire function or macro graph from a Blueprint. Cannot delete EventGraph (Ubergraph pages). All nodes in the graph are removed. Use get_blueprint to see available graphs first.", {
blueprint: z.string().describe("Blueprint name or package path"),
graphName: z.string().describe("Name of the function or macro graph to delete"),
}, async ({ blueprint, graphName }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/delete-graph", { blueprint, graphName });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Graph deleted successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Graph: ${data.graphName}`);
lines.push(`Type: ${data.graphType}`);
lines.push(`Nodes removed: ${data.nodeCount}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("rename_graph", "Rename a function or macro graph in a Blueprint. Cannot rename EventGraph (Ubergraph pages). Updates all internal references.", {
blueprint: z.string().describe("Blueprint name or package path"),
graphName: z.string().describe("Current name of the function or macro graph"),
newName: z.string().describe("New name for the graph"),
}, async ({ blueprint, graphName, newName }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/rename-graph", { blueprint, graphName, newName });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Graph renamed successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Old name: ${data.oldName}`);
lines.push(`New name: ${data.newName}`);
lines.push(`Type: ${data.graphType}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(``);
lines.push(`Next steps:`);
lines.push(` get_blueprint_graph(blueprint="${blueprint}", graph="${data.newName}") — inspect the renamed graph`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=graphs.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerInterfaceTools(server: McpServer): void;

View File

@@ -1,87 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost } from "../ue-bridge.js";
export function registerInterfaceTools(server) {
server.tool("list_interfaces", "List all Blueprint Interfaces implemented by a Blueprint. Shows interface name, class path, and function graphs for each.", {
blueprint: z.string().describe("Blueprint name or package path"),
}, async ({ blueprint }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/list-interfaces", { blueprint });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Interfaces implemented: ${data.count}`);
if (data.interfaces?.length) {
lines.push(``);
for (const iface of data.interfaces) {
lines.push(` ${iface.name}`);
lines.push(` Class path: ${iface.classPath}`);
if (iface.functions?.length) {
lines.push(` Functions: ${iface.functions.join(", ")}`);
}
}
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("add_interface", "Add a Blueprint Interface implementation to a Blueprint. The interface must be a Blueprint Interface asset (e.g. 'BPI_MyInterface') or a native UInterface class. Automatically creates function stub graphs for the interface's methods.", {
blueprint: z.string().describe("Blueprint name or package path"),
interfaceName: z.string().describe("Interface name — Blueprint Interface asset name (e.g. 'BPI_MyInterface') or native UInterface class name"),
}, async ({ blueprint, interfaceName }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/add-interface", { blueprint, interfaceName });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Interface added successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Interface: ${data.interfaceName}`);
lines.push(`Interface path: ${data.interfacePath}`);
if (data.functionGraphsAdded?.length) {
lines.push(`Function stubs created: ${data.functionGraphsAdded.join(", ")}`);
}
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(``);
lines.push(`Next steps:`);
lines.push(` list_interfaces(blueprint="${blueprint}") — verify the interface was added`);
lines.push(` get_blueprint_graph(blueprint="${blueprint}", graph="<functionName>") — inspect a function stub`);
lines.push(` add_node(blueprint="${blueprint}", graph="<functionName>", ...) — add logic to a function stub`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("remove_interface", "Remove a Blueprint Interface implementation from a Blueprint. Optionally preserve the function graphs as regular functions.", {
blueprint: z.string().describe("Blueprint name or package path"),
interfaceName: z.string().describe("Interface name to remove (e.g. 'BPI_MyInterface' or 'BPI_MyInterface_C')"),
preserveFunctions: z.boolean().optional().describe("If true, keep the function graphs as regular Blueprint functions instead of deleting them (default: false)"),
}, async ({ blueprint, interfaceName, preserveFunctions }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, interfaceName };
if (preserveFunctions !== undefined)
body.preserveFunctions = preserveFunctions;
const data = await uePost("/api/remove-interface", body);
if (data.error) {
let msg = `Error: ${data.error}`;
if (data.implementedInterfaces?.length) {
msg += `\nImplemented interfaces: ${data.implementedInterfaces.join(", ")}`;
}
return { content: [{ type: "text", text: msg }] };
}
const lines = [];
lines.push(`Interface removed successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Interface: ${data.interfaceName}`);
lines.push(`Functions preserved: ${data.preservedFunctions}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(``);
lines.push(`Next steps:`);
lines.push(` list_interfaces(blueprint="${blueprint}") — verify the interface was removed`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=interfaces.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"interfaces.js","sourceRoot":"","sources":["../../src/tools/interfaces.ts"],"names":[],"mappings":"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"}

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerMaterialMutationTools(server: McpServer): void;

View File

@@ -1,671 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost, ueGet } from "../ue-bridge.js";
export function registerMaterialMutationTools(server) {
// ---------------------------------------------------------------------------
// Phase 2: Material Mutations
// ---------------------------------------------------------------------------
server.tool("create_material", "Create a new Material asset.", {
name: z.string().describe("Material asset name (e.g. 'M_MyMaterial')"),
packagePath: z.string().default("/Game").describe("Package path to create the material in (e.g. '/Game/Materials')"),
domain: z.enum(["Surface", "DeferredDecal", "LightFunction", "Volume", "PostProcess", "UI"]).optional().describe("Material domain"),
blendMode: z.enum(["Opaque", "Masked", "Translucent", "Additive", "Modulate"]).optional().describe("Blend mode"),
twoSided: z.boolean().optional().describe("Whether the material is two-sided"),
}, async ({ name, packagePath, domain, blendMode, twoSided }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { name, packagePath };
if (domain)
body.domain = domain;
if (blendMode)
body.blendMode = blendMode;
if (twoSided !== undefined)
body.twoSided = twoSided;
const data = await uePost("/api/create-material", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Created material ${data.name || name} at ${data.packagePath || packagePath}`);
if (data.domain)
lines.push(`Domain: ${data.domain}`);
if (data.blendMode)
lines.push(`Blend mode: ${data.blendMode}`);
if (data.twoSided !== undefined)
lines.push(`Two-sided: ${data.twoSided}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(`\nNext steps:`);
lines.push(` 1. Use add_material_expression to add nodes to the material graph`);
lines.push(` 2. Use connect_material_pins to wire expressions together`);
lines.push(` 3. Use set_material_property to adjust material settings`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("set_material_property", "Set a top-level property on a Material. Supported properties: domain, blendMode, twoSided, shadingModel, opacity/opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, ditheredLODTransition, bAllowNegativeEmissiveColor.", {
material: z.string().describe("Material name or package path (e.g. 'M_MyMaterial')"),
property: z.string().describe("Property name to set"),
value: z.union([z.string(), z.number(), z.boolean()]).describe("New value for the property (string for enums, number for floats, boolean for flags)"),
dryRun: z.boolean().optional().describe("If true, preview changes without modifying the Material"),
}, async ({ material, property, value, dryRun }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { material, property, value };
if (dryRun)
body.dryRun = true;
const data = await uePost("/api/set-material-property", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (dryRun)
lines.push(`[DRY RUN - no changes saved]`);
lines.push(`Material: ${data.material || material}`);
lines.push(`Property: ${data.property || property}`);
lines.push(`Old value: ${data.oldValue ?? "(empty)"} -> New value: ${data.newValue ?? value}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
if (!dryRun) {
lines.push(`\nNext steps:`);
lines.push(` 1. Use get_material_graph to verify the changes`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("add_material_expression", "Add a new expression node to a Material or Material Function graph. Supports any UMaterialExpression subclass — use the class name without the 'MaterialExpression' prefix (e.g. 'Constant', 'Add', 'Subtract', 'Fresnel', 'Comment', 'If', 'Lerp').", {
material: z.string().optional().describe("Material name or package path (e.g. 'M_MyMaterial'). Provide either material or materialFunction."),
materialFunction: z.string().optional().describe("Material Function name or package path (e.g. 'MF_MyFunction'). Provide either material or materialFunction."),
expressionClass: z.string().describe("Expression class name without the 'MaterialExpression' prefix. Any UMaterialExpression subclass is supported (e.g. 'Constant', 'ScalarParameter', 'Add', 'Subtract', 'Fresnel', 'Comment', 'If', 'Lerp')."),
posX: z.number().default(0).describe("X position in the graph editor"),
posY: z.number().default(0).describe("Y position in the graph editor"),
dryRun: z.boolean().optional().describe("If true, preview changes without modifying the asset"),
}, async ({ material, materialFunction, expressionClass, posX, posY, dryRun }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
if (!material && !materialFunction)
return { content: [{ type: "text", text: "Error: Provide either 'material' or 'materialFunction'" }] };
if (material && materialFunction)
return { content: [{ type: "text", text: "Error: Provide either 'material' or 'materialFunction', not both" }] };
const body = { expressionClass, posX, posY };
if (material)
body.material = material;
if (materialFunction)
body.materialFunction = materialFunction;
if (dryRun)
body.dryRun = true;
const data = await uePost("/api/add-material-expression", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (dryRun)
lines.push(`[DRY RUN - no changes saved]`);
lines.push(`Expression added: ${data.expressionClass || expressionClass}`);
lines.push(`Material: ${data.material || material}`);
if (data.nodeId)
lines.push(`Node ID: ${data.nodeId}`);
if (data.posX !== undefined && data.posY !== undefined)
lines.push(`Position: (${data.posX}, ${data.posY})`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
if (data.pins?.length) {
lines.push(`\nPins:`);
for (const pin of data.pins) {
const dir = pin.direction === "Output" ? "\u2192" : "\u2190";
lines.push(` ${dir} ${pin.name}: ${pin.type}${pin.subtype ? ` (${pin.subtype})` : ""}`);
}
}
if (!dryRun) {
lines.push(`\nNext steps:`);
lines.push(` 1. Use set_expression_value to configure the expression`);
lines.push(` 2. Use connect_material_pins to wire it to other nodes`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("delete_material_expression", "Delete an expression node from a Material or Material Function graph.", {
material: z.string().optional().describe("Material name or package path. Provide either material or materialFunction."),
materialFunction: z.string().optional().describe("Material Function name or package path. Provide either material or materialFunction."),
nodeId: z.string().describe("GUID of the expression node to delete"),
dryRun: z.boolean().optional().describe("If true, preview changes without modifying the asset"),
}, async ({ material, materialFunction, nodeId, dryRun }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
if (!material && !materialFunction)
return { content: [{ type: "text", text: "Error: Provide either 'material' or 'materialFunction'" }] };
const body = { nodeId };
if (material)
body.material = material;
if (materialFunction)
body.materialFunction = materialFunction;
if (dryRun)
body.dryRun = true;
const data = await uePost("/api/delete-material-expression", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (dryRun)
lines.push(`[DRY RUN - no changes saved]`);
lines.push(`Expression deleted.`);
lines.push(`Material: ${data.material || material}`);
if (data.nodeId)
lines.push(`Node ID: ${data.nodeId}`);
if (data.expressionClass)
lines.push(`Expression class: ${data.expressionClass}`);
if (data.disconnectedPins !== undefined)
lines.push(`Disconnected pins: ${data.disconnectedPins}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
if (!dryRun) {
lines.push(`\nNext steps:`);
lines.push(` 1. Use get_material_graph to verify the changes`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("connect_material_pins", "Connect two pins in a Material or Material Function graph.", {
material: z.string().optional().describe("Material name or package path. Provide either material or materialFunction."),
materialFunction: z.string().optional().describe("Material Function name or package path. Provide either material or materialFunction."),
sourceNodeId: z.string().describe("GUID of the source expression node"),
sourcePinName: z.string().describe("Name of the output pin on the source node"),
targetNodeId: z.string().describe("GUID of the target expression node (or 'Result' for the material result node)"),
targetPinName: z.string().describe("Name of the input pin on the target node"),
dryRun: z.boolean().optional().describe("If true, preview changes without modifying the asset"),
}, async ({ material, materialFunction, sourceNodeId, sourcePinName, targetNodeId, targetPinName, dryRun }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
if (!material && !materialFunction)
return { content: [{ type: "text", text: "Error: Provide either 'material' or 'materialFunction'" }] };
const body = { sourceNodeId, sourcePinName, targetNodeId, targetPinName };
if (material)
body.material = material;
if (materialFunction)
body.materialFunction = materialFunction;
if (dryRun)
body.dryRun = true;
const data = await uePost("/api/connect-material-pins", body);
if (data.error) {
let msg = `Error: ${data.error}`;
if (data.availablePins) {
msg += `\nAvailable pins: ${data.availablePins.join(", ")}`;
}
return { content: [{ type: "text", text: msg }] };
}
const lines = [];
if (dryRun)
lines.push(`[DRY RUN - no changes saved]`);
lines.push(`Connection ${data.success ? "succeeded" : "failed"}.`);
lines.push(`Material: ${data.material || material}`);
lines.push(`${sourcePinName} \u2192 ${targetPinName}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
if (!dryRun) {
lines.push(`\nNext steps:`);
lines.push(` 1. Use get_material_graph to verify the connection`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("disconnect_material_pin", "Disconnect all links from a specific pin in a Material or Material Function graph.", {
material: z.string().optional().describe("Material name or package path. Provide either material or materialFunction."),
materialFunction: z.string().optional().describe("Material Function name or package path. Provide either material or materialFunction."),
nodeId: z.string().describe("GUID of the expression node containing the pin"),
pinName: z.string().describe("Name of the pin to disconnect"),
dryRun: z.boolean().optional().describe("If true, preview changes without modifying the asset"),
}, async ({ material, materialFunction, nodeId, pinName, dryRun }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
if (!material && !materialFunction)
return { content: [{ type: "text", text: "Error: Provide either 'material' or 'materialFunction'" }] };
const body = { nodeId, pinName };
if (material)
body.material = material;
if (materialFunction)
body.materialFunction = materialFunction;
if (dryRun)
body.dryRun = true;
const data = await uePost("/api/disconnect-material-pin", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (dryRun)
lines.push(`[DRY RUN - no changes saved]`);
lines.push(`Disconnected ${data.disconnectedCount ?? 0} link(s).`);
lines.push(`Material: ${data.material || material}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
if (!dryRun) {
lines.push(`\nNext steps:`);
lines.push(` 1. Use get_material_graph to verify the changes`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("set_expression_value", "Set the value of a material expression (constants, parameter defaults, custom code, etc.) in a Material or Material Function.", {
material: z.string().optional().describe("Material name or package path. Provide either material or materialFunction."),
materialFunction: z.string().optional().describe("Material Function name or package path. Provide either material or materialFunction."),
nodeId: z.string().describe("GUID of the expression node"),
value: z.union([
z.number(),
z.object({ r: z.number(), g: z.number(), b: z.number(), a: z.number().optional() }),
z.string(),
]).describe("Value to set: number (for scalar), {r,g,b,a?} (for vector/color), or string"),
parameterName: z.string().optional().describe("Parameter name override (for parameter expressions)"),
code: z.string().optional().describe("Custom HLSL code (for Custom expression nodes)"),
dryRun: z.boolean().optional().describe("If true, preview changes without modifying the Material"),
}, async ({ material, materialFunction, nodeId, value, parameterName, code, dryRun }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
if (!material && !materialFunction)
return { content: [{ type: "text", text: "Error: Provide either 'material' or 'materialFunction'" }] };
const body = { nodeId, value };
if (material)
body.material = material;
if (materialFunction)
body.materialFunction = materialFunction;
if (parameterName)
body.parameterName = parameterName;
if (code)
body.code = code;
if (dryRun)
body.dryRun = true;
const data = await uePost("/api/set-expression-value", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (dryRun)
lines.push(`[DRY RUN - no changes saved]`);
lines.push(`Expression value set.`);
lines.push(`Material: ${data.material || material}`);
if (data.nodeId)
lines.push(`Node ID: ${data.nodeId}`);
if (data.expressionClass)
lines.push(`Expression class: ${data.expressionClass}`);
if (data.oldValue !== undefined)
lines.push(`Old value: ${typeof data.oldValue === "object" ? JSON.stringify(data.oldValue) : data.oldValue}`);
if (data.newValue !== undefined)
lines.push(`New value: ${typeof data.newValue === "object" ? JSON.stringify(data.newValue) : data.newValue}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
if (!dryRun) {
lines.push(`\nNext steps:`);
lines.push(` 1. Use get_material_graph to verify the changes`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("move_material_expression", "Move a material expression node to a new position in the graph editor of a Material or Material Function.", {
material: z.string().optional().describe("Material name or package path. Provide either material or materialFunction."),
materialFunction: z.string().optional().describe("Material Function name or package path. Provide either material or materialFunction."),
nodeId: z.string().describe("GUID of the expression node to move"),
posX: z.number().describe("New X position"),
posY: z.number().describe("New Y position"),
dryRun: z.boolean().optional().describe("If true, preview changes without modifying the asset"),
}, async ({ material, materialFunction, nodeId, posX, posY, dryRun }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
if (!material && !materialFunction)
return { content: [{ type: "text", text: "Error: Provide either 'material' or 'materialFunction'" }] };
const body = { nodeId, posX, posY };
if (material)
body.material = material;
if (materialFunction)
body.materialFunction = materialFunction;
if (dryRun)
body.dryRun = true;
const data = await uePost("/api/move-material-expression", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (dryRun)
lines.push(`[DRY RUN - no changes saved]`);
lines.push(`Expression repositioned.`);
lines.push(`Material: ${data.material || material}`);
if (data.nodeId)
lines.push(`Node ID: ${data.nodeId}`);
if (data.oldPosX !== undefined && data.oldPosY !== undefined) {
lines.push(`Position: (${data.oldPosX}, ${data.oldPosY}) -> (${data.newPosX ?? posX}, ${data.newPosY ?? posY})`);
}
else {
lines.push(`Position: (${data.newPosX ?? posX}, ${data.newPosY ?? posY})`);
}
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// Phase 3: Material Instances
// ---------------------------------------------------------------------------
server.tool("create_material_instance", "Create a new Material Instance asset with a specified parent material.", {
name: z.string().describe("Material Instance asset name (e.g. 'MI_MyMaterial')"),
packagePath: z.string().default("/Game").describe("Package path to create the instance in (e.g. '/Game/Materials')"),
parentMaterial: z.string().describe("Parent material name or package path (e.g. 'M_MyMaterial')"),
}, async ({ name, packagePath, parentMaterial }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { name, packagePath, parentMaterial };
const data = await uePost("/api/create-material-instance", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Created material instance ${data.name || name} at ${data.packagePath || packagePath}`);
if (data.parentMaterial)
lines.push(`Parent: ${data.parentMaterial}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(`\nNext steps:`);
lines.push(` 1. Use set_material_instance_parameter to override parameter values`);
lines.push(` 2. Use get_material_instance_parameters to inspect available parameters`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("set_material_instance_parameter", "Override a parameter value in a Material Instance.", {
materialInstance: z.string().describe("Material Instance name or package path (e.g. 'MI_MyMaterial')"),
parameterName: z.string().describe("Name of the parameter to override"),
value: z.union([
z.number(),
z.object({ r: z.number(), g: z.number(), b: z.number(), a: z.number().optional() }),
z.string(),
z.boolean(),
]).describe("Value to set: number (scalar), {r,g,b,a?} (vector/color), string (texture path), or boolean (static switch)"),
type: z.enum(["scalar", "vector", "texture", "staticSwitch"]).optional().describe("Parameter type hint (auto-detected if omitted)"),
dryRun: z.boolean().optional().describe("If true, preview changes without modifying the Material Instance"),
}, async ({ materialInstance, parameterName, value, type, dryRun }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { materialInstance, parameterName, value };
if (type)
body.type = type;
if (dryRun)
body.dryRun = true;
const data = await uePost("/api/set-material-instance-parameter", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (dryRun)
lines.push(`[DRY RUN - no changes saved]`);
lines.push(`Parameter override set.`);
lines.push(`Material Instance: ${data.materialInstance || materialInstance}`);
lines.push(`Parameter: ${data.parameterName || parameterName}`);
if (data.type)
lines.push(`Type: ${data.type}`);
if (data.oldValue !== undefined)
lines.push(`Old value: ${typeof data.oldValue === "object" ? JSON.stringify(data.oldValue) : data.oldValue}`);
if (data.newValue !== undefined)
lines.push(`New value: ${typeof data.newValue === "object" ? JSON.stringify(data.newValue) : data.newValue}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
if (!dryRun) {
lines.push(`\nNext steps:`);
lines.push(` 1. Use get_material_instance_parameters to verify all overrides`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("get_material_instance_parameters", "Get all parameters of a Material Instance, showing which are overridden vs inherited from parent.", {
name: z.string().describe("Material Instance name or package path (e.g. 'MI_MyMaterial')"),
}, async ({ name }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await ueGet("/api/material-instance-params", { name });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Material Instance: ${data.name || name}`);
if (data.parentMaterial)
lines.push(`Parent: ${data.parentMaterial}`);
if (data.parentChain?.length) {
lines.push(`Parent chain: ${data.parentChain.join(" -> ")}`);
}
const formatParamValue = (val) => {
if (val === undefined || val === null)
return "(default)";
if (typeof val === "object")
return JSON.stringify(val);
return String(val);
};
if (data.scalarParameters?.length) {
lines.push(`\nScalar Parameters:`);
for (const p of data.scalarParameters) {
const override = p.overridden ? " [OVERRIDDEN]" : "";
lines.push(` ${p.name}: ${formatParamValue(p.value)}${override}`);
}
}
if (data.vectorParameters?.length) {
lines.push(`\nVector Parameters:`);
for (const p of data.vectorParameters) {
const override = p.overridden ? " [OVERRIDDEN]" : "";
lines.push(` ${p.name}: ${formatParamValue(p.value)}${override}`);
}
}
if (data.textureParameters?.length) {
lines.push(`\nTexture Parameters:`);
for (const p of data.textureParameters) {
const override = p.overridden ? " [OVERRIDDEN]" : "";
lines.push(` ${p.name}: ${formatParamValue(p.value)}${override}`);
}
}
if (data.staticSwitchParameters?.length) {
lines.push(`\nStatic Switch Parameters:`);
for (const p of data.staticSwitchParameters) {
const override = p.overridden ? " [OVERRIDDEN]" : "";
lines.push(` ${p.name}: ${formatParamValue(p.value)}${override}`);
}
}
if (!data.scalarParameters?.length && !data.vectorParameters?.length &&
!data.textureParameters?.length && !data.staticSwitchParameters?.length) {
lines.push(`\nNo parameters found.`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("reparent_material_instance", "Change the parent of a Material Instance to a different Material or Material Instance.", {
materialInstance: z.string().describe("Material Instance name or package path (e.g. 'MI_MyMaterial')"),
newParent: z.string().describe("New parent material name or package path (e.g. 'M_NewParent')"),
dryRun: z.boolean().optional().describe("If true, preview changes without modifying the Material Instance"),
}, async ({ materialInstance, newParent, dryRun }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { materialInstance, newParent };
if (dryRun)
body.dryRun = true;
const data = await uePost("/api/reparent-material-instance", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (dryRun)
lines.push(`[DRY RUN - no changes saved]`);
lines.push(`Material Instance reparented.`);
lines.push(`Material Instance: ${data.materialInstance || materialInstance}`);
if (data.oldParent)
lines.push(`Old parent: ${data.oldParent}`);
lines.push(`New parent: ${data.newParent || newParent}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
if (!dryRun) {
lines.push(`\nNext steps:`);
lines.push(` 1. Use get_material_instance_parameters to check parameter compatibility`);
lines.push(` 2. Use set_material_instance_parameter to update overrides if needed`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// Phase 4: Create Material Function
// ---------------------------------------------------------------------------
server.tool("create_material_function", "Create a new Material Function asset.", {
name: z.string().describe("Material Function asset name (e.g. 'MF_MyFunction')"),
packagePath: z.string().default("/Game").describe("Package path to create the function in (e.g. '/Game/Materials/Functions')"),
description: z.string().optional().describe("Description of the material function"),
}, async ({ name, packagePath, description }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { name, packagePath };
if (description)
body.description = description;
const data = await uePost("/api/create-material-function", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Created material function ${data.name || name} at ${data.packagePath || packagePath}`);
if (data.description)
lines.push(`Description: ${data.description}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(`\nNext steps:`);
lines.push(` 1. Use add_material_expression to add expression nodes to the function`);
lines.push(` 2. Use connect_material_pins to wire expressions together`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// Phase 5: Snapshot/Diff/Restore
// ---------------------------------------------------------------------------
server.tool("snapshot_material_graph", "Take a snapshot of a Material's graph for later comparison or restoration.", {
material: z.string().describe("Material name or package path (e.g. 'M_MyMaterial')"),
}, async ({ material }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/snapshot-material-graph", { material });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Snapshot ${data.snapshotId} created for material ${data.material || material}`);
if (data.expressionCount !== undefined)
lines.push(`Expressions captured: ${data.expressionCount}`);
if (data.connectionCount !== undefined)
lines.push(`Connections captured: ${data.connectionCount}`);
lines.push(`\nNext steps:`);
lines.push(` 1. Make your changes to the material`);
lines.push(` 2. Use diff_material_graph to see what changed`);
lines.push(` 3. Use restore_material_graph to reconnect severed pins`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("diff_material_graph", "Compare a Material's current graph against a previously taken snapshot.", {
material: z.string().describe("Material name or package path (e.g. 'M_MyMaterial')"),
snapshotId: z.string().describe("Snapshot ID from snapshot_material_graph"),
}, async ({ material, snapshotId }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/diff-material-graph", { material, snapshotId });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`# Diff: ${data.material || material} vs ${data.snapshotId || snapshotId}`);
if (data.severedConnections?.length) {
lines.push(`\nSevered connections (${data.severedConnections.length}):`);
for (const sc of data.severedConnections) {
lines.push(` ${sc.sourceNodeTitle || sc.sourceNodeId}.${sc.sourcePinName} was -> ${sc.targetNodeTitle || sc.targetNodeId}.${sc.targetPinName}`);
}
}
if (data.newConnections?.length) {
lines.push(`\nNew connections (${data.newConnections.length}):`);
for (const nc of data.newConnections) {
lines.push(` ${nc.sourceNodeTitle || nc.sourceNodeId}.${nc.sourcePinName} -> ${nc.targetNodeTitle || nc.targetNodeId}.${nc.targetPinName}`);
}
}
if (data.missingNodes?.length) {
lines.push(`\nMissing nodes (${data.missingNodes.length}):`);
for (const mn of data.missingNodes) {
lines.push(` ${mn.nodeId} (${mn.nodeTitle || mn.expressionClass || "unknown"})`);
}
}
if (data.newNodes?.length) {
lines.push(`\nNew nodes (${data.newNodes.length}):`);
for (const nn of data.newNodes) {
lines.push(` ${nn.nodeId} (${nn.nodeTitle || nn.expressionClass || "unknown"})`);
}
}
if (!data.severedConnections?.length && !data.newConnections?.length &&
!data.missingNodes?.length && !data.newNodes?.length) {
lines.push(`\nNo changes detected.`);
}
if (data.summary) {
lines.push(`\nSummary: ${data.summary.severed ?? 0} severed, ${data.summary.new ?? 0} new connections, ${data.summary.missingNodes ?? 0} missing nodes`);
}
lines.push(`\nNext steps:`);
if (data.severedConnections?.length) {
lines.push(` 1. Use restore_material_graph to reconnect severed pins`);
}
else {
lines.push(` 1. No action needed \u2014 graph is intact`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("restore_material_graph", "Restore severed connections in a Material's graph from a snapshot.", {
material: z.string().describe("Material name or package path (e.g. 'M_MyMaterial')"),
snapshotId: z.string().describe("Snapshot ID from snapshot_material_graph"),
dryRun: z.boolean().optional().describe("If true, preview reconnections without making changes"),
}, async ({ material, snapshotId, dryRun }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { material, snapshotId };
if (dryRun)
body.dryRun = true;
const data = await uePost("/api/restore-material-graph", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (dryRun)
lines.push(`[DRY RUN - no changes saved]\n`);
lines.push(`Restore: ${data.material || material} from ${data.snapshotId || snapshotId}`);
lines.push(`Reconnected: ${data.reconnected ?? 0}/${(data.reconnected ?? 0) + (data.failed ?? 0)}`);
if ((data.failed ?? 0) > 0) {
lines.push(`Failed: ${data.failed}`);
}
if (data.details?.length) {
lines.push(`\nDetails:`);
for (const d of data.details) {
const status = d.result === "ok" ? "OK" : "FAILED";
const reason = d.reason ? ` (${d.reason})` : "";
lines.push(` ${status}: ${d.sourcePinName} -> ${d.targetNodeTitle || d.targetNodeId}.${d.targetPinName}${reason}`);
}
}
if (!dryRun && data.saved !== undefined) {
lines.push(`\nSaved: ${data.saved}`);
}
lines.push(`\nNext steps:`);
if (dryRun) {
lines.push(` 1. Re-run restore_material_graph without dryRun to apply changes`);
}
if ((data.failed ?? 0) > 0) {
lines.push(` 1. Fix ${data.failed} failed reconnection(s) manually with connect_material_pins`);
}
lines.push(` 2. Use get_material_graph to verify the final state`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
// ---------------------------------------------------------------------------
// Material Validation
// ---------------------------------------------------------------------------
server.tool("validate_material", "Force-recompile a Material and check for compilation errors. Returns valid/invalid status with error details.", {
material: z.string().describe("Material name or package path (e.g. 'M_MyMaterial')"),
}, async ({ material }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/validate-material", { material });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Material: ${data.material || material}`);
lines.push(`Valid: ${data.valid ? "Yes" : "No"}`);
lines.push(`Expressions: ${data.expressionCount ?? 0}`);
lines.push(`Connections: ${data.connectionCount ?? 0}`);
if (data.errors?.length) {
lines.push(`\nCompilation errors (${data.errorCount}):`);
for (const e of data.errors) {
lines.push(` - ${e}`);
}
}
if (data.valid) {
lines.push(`\nMaterial compiled successfully.`);
}
else {
lines.push(`\nNext steps:`);
lines.push(` 1. Use get_material_graph to inspect the graph`);
lines.push(` 2. Fix the errors and re-validate`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=material-mutation.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerMaterialReadTools(server: McpServer): void;

View File

@@ -1,135 +0,0 @@
import { z } from "zod";
import { ensureUE, ueGet, uePost } from "../ue-bridge.js";
import { describeMaterial } from "../material-describe.js";
export function registerMaterialReadTools(server) {
server.tool("list_materials", "List all Material and Material Instance assets in the UE5 project. Filter by name/path and type.", {
filter: z.string().optional().describe("Substring to match against material name or path"),
type: z.enum(["all", "material", "instance"]).optional().default("all").describe("Filter by type: 'all' (default), 'material' (base materials only), 'instance' (material instances only)"),
}, async ({ filter, type: matType }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await ueGet("/api/materials", {
filter: filter || "",
type: matType || "all",
});
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = data.materials.map((mat) => `${mat.name} (${mat.path}) [${mat.type}]`);
const summary = `Found ${data.count} of ${data.total} materials.\n\n${lines.join("\n")}`;
return { content: [{ type: "text", text: summary }] };
});
server.tool("get_material", "Get full details of a Material or Material Instance: domain, blend mode, shading model, parameters, expressions, referenced textures, usage flags, opacity clip value, texture sample count.", {
name: z.string().describe("Material name or package path"),
}, async ({ name }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await ueGet("/api/material", { name });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
return { content: [{ type: "text", text: JSON.stringify(data) }] };
});
server.tool("get_material_graph", "Get the material editor graph for a Material, with all expression nodes and connections.", {
name: z.string().describe("Material name or package path"),
}, async ({ name }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await ueGet("/api/material-graph", { name });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
return { content: [{ type: "text", text: JSON.stringify(data) }] };
});
server.tool("describe_material", "Get a human-readable description of a Material's graph, showing what feeds into each material input (BaseColor, Roughness, Normal, etc.).", {
name: z.string().describe("Material name or package path"),
}, async ({ name }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/describe-material", { material: name });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const text = data.description ? data.description : describeMaterial(data);
return { content: [{ type: "text", text }] };
});
server.tool("search_materials", "Search across Materials for expressions matching a query (parameter names, expression types).", {
query: z.string().describe("Search term to match against expression types, parameter names, texture names"),
maxResults: z.number().optional().default(50).describe("Maximum results to return"),
}, async ({ query, maxResults }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await ueGet("/api/search-materials", {
query,
maxResults: String(maxResults),
});
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Found ${data.resultCount} results for "${query}":\n`);
for (const r of data.results) {
let line = `[${r.material}] ${r.matchType}: ${r.matchValue}`;
if (r.expressionType)
line += ` (${r.expressionType})`;
if (r.parameterName)
line += ` param:${r.parameterName}`;
lines.push(line);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("find_material_references", "Find all assets that reference a given Material or Material Instance.", {
material: z.string().describe("Material name or package path"),
}, async ({ material }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/material-references", { material });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`References to: ${data.material || material}`);
lines.push(`Total referencers: ${data.totalReferencers}`);
if (data.referencers?.length) {
lines.push("");
for (const ref of data.referencers) {
lines.push(` ${ref}`);
}
}
if (data.totalReferencers === 0) {
lines.push("\nNo referencers found.");
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("list_material_functions", "List all Material Function assets in the project.", {
filter: z.string().optional().describe("Substring to match against function name or path"),
}, async ({ filter }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await ueGet("/api/material-functions", { filter: filter || "" });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Found ${data.count} material functions.\n`);
for (const fn of data.functions) {
let line = `${fn.name} (${fn.path})`;
if (fn.description)
line += `${fn.description}`;
lines.push(line);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("get_material_function", "Get details of a Material Function: description, inputs, outputs, expressions.", {
name: z.string().describe("Material Function name or package path"),
}, async ({ name }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await ueGet("/api/material-function", { name });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
return { content: [{ type: "text", text: JSON.stringify(data) }] };
});
}
//# sourceMappingURL=material-read.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerMutationTools(server: McpServer): void;

View File

@@ -1,662 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost } from "../ue-bridge.js";
import { TYPE_NAME_DOCS, formatUpdatedState } from "../helpers.js";
export function registerMutationTools(server) {
server.tool("replace_function_calls", "In a Blueprint, redirect all function call nodes from one function library class to another (matched by function name). Reports which pin connections were broken due to type changes. Use this for migrating Blueprints from one function library to another. Pass dryRun=true to preview changes without saving.", {
blueprint: z.string().describe("Blueprint name or package path (e.g. 'BP_PatientJson')"),
oldClass: z.string().describe("Current function library class name (e.g. 'FL_StateParsers')"),
newClass: z.string().describe("New function library class name (e.g. 'StateParsersLibrary')"),
dryRun: z.boolean().optional().describe("If true, preview changes without modifying the Blueprint"),
}, async ({ blueprint, oldClass, newClass, dryRun }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, oldClass, newClass };
if (dryRun)
body.dryRun = true;
const data = await uePost("/api/replace-function-calls", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (dryRun)
lines.push(`[DRY RUN - no changes saved]`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Replaced: ${data.replacedCount} function call node(s)`);
if (data.saved !== undefined) {
lines.push(`Saved: ${data.saved}`);
}
if (data.message) {
lines.push(data.message);
}
if (data.brokenConnectionCount > 0) {
lines.push(`\nBroken connections (${data.brokenConnectionCount}):`);
for (const bc of data.brokenConnections) {
if (bc.type === "functionNotFound") {
lines.push(` WARNING: Function '${bc.functionName}' not found in new class (node ${bc.nodeId})`);
}
else if (bc.type === "connectionLost") {
lines.push(` BROKEN: ${bc.functionName} pin '${bc.pinName}' was connected to node ${bc.wasConnectedToNode}.${bc.wasConnectedToPin}`);
}
}
lines.push("\nThese connections must be fixed manually in the editor.");
}
// Updated state (#11)
lines.push(...formatUpdatedState(data));
// Tool chaining hints (#12)
if (!dryRun) {
lines.push(`\nNext steps:`);
lines.push(` 1. Verify with get_blueprint_graph to inspect the updated graphs`);
lines.push(` 2. Run refresh_all_nodes to propagate pin type changes`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("delete_asset", "Delete a .uasset file after confirming no remaining references. By default refuses to delete if the asset is still referenced. Use force=true to delete anyway (references become stale). Use find_asset_references first to check dependencies.", {
assetPath: z.string().describe("Full asset path to delete (e.g. '/Game/Blueprints/WebUI/S_Vitals')"),
force: z.boolean().optional().describe("If true, force-delete even if references exist. Stale references will remain and must be cleaned up manually."),
batch: z.array(z.object({
assetPath: z.string(),
force: z.boolean().optional(),
})).optional().describe("Batch mode: array of {assetPath, force?} objects. When provided, single params are ignored."),
}, async ({ assetPath, force, batch }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = batch
? { batch }
: { assetPath };
if (force && !batch)
body.force = true;
const data = await uePost("/api/delete-asset", body);
if (data.error) {
let msg = `Error: ${data.error}`;
if (data.referencers) {
// Classify live vs stale references (#16)
const liveRefs = data.liveReferencers || [];
const staleRefs = data.staleReferencers || [];
if (liveRefs.length > 0 || staleRefs.length > 0) {
if (liveRefs.length > 0) {
msg += `\n\nLive references (${liveRefs.length}) \u2014 these assets actively use this asset:`;
msg += liveRefs.map((r) => `\n ${r}`).join("");
}
if (staleRefs.length > 0) {
msg += `\n\nStale references (${staleRefs.length}) \u2014 these may be outdated/cached:`;
msg += staleRefs.map((r) => `\n ${r}`).join("");
}
msg += `\n\nNext steps:`;
msg += `\n - Fix live references by updating the referencing Blueprints`;
msg += `\n - Use force=true to delete despite stale references`;
msg += `\n - Or run find_asset_references to inspect each one`;
}
else {
msg += `\n\nStill referenced by (${data.referencerCount}):\n`;
msg += data.referencers.map((r) => ` ${r}`).join("\n");
msg += `\n\nNext steps:`;
msg += `\n - Update or remove references in the listed assets first`;
msg += `\n - Or use force=true to force-delete (references become stale)`;
}
}
return { content: [{ type: "text", text: msg }] };
}
if (data.results) {
// Batch response
const lines = [`Batch delete: ${data.results.length} operation(s)`];
for (const r of data.results) {
if (r.error) {
lines.push(` FAILED ${r.assetPath}: ${r.error}`);
}
else {
lines.push(` DELETED ${r.assetPath}`);
}
}
// Tool chaining hints (#12)
lines.push(`\nNext steps:`);
lines.push(` 1. Verify no orphaned references remain with find_asset_references`);
return { content: [{ type: "text", text: lines.join("\n") }] };
}
const lines = [];
lines.push(`Asset deleted successfully.`);
lines.push(`Path: ${data.assetPath}`);
lines.push(`File: ${data.filename}`);
// Show warning-based reference info when force was used (#1)
if (data.warnings?.length) {
lines.push(`\nWarnings:`);
for (const w of data.warnings) {
lines.push(` \u26A0 ${w}`);
}
}
if (data.forcedReferencers?.length) {
lines.push(`\nForce-deleted despite references from:`);
for (const ref of data.forcedReferencers) {
lines.push(` ${ref}`);
}
lines.push(`These references are now stale and should be cleaned up.`);
}
// Tool chaining hints (#12)
lines.push(`\nNext steps:`);
lines.push(` 1. Verify no orphaned references remain with find_asset_references`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("connect_pins", "Wire two pins together in a Blueprint graph. Uses type-validated connection (TryCreateConnection) so incompatible types will fail with details. Get node IDs and pin names from get_blueprint_graph first.", {
blueprint: z.string().describe("Blueprint name or package path (e.g. 'BP_PatientJson')"),
sourceNodeId: z.string().describe("GUID of the source node (from get_blueprint_graph node 'id' field)"),
sourcePinName: z.string().describe("Name of the output pin on the source node"),
targetNodeId: z.string().describe("GUID of the target node"),
targetPinName: z.string().describe("Name of the input pin on the target node"),
batch: z.array(z.object({
blueprint: z.string(),
sourceNodeId: z.string(),
sourcePinName: z.string(),
targetNodeId: z.string(),
targetPinName: z.string(),
})).optional().describe("Batch mode: array of connection objects. When provided, single params are ignored."),
}, async ({ blueprint, sourceNodeId, sourcePinName, targetNodeId, targetPinName, batch }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = batch
? { batch }
: { blueprint, sourceNodeId, sourcePinName, targetNodeId, targetPinName };
const data = await uePost("/api/connect-pins", body);
if (data.error && !data.success) {
let msg = `Error: ${data.error}`;
if (data.availablePins) {
msg += `\nAvailable pins: ${data.availablePins.join(", ")}`;
}
if (data.sourcePinType)
msg += `\nSource pin type: ${data.sourcePinType}${data.sourcePinSubtype ? ` (${data.sourcePinSubtype})` : ""}`;
if (data.targetPinType)
msg += `\nTarget pin type: ${data.targetPinType}${data.targetPinSubtype ? ` (${data.targetPinSubtype})` : ""}`;
return { content: [{ type: "text", text: msg }] };
}
const lines = [];
if (data.results) {
// Batch response
lines.push(`Batch connect: ${data.results.length} operation(s)`);
for (const r of data.results) {
if (r.error) {
lines.push(` FAILED: ${r.error}`);
}
else {
lines.push(` OK: ${r.sourcePinName} \u2192 ${r.targetPinName}`);
}
}
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
}
else {
lines.push(`Connection ${data.success ? "succeeded" : "failed"}.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Source pin type: ${data.sourcePinType}${data.sourcePinSubtype ? ` (${data.sourcePinSubtype})` : ""}`);
lines.push(`Target pin type: ${data.targetPinType}${data.targetPinSubtype ? ` (${data.targetPinSubtype})` : ""}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
}
// Updated state (#11)
lines.push(...formatUpdatedState(data));
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("disconnect_pin", "Break connections on a specific pin. By default breaks ALL connections on the pin. Optionally specify targetNodeId + targetPinName to break only a single specific link.", {
blueprint: z.string().describe("Blueprint name or package path"),
nodeId: z.string().describe("GUID of the node containing the pin"),
pinName: z.string().describe("Name of the pin to disconnect"),
targetNodeId: z.string().optional().describe("GUID of a specific connected node to disconnect from (optional)"),
targetPinName: z.string().optional().describe("Pin name on the target node to disconnect from (optional, required if targetNodeId is set)"),
}, async ({ blueprint, nodeId, pinName, targetNodeId, targetPinName }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, nodeId, pinName };
if (targetNodeId)
body.targetNodeId = targetNodeId;
if (targetPinName)
body.targetPinName = targetPinName;
const data = await uePost("/api/disconnect-pin", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Disconnected ${data.disconnectedCount} link(s).`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("change_struct_node_type", `Change a BreakStruct or MakeStruct node to use a different struct type. Reconstructs the node and attempts to reconnect pins by matching property names. Get node IDs from get_blueprint_graph first. ${TYPE_NAME_DOCS}`, {
blueprint: z.string().describe("Blueprint name or package path (e.g. 'BP_PatientJson')"),
nodeId: z.string().describe("GUID of the BreakStruct or MakeStruct node"),
newType: z.string().describe("New struct type name with F prefix (e.g. 'FVitals', 'FSkinState')"),
}, async ({ blueprint, nodeId, newType }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/change-struct-node-type", { blueprint, nodeId, newType });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Struct node type changed successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Node: ${data.nodeId} (${data.nodeClass})`);
lines.push(`New type: ${data.newStructType}`);
lines.push(`Reconnected: ${data.reconnected}, Failed: ${data.failed}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
if (data.reconnectDetails?.length) {
lines.push(`\nReconnection details:`);
for (const d of data.reconnectDetails) {
const status = d.connected ? "OK" : "FAILED";
lines.push(` ${d.property}: ${status}${d.reason ? ` (${d.reason})` : ""}`);
}
}
// Updated state (#11)
lines.push(...formatUpdatedState(data));
// Tool chaining hints (#12)
lines.push(`\nNext steps:`);
lines.push(` 1. Run refresh_all_nodes to propagate type changes throughout the Blueprint`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("refresh_all_nodes", "Refresh all nodes in a Blueprint to update pin types and connections after modifications (e.g. after replace_function_calls or change_variable_type). Recompiles and saves the Blueprint.", {
blueprint: z.string().describe("Blueprint name or package path (e.g. 'BP_PatientManager')"),
}, async ({ blueprint }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/refresh-all-nodes", { blueprint });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Refresh ${data.success ? "succeeded" : "completed with issues"}.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Graphs: ${data.graphCount}, Nodes: ${data.nodeCount}`);
lines.push(`Saved: ${data.saved}`);
if (data.warning)
lines.push(`Warning: ${data.warning}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("delete_node", "Remove a node from a Blueprint graph. Disconnects all pins and removes the node. Use get_blueprint_graph to find node IDs first. Entry/root nodes (FunctionEntry, Event, CustomEvent) cannot be deleted as this would leave the graph uncompilable.", {
blueprint: z.string().describe("Blueprint name or package path"),
nodeId: z.string().describe("GUID of the node to delete (from get_blueprint_graph node 'id' field)"),
}, async ({ blueprint, nodeId }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/delete-node", { blueprint, nodeId });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Node removed successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
if (data.nodeId)
lines.push(`Node ID: ${data.nodeId}`);
if (data.nodeClass)
lines.push(`Node class: ${data.nodeClass}`);
if (data.nodeTitle)
lines.push(`Node title: ${data.nodeTitle}`);
if (data.disconnectedPins !== undefined)
lines.push(`Disconnected pins: ${data.disconnectedPins}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("add_node", "Add a new node to a Blueprint graph. Supports: BreakStruct, MakeStruct, CallFunction, VariableGet, VariableSet, DynamicCast, OverrideEvent, CallParentFunction, Branch, Sequence, CustomEvent, ForEachLoop, ForLoop, ForLoopWithBreak, WhileLoop, SpawnActorFromClass, Select, Comment, Reroute. For Delay/IsValid/PrintString, use CallFunction with className 'KismetSystemLibrary'.", {
blueprint: z.string().describe("Blueprint name or package path"),
graph: z.string().describe("Graph name (e.g. 'EventGraph')"),
nodeType: z.enum([
"BreakStruct", "MakeStruct", "CallFunction", "VariableGet", "VariableSet",
"DynamicCast", "OverrideEvent", "CallParentFunction",
"Branch", "Sequence", "CustomEvent",
"ForEachLoop", "ForLoop", "ForLoopWithBreak", "WhileLoop",
"SpawnActorFromClass", "Select", "Comment", "Reroute"
]).describe("Type of node to add"),
typeName: z.string().optional().describe("Struct type name for BreakStruct/MakeStruct (e.g. 'FVitals')"),
functionName: z.string().optional().describe("Function name for CallFunction, OverrideEvent, or CallParentFunction (e.g. 'PrintString')"),
className: z.string().optional().describe("Class name for CallFunction (e.g. 'KismetSystemLibrary')"),
variableName: z.string().optional().describe("Variable name for VariableGet/VariableSet"),
castTarget: z.string().optional().describe("Target class name for DynamicCast (e.g. 'BP_PatientJson')"),
eventName: z.string().optional().describe("Event name for CustomEvent (e.g. 'OnDataReady')"),
actorClass: z.string().optional().describe("Actor class for SpawnActorFromClass (e.g. 'BP_Patient_Base'). Optional — can also be set via the class pin."),
comment: z.string().optional().describe("Comment text for Comment node type"),
width: z.number().optional().describe("Width for Comment node (default: 400)"),
height: z.number().optional().describe("Height for Comment node (default: 200)"),
posX: z.number().optional().describe("X position in the graph (optional)"),
posY: z.number().optional().describe("Y position in the graph (optional)"),
}, async ({ blueprint, graph, nodeType, typeName, functionName, className, variableName, castTarget, eventName, actorClass, comment, width, height, posX, posY }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, graph, nodeType };
if (typeName)
body.typeName = typeName;
if (functionName)
body.functionName = functionName;
if (className)
body.className = className;
if (variableName)
body.variableName = variableName;
if (castTarget)
body.castTarget = castTarget;
if (eventName)
body.eventName = eventName;
if (actorClass)
body.actorClass = actorClass;
if (comment)
body.comment = comment;
if (width !== undefined)
body.width = width;
if (height !== undefined)
body.height = height;
if (posX !== undefined)
body.posX = posX;
if (posY !== undefined)
body.posY = posY;
const data = await uePost("/api/add-node", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (data.alreadyExists) {
lines.push(`Node already exists (returning existing).`);
}
else {
lines.push(`Node added successfully.`);
}
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Graph: ${data.graph}`);
if (data.nodeId)
lines.push(`Node ID: ${data.nodeId}`);
if (data.nodeClass)
lines.push(`Node class: ${data.nodeClass}`);
if (data.nodeTitle)
lines.push(`Node title: ${data.nodeTitle}`);
if (data.node?.pins?.length) {
lines.push(`\nPins:`);
for (const pin of data.node.pins) {
const dir = pin.direction === "Output" ? "\u2192" : "\u2190";
lines.push(` ${dir} ${pin.name}: ${pin.type}${pin.subtype ? ` (${pin.subtype})` : ""}`);
}
}
else if (data.pins?.length) {
lines.push(`\nPins:`);
for (const pin of data.pins) {
const dir = pin.direction === "Output" ? "\u2192" : "\u2190";
lines.push(` ${dir} ${pin.name}: ${pin.type}${pin.subtype ? ` (${pin.subtype})` : ""}`);
}
}
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("rename_asset", "Rename or move an asset (Blueprint, Material, Material Instance, or Material Function) and update all references.", {
assetPath: z.string().describe("Current full asset path (e.g. '/Game/Blueprints/Old/BP_MyActor' or '/Game/Materials/MI_Skin')"),
newPath: z.string().describe("New full asset path (e.g. '/Game/Blueprints/New/BP_MyRenamedActor')"),
}, async ({ assetPath, newPath }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/rename-asset", { assetPath, newPath });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Asset renamed/moved successfully.`);
lines.push(`From: ${data.oldPath || assetPath}`);
lines.push(`To: ${data.newPath || newPath}`);
if (data.referencesUpdated !== undefined)
lines.push(`References updated: ${data.referencesUpdated}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("set_pin_default", "Set the default value of an input pin on a Blueprint node. Supports batch mode for setting multiple pins at once. Use this to set literal/constant values on pins that are not connected to other nodes.", {
blueprint: z.string().optional().describe("Blueprint name or package path (required for single mode)"),
nodeId: z.string().optional().describe("Node GUID (required for single mode)"),
pinName: z.string().optional().describe("Pin name (required for single mode)"),
value: z.string().optional().describe("Default value to set (required for single mode)"),
batch: z.array(z.object({
blueprint: z.string(),
nodeId: z.string(),
pinName: z.string(),
value: z.string(),
})).optional().describe("Batch mode: array of {blueprint, nodeId, pinName, value} objects. When provided, single params are ignored."),
}, async ({ blueprint, nodeId, pinName, value, batch }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = batch
? { batch }
: { blueprint, nodeId, pinName, value };
const data = await uePost("/api/set-pin-default", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (data.results) {
// Batch response
lines.push(`Batch set_pin_default: ${data.successCount}/${data.totalCount} succeeded.`);
for (const r of data.results) {
if (r.error) {
lines.push(` FAILED ${r.nodeId || "?"}.${r.pinName || "?"}: ${r.error}`);
}
else {
lines.push(` OK ${r.nodeId}.${r.pinName}: ${r.oldValue || "(empty)"} -> ${r.newValue}`);
}
}
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
}
else {
lines.push(`Pin default set successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Node: ${data.nodeId}`);
lines.push(`Pin: ${data.pinName}`);
lines.push(`Old value: ${data.oldValue || "(empty)"}`);
lines.push(`New value: ${data.newValue}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("move_node", "Reposition one or more nodes in a Blueprint graph by setting their X/Y coordinates. Use batch mode with 'nodes' array for multiple moves in one call.", {
blueprint: z.string().describe("Blueprint name or package path"),
nodeId: z.string().optional().describe("Node GUID (for single-node mode)"),
x: z.number().optional().describe("New X position (for single-node mode)"),
y: z.number().optional().describe("New Y position (for single-node mode)"),
nodes: z.array(z.object({
nodeId: z.string(),
x: z.number(),
y: z.number(),
})).optional().describe("Batch mode: array of {nodeId, x, y} objects"),
}, async ({ blueprint, nodeId, x, y, nodes }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint };
if (nodes) {
body.nodes = nodes;
}
else {
if (nodeId)
body.nodeId = nodeId;
if (x !== undefined)
body.x = x;
if (y !== undefined)
body.y = y;
}
const data = await uePost("/api/move-node", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (data.results) {
// Batch response
lines.push(`Batch move: ${data.movedCount}/${data.totalRequested} node(s) repositioned.`);
lines.push(`Blueprint: ${data.blueprint}`);
for (const r of data.results) {
if (r.error) {
lines.push(` FAILED ${r.nodeId}: ${r.error}`);
}
else {
lines.push(` OK ${r.nodeId}: (${r.oldX},${r.oldY}) -> (${r.newX},${r.newY})`);
}
}
}
else {
lines.push(`Node repositioned successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Node: ${data.nodeId}`);
lines.push(`Position: (${data.oldX},${data.oldY}) -> (${data.newX},${data.newY})`);
}
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("set_blueprint_default", "Set a default property value on a Blueprint's Class Default Object (CDO). Supports TSubclassOf (class references), object references, and simple types (bool, int, float, string, enum). For class/object values, provide the Blueprint asset name (e.g. 'MyWidget') or C++ class name.", {
blueprint: z.string().describe("Blueprint name or package path (e.g. 'HUD_WebUIInterface')"),
property: z.string().describe("Property name as declared in C++ or Blueprint (e.g. 'WebUIWidgetClass')"),
value: z.string().describe("Value to set. For class properties: Blueprint name or C++ class name. For simple types: the literal value (e.g. 'true', '42', '0.5')"),
}, async ({ blueprint, property, value }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/set-blueprint-default", { blueprint, property, value });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Default property set successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Property: ${data.property} (${data.propertyType})`);
lines.push(`Old value: ${data.oldValue || "(empty)"}`);
lines.push(`New value: ${data.newValue}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("duplicate_nodes", "Duplicate one or more nodes within a Blueprint graph. Creates copies at an offset from the originals. The duplicated nodes are not connected to anything — use connect_pins to wire them up.", {
blueprint: z.string().describe("Blueprint name or package path"),
graph: z.string().describe("Graph name (e.g. 'EventGraph')"),
nodeIds: z.array(z.string()).describe("Array of node GUIDs to duplicate"),
offsetX: z.number().optional().describe("X offset for duplicated nodes (default: 50)"),
offsetY: z.number().optional().describe("Y offset for duplicated nodes (default: 50)"),
}, async ({ blueprint, graph, nodeIds, offsetX, offsetY }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, graph, nodeIds };
if (offsetX !== undefined)
body.offsetX = offsetX;
if (offsetY !== undefined)
body.offsetY = offsetY;
const data = await uePost("/api/duplicate-nodes", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Duplicated ${data.duplicatedCount} node(s).`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Graph: ${data.graph}`);
if (data.nodes?.length) {
lines.push(``);
for (const n of data.nodes) {
if (n.error) {
lines.push(` FAILED ${n.sourceNodeId}: ${n.error}`);
}
else {
lines.push(` ${n.sourceNodeId} -> ${n.newNodeId} at (${n.posX},${n.posY})`);
}
}
}
if (data.notFound?.length) {
lines.push(`\nNot found: ${data.notFound.join(", ")}`);
}
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(``);
lines.push(`Next steps:`);
lines.push(` connect_pins — wire the duplicated nodes to other nodes`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("get_node_comment", "Read the comment text (comment bubble) on a Blueprint node.", {
blueprint: z.string().describe("Blueprint name or package path"),
nodeId: z.string().describe("Node GUID"),
}, async ({ blueprint, nodeId }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/get-node-comment", { blueprint, nodeId });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Node: ${data.nodeId}`);
lines.push(`Comment: ${data.comment || "(empty)"}`);
lines.push(`Comment bubble visible: ${data.commentBubbleVisible}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("set_node_comment", "Set or clear the comment text (comment bubble) on a Blueprint node. When setting a non-empty comment, the comment bubble is automatically made visible and pinned.", {
blueprint: z.string().describe("Blueprint name or package path"),
nodeId: z.string().describe("Node GUID"),
comment: z.string().describe("Comment text to set (empty string to clear)"),
}, async ({ blueprint, nodeId, comment }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/set-node-comment", { blueprint, nodeId, comment });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Node comment set successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Node: ${data.nodeId}`);
lines.push(`Old comment: ${data.oldComment || "(empty)"}`);
lines.push(`New comment: ${data.newComment || "(empty)"}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("search_node_actions", "Search the Blueprint action database for node types that can be created. This searches the same pool of actions shown in the editor's right-click menu, including custom K2 nodes. Returns full action names (Category|Name) that can be passed to spawn_node.", {
query: z.string().describe("Search term to match against action names, categories, and keywords (e.g. 'ReadLua', 'Print String', 'Branch')"),
maxResults: z.number().optional().default(50).describe("Maximum results to return (default: 50, max: 500)"),
}, async ({ query, maxResults }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/search-node-actions", { query, maxResults });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Found ${data.count} matching actions:\n`);
for (const name of data.actions) {
lines.push(name);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("spawn_node", "Create a node in a Blueprint graph using the editor's action database. Unlike add_node which only supports a fixed set of node types, spawn_node can create ANY node type that appears in the editor's right-click menu, including custom K2 nodes. Use search_node_actions first to find the exact action name.", {
blueprint: z.string().describe("Blueprint name or package path"),
graph: z.string().describe("Graph name (e.g. 'EventGraph')"),
actionName: z.string().describe("Full action name from search_node_actions (e.g. 'Luprex|Lua|Read Lua Values')"),
posX: z.number().optional().describe("X position in the graph"),
posY: z.number().optional().describe("Y position in the graph"),
}, async ({ blueprint, graph, actionName, posX, posY }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, graph, actionName };
if (posX !== undefined)
body.posX = posX;
if (posY !== undefined)
body.posY = posY;
const data = await uePost("/api/spawn-node", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Node spawned successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Graph: ${data.graph}`);
lines.push(`Node ID: ${data.nodeId}`);
lines.push(`Node class: ${data.nodeClass}`);
lines.push(`Node title: ${data.nodeTitle}`);
if (data.node?.pins?.length) {
lines.push(`\nPins:`);
for (const pin of data.node.pins) {
const dir = pin.direction === "Output" ? "\u2192" : "\u2190";
lines.push(` ${dir} ${pin.name}: ${pin.type}${pin.subtype ? ` (${pin.subtype})` : ""}`);
}
}
if (data.saved !== undefined)
lines.push(`\nSaved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=mutation.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerParamTools(server: McpServer): void;

View File

@@ -1,133 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost } from "../ue-bridge.js";
import { TYPE_NAME_DOCS, formatUpdatedState } from "../helpers.js";
export function registerParamTools(server) {
server.tool("change_function_parameter_type", `Change a function or custom event parameter's type. Supports all types: primitives (bool, int, float, string), structs, enums, and object references. Works with both Blueprint functions (K2Node_FunctionEntry) and custom events (K2Node_CustomEvent). Reconstructs the node to update output pins. Call refresh_all_nodes afterwards to propagate changes to downstream Break nodes. ${TYPE_NAME_DOCS} Pass dryRun=true to preview changes without saving.`, {
blueprint: z.string().describe("Blueprint name or package path (e.g. 'BP_PatientManager')"),
functionName: z.string().describe("Function or custom event name (e.g. 'UpdateVitals', 'SetSkinState')"),
paramName: z.string().describe("Parameter name to change (e.g. 'Vitals', 'SkinState')"),
newType: z.string().describe("New type: primitive ('bool', 'float'), struct ('FVitals'), enum ('EMyEnum'), or reference ('object:Actor', 'class:Actor', 'softobject:Actor')"),
dryRun: z.boolean().optional().describe("If true, preview changes without modifying the Blueprint"),
batch: z.array(z.object({
blueprint: z.string(),
functionName: z.string(),
paramName: z.string(),
newType: z.string(),
})).optional().describe("Batch mode: array of {blueprint, functionName, paramName, newType} objects. When provided, single params are ignored."),
}, async ({ blueprint, functionName, paramName, newType, dryRun, batch }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = batch
? { batch }
: { blueprint, functionName, paramName, newType };
if (dryRun)
body.dryRun = true;
const data = await uePost("/api/change-function-param-type", body);
if (data.error) {
let msg = `Error: ${data.error}`;
if (data.availableParams) {
msg += `\nAvailable parameters: ${data.availableParams.join(", ")}`;
}
if (data.availableFunctionsAndEvents) {
msg += `\nAvailable functions/events: ${data.availableFunctionsAndEvents.join(", ")}`;
}
return { content: [{ type: "text", text: msg }] };
}
const lines = [];
if (dryRun)
lines.push(`[DRY RUN - no changes saved]`);
if (data.results) {
// Batch response
lines.push(`Batch parameter type change: ${data.results.length} operation(s)`);
for (const r of data.results) {
if (r.error) {
lines.push(` FAILED ${r.blueprint}.${r.functionName}.${r.paramName}: ${r.error}`);
}
else {
lines.push(` OK ${r.blueprint}.${r.functionName}.${r.paramName} \u2192 ${r.newType}`);
}
}
}
else {
lines.push(`Parameter type changed successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`${data.nodeType}: ${data.functionName}`);
lines.push(`Parameter: ${data.paramName} \u2192 ${data.newType}`);
lines.push(`Node ID: ${data.nodeId}`);
lines.push(`Saved: ${data.saved}`);
}
// Updated state (#11)
lines.push(...formatUpdatedState(data));
// Tool chaining hints (#12)
if (!dryRun) {
lines.push(`\nNext steps:`);
lines.push(` 1. Check delegate graphs that bind to this function/event`);
lines.push(` 2. Run refresh_all_nodes to propagate pin changes downstream`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("remove_function_parameter", "Remove a parameter from a Blueprint function, custom event, or event dispatcher delegate. Works by finding the FunctionEntry/CustomEvent node in the function/delegate signature graph and removing the UserDefinedPin. Reconstructs the node and saves. Use this to remove delegate parameters that reference deleted types.", {
blueprint: z.string().describe("Blueprint name or package path (e.g. 'BPC_DeviceController')"),
functionName: z.string().describe("Function, custom event, or event dispatcher name (e.g. 'OnDeviceStateChanged')"),
paramName: z.string().describe("Parameter name to remove (e.g. 'DeviceState')"),
}, async ({ blueprint, functionName, paramName }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/remove-function-parameter", {
blueprint, functionName, paramName,
});
if (data.error) {
let msg = `Error: ${data.error}`;
if (data.availableParams) {
msg += `\nAvailable parameters: ${data.availableParams.join(", ")}`;
}
if (data.availableFunctionsAndEvents) {
msg += `\nAvailable functions/events: ${data.availableFunctionsAndEvents.join(", ")}`;
}
return { content: [{ type: "text", text: msg }] };
}
const lines = [];
lines.push(`Parameter removed successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`${data.nodeType}: ${data.functionName}`);
lines.push(`Removed parameter: ${data.paramName}`);
lines.push(`Node ID: ${data.nodeId}`);
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("add_function_parameter", `Add a typed parameter to an existing function, custom event, or event dispatcher signature. Works with all three — specify the function/event/dispatcher name in functionName. ${TYPE_NAME_DOCS}`, {
blueprint: z.string().describe("Blueprint name or package path"),
functionName: z.string().describe("Name of the function, custom event, or event dispatcher to add the parameter to"),
paramName: z.string().describe("Name for the new parameter"),
paramType: z.string().describe("Type for the new parameter (e.g. 'float', 'bool', 'string', 'FVector', 'object')"),
}, async ({ blueprint, functionName, paramName, paramType }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/add-function-parameter", {
blueprint, functionName, paramName, paramType,
});
if (data.error) {
let msg = `Error: ${data.error}`;
if (data.availableFunctions?.length) {
msg += `\nAvailable functions/events/dispatchers:\n ${data.availableFunctions.join("\n ")}`;
}
return { content: [{ type: "text", text: msg }] };
}
const lines = [];
lines.push(`Parameter added successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Function: ${data.functionName} (${data.nodeType})`);
lines.push(`Parameter: ${data.paramName}: ${data.paramType}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(``);
lines.push(`Next steps:`);
lines.push(` get_blueprint_graph(blueprint="${blueprint}", graph="${functionName}") — inspect the updated signature`);
lines.push(` add_function_parameter(blueprint="${blueprint}", functionName="${functionName}", ...) — add another parameter`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=params.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerReadTools(server: McpServer): void;

View File

@@ -1,205 +0,0 @@
import { z } from "zod";
import { ensureUE, ueGet } from "../ue-bridge.js";
import { summarizeBlueprint } from "../graph-describe.js";
import { describeGraph } from "../graph-describe.js";
export function registerReadTools(server) {
server.tool("list_blueprints", "List all Blueprint assets in the UE5 project, including level blueprints from .umap files. Optionally filter by name/path substring, parent class, or type (regular vs level).", {
filter: z.string().optional().describe("Substring to match against Blueprint name or path"),
parentClass: z.string().optional().describe("Filter by parent class name"),
type: z.enum(["all", "regular", "level"]).optional().default("all").describe("Filter by blueprint type: 'all' (default), 'regular' (standard BPs only), 'level' (level blueprints only)"),
}, async ({ filter, parentClass, type: bpType }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await ueGet("/api/list", {
filter: filter || "",
parentClass: parentClass || "",
type: bpType || "all",
});
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = data.blueprints.map((bp) => {
const levelTag = bp.isLevelBlueprint ? " Level" : "";
return `${bp.name} (${bp.path}) [${bp.parentClass || "?"}${levelTag}]`;
});
const summary = `Found ${data.count} of ${data.total} blueprints.\n\n${lines.join("\n")}`;
return { content: [{ type: "text", text: summary }] };
});
server.tool("get_blueprint", "Get full details of a specific Blueprint: variables, interfaces, and all graphs with nodes and connections. Also supports level blueprints from .umap files (e.g. 'MAP_Ward').", {
name: z.string().describe("Blueprint name or package path (e.g. 'BP_Patient_Base', 'MAP_Ward')"),
}, async ({ name }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await ueGet("/api/blueprint", { name });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
return { content: [{ type: "text", text: JSON.stringify(data) }] };
});
server.tool("get_blueprint_graph", "Get a specific named graph from a Blueprint (e.g. 'EventGraph', a function name). Graph names are URL-encoded automatically.", {
name: z.string().describe("Blueprint name or package path"),
graph: z.string().describe("Graph name (e.g. 'EventGraph')"),
}, async ({ name, graph }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
// ueGet uses URL.searchParams.set which handles encoding via encodeURIComponent (#8)
const data = await ueGet("/api/graph", { name, graph });
if (data.error) {
let msg = `Error: ${data.error}`;
if (data.availableGraphs)
msg += `\nAvailable: ${data.availableGraphs.join(", ")}`;
return { content: [{ type: "text", text: msg }] };
}
return { content: [{ type: "text", text: JSON.stringify(data) }] };
});
server.tool("search_blueprints", "Search across Blueprints for nodes matching a query (function calls, events, variables). Loads BPs on demand so use 'path' filter to scope large searches.", {
query: z.string().describe("Search term to match against node titles, function names, event names, variable names"),
path: z.string().optional().describe("Filter to Blueprints whose path contains this substring (e.g. '/Game/Blueprints/Patients/')"),
maxResults: z.number().optional().default(50).describe("Maximum results to return"),
}, async ({ query, path: pathFilter, maxResults }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await ueGet("/api/search", {
query,
path: pathFilter || "",
maxResults: String(maxResults),
});
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = data.results.map((r) => {
const levelTag = r.isLevelBlueprint ? " Level" : "";
return `[${r.blueprint}${levelTag}] ${r.graph} > ${r.nodeTitle}` +
(r.functionName ? ` fn:${r.functionName}` : "") +
(r.eventName ? ` event:${r.eventName}` : "") +
(r.variableName ? ` var:${r.variableName}` : "");
});
const summary = `Found ${data.resultCount} results for "${query}":\n\n${lines.join("\n")}`;
return { content: [{ type: "text", text: summary }] };
});
server.tool("get_blueprint_summary", "Get a concise human-readable summary of a Blueprint: variables with types, graphs with node counts, events, and function calls. Returns ~1-2K chars instead of 300K+ raw JSON. Use this first to understand a Blueprint before diving into specific graphs.", {
name: z.string().describe("Blueprint name or package path (e.g. 'BPC_3LeadECG')"),
}, async ({ name }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await ueGet("/api/blueprint", { name });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
return { content: [{ type: "text", text: summarizeBlueprint(data) }] };
});
server.tool("describe_graph", "Get a pseudo-code description of a specific Blueprint graph by walking execution pin chains. Shows the control flow as readable pseudo-code (IF/CALL/SET/SEQUENCE etc) with data flow annotations showing where each node gets its inputs. Use after get_blueprint_summary to understand a specific graph's logic. Graph names are URL-encoded automatically.", {
name: z.string().describe("Blueprint name or package path"),
graph: z.string().describe("Graph name (e.g. 'EventGraph', 'Set Connection Progress')"),
}, async ({ name, graph }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
// ueGet uses URL.searchParams.set which handles encoding via encodeURIComponent (#8)
const data = await ueGet("/api/graph", { name, graph });
if (data.error) {
let msg = `Error: ${data.error}`;
if (data.availableGraphs)
msg += `\nAvailable: ${data.availableGraphs.join(", ")}`;
return { content: [{ type: "text", text: msg }] };
}
return { content: [{ type: "text", text: describeGraph(data) }] };
});
server.tool("find_asset_references", "Find all Blueprints (and other assets) that reference a given asset path. Equivalent to the editor's Reference Viewer. Use this to check dependencies before deleting assets or to map out which Blueprints use a specific struct, function library, or enum.", {
assetPath: z.string().describe("Full asset path, e.g. '/Game/Blueprints/WebUI/S_Vitals'"),
}, async ({ assetPath }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await ueGet("/api/references", { assetPath });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`References to: ${data.assetPath}`);
lines.push(`Total referencers: ${data.totalReferencers}`);
if (data.blueprintReferencerCount > 0) {
lines.push(`\nBlueprint referencers (${data.blueprintReferencerCount}):`);
for (const ref of data.blueprintReferencers) {
lines.push(` ${ref}`);
}
}
if (data.otherReferencerCount > 0) {
lines.push(`\nOther referencers (${data.otherReferencerCount}):`);
for (const ref of data.otherReferencers) {
lines.push(` ${ref}`);
}
}
if (data.totalReferencers === 0) {
lines.push("\nNo referencers found. Asset is safe to delete.");
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("search_by_type", "Find all usages of a specific type across Blueprints: variables, function/event parameters, Break/Make struct nodes. More granular than find_asset_references.", {
typeName: z.string().describe("Type name to search for (e.g. 'FVitals', 'S_Vitals', 'ELungSound')"),
filter: z.string().optional().describe("Optional path filter to scope the search (e.g. '/Game/Blueprints/')"),
}, async ({ typeName, filter }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const params = { typeName };
if (filter)
params.filter = filter;
const data = await ueGet("/api/search-by-type", params);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
// C++ returns a flat `results` array with a `usage` field on each entry.
// Categorize by usage type for readable output.
const results = data.results || [];
const variables = results.filter((r) => r.usage === "variable");
const funcParams = results.filter((r) => r.usage === "functionParameter");
const eventParams = results.filter((r) => r.usage === "eventParameter");
const breakStructs = results.filter((r) => r.usage === "breakStruct");
const makeStructs = results.filter((r) => r.usage === "makeStruct");
const pinConns = results.filter((r) => r.usage === "pinConnection");
const tag = (r) => r.isLevelBlueprint ? " Level" : "";
const lines = [];
lines.push(`Usages of type "${typeName}" (${data.resultCount} result(s)):`);
if (variables.length) {
lines.push(`\nVariables (${variables.length}):`);
for (const v of variables) {
lines.push(` [${v.blueprint}${tag(v)}] ${v.location}: ${v.currentType}${v.currentSubtype ? `<${v.currentSubtype}>` : ""}`);
}
}
if (funcParams.length) {
lines.push(`\nFunction Parameters (${funcParams.length}):`);
for (const p of funcParams) {
lines.push(` [${p.blueprint}${tag(p)}] ${p.location}: ${p.currentType}${p.currentSubtype ? `<${p.currentSubtype}>` : ""}`);
}
}
if (eventParams.length) {
lines.push(`\nEvent Parameters (${eventParams.length}):`);
for (const p of eventParams) {
lines.push(` [${p.blueprint}${tag(p)}] ${p.location}: ${p.currentType}${p.currentSubtype ? `<${p.currentSubtype}>` : ""}`);
}
}
if (breakStructs.length) {
lines.push(`\nBreak Struct Nodes (${breakStructs.length}):`);
for (const n of breakStructs) {
lines.push(` [${n.blueprint}${tag(n)}] ${n.location} (${n.structType})`);
}
}
if (makeStructs.length) {
lines.push(`\nMake Struct Nodes (${makeStructs.length}):`);
for (const n of makeStructs) {
lines.push(` [${n.blueprint}${tag(n)}] ${n.location} (${n.structType})`);
}
}
if (pinConns.length) {
lines.push(`\nPin Connections (${pinConns.length}):`);
for (const p of pinConns) {
lines.push(` [${p.blueprint}${tag(p)}] ${p.graph} > ${p.location} (${p.connectionCount} connection(s))`);
}
}
if (results.length === 0) {
lines.push(`\nNo usages found.`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=read.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerSnapshotTools(server: McpServer): void;

View File

@@ -1,294 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost } from "../ue-bridge.js";
import { TYPE_NAME_DOCS, flagType } from "../helpers.js";
export function registerSnapshotTools(server) {
server.tool("snapshot_graph", "Create a backup snapshot of a Blueprint graph's state (all nodes, pins, and connections). Use BEFORE any destructive operation (C++ rebuild, change_struct_node_type, bulk edits). Returns a snapshot ID for later use with diff_graph or restore_graph. Snapshots are stored server-side and persist to disk.", {
blueprint: z.string().describe("Blueprint name or package path"),
graph: z.string().optional().describe("Specific graph name. If omitted, snapshots ALL graphs in the Blueprint"),
}, async (params) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: `Error: ${err}` }] };
const data = await uePost("/api/snapshot-graph", {
blueprint: params.blueprint,
graph: params.graph,
});
if (data.error) {
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
}
const lines = [];
lines.push(`Snapshot created: ${data.snapshotId}`);
lines.push(`Blueprint: ${data.blueprint}`);
if (data.graphs) {
const graphNames = data.graphs.map((g) => `${g.name} (${g.nodeCount} nodes, ${g.connectionCount} connections)`);
lines.push(`Graphs: ${graphNames.join(", ")}`);
}
lines.push(`Total connections captured: ${data.totalConnections}`);
lines.push(``);
lines.push(`**Next steps:**`);
lines.push(`1. Make your changes (C++ rebuild, change_struct_node_type, etc.)`);
lines.push(`2. Run **diff_graph** to see what changed`);
lines.push(`3. Run **restore_graph** to reconnect any severed pins`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("diff_graph", "Compare current Blueprint graph state against a snapshot. Shows severed connections, new connections, type changes, and missing nodes. Use AFTER a potentially destructive operation to assess damage before restoring.", {
blueprint: z.string().describe("Blueprint name or package path"),
snapshotId: z.string().describe("Snapshot ID from snapshot_graph"),
graph: z.string().optional().describe("Specific graph to diff. If omitted, diffs all graphs in the snapshot"),
}, async (params) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: `Error: ${err}` }] };
const data = await uePost("/api/diff-graph", {
blueprint: params.blueprint,
snapshotId: params.snapshotId,
graph: params.graph,
});
if (data.error) {
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
}
const lines = [];
lines.push(`# Diff: ${data.blueprint} vs ${data.snapshotId}`);
// Per-graph diffs
if (data.graphDiffs) {
for (const gd of data.graphDiffs) {
lines.push(``);
lines.push(`## ${gd.graphName}`);
if (gd.severedConnections?.length) {
lines.push(` Severed connections (${gd.severedConnections.length}):`);
for (const sc of gd.severedConnections) {
lines.push(` ${sc.sourceNodeTitle}.${sc.sourcePinName} was -> ${sc.targetNodeTitle}.${sc.targetPinName}`);
}
}
if (gd.newConnections?.length) {
lines.push(` New connections (${gd.newConnections.length}):`);
for (const nc of gd.newConnections) {
lines.push(` ${nc.sourceNodeTitle}.${nc.sourcePinName} -> ${nc.targetNodeTitle}.${nc.targetPinName}`);
}
}
if (gd.typeChanges?.length) {
lines.push(` Type changes (${gd.typeChanges.length}):`);
for (const tc of gd.typeChanges) {
lines.push(` Node ${tc.nodeId} (${tc.nodeTitle}): ${tc.oldType} -> ${tc.newType}`);
}
}
if (gd.missingNodes?.length) {
lines.push(` Missing nodes (${gd.missingNodes.length}):`);
for (const mn of gd.missingNodes) {
lines.push(` ${mn.nodeId} (${mn.nodeTitle}) - was ${mn.nodeClass}`);
}
}
if (!gd.severedConnections?.length && !gd.newConnections?.length && !gd.typeChanges?.length && !gd.missingNodes?.length) {
lines.push(` No changes detected.`);
}
}
}
// Summary
lines.push(``);
if (data.summary) {
lines.push(`Summary: ${data.summary.severed} severed, ${data.summary.new} new, ${data.summary.typeChanges} type changes, ${data.summary.missingNodes} missing nodes`);
}
// Next steps based on what was found
lines.push(``);
lines.push(`**Next steps:**`);
if (data.summary?.typeChanges > 0) {
lines.push(`1. Fix type changes first with **change_struct_node_type**`);
lines.push(`2. Then run **restore_graph** to reconnect severed pins`);
}
else if (data.summary?.severed > 0) {
lines.push(`1. Run **restore_graph** to reconnect severed pins`);
}
else {
lines.push(`1. No action needed — graph is intact`);
}
lines.push(`3. Run **validate_blueprint** to verify clean compilation`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("restore_graph", "Reconnect severed pin connections from a snapshot. Use after diff_graph shows damage. Can restore an entire graph, a single node (nodeId), or use an explicit pin map. For Break/Make struct nodes that lost connections after change_struct_node_type or C++ rebuild, this bulk-reconnects all pins in one call instead of individual connect_pins calls.", {
blueprint: z.string().describe("Blueprint name or package path"),
snapshotId: z.string().describe("Snapshot ID from snapshot_graph"),
graph: z.string().optional().describe("Specific graph to restore. If omitted, restores all graphs"),
nodeId: z.string().optional().describe("Scope restore to a single node (e.g. a Break struct node). Useful after change_struct_node_type"),
pinMap: z.record(z.string(), z.object({
targetNodeId: z.string(),
targetPinName: z.string(),
})).optional().describe("Explicit pin mapping override: {outputPinName: {targetNodeId, targetPinName}}. Use when no snapshot exists or snapshot is stale"),
dryRun: z.boolean().optional().describe("Preview reconnections without making changes"),
}, async (params) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: `Error: ${err}` }] };
const data = await uePost("/api/restore-graph", {
blueprint: params.blueprint,
snapshotId: params.snapshotId,
graph: params.graph,
nodeId: params.nodeId,
pinMap: params.pinMap,
dryRun: params.dryRun ?? false,
});
if (data.error) {
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
}
const lines = [];
if (params.dryRun) {
lines.push(`[DRY RUN - no changes will be made]\n`);
}
lines.push(`Restore: ${data.blueprint} from ${data.snapshotId}`);
if (params.nodeId) {
lines.push(`Scoped to node: ${params.nodeId}`);
}
lines.push(``);
lines.push(`Reconnected: ${data.reconnected}/${data.reconnected + data.failed}`);
if (data.failed > 0) {
lines.push(`Failed: ${data.failed}`);
}
if (data.details?.length) {
lines.push(``);
lines.push(`Details:`);
for (const d of data.details) {
const status = d.result === "ok" ? "OK" : "FAILED";
const reason = d.reason ? ` (${d.reason})` : "";
lines.push(` ${status}: ${d.sourcePinName} -> ${d.targetNodeTitle}.${d.targetPinName}${reason}`);
}
}
if (!params.dryRun && data.saved !== undefined) {
lines.push(``);
lines.push(`Saved: ${data.saved}`);
}
lines.push(``);
lines.push(`**Next steps:**`);
if (data.failed > 0) {
lines.push(`1. Fix ${data.failed} failed reconnection(s) manually with **connect_pins**`);
}
if (params.dryRun) {
lines.push(`1. Re-run **restore_graph** without dryRun to apply changes`);
}
lines.push(`2. Run **validate_blueprint** to verify clean compilation`);
lines.push(`3. Run **find_disconnected_pins** to verify no pins were missed`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("find_disconnected_pins", "Scan Blueprint(s) for pins that should be connected but aren't. Detects Break/Make struct nodes with broken types (HIGH confidence) or zero connections (MEDIUM confidence). Use after C++ rebuilds, change_struct_node_type, or refresh_all_nodes. Catches silent data flow breaks that validate_blueprint misses. Provide at least one of: blueprint, filter, or snapshotId.", {
blueprint: z.string().optional().describe("Blueprint name or path. If omitted, scans multiple BPs using filter"),
filter: z.string().optional().describe("Path filter when scanning multiple BPs (e.g. '/Game/Blueprints/Patients/'). Ignored when blueprint is specified"),
snapshotId: z.string().optional().describe("Compare against snapshot for definite break detection. Without this, uses heuristics only"),
sensitivity: z.enum(["high", "medium", "all"]).optional().describe("Detection sensitivity: 'high' = only broken types, 'medium' (default) = broken types + zero-connection Break nodes, 'all' = include partially connected Break nodes"),
}, async (params) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: `Error: ${err}` }] };
const data = await uePost("/api/find-disconnected-pins", {
blueprint: params.blueprint,
filter: params.filter,
snapshotId: params.snapshotId,
sensitivity: params.sensitivity ?? "medium",
});
if (data.error) {
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
}
const lines = [];
if (!data.results || data.results.length === 0) {
lines.push(`No disconnected pins found.`);
if (data.summary) {
lines.push(`Scanned ${data.summary.blueprintsScanned} Blueprint(s) — all Break/Make struct nodes have valid types and connected outputs.`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
}
// Group by blueprint
const byBP = {};
for (const r of data.results) {
const bp = r.blueprint || params.blueprint || "unknown";
if (!byBP[bp])
byBP[bp] = [];
byBP[bp].push(r);
}
lines.push(`Disconnected pins found:\n`);
for (const [bp, results] of Object.entries(byBP)) {
lines.push(`## ${bp}`);
for (const r of results) {
const conf = r.confidence === "high" ? "HIGH" : r.confidence === "medium" ? "MEDIUM" : "LOW";
const icon = r.confidence === "high" ? "\u26A0" : r.confidence === "medium" ? "\u26A1" : "\u2139";
lines.push(` ${icon} ${conf}${r.nodeTitle || "BreakStruct"} (${r.nodeId}) in ${r.graph}`);
lines.push(` Type: ${flagType(r.structType || "")}`);
lines.push(` Reason: ${r.reason}`);
if (r.pins?.length) {
for (const p of r.pins) {
const was = p.wasConnectedTo ? ` (was -> ${p.wasConnectedTo})` : "";
lines.push(` ${p.name}: ${p.type} — disconnected${was}`);
}
}
}
lines.push(``);
}
if (data.summary) {
lines.push(`Summary: ${data.summary.high} HIGH, ${data.summary.medium} MEDIUM across ${data.summary.blueprintsScanned} Blueprint(s)`);
}
lines.push(``);
lines.push(`**Next steps:**`);
if (data.summary?.high > 0) {
lines.push(`1. Fix HIGH-confidence issues: use **change_struct_node_type** to restore struct types`);
}
lines.push(`2. Use **restore_graph** (if snapshot exists) to bulk-reconnect severed pins`);
lines.push(`3. Or use **connect_pins** for individual reconnections`);
lines.push(`4. Run **validate_blueprint** to verify compilation`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("analyze_rebuild_impact", "Predict which Blueprints will be affected by a C++ module rebuild. Scans for Break/Make struct nodes, variables, and function parameters that reference USTRUCTs/UENUMs defined in the specified module. Use BEFORE rebuilding to know what to snapshot. " + TYPE_NAME_DOCS, {
moduleName: z.string().describe("C++ module name (e.g. 'MyGame'). Finds all Blueprints using types from this module"),
structNames: z.array(z.string()).optional().describe("Specific struct/enum names to check (e.g. ['FVitals', 'FSkinState']). If omitted, checks ALL types from the module"),
}, async (params) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: `Error: ${err}` }] };
const data = await uePost("/api/analyze-rebuild-impact", {
moduleName: params.moduleName,
structNames: params.structNames,
});
if (data.error) {
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
}
const lines = [];
lines.push(`# Rebuild Impact Analysis: ${data.moduleName}`);
lines.push(``);
if (data.typesFound?.length) {
lines.push(`Types in module (${data.typesFound.length}): ${data.typesFound.join(", ")}`);
lines.push(``);
}
if (!data.affectedBlueprints?.length) {
lines.push(`No Blueprints reference types from this module.`);
return { content: [{ type: "text", text: lines.join("\n") }] };
}
lines.push(`Affected Blueprints (${data.affectedBlueprints.length}):\n`);
for (const bp of data.affectedBlueprints) {
const risk = bp.risk || "UNKNOWN";
lines.push(` **${bp.name}** (${risk} risk)`);
if (bp.breakNodes > 0)
lines.push(` Break nodes: ${bp.breakNodes}${bp.breakNodeTypes ? ` (${bp.breakNodeTypes.join(", ")})` : ""}`);
if (bp.makeNodes > 0)
lines.push(` Make nodes: ${bp.makeNodes}${bp.makeNodeTypes ? ` (${bp.makeNodeTypes.join(", ")})` : ""}`);
if (bp.variables > 0)
lines.push(` Variables: ${bp.variables}`);
if (bp.functionParams > 0)
lines.push(` Function params: ${bp.functionParams}`);
if (bp.connectionsAtRisk > 0)
lines.push(` Connections at risk: ~${bp.connectionsAtRisk}`);
lines.push(``);
}
if (data.summary) {
lines.push(`Total: ${data.summary.totalBlueprints} Blueprints, ${data.summary.totalBreakMakeNodes} Break/Make nodes, ~${data.summary.totalConnectionsAtRisk} connections at risk`);
}
lines.push(``);
lines.push(`**Next steps:**`);
lines.push(`1. Run **snapshot_graph** on each HIGH-risk Blueprint BEFORE rebuilding:`);
const highRisk = (data.affectedBlueprints || []).filter((bp) => bp.risk === "HIGH");
for (const bp of highRisk.slice(0, 5)) {
lines.push(` snapshot_graph(blueprint="${bp.name}")`);
}
if (highRisk.length > 5) {
lines.push(` ... and ${highRisk.length - 5} more`);
}
lines.push(`2. After rebuild, run **find_disconnected_pins** to assess damage`);
lines.push(`3. Use **restore_graph** on each Blueprint to reconnect severed pins`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=snapshot.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerUserTypeTools(server: McpServer): void;

View File

@@ -1,94 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost } from "../ue-bridge.js";
import { TYPE_NAME_DOCS } from "../helpers.js";
export function registerUserTypeTools(server) {
server.tool("create_struct", `Create a new UserDefinedStruct asset. Optionally provide initial properties.\n\nType names for properties:\n${TYPE_NAME_DOCS}`, {
assetPath: z.string().describe("Full asset path (e.g. '/Game/DataTypes/S_MyStruct')"),
properties: z.array(z.object({
name: z.string().describe("Property name"),
type: z.string().describe("Property type (e.g. 'bool', 'int', 'float', 'string', 'FVector', 'FRotator')"),
})).optional().describe("Initial properties to add"),
}, async ({ assetPath, properties }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { assetPath };
if (properties)
body.properties = properties;
const data = await uePost("/api/create-struct", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Struct created successfully.`);
lines.push(`Asset: ${data.assetPath}`);
lines.push(`Properties added: ${data.propertiesAdded}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(``);
lines.push(`Next steps:`);
lines.push(` add_struct_property — add more properties`);
lines.push(` search_by_type — find usages of this struct`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("create_enum", "Create a new UserDefinedEnum asset with the given values.", {
assetPath: z.string().describe("Full asset path (e.g. '/Game/DataTypes/E_MyEnum')"),
values: z.array(z.string()).describe("Enum value display names (e.g. ['Low', 'Medium', 'High'])"),
}, async ({ assetPath, values }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/create-enum", { assetPath, values });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Enum created successfully.`);
lines.push(`Asset: ${data.assetPath}`);
lines.push(`Values: ${data.valueCount}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("add_struct_property", `Add a property to an existing UserDefinedStruct.\n\nType names:\n${TYPE_NAME_DOCS}`, {
assetPath: z.string().describe("Struct asset path (e.g. '/Game/DataTypes/S_MyStruct')"),
name: z.string().describe("Property name"),
type: z.string().describe("Property type (e.g. 'bool', 'int', 'float', 'string', 'FVector')"),
}, async ({ assetPath, name, type }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/add-struct-property", { assetPath, name, type });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Property added successfully.`);
lines.push(`Struct: ${data.assetPath}`);
lines.push(`Property: ${data.propertyName}: ${data.propertyType}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("remove_struct_property", "Remove a property from an existing UserDefinedStruct.", {
assetPath: z.string().describe("Struct asset path (e.g. '/Game/DataTypes/S_MyStruct')"),
name: z.string().describe("Property name to remove"),
}, async ({ assetPath, name }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/remove-struct-property", { assetPath, name });
if (data.error) {
let msg = `Error: ${data.error}`;
if (data.availableProperties?.length) {
msg += `\nAvailable properties: ${data.availableProperties.join(", ")}`;
}
return { content: [{ type: "text", text: msg }] };
}
const lines = [];
lines.push(`Property removed successfully.`);
lines.push(`Struct: ${data.assetPath}`);
lines.push(`Removed: ${data.removedProperty}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=user-types.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerUtilityTools(server: McpServer): void;

View File

@@ -1,55 +0,0 @@
import { ensureUE, ueGet, uePost, isUEHealthy, gracefulShutdown, state } from "../ue-bridge.js";
export function registerUtilityTools(server) {
server.tool("server_status", "Check UE5 Blueprint server status. Starts the server if not running (blocks until ready).", {}, async () => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await ueGet("/api/health");
return {
content: [{
type: "text",
text: `UE5 Blueprint server is running (${data.mode ?? (state.editorMode ? "editor" : "commandlet")} mode).\nBlueprints indexed: ${data.blueprintCount}\nMaps indexed: ${data.mapCount ?? "?"}`,
}],
};
});
server.tool("rescan_assets", "Re-scan the UE5 asset registry and refresh the server's cached asset lists. Use this if newly created assets are not appearing in list_blueprints/list_materials, or if the server started before the editor finished loading assets.", {}, async () => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/rescan", {});
if (data.error) {
return { content: [{ type: "text", text: `Rescan failed: ${data.error}` }] };
}
const lines = [
"Asset registry rescanned.",
`Blueprints: ${data.blueprintCount}${data.delta?.blueprints ? ` (${data.delta.blueprints >= 0 ? "+" : ""}${data.delta.blueprints})` : ""}`,
`Maps: ${data.mapCount}${data.delta?.maps ? ` (${data.delta.maps >= 0 ? "+" : ""}${data.delta.maps})` : ""}`,
`Materials: ${data.materialCount}${data.delta?.materials ? ` (${data.delta.materials >= 0 ? "+" : ""}${data.delta.materials})` : ""}`,
`Material Instances: ${data.materialInstanceCount}${data.delta?.materialInstances ? ` (${data.delta.materialInstances >= 0 ? "+" : ""}${data.delta.materialInstances})` : ""}`,
`Material Functions: ${data.materialFunctionCount}${data.delta?.materialFunctions ? ` (${data.delta.materialFunctions >= 0 ? "+" : ""}${data.delta.materialFunctions})` : ""}`,
];
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("shutdown_server", "Shut down the UE5 Blueprint server to free memory (~2-4 GB). The server will auto-restart on the next blueprint tool call. Use this when done with blueprint analysis. Cannot shut down the editor — only the standalone commandlet.", {}, async () => {
if (state.editorMode) {
return {
content: [{
type: "text",
text: "Connected to UE5 editor \u2014 cannot shut down the editor's MCP server. Close the editor to stop serving.",
}],
};
}
if (!state.ueProcess && !state.startupPromise && !(await isUEHealthy())) {
return { content: [{ type: "text", text: "UE5 server is already stopped." }] };
}
await gracefulShutdown();
state.startupPromise = null;
return {
content: [{
type: "text",
text: "UE5 Blueprint server shut down. Memory freed. It will auto-restart on the next blueprint tool call.",
}],
};
});
}
//# sourceMappingURL=utility.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"utility.js","sourceRoot":"","sources":["../../src/tools/utility.ts"],"names":[],"mappings":"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"}

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerValidationTools(server: McpServer): void;

View File

@@ -1,166 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost } from "../ue-bridge.js";
export function registerValidationTools(server) {
server.tool("validate_blueprint", "Compile a Blueprint and report errors/warnings without saving. Captures both node-level compiler messages AND log-level messages (e.g. 'Can\\'t connect pins', 'Fixed up function'). Use after making changes to verify correctness.", {
blueprint: z.string().describe("Blueprint name or package path (e.g. 'BP_PatientManager')"),
}, async ({ blueprint }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/validate-blueprint", { blueprint });
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Validation: ${data.blueprint}`);
lines.push(`Status: ${data.status || (data.errors?.length ? "ERRORS" : "OK")}`);
lines.push(`Valid: ${data.isValid}`);
if (data.compileWarning) {
lines.push(`\n\u26A0 ${data.compileWarning}`);
}
if (data.errors?.length) {
lines.push(`\nErrors (${data.errorCount}):`);
for (const e of data.errors) {
if (typeof e === "string") {
lines.push(` \u2716 ${e}`);
}
else {
const src = e.source === "log" ? "[log] " : "";
const loc = e.graph ? `[${e.graph}] ` : "";
const node = e.nodeTitle ? `${e.nodeTitle}: ` : "";
lines.push(` \u2716 ${src}${loc}${node}${e.message}`);
}
}
}
if (data.warnings?.length) {
lines.push(`\nWarnings (${data.warningCount}):`);
for (const w of data.warnings) {
if (typeof w === "string") {
lines.push(` \u26A0 ${w}`);
}
else {
const src = w.source === "log" ? "[log] " : "";
const loc = w.graph ? `[${w.graph}] ` : "";
const node = w.nodeTitle ? `${w.nodeTitle}: ` : "";
lines.push(` \u26A0 ${src}${loc}${node}${w.message}`);
}
}
}
if (!data.errors?.length && !data.warnings?.length) {
lines.push(`\nNo errors or warnings. Blueprint compiles cleanly.`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("validate_all_blueprints", "Bulk-validate all Blueprints (or a filtered subset) by compiling each one and reporting errors. Use after reparenting, C++ changes, or any operation that could cause cascading breakage. Returns only failed Blueprints to keep output manageable. Sends progress notifications during validation.", {
filter: z.string().optional().describe("Optional path or name filter (e.g. '/Game/Blueprints/WebUI/' or 'HUD'). If omitted, validates ALL blueprints."),
batchSize: z.number().optional().describe("Number of blueprints to validate per batch (default 50). Smaller batches give more frequent progress updates."),
}, async ({ filter, batchSize: batchSizeParam }, extra) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const batchSize = batchSizeParam ?? 50;
// Step 1: Get total count (fast, no compilation)
const countBody = { countOnly: true };
if (filter)
countBody.filter = filter;
const countData = await uePost("/api/validate-all-blueprints", countBody);
if (countData.error)
return { content: [{ type: "text", text: `Error: ${countData.error}` }] };
const totalMatching = countData.totalMatching ?? 0;
if (totalMatching === 0) {
const lines = ["# Bulk Validation Results"];
if (filter)
lines.push(`Filter: ${filter}`);
lines.push(`No matching blueprints found.`);
return { content: [{ type: "text", text: lines.join("\n") }] };
}
// Extract progress token from MCP request metadata
const progressToken = extra._meta?.progressToken;
// Step 2: Iterate in batches
let totalChecked = 0;
let totalPassed = 0;
let totalFailed = 0;
let totalCrashed = 0;
const allFailed = [];
for (let offset = 0; offset < totalMatching; offset += batchSize) {
const body = { offset, limit: batchSize };
if (filter)
body.filter = filter;
const data = await uePost("/api/validate-all-blueprints", body);
if (data.error)
return { content: [{ type: "text", text: `Error at offset ${offset}: ${data.error}` }] };
totalChecked += data.totalChecked ?? 0;
totalPassed += data.totalPassed ?? 0;
totalFailed += data.totalFailed ?? 0;
totalCrashed += (data.totalCrashed ?? 0);
if (data.failed?.length) {
allFailed.push(...data.failed);
}
// Send MCP progress notification if client requested it
if (progressToken !== undefined) {
try {
await extra.sendNotification({
method: "notifications/progress",
params: {
progressToken,
progress: Math.min(offset + batchSize, totalMatching),
total: totalMatching,
message: `Validated ${totalChecked}/${totalMatching} blueprints (${totalFailed} failed)`,
},
});
}
catch {
// Progress notifications are best-effort per MCP spec
}
}
}
// Step 3: Format aggregated results
const lines = [];
lines.push(`# Bulk Validation Results`);
if (filter)
lines.push(`Filter: ${filter}`);
lines.push(`Checked: ${totalChecked}`);
lines.push(`Passed: ${totalPassed}`);
lines.push(`Failed: ${totalFailed}`);
if (totalCrashed)
lines.push(`Crashed: ${totalCrashed}`);
if (allFailed.length) {
lines.push(`\n## Failed Blueprints\n`);
for (const bp of allFailed) {
lines.push(`### ${bp.blueprint} (${bp.path || ""})`);
lines.push(`Status: ${bp.status} | Errors: ${bp.errorCount} | Warnings: ${bp.warningCount}`);
if (bp.errors?.length) {
for (const e of bp.errors) {
if (typeof e === "string") {
lines.push(` \u2716 ${e}`);
}
else {
const src = e.source === "log" ? "[log] " : "";
const loc = e.graph ? `[${e.graph}] ` : "";
const node = e.nodeTitle ? `${e.nodeTitle}: ` : "";
lines.push(` \u2716 ${src}${loc}${node}${e.message}`);
}
}
}
if (bp.warnings?.length) {
for (const w of bp.warnings) {
if (typeof w === "string") {
lines.push(` \u26A0 ${w}`);
}
else {
const src = w.source === "log" ? "[log] " : "";
const loc = w.graph ? `[${w.graph}] ` : "";
const node = w.nodeTitle ? `${w.nodeTitle}: ` : "";
lines.push(` \u26A0 ${src}${loc}${node}${w.message}`);
}
}
}
lines.push("");
}
}
else {
lines.push(`\nAll blueprints compile cleanly!`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=validation.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
export declare function registerVariableTools(server: McpServer): void;

View File

@@ -1,175 +0,0 @@
import { z } from "zod";
import { ensureUE, uePost } from "../ue-bridge.js";
import { TYPE_NAME_DOCS, formatUpdatedState } from "../helpers.js";
export function registerVariableTools(server) {
server.tool("change_variable_type", `Change a Blueprint member variable's type. Supports structs, enums, and object reference types. Compiles and saves the Blueprint. Downstream Make/Break nodes using the old type will need manual fixing. ${TYPE_NAME_DOCS} For object references, either use colon syntax in newType (e.g. 'object:Actor') or pass typeCategory + class name in newType. Pass dryRun=true to preview changes without saving.`, {
blueprint: z.string().describe("Blueprint name or package path (e.g. 'BP_PatientManager')"),
variable: z.string().describe("Variable name (e.g. 'Vitals')"),
newType: z.string().describe("New type name: struct ('FVitals'), enum ('ELungSound'), or colon syntax for references ('object:Actor', 'class:Actor', 'softobject:Actor', 'softclass:Actor', 'interface:MyInterface')"),
typeCategory: z.enum(["struct", "enum", "object", "softobject", "class", "softclass", "interface"]).optional().describe("Type category. Optional — auto-detected from newType when using colon syntax or F/E prefix."),
dryRun: z.boolean().optional().describe("If true, preview changes without modifying the Blueprint"),
batch: z.array(z.object({
blueprint: z.string(),
variable: z.string(),
newType: z.string(),
typeCategory: z.enum(["struct", "enum", "object", "softobject", "class", "softclass", "interface"]).optional(),
})).optional().describe("Batch mode: array of {blueprint, variable, newType, typeCategory?} objects. When provided, single params are ignored."),
}, async ({ blueprint, variable, newType, typeCategory, dryRun, batch }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = batch
? { batch }
: { blueprint, variable, newType, typeCategory };
if (dryRun)
body.dryRun = true;
const data = await uePost("/api/change-variable-type", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
if (dryRun)
lines.push(`[DRY RUN - no changes saved]`);
if (data.results) {
// Batch response
lines.push(`Batch variable type change: ${data.results.length} operation(s)`);
for (const r of data.results) {
if (r.error) {
lines.push(` FAILED ${r.blueprint}.${r.variable}: ${r.error}`);
}
else {
lines.push(` OK ${r.blueprint}.${r.variable} \u2192 ${r.newType}`);
}
}
}
else {
lines.push(`Variable type changed successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Variable: ${data.variable}`);
lines.push(`New type: ${data.newType} (${data.typeCategory})`);
lines.push(`Saved: ${data.saved}`);
}
// Updated state (#11)
lines.push(...formatUpdatedState(data));
// Tool chaining hints (#12)
if (!dryRun) {
lines.push(`\nNext steps:`);
lines.push(` 1. Run refresh_all_nodes to update all nodes in the Blueprint`);
lines.push(` 2. Check Break/Make struct nodes \u2014 they may need change_struct_node_type`);
}
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("add_variable", `Add a new member variable to a Blueprint. Supports simple types (bool, int, float, string, name, text, byte), built-in structs (vector, rotator, transform), and custom struct/enum types. ${TYPE_NAME_DOCS}`, {
blueprint: z.string().describe("Blueprint name or package path"),
variableName: z.string().describe("Name for the new variable (e.g. 'Health', 'bIsActive')"),
variableType: z.string().describe("Type: 'bool', 'int', 'float', 'string', 'name', 'text', 'byte', 'vector', 'rotator', 'transform', or struct/enum name (e.g. 'FVitals', 'EMyEnum')"),
category: z.string().optional().describe("Variable category for organization in the Blueprint editor"),
isArray: z.boolean().optional().describe("Create as an array variable (default: false)"),
defaultValue: z.string().optional().describe("Default value as a string (e.g. 'true', '42', '0.5')"),
}, async ({ blueprint, variableName, variableType, category, isArray, defaultValue }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, variableName, variableType };
if (category)
body.category = category;
if (isArray !== undefined)
body.isArray = isArray;
if (defaultValue !== undefined)
body.defaultValue = defaultValue;
const data = await uePost("/api/add-variable", body);
if (data.error)
return { content: [{ type: "text", text: `Error: ${data.error}` }] };
const lines = [];
lines.push(`Variable added successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Variable: ${data.variableName}`);
lines.push(`Type: ${data.variableType}${data.isArray ? " (Array)" : ""}`);
if (data.category)
lines.push(`Category: ${data.category}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
lines.push(``);
lines.push(`Next steps:`);
lines.push(` add_node(blueprint="${blueprint}", graph="EventGraph", nodeType="VariableGet", variableName="${variableName}") — read the variable`);
lines.push(` add_node(blueprint="${blueprint}", graph="EventGraph", nodeType="VariableSet", variableName="${variableName}") — write the variable`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("remove_variable", "Remove a member variable from a Blueprint. Also cleans up any VariableGet/VariableSet nodes referencing it.", {
blueprint: z.string().describe("Blueprint name or package path"),
variableName: z.string().describe("Name of the variable to remove"),
}, async ({ blueprint, variableName }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const data = await uePost("/api/remove-variable", { blueprint, variableName });
if (data.error) {
let msg = `Error: ${data.error}`;
if (data.availableVariables?.length) {
msg += `\nAvailable variables: ${data.availableVariables.join(", ")}`;
}
return { content: [{ type: "text", text: msg }] };
}
const lines = [];
lines.push(`Variable removed successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Variable: ${data.variableName}`);
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
server.tool("set_variable_metadata", "Set variable properties beyond type: category, tooltip, replication, exposeOnSpawn, editability, isPrivate. Provide any combination of fields to update.", {
blueprint: z.string().describe("Blueprint name or package path"),
variable: z.string().describe("Variable name"),
category: z.string().optional().describe("Variable category for organization in the editor"),
tooltip: z.string().optional().describe("Tooltip text shown in the editor"),
replication: z.enum(["none", "replicated", "repNotify"]).optional().describe("Replication mode"),
exposeOnSpawn: z.boolean().optional().describe("Whether to expose the variable as a pin on SpawnActor"),
editability: z.enum(["editAnywhere", "editDefaultsOnly", "editInstanceOnly", "none"]).optional()
.describe("Edit visibility: editAnywhere (CDO + instances), editDefaultsOnly (CDO only), editInstanceOnly (instances only), none"),
isPrivate: z.boolean().optional().describe("Mark variable as private (only accessible within this Blueprint)"),
}, async ({ blueprint, variable, category, tooltip, replication, exposeOnSpawn, editability, isPrivate }) => {
const err = await ensureUE();
if (err)
return { content: [{ type: "text", text: err }] };
const body = { blueprint, variable };
if (category !== undefined)
body.category = category;
if (tooltip !== undefined)
body.tooltip = tooltip;
if (replication !== undefined)
body.replication = replication;
if (exposeOnSpawn !== undefined)
body.exposeOnSpawn = exposeOnSpawn;
if (editability !== undefined)
body.editability = editability;
if (isPrivate !== undefined)
body.isPrivate = isPrivate;
const data = await uePost("/api/set-variable-metadata", body);
if (data.error) {
let msg = `Error: ${data.error}`;
if (data.availableVariables?.length) {
msg += `\nAvailable variables: ${data.availableVariables.join(", ")}`;
}
return { content: [{ type: "text", text: msg }] };
}
const lines = [];
lines.push(`Variable metadata updated successfully.`);
lines.push(`Blueprint: ${data.blueprint}`);
lines.push(`Variable: ${data.variable}`);
if (data.changes?.length) {
lines.push(`\nChanges:`);
for (const c of data.changes) {
if (c.oldValue !== undefined) {
lines.push(` ${c.field}: ${c.oldValue} -> ${c.newValue}`);
}
else {
lines.push(` ${c.field}: ${c.newValue}`);
}
}
}
if (data.saved !== undefined)
lines.push(`Saved: ${data.saved}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
});
}
//# sourceMappingURL=variables.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,43 +0,0 @@
import { type ChildProcess } from "node:child_process";
export declare const UE_PROJECT_DIR: string;
export declare const UE_PORT: number;
export declare const UE_BASE_URL: string;
export declare const PLUGIN_MODULE_NAME = "BlueprintMCP";
export declare const PLUGIN_DLL_NAME = "UnrealEditor-BlueprintMCP.dll";
export declare const state: {
ueProcess: ChildProcess | null;
editorMode: boolean;
startupPromise: Promise<string | null> | null;
};
/**
* Read the EngineAssociation field from the .uproject file.
* Returns a short version string like "5.4" or "5.7", or null.
*/
export declare function readEngineVersion(): string | null;
export declare function findEditorCmd(): string | null;
/** Find the .uproject file in UE_PROJECT_DIR (auto-detect by globbing). */
export declare function findUProject(): string | null;
/**
* Ensure the .modules file in Binaries/Win64/ lists the BlueprintMCP module.
* A build of only the Game target will overwrite this file without the editor module,
* causing the commandlet to fail with "module could not be found".
*/
export declare function ensureModulesFile(): void;
/**
* Ask the UE5 server to shut down gracefully via /api/shutdown, then wait for
* the process to exit. Falls back to kill after a timeout.
*/
export declare function gracefulShutdown(): Promise<void>;
/** Returns the health payload if the server is reachable, or null. */
export declare function getUEHealth(): Promise<{
status: string;
mode: string;
blueprintCount: number;
mapCount: number;
} | null>;
export declare function isUEHealthy(): Promise<boolean>;
export declare function waitForHealthy(timeoutSeconds?: number): Promise<boolean>;
export declare function spawnAndWait(): Promise<string | null>;
export declare function ensureUE(): Promise<string | null>;
export declare function ueGet(endpoint: string, params?: Record<string, string>): Promise<any>;
export declare function uePost(endpoint: string, body: Record<string, any>): Promise<any>;

View File

@@ -1,237 +0,0 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { spawn } from "node:child_process";
// --- Constants ---
export const UE_PROJECT_DIR = process.env.UE_PROJECT_DIR || process.cwd();
export const UE_PORT = parseInt(process.env.UE_PORT || "9847", 10);
export const UE_BASE_URL = `http://localhost:${UE_PORT}`;
export const PLUGIN_MODULE_NAME = "BlueprintMCP";
export const PLUGIN_DLL_NAME = `UnrealEditor-${PLUGIN_MODULE_NAME}.dll`;
// --- Mutable state singleton ---
export const state = {
ueProcess: null,
editorMode: false,
startupPromise: null,
};
// --- UE5 Process Manager ---
/**
* Read the EngineAssociation field from the .uproject file.
* Returns a short version string like "5.4" or "5.7", or null.
*/
export function readEngineVersion() {
const uproject = findUProject();
if (!uproject)
return null;
try {
const data = JSON.parse(fs.readFileSync(uproject, "utf-8"));
if (typeof data.EngineAssociation === "string" && data.EngineAssociation) {
return data.EngineAssociation;
}
}
catch { /* ignore parse errors */ }
return null;
}
export function findEditorCmd() {
// Priority 1: explicit env var
if (process.env.UE_EDITOR_CMD && fs.existsSync(process.env.UE_EDITOR_CMD)) {
return process.env.UE_EDITOR_CMD;
}
// Priority 2: auto-detect from .uproject EngineAssociation
const engineVersion = readEngineVersion();
if (engineVersion) {
const versionCandidates = [
`C:\\Program Files\\Epic Games\\UE_${engineVersion}\\Engine\\Binaries\\Win64\\UnrealEditor-Cmd.exe`,
`C:\\Program Files (x86)\\Epic Games\\UE_${engineVersion}\\Engine\\Binaries\\Win64\\UnrealEditor-Cmd.exe`,
];
for (const p of versionCandidates) {
if (fs.existsSync(p)) {
console.error(`[BlueprintMCP] Auto-detected engine ${engineVersion} from .uproject`);
return p;
}
}
}
// Priority 3: scan for any installed UE5 version
const epicGamesDir = "C:\\Program Files\\Epic Games";
try {
const entries = fs.readdirSync(epicGamesDir);
for (const entry of entries.sort().reverse()) {
if (entry.startsWith("UE_")) {
const candidate = path.join(epicGamesDir, entry, "Engine", "Binaries", "Win64", "UnrealEditor-Cmd.exe");
if (fs.existsSync(candidate)) {
const detectedVersion = entry.replace("UE_", "");
console.error(`[BlueprintMCP] Found engine ${detectedVersion} (no match for .uproject version${engineVersion ? ` ${engineVersion}` : ""})`);
return candidate;
}
}
}
}
catch { /* directory may not exist */ }
return null;
}
/** Find the .uproject file in UE_PROJECT_DIR (auto-detect by globbing). */
export function findUProject() {
try {
const entries = fs.readdirSync(UE_PROJECT_DIR);
const uprojectFile = entries.find((e) => e.endsWith(".uproject"));
if (uprojectFile)
return path.join(UE_PROJECT_DIR, uprojectFile);
}
catch { /* ignore */ }
return null;
}
/**
* Ensure the .modules file in Binaries/Win64/ lists the BlueprintMCP module.
* A build of only the Game target will overwrite this file without the editor module,
* causing the commandlet to fail with "module could not be found".
*/
export function ensureModulesFile() {
const modulesPath = path.join(UE_PROJECT_DIR, "Binaries", "Win64", "UnrealEditor.modules");
try {
if (!fs.existsSync(modulesPath))
return; // nothing to fix
const data = JSON.parse(fs.readFileSync(modulesPath, "utf-8"));
if (data.Modules && !data.Modules[PLUGIN_MODULE_NAME]) {
const dllPath = path.join(UE_PROJECT_DIR, "Binaries", "Win64", PLUGIN_DLL_NAME);
if (!fs.existsSync(dllPath)) {
console.error(`[BlueprintMCP] Warning: ${PLUGIN_DLL_NAME} not found — editor module may not be compiled.`);
return;
}
data.Modules[PLUGIN_MODULE_NAME] = PLUGIN_DLL_NAME;
fs.writeFileSync(modulesPath, JSON.stringify(data, null, "\t") + "\n", "utf-8");
console.error(`[BlueprintMCP] Fixed .modules file — added ${PLUGIN_MODULE_NAME} entry.`);
}
}
catch (e) {
console.error("[BlueprintMCP] Warning: could not check/fix .modules file:", e);
}
}
/**
* Ask the UE5 server to shut down gracefully via /api/shutdown, then wait for
* the process to exit. Falls back to kill after a timeout.
*/
export async function gracefulShutdown() {
// Try graceful shutdown via HTTP
try {
await fetch(`${UE_BASE_URL}/api/shutdown`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
signal: AbortSignal.timeout(3000),
});
}
catch { /* server may already be gone */ }
// Wait up to 15 seconds for process to exit on its own
if (state.ueProcess) {
const proc = state.ueProcess;
const exited = await new Promise((resolve) => {
const timer = setTimeout(() => resolve(false), 15000);
proc.on("exit", () => { clearTimeout(timer); resolve(true); });
});
if (!exited && state.ueProcess) {
console.error("[BlueprintMCP] Graceful shutdown timed out, force-killing.");
state.ueProcess.kill();
}
state.ueProcess = null;
}
}
/** Returns the health payload if the server is reachable, or null. */
export async function getUEHealth() {
try {
const resp = await fetch(`${UE_BASE_URL}/api/health`, { signal: AbortSignal.timeout(2000) });
if (!resp.ok)
return null;
return await resp.json();
}
catch {
return null;
}
}
export async function isUEHealthy() {
return (await getUEHealth()) !== null;
}
export async function waitForHealthy(timeoutSeconds = 180) {
const deadline = Date.now() + timeoutSeconds * 1000;
while (Date.now() < deadline) {
if (await isUEHealthy())
return true;
// If the process died while we were waiting, bail out
if (!state.ueProcess)
return false;
await new Promise((r) => setTimeout(r, 2000));
}
return false;
}
export async function spawnAndWait() {
const editorCmd = findEditorCmd();
if (!editorCmd) {
return "Could not find UnrealEditor-Cmd.exe. Set UE_EDITOR_CMD environment variable.";
}
const uproject = findUProject();
if (!uproject) {
return `No .uproject file found in ${UE_PROJECT_DIR}`;
}
const logPath = path.join(UE_PROJECT_DIR, "Saved", "Logs", "BlueprintMCP_server.log");
console.error("[BlueprintMCP] Spawning UE5 commandlet...");
state.ueProcess = spawn(editorCmd, [
uproject,
"-run=BlueprintMCP",
`-port=${UE_PORT}`,
"-unattended",
"-nopause",
"-nullrhi",
`-LOG=${logPath}`,
], {
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
});
state.ueProcess.on("exit", (code) => {
console.error(`[BlueprintMCP] UE5 server exited with code ${code}`);
state.ueProcess = null;
});
state.ueProcess.stdout?.on("data", (data) => {
console.error(`[UE5:out] ${data.toString().trim()}`);
});
state.ueProcess.stderr?.on("data", (data) => {
console.error(`[UE5:err] ${data.toString().trim()}`);
});
console.error("[BlueprintMCP] Waiting for health check (up to 3 min)...");
const ok = await waitForHealthy(180);
if (ok) {
console.error("[BlueprintMCP] UE5 Blueprint server is ready.");
return null; // success
}
// Failed — clean up
if (state.ueProcess) {
state.ueProcess.kill();
state.ueProcess = null;
}
return "UE5 Blueprint server failed to start within 3 minutes. Check Saved/Logs/BlueprintMCP_server.log.";
}
export async function ensureUE() {
const health = await getUEHealth();
if (health) {
state.editorMode = health.mode === "editor";
return null;
}
return "UE5 editor is not running or BlueprintMCP plugin is not loaded. Start the editor first.";
}
// --- HTTP helpers ---
export async function ueGet(endpoint, params = {}) {
const url = new URL(endpoint, UE_BASE_URL);
for (const [k, v] of Object.entries(params)) {
if (v)
url.searchParams.set(k, v);
}
const resp = await fetch(url.toString(), { signal: AbortSignal.timeout(300000) }); // 5 min for search
return resp.json();
}
export async function uePost(endpoint, body) {
const resp = await fetch(`${UE_BASE_URL}${endpoint}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(300000), // 5 min for compile+save
});
return resp.json();
}
//# sourceMappingURL=ue-bridge.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
../esbuild/bin/esbuild

View File

@@ -1 +0,0 @@
../nanoid/bin/nanoid.cjs

View File

@@ -1 +0,0 @@
../which/bin/node-which

View File

@@ -1 +0,0 @@
../rollup/dist/bin/rollup

View File

@@ -1 +0,0 @@
../typescript/bin/tsc

View File

@@ -1 +0,0 @@
../typescript/bin/tsserver

View File

@@ -1 +0,0 @@
../vite/bin/vite.js

View File

@@ -1 +0,0 @@
../vitest/vitest.mjs

View File

@@ -1 +0,0 @@
../why-is-node-running/cli.js

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +0,0 @@
# esbuild
This is the Linux 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.

Binary file not shown.

View File

@@ -1,20 +0,0 @@
{
"name": "@esbuild/linux-x64",
"version": "0.27.3",
"description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
"url": "git+https://github.com/evanw/esbuild.git"
},
"license": "MIT",
"preferUnplugged": true,
"engines": {
"node": ">=18"
},
"os": [
"linux"
],
"cpu": [
"x64"
]
}

View File

@@ -1,357 +0,0 @@
# Node.js Adapter for Hono
This adapter `@hono/node-server` allows you to run your Hono application on Node.js.
Initially, Hono wasn't designed for Node.js, but with this adapter, you can now use Hono on Node.js.
It utilizes web standard APIs implemented in Node.js version 18 or higher.
## Benchmarks
Hono is 3.5 times faster than Express.
Express:
```txt
$ bombardier -d 10s --fasthttp http://localhost:3000/
Statistics Avg Stdev Max
Reqs/sec 16438.94 1603.39 19155.47
Latency 7.60ms 7.51ms 559.89ms
HTTP codes:
1xx - 0, 2xx - 164494, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 4.55MB/s
```
Hono + `@hono/node-server`:
```txt
$ bombardier -d 10s --fasthttp http://localhost:3000/
Statistics Avg Stdev Max
Reqs/sec 58296.56 5512.74 74403.56
Latency 2.14ms 1.46ms 190.92ms
HTTP codes:
1xx - 0, 2xx - 583059, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 12.56MB/s
```
## Requirements
It works on Node.js versions greater than 18.x. The specific required Node.js versions are as follows:
- 18.x => 18.14.1+
- 19.x => 19.7.0+
- 20.x => 20.0.0+
Essentially, you can simply use the latest version of each major release.
## Installation
You can install it from the npm registry with `npm` command:
```sh
npm install @hono/node-server
```
Or use `yarn`:
```sh
yarn add @hono/node-server
```
## Usage
Just import `@hono/node-server` at the top and write the code as usual.
The same code that runs on Cloudflare Workers, Deno, and Bun will work.
```ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hono meets Node.js'))
serve(app, (info) => {
console.log(`Listening on http://localhost:${info.port}`) // Listening on http://localhost:3000
})
```
For example, run it using `ts-node`. Then an HTTP server will be launched. The default port is `3000`.
```sh
ts-node ./index.ts
```
Open `http://localhost:3000` with your browser.
## Options
### `port`
```ts
serve({
fetch: app.fetch,
port: 8787, // Port number, default is 3000
})
```
### `createServer`
```ts
import { createServer } from 'node:https'
import fs from 'node:fs'
//...
serve({
fetch: app.fetch,
createServer: createServer,
serverOptions: {
key: fs.readFileSync('test/fixtures/keys/agent1-key.pem'),
cert: fs.readFileSync('test/fixtures/keys/agent1-cert.pem'),
},
})
```
### `overrideGlobalObjects`
The default value is `true`. The Node.js Adapter rewrites the global Request/Response and uses a lightweight Request/Response to improve performance. If you don't want to do that, set `false`.
```ts
serve({
fetch: app.fetch,
overrideGlobalObjects: false,
})
```
### `autoCleanupIncoming`
The default value is `true`. The Node.js Adapter automatically cleans up (explicitly call `destroy()` method) if application is not finished to consume the incoming request. If you don't want to do that, set `false`.
If the application accepts connections from arbitrary clients, this cleanup must be done otherwise incomplete requests from clients may cause the application to stop responding. If your application only accepts connections from trusted clients, such as in a reverse proxy environment and there is no process that returns a response without reading the body of the POST request all the way through, you can improve performance by setting it to `false`.
```ts
serve({
fetch: app.fetch,
autoCleanupIncoming: false,
})
```
## Middleware
Most built-in middleware also works with Node.js.
Read [the documentation](https://hono.dev/middleware/builtin/basic-auth) and use the Middleware of your liking.
```ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { prettyJSON } from 'hono/pretty-json'
const app = new Hono()
app.get('*', prettyJSON())
app.get('/', (c) => c.json({ 'Hono meets': 'Node.js' }))
serve(app)
```
## Serve Static Middleware
Use Serve Static Middleware that has been created for Node.js.
```ts
import { serveStatic } from '@hono/node-server/serve-static'
//...
app.use('/static/*', serveStatic({ root: './' }))
```
If using a relative path, `root` will be relative to the current working directory from which the app was started.
This can cause confusion when running your application locally.
Imagine your project structure is:
```
my-hono-project/
src/
index.ts
static/
index.html
```
Typically, you would run your app from the project's root directory (`my-hono-project`),
so you would need the following code to serve the `static` folder:
```ts
app.use('/static/*', serveStatic({ root: './static' }))
```
Notice that `root` here is not relative to `src/index.ts`, rather to `my-hono-project`.
### Options
#### `rewriteRequestPath`
If you want to serve files in `./.foojs` with the request path `/__foo/*`, you can write like the following.
```ts
app.use(
'/__foo/*',
serveStatic({
root: './.foojs/',
rewriteRequestPath: (path: string) => path.replace(/^\/__foo/, ''),
})
)
```
#### `onFound`
You can specify handling when the requested file is found with `onFound`.
```ts
app.use(
'/static/*',
serveStatic({
// ...
onFound: (_path, c) => {
c.header('Cache-Control', `public, immutable, max-age=31536000`)
},
})
)
```
#### `onNotFound`
The `onNotFound` is useful for debugging. You can write a handle for when a file is not found.
```ts
app.use(
'/static/*',
serveStatic({
root: './non-existent-dir',
onNotFound: (path, c) => {
console.log(`${path} is not found, request to ${c.req.path}`)
},
})
)
```
#### `precompressed`
The `precompressed` option checks if files with extensions like `.br` or `.gz` are available and serves them based on the `Accept-Encoding` header. It prioritizes Brotli, then Zstd, and Gzip. If none are available, it serves the original file.
```ts
app.use(
'/static/*',
serveStatic({
precompressed: true,
})
)
```
## ConnInfo Helper
You can use the [ConnInfo Helper](https://hono.dev/docs/helpers/conninfo) by importing `getConnInfo` from `@hono/node-server/conninfo`.
```ts
import { getConnInfo } from '@hono/node-server/conninfo'
app.get('/', (c) => {
const info = getConnInfo(c) // info is `ConnInfo`
return c.text(`Your remote address is ${info.remote.address}`)
})
```
## Accessing Node.js API
You can access the Node.js API from `c.env` in Node.js. For example, if you want to specify a type, you can write the following.
```ts
import { serve } from '@hono/node-server'
import type { HttpBindings } from '@hono/node-server'
import { Hono } from 'hono'
const app = new Hono<{ Bindings: HttpBindings }>()
app.get('/', (c) => {
return c.json({
remoteAddress: c.env.incoming.socket.remoteAddress,
})
})
serve(app)
```
The APIs that you can get from `c.env` are as follows.
```ts
type HttpBindings = {
incoming: IncomingMessage
outgoing: ServerResponse
}
type Http2Bindings = {
incoming: Http2ServerRequest
outgoing: Http2ServerResponse
}
```
## Direct response from Node.js API
You can directly respond to the client from the Node.js API.
In that case, the response from Hono should be ignored, so return `RESPONSE_ALREADY_SENT`.
> [!NOTE]
> This feature can be used when migrating existing Node.js applications to Hono, but we recommend using Hono's API for new applications.
```ts
import { serve } from '@hono/node-server'
import type { HttpBindings } from '@hono/node-server'
import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response'
import { Hono } from 'hono'
const app = new Hono<{ Bindings: HttpBindings }>()
app.get('/', (c) => {
const { outgoing } = c.env
outgoing.writeHead(200, { 'Content-Type': 'text/plain' })
outgoing.end('Hello World\n')
return RESPONSE_ALREADY_SENT
})
serve(app)
```
## Listen to a UNIX domain socket
You can configure the HTTP server to listen to a UNIX domain socket instead of a TCP port.
```ts
import { createAdaptorServer } from '@hono/node-server'
// ...
const socketPath ='/tmp/example.sock'
const server = createAdaptorServer(app)
server.listen(socketPath, () => {
console.log(`Listening on ${socketPath}`)
})
```
## Related projects
- Hono - <https://hono.dev>
- Hono GitHub repository - <https://github.com/honojs/hono>
## Author
Yusuke Wada <https://github.com/yusukebe>
## License
MIT

View File

@@ -1,10 +0,0 @@
import { GetConnInfo } from 'hono/conninfo';
/**
* ConnInfo Helper for Node.js
* @param c Context
* @returns ConnInfo
*/
declare const getConnInfo: GetConnInfo;
export { getConnInfo };

View File

@@ -1,10 +0,0 @@
import { GetConnInfo } from 'hono/conninfo';
/**
* ConnInfo Helper for Node.js
* @param c Context
* @returns ConnInfo
*/
declare const getConnInfo: GetConnInfo;
export { getConnInfo };

View File

@@ -1,42 +0,0 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/conninfo.ts
var conninfo_exports = {};
__export(conninfo_exports, {
getConnInfo: () => getConnInfo
});
module.exports = __toCommonJS(conninfo_exports);
var getConnInfo = (c) => {
const bindings = c.env.server ? c.env.server : c.env;
const address = bindings.incoming.socket.remoteAddress;
const port = bindings.incoming.socket.remotePort;
const family = bindings.incoming.socket.remoteFamily;
return {
remote: {
address,
port,
addressType: family === "IPv4" ? "IPv4" : family === "IPv6" ? "IPv6" : void 0
}
};
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
getConnInfo
});

View File

@@ -1,17 +0,0 @@
// src/conninfo.ts
var getConnInfo = (c) => {
const bindings = c.env.server ? c.env.server : c.env;
const address = bindings.incoming.socket.remoteAddress;
const port = bindings.incoming.socket.remotePort;
const family = bindings.incoming.socket.remoteFamily;
return {
remote: {
address,
port,
addressType: family === "IPv4" ? "IPv4" : family === "IPv6" ? "IPv6" : void 0
}
};
};
export {
getConnInfo
};

View File

@@ -1,2 +0,0 @@
export { }

View File

@@ -1,2 +0,0 @@
export { }

View File

@@ -1,29 +0,0 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// src/globals.ts
var import_node_crypto = __toESM(require("crypto"));
if (typeof global.crypto === "undefined") {
global.crypto = import_node_crypto.default;
}

View File

@@ -1,5 +0,0 @@
// src/globals.ts
import crypto from "crypto";
if (typeof global.crypto === "undefined") {
global.crypto = crypto;
}

View File

@@ -1,8 +0,0 @@
export { createAdaptorServer, serve } from './server.mjs';
export { getRequestListener } from './listener.mjs';
export { RequestError } from './request.mjs';
export { Http2Bindings, HttpBindings, ServerType } from './types.mjs';
import 'node:net';
import 'node:http';
import 'node:http2';
import 'node:https';

Some files were not shown because too many files have changed in this diff Show More