Files
unreal-mcp/Source/UnrealMCPSetup/Private/ClaudeCodeInitializer.cpp
T

488 lines
16 KiB
C++

#include "ClaudeCodeInitializer.h"
#include "UnrealMCPSetupLog.h"
#include "ClaudeCodeResultDialog.h"
#include "Dom/JsonObject.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformFileManager.h"
#include "HAL/PlatformProcess.h"
#include "Misc/App.h"
#include "Misc/FileHelper.h"
#include "Misc/MessageDialog.h"
#include "Misc/Paths.h"
#include "Misc/ScopedSlowTask.h"
#include "Interfaces/IPluginManager.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#define LOCTEXT_NAMESPACE "FClaudeCodeInitializer"
void FClaudeCodeInitializer::Run()
{
if (!IsCppProject())
{
UE_LOG(LogUnrealMCPSetup, Warning, TEXT("Init aborted: not a C++ project (no .Build.cs found)"));
FMessageDialog::Open(EAppMsgType::Ok,
LOCTEXT("NotCppError", "Claude Code ne peut être initialisé que dans un projet C++."));
return;
}
const EAppReturnType::Type Answer = FMessageDialog::Open(EAppMsgType::YesNo,
LOCTEXT("ConfirmInit", "Êtes-vous sûr de vouloir initialiser Claude Code pour ce projet ?"));
if (Answer != EAppReturnType::Yes)
{
UE_LOG(LogUnrealMCPSetup, Display, TEXT("Init cancelled by user"));
return;
}
TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("UnrealMCP"));
if (!Plugin.IsValid())
{
UE_LOG(LogUnrealMCPSetup, Error, TEXT("Plugin 'UnrealMCP' not found via IPluginManager"));
FMessageDialog::Open(EAppMsgType::Ok,
LOCTEXT("PluginNotFound", "Plugin UnrealMCP introuvable."));
return;
}
const FString PluginDir = Plugin->GetBaseDir();
const FString ProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
UE_LOG(LogUnrealMCPSetup, Display, TEXT("Starting Claude Code init — PluginDir: %s | ProjectDir: %s"), *PluginDir, *ProjectDir);
FInitResult Result;
{
FScopedSlowTask SlowTask(5.0f, LOCTEXT("Working", "Initialisation de Claude Code..."));
SlowTask.MakeDialog(false, false);
SlowTask.EnterProgressFrame(1.0f, LOCTEXT("StepCopy", "Vérification du serveur Python..."));
Result.Steps.Add(CopyPythonServer(PluginDir, ProjectDir));
SlowTask.EnterProgressFrame(1.0f, LOCTEXT("StepClaudeAssets", "Copie des commandes et skills Claude Code..."));
Result.Steps.Add(CopyClaudeAssets(PluginDir, ProjectDir));
SlowTask.EnterProgressFrame(1.0f, LOCTEXT("StepMcp", "Vérification de .mcp.json..."));
Result.Steps.Add(WriteMcpJson(ProjectDir));
SlowTask.EnterProgressFrame(1.0f, LOCTEXT("StepSettings", "Vérification des permissions MCP..."));
Result.Steps.Add(WriteClaudeLocalSettings(ProjectDir));
SlowTask.EnterProgressFrame(1.0f, LOCTEXT("StepUv", "Initialisation de l'environnement Python (uv sync)..."));
Result.Steps.Add(RunUvSync(ProjectDir));
}
Result.bOverallSuccess = true;
for (const FInitStep& Step : Result.Steps)
{
if (!Step.bSuccess)
{
Result.bOverallSuccess = false;
break;
}
}
if (Result.bOverallSuccess)
{
UE_LOG(LogUnrealMCPSetup, Display, TEXT("Claude Code init completed successfully"));
}
else
{
UE_LOG(LogUnrealMCPSetup, Error, TEXT("Claude Code init finished with errors — check Output Log for details"));
}
FClaudeCodeResultDialog::Show(Result);
}
bool FClaudeCodeInitializer::IsCppProject()
{
const FString BuildCs = FPaths::ProjectDir()
/ TEXT("Source")
/ FApp::GetProjectName()
/ (FString(FApp::GetProjectName()) + TEXT(".Build.cs"));
return FPaths::FileExists(BuildCs);
}
// ---------------------------------------------------------------------------
// Validation helpers
// ---------------------------------------------------------------------------
bool FClaudeCodeInitializer::IsMcpJsonValid(const FString& ProjectDir)
{
const FString FilePath = ProjectDir / TEXT(".mcp.json");
FString RawContent;
if (!FFileHelper::LoadFileToString(RawContent, *FilePath))
{
return false;
}
TSharedPtr<FJsonObject> Root;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(RawContent);
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
{
return false;
}
const TSharedPtr<FJsonObject>* McpServers;
if (!Root->TryGetObjectField(TEXT("mcpServers"), McpServers))
{
return false;
}
const TSharedPtr<FJsonObject>* UnrealMCP;
if (!(*McpServers)->TryGetObjectField(TEXT("unrealMCP"), UnrealMCP))
{
return false;
}
FString Command;
if (!(*UnrealMCP)->TryGetStringField(TEXT("command"), Command) || Command != TEXT("uv"))
{
return false;
}
const TArray<TSharedPtr<FJsonValue>>* Args;
if (!(*UnrealMCP)->TryGetArrayField(TEXT("args"), Args))
{
return false;
}
for (const TSharedPtr<FJsonValue>& Arg : *Args)
{
if (Arg->AsString() == TEXT("unreal_mcp_server.py"))
{
return true;
}
}
return false;
}
bool FClaudeCodeInitializer::IsMcpServerFolderValid(const FString& ProjectDir)
{
const FString EntryPoint = ProjectDir / TEXT(".claude/mcp-server/unreal_mcp_server.py");
return FPaths::FileExists(EntryPoint);
}
bool FClaudeCodeInitializer::IsClaudeLocalSettingsValid(const FString& ProjectDir)
{
const FString FilePath = ProjectDir / TEXT(".claude/settings.local.json");
FString RawContent;
if (!FFileHelper::LoadFileToString(RawContent, *FilePath))
{
return false;
}
TSharedPtr<FJsonObject> Root;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(RawContent);
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
{
return false;
}
const TSharedPtr<FJsonObject>* Permissions;
if (!Root->TryGetObjectField(TEXT("permissions"), Permissions))
{
return false;
}
const TArray<TSharedPtr<FJsonValue>>* Allow;
if (!(*Permissions)->TryGetArrayField(TEXT("allow"), Allow))
{
return false;
}
for (const TSharedPtr<FJsonValue>& Entry : *Allow)
{
if (Entry->AsString().StartsWith(TEXT("mcp__unrealMCP__")))
{
return true;
}
}
return false;
}
// ---------------------------------------------------------------------------
// Init steps — validate first, act only if needed
// ---------------------------------------------------------------------------
bool FClaudeCodeInitializer::CopyDirectoryFiltered(const FString& Src, const FString& Dst)
{
static const TArray<FString> SkippedDirs = { TEXT(".venv"), TEXT("__pycache__"), TEXT("unreal_mcp.egg-info") };
static const TArray<FString> SkippedExts = { TEXT(".log"), TEXT(".bck") };
IPlatformFile& PF = FPlatformFileManager::Get().GetPlatformFile();
PF.CreateDirectoryTree(*Dst);
bool bAllOk = true;
PF.IterateDirectory(*Src, [&](const TCHAR* Path, bool bIsDirectory) -> bool
{
const FString Name = FPaths::GetCleanFilename(Path);
const FString DstPath = Dst / Name;
if (bIsDirectory)
{
if (!SkippedDirs.Contains(Name))
{
bAllOk &= CopyDirectoryFiltered(Path, DstPath);
}
}
else
{
const FString Ext = FPaths::GetExtension(Name, /*bIncludeDot*/ true).ToLower();
if (!SkippedExts.Contains(Ext))
{
if (!PF.CopyFile(*DstPath, Path))
{
UE_LOG(LogUnrealMCPSetup, Warning, TEXT("[Copy] Failed to copy file: %s"), Path);
bAllOk = false;
}
}
}
return true;
});
return bAllOk;
}
FInitStep FClaudeCodeInitializer::CopyPythonServer(const FString& PluginDir, const FString& ProjectDir)
{
FInitStep Step;
Step.Name = LOCTEXT("StepNameCopy", "Serveur Python (.claude/mcp-server)");
if (IsMcpServerFolderValid(ProjectDir))
{
UE_LOG(LogUnrealMCPSetup, Display, TEXT("[PythonServer] Already valid — skipping copy"));
Step.bSuccess = true;
Step.Details = TEXT("Déjà présent et valide — aucune modification.");
return Step;
}
const FString Src = PluginDir / TEXT("PythonServer");
const FString Dst = ProjectDir / TEXT(".claude/mcp-server");
if (!FPaths::DirectoryExists(Src))
{
UE_LOG(LogUnrealMCPSetup, Error, TEXT("[PythonServer] Source folder not found: %s"), *Src);
Step.bSuccess = false;
Step.Details = FString::Printf(TEXT("Dossier source introuvable : %s"), *Src);
return Step;
}
UE_LOG(LogUnrealMCPSetup, Display, TEXT("[PythonServer] Copying %s → %s (skipping .venv, __pycache__, egg-info)"), *Src, *Dst);
if (CopyDirectoryFiltered(Src, Dst))
{
UE_LOG(LogUnrealMCPSetup, Display, TEXT("[PythonServer] Copy successful"));
Step.bSuccess = true;
Step.Details = FString::Printf(TEXT("Copié : %s → %s"), *Src, *Dst);
}
else
{
UE_LOG(LogUnrealMCPSetup, Error, TEXT("[PythonServer] Copy failed to %s"), *Dst);
Step.bSuccess = false;
Step.Details = FString::Printf(TEXT("Échec de la copie vers %s"), *Dst);
}
return Step;
}
FInitStep FClaudeCodeInitializer::CopyClaudeAssets(const FString& PluginDir, const FString& ProjectDir)
{
FInitStep Step;
Step.Name = LOCTEXT("StepNameClaudeAssets", "Commandes & Skills (.claude/commands, .claude/skills)");
const FString ClaudeSrc = PluginDir / TEXT(".claude");
const FString ClaudeDst = ProjectDir / TEXT(".claude");
// Check if both directories are already present
const bool bCommandsOk = FPaths::DirectoryExists(ClaudeDst / TEXT("commands"));
const bool bSkillsOk = FPaths::DirectoryExists(ClaudeDst / TEXT("skills"));
if (bCommandsOk && bSkillsOk)
{
UE_LOG(LogUnrealMCPSetup, Display, TEXT("[ClaudeAssets] commands/ and skills/ already present — skipping"));
Step.bSuccess = true;
Step.Details = TEXT("Déjà présents — aucune modification.");
return Step;
}
UE_LOG(LogUnrealMCPSetup, Display, TEXT("[ClaudeAssets] Copying commands/ and skills/ from %s → %s"), *ClaudeSrc, *ClaudeDst);
bool bAllOk = true;
for (const FString& SubDir : { FString(TEXT("commands")), FString(TEXT("skills")) })
{
const FString Src = ClaudeSrc / SubDir;
const FString Dst = ClaudeDst / SubDir;
if (FPaths::DirectoryExists(Src))
{
if (!CopyDirectoryFiltered(Src, Dst))
{
UE_LOG(LogUnrealMCPSetup, Error, TEXT("[ClaudeAssets] Failed to copy %s"), *Src);
bAllOk = false;
}
}
else
{
UE_LOG(LogUnrealMCPSetup, Warning, TEXT("[ClaudeAssets] Source not found, skipping: %s"), *Src);
}
}
Step.bSuccess = bAllOk;
Step.Details = bAllOk
? FString::Printf(TEXT("Copié : commands/ et skills/ → %s"), *ClaudeDst)
: FString::Printf(TEXT("Échec partiel — voir Output Log pour les détails"));
return Step;
}
FInitStep FClaudeCodeInitializer::WriteMcpJson(const FString& ProjectDir)
{
FInitStep Step;
Step.Name = LOCTEXT("StepNameMcp", ".mcp.json");
if (IsMcpJsonValid(ProjectDir))
{
UE_LOG(LogUnrealMCPSetup, Display, TEXT("[.mcp.json] Already valid — skipping write"));
Step.bSuccess = true;
Step.Details = TEXT("Déjà configuré et valide — aucune modification.");
return Step;
}
const FString FilePath = ProjectDir / TEXT(".mcp.json");
UE_LOG(LogUnrealMCPSetup, Display, TEXT("[.mcp.json] Writing %s"), *FilePath);
const FString Content =
TEXT("{\n")
TEXT(" \"mcpServers\": {\n")
TEXT(" \"unrealMCP\": {\n")
TEXT(" \"type\": \"stdio\",\n")
TEXT(" \"command\": \"uv\",\n")
TEXT(" \"args\": [\"--directory\", \".claude/mcp-server\", \"run\", \"unreal_mcp_server.py\"]\n")
TEXT(" }\n")
TEXT(" }\n")
TEXT("}\n");
if (FFileHelper::SaveStringToFile(Content, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
UE_LOG(LogUnrealMCPSetup, Display, TEXT("[.mcp.json] Written successfully"));
Step.bSuccess = true;
Step.Details = FString::Printf(TEXT("Créé : %s"), *FilePath);
}
else
{
UE_LOG(LogUnrealMCPSetup, Error, TEXT("[.mcp.json] Failed to write %s"), *FilePath);
Step.bSuccess = false;
Step.Details = FString::Printf(TEXT("Impossible d'écrire %s"), *FilePath);
}
return Step;
}
FInitStep FClaudeCodeInitializer::WriteClaudeLocalSettings(const FString& ProjectDir)
{
FInitStep Step;
Step.Name = LOCTEXT("StepNameSettings", "Permissions MCP (.claude/settings.local.json)");
if (IsClaudeLocalSettingsValid(ProjectDir))
{
UE_LOG(LogUnrealMCPSetup, Display, TEXT("[settings.local.json] Already valid — skipping write"));
Step.bSuccess = true;
Step.Details = TEXT("Déjà configuré et valide — aucune modification.");
return Step;
}
const FString ClaudeDir = ProjectDir / TEXT(".claude");
const FString FilePath = ClaudeDir / TEXT("settings.local.json");
UE_LOG(LogUnrealMCPSetup, Display, TEXT("[settings.local.json] Writing %s"), *FilePath);
IPlatformFile& PF = FPlatformFileManager::Get().GetPlatformFile();
if (!PF.DirectoryExists(*ClaudeDir))
{
PF.CreateDirectoryTree(*ClaudeDir);
}
const FString Content =
TEXT("{\n")
TEXT(" \"permissions\": {\n")
TEXT(" \"allow\": [\n")
TEXT(" \"mcp__unrealMCP__get_actors_in_level\",\n")
TEXT(" \"mcp__unrealMCP__find_actors_by_name\",\n")
TEXT(" \"mcp__unrealMCP__spawn_actor\",\n")
TEXT(" \"mcp__unrealMCP__delete_actor\",\n")
TEXT(" \"mcp__unrealMCP__set_actor_transform\",\n")
TEXT(" \"mcp__unrealMCP__get_actor_properties\",\n")
TEXT(" \"mcp__unrealMCP__set_actor_property\",\n")
TEXT(" \"mcp__unrealMCP__spawn_blueprint_actor\",\n")
TEXT(" \"mcp__unrealMCP__create_blueprint\",\n")
TEXT(" \"mcp__unrealMCP__add_component_to_blueprint\",\n")
TEXT(" \"mcp__unrealMCP__set_static_mesh_properties\",\n")
TEXT(" \"mcp__unrealMCP__set_component_property\",\n")
TEXT(" \"mcp__unrealMCP__set_physics_properties\",\n")
TEXT(" \"mcp__unrealMCP__set_blueprint_property\",\n")
TEXT(" \"mcp__unrealMCP__compile_blueprint\",\n")
TEXT(" \"mcp__unrealMCP__find_blueprint_nodes\",\n")
TEXT(" \"mcp__unrealMCP__add_blueprint_event_node\",\n")
TEXT(" \"mcp__unrealMCP__add_blueprint_input_action_node\",\n")
TEXT(" \"mcp__unrealMCP__add_blueprint_function_node\",\n")
TEXT(" \"mcp__unrealMCP__add_blueprint_variable\",\n")
TEXT(" \"mcp__unrealMCP__add_blueprint_self_reference\",\n")
TEXT(" \"mcp__unrealMCP__add_blueprint_get_self_component_reference\",\n")
TEXT(" \"mcp__unrealMCP__connect_blueprint_nodes\",\n")
TEXT(" \"mcp__unrealMCP__create_input_mapping\",\n")
TEXT(" \"mcp__unrealMCP__create_umg_widget_blueprint\",\n")
TEXT(" \"mcp__unrealMCP__add_text_block_to_widget\",\n")
TEXT(" \"mcp__unrealMCP__add_button_to_widget\",\n")
TEXT(" \"mcp__unrealMCP__bind_widget_event\",\n")
TEXT(" \"mcp__unrealMCP__set_text_block_binding\",\n")
TEXT(" \"mcp__unrealMCP__add_widget_to_viewport\"\n")
TEXT(" ]\n")
TEXT(" }\n")
TEXT("}\n");
if (FFileHelper::SaveStringToFile(Content, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
UE_LOG(LogUnrealMCPSetup, Display, TEXT("[settings.local.json] Written successfully"));
Step.bSuccess = true;
Step.Details = FString::Printf(TEXT("Créé : %s"), *FilePath);
}
else
{
UE_LOG(LogUnrealMCPSetup, Error, TEXT("[settings.local.json] Failed to write %s"), *FilePath);
Step.bSuccess = false;
Step.Details = FString::Printf(TEXT("Impossible d'écrire %s"), *FilePath);
}
return Step;
}
FInitStep FClaudeCodeInitializer::RunUvSync(const FString& ProjectDir)
{
FInitStep Step;
Step.Name = LOCTEXT("StepNameUv", "Environnement Python (uv sync)");
const FString McpServerDir = ProjectDir / TEXT(".claude/mcp-server");
// --no-install-project: install dependencies only, skip building the local package.
// Building it spawns a grandchild Python process that inherits invalid pipe handles
// from FPlatformProcess::ExecProcess on Windows (os error 6 / ERROR_INVALID_HANDLE).
// unreal_mcp_server.py is a plain script — it doesn't need to be installed as a package.
const FString Args = FString::Printf(TEXT("sync --directory \"%s\" --no-install-project"), *McpServerDir);
UE_LOG(LogUnrealMCPSetup, Display, TEXT("[uv sync] Running: uv %s"), *Args);
int32 ReturnCode = 0;
FString StdOut, StdErr;
const bool bLaunched = FPlatformProcess::ExecProcess(
TEXT("uv"), *Args, &ReturnCode, &StdOut, &StdErr);
if (!bLaunched)
{
UE_LOG(LogUnrealMCPSetup, Error, TEXT("[uv sync] 'uv' not found in PATH"));
Step.bSuccess = false;
Step.Details = TEXT("uv introuvable dans le PATH. Installez uv (https://docs.astral.sh/uv/) puis relancez.");
}
else if (ReturnCode == 0)
{
UE_LOG(LogUnrealMCPSetup, Display, TEXT("[uv sync] Completed successfully"));
Step.bSuccess = true;
Step.Details = TEXT("Environnement Python prêt.");
}
else
{
const FString ErrorMsg = StdErr.IsEmpty() ? StdOut : StdErr;
UE_LOG(LogUnrealMCPSetup, Error, TEXT("[uv sync] Failed (code %d): %s"), ReturnCode, *ErrorMsg);
Step.bSuccess = false;
Step.Details = ErrorMsg;
}
return Step;
}
#undef LOCTEXT_NAMESPACE