돌아가기
#Unreal Engine
#MCP
#Codex
#TypeScript
#C++

Unreal Editor와 MCP를 연결하는 Ampliare 개발기

Ampliare는 Unreal Editor와 MCP 클라이언트를 연결하기 위해 만든 에디터 플러그인이다. Codex 같은 MCP 클라이언트가 실행 중인 Unreal Editor의 월드, 액터, 에셋, Blueprint, Material Graph 상태를 읽고, 필요한 편집 작업을 요청할 수 있게 하는 중간 계층이다.

LLM은 텍스트 파일은 잘 읽고 고치지만, Unreal Engine 프로젝트의 핵심 작업 단위인 .uasset은 같은 방식으로 다루기 어렵다.

.uasset을 일반 텍스트 파일처럼 직접 수정하면 에셋 손상, 참조 관계 깨짐, 에디터 크래시가 생길 수 있다. 그래서 Ampliare는 파일을 직접 패치하는 대신 실행 중인 Unreal Editor 안에서 Unreal API를 호출하는 구조로 설계했다.

로딩 중...uasset problem

LLM의 텍스트 편집 방식과 Unreal Engine 에셋 변경 방식의 차이

왜 만들었는가

AI 에이전트가 Unreal 프로젝트를 도와주려면 현재 에디터가 어떤 상태인지 알아야 한다. 현재 열린 레벨이 무엇인지, 어떤 액터가 선택되어 있는지, 콘텐츠 브라우저의 에셋이 어떤 참조를 갖는지, Blueprint Graph의 노드와 핀이 어떻게 연결되어 있는지를 읽을 수 있어야 한다.

반대로 변경은 별도 경로가 필요했다. 파일을 직접 고치는 방식이 아니라, Unreal Editor가 제공하는 API와 패키지 저장 흐름을 지나야 한다. 그래서 요구사항을 다음처럼 잡았다.

  • 에디터 내부 상태를 AI가 읽기 쉬운 JSON으로 제공한다.
  • 에셋과 월드 변경은 Unreal Editor API를 통해서만 수행한다.
  • 변경 요청은 기본적으로 드라이런으로 처리한다.
  • 실제 변경에는 명시적인 실행 의도를 요구한다.
  • 외부 네트워크가 아니라 로컬 루프백 요청만 허용한다.

이 요구사항을 만족시키기 위해 Unreal 플러그인과 MCP 서버를 분리했다. Unreal 쪽은 에디터 내부 상태와 실제 편집 작업을 맡고, MCP 서버는 Codex가 사용할 도구 스키마와 요청 중계를 맡는다.

전체 구조

Ampliare는 크게 네 계층으로 나뉜다.

  1. MCP Client: Codex 같은 클라이언트가 MCP 도구를 호출한다.
  2. MCP Server: Node.js 서버가 MCP 요청을 HTTP 요청으로 변환한다.
  3. HTTP Bridge: Unreal Editor 안에서 실행되는 C++ 플러그인이 요청을 받는다.
  4. Unreal Editor API: GEditor, Asset Registry, Blueprint/Material Graph API를 통해 실제 상태를 읽거나 변경한다.

로딩 중...architecture flow

MCP 클라이언트, Node.js MCP 서버, Unreal HTTP 브리지, Unreal Editor API의 계층 흐름

구조를 파일 단위로 보면 다음과 같다.

Ampliare/
Ampliare.upluginEditor 모듈 메타데이터
Source/
Ampliare/
Private/
Ampliare.cpp모듈 시작과 라우트 등록
AmpliareBridgeCommon.h/.cppJSON, 루프백, 변경 가드
AmpliareReadRoutes.cpp읽기 엔드포인트
AmpliareMutationRoutes.cpp변경 엔드포인트
AmpliareGraphRoutes.cppBlueprint/Material Graph 엔드포인트
McpServer/
src/
index.tsMCP 서버 진입점
bridge.tsHTTP 브리지 클라이언트
tools/read.ts읽기 MCP 도구
tools/mutate.ts변경 MCP 도구
tools/graph.ts그래프 MCP 도구
docs/아키텍처와 엔드포인트 문서
reports/images/검증 스크린샷과 보고서 이미지

이 구조에서는 MCP 클라이언트가 Unreal 내부 구현을 몰라도 된다. 클라이언트는 ampliare_world_actors, ampliare_actor_set_transform, ampliare_graph_get 같은 도구를 호출한다. MCP 서버는 이 요청을 HTTP API로 바꾸고, Unreal 플러그인은 실제 에디터 API를 호출한다.

Unreal 플러그인 쪽 설계

Unreal 플러그인은 Editor 전용 모듈이다. Ampliare.uplugin에서는 모듈 타입을 Editor로 두고, PostEngineInit 시점에 로드되도록 했다. 에디터가 열린 뒤 브리지가 뜨는 구조가 필요했기 때문이다.

핵심 진입점은 FAmpliareModule::StartupModule()이다. 여기서 FHttpServerModule을 통해 로컬 HTTP 서버를 시작하고, 읽기/변경/그래프 라우트를 등록한다.

cpp
void StartBridge()
{
    FHttpServerModule& HttpServerModule = FHttpServerModule::Get();
    AmpliareRouter = HttpServerModule.GetHttpRouter(AmpliareBridge::BridgePort, true);
 
    if (!AmpliareRouter.IsValid())
    {
        UE_LOG(
            AmpliareBridge::LogAmpliare,
            Error,
            TEXT("Failed to start Ampliare MCP bridge on 127.0.0.1:%u."),
            AmpliareBridge::BridgePort
        );
        return;
    }
 
    AmpliareBridge::RegisterReadRoutes(BindRoute);
    AmpliareBridge::RegisterMutationRoutes(BindRoute);
    AmpliareBridge::RegisterGraphReadRoutes(BindRoute);
    AmpliareBridge::RegisterGraphMutationRoutes(BindRoute);
 
    HttpServerModule.StartAllListeners();
}

처음에는 모든 라우트를 한 파일에 둘 수도 있었지만, 금방 늘어날 코드였다. 그래서 라우트는 세 그룹으로 나눴다.

  • AmpliareReadRoutes.cpp: 프로젝트 정보, 에디터 상태, 월드 액터, 에셋, 선택 항목, 최근 로그처럼 상태를 읽는 기능
  • AmpliareMutationRoutes.cpp: 액터 스폰, 삭제, 트랜스폼 변경, UObject 프로퍼티 변경, 에셋 생성, 패키지 저장 같은 변경 기능
  • AmpliareGraphRoutes.cpp: Blueprint Graph와 Material Graph의 노드, 핀, 링크를 읽고 조작하는 기능

공통 처리는 AmpliareBridgeCommon.h/.cpp에 뒀다. JSON 직렬화, 에러 응답, 쿼리 파라미터 파싱, 벡터/로테이터 변환, 액터/에셋 JSON 변환, 변경 가드가 여기에 모인다. 라우트 파일에는 Unreal 작업만 남기고, 반복되는 HTTP 처리 코드는 공통 유틸리티로 뺐다.

MCP 서버 쪽 설계

MCP 서버는 TypeScript로 작성했다. @modelcontextprotocol/sdk로 stdio 서버를 열고, zod로 각 도구 입력 스키마를 정의한다. Codex가 도구를 호출하면 MCP 서버가 Unreal 브리지로 HTTP 요청을 보낸다.

typescript
const DEFAULT_BRIDGE_URL = "http://127.0.0.1:8765";
 
export async function bridgeGetJson(path: string): Promise<JsonValue> {
  return fetchBridgeJson("GET", path);
}
 
export async function bridgePostJson(path: string, body: unknown): Promise<JsonValue> {
  return fetchBridgeJson("POST", path, body);
}
 
export function toolResult(title: string, data: JsonValue) {
  return {
    content: [
      {
        type: "text" as const,
        text: `${title}\n${JSON.stringify(data, null, 2)}`,
      },
    ],
  };
}

MCP 서버가 직접 Unreal API를 알 필요는 없다. 대신 도구 이름, 입력 스키마, HTTP 경로만 알고 있다. Unreal 쪽 구현이 C++ API 사정에 맞춰 바뀌더라도, MCP 도구의 외부 인터페이스는 안정적으로 유지할 수 있다.

예를 들어 읽기 도구는 다음처럼 구성된다.

typescript
server.tool(
  "ampliare_world_actors",
  "List actors in the current Unreal Editor world.",
  {
    limit: z.number().int().min(1).max(5000).optional(),
  },
  async ({ limit }) => {
    return toolResult(
      "Ampliare world actors",
      await bridgeGetJson(bridgePath("/world/actors", { limit }))
    );
  },
);

이런 방식으로 read.ts, mutate.ts, graph.ts를 분리했다. 읽기 기능은 GET 요청, 변경 기능은 POST 요청, 그래프 기능은 Blueprint와 Material Graph를 다루는 별도 도구 묶음으로 유지했다.

안전 장치

AI 에이전트와 에디터를 연결할 때 가장 먼저 신경 쓴 부분은 안전 장치였다. Ampliare는 두 가지 원칙을 둔다.

Ampliare의 변경 요청은 기본적으로 실행되지 않는다. 실제 변경을 수행하려면 요청 본문에 dryRun: falseconfirm: true가 함께 있어야 한다.

Unreal 플러그인 쪽에서는 모든 요청이 루프백 주소에서 왔는지 확인한다. 비루프백 요청은 바로 403 Forbidden 응답을 돌려준다. 외부 네트워크에서 에디터 브리지에 접근하는 것을 막기 위한 장치다.

변경 실행 여부는 작은 구조체 하나로 통일했다.

cpp
struct FMutationGuard
{
    bool bDryRun = true;
    bool bConfirm = false;
 
    bool ShouldExecute() const
    {
        return !bDryRun && bConfirm;
    }
};

MCP 서버 쪽 입력 스키마도 같은 규칙을 따른다.

typescript
const mutationGuardSchema = {
  dryRun: z.boolean().optional().describe(
    "Defaults to true. Set false together with confirm=true to perform the change."
  ),
  confirm: z.boolean().optional().describe(
    "Must be true together with dryRun=false to perform the change."
  ),
};

이렇게 해 두면 실수로 변경 도구를 호출해도 기본 응답은 드라이런이다. 실제 실행이 필요할 때만 dryRun: false, confirm: true를 함께 전달한다.

기능을 넓혀간 과정

처음 구현한 것은 /health로 Unreal Editor 안에서 HTTP 서버가 뜨고, MCP 서버가 거기에 연결할 수 있는지만 확인했다. 이 단계에서는 기능보다 연결 구조를 먼저 봤다.

그 다음에는 읽기 기능을 추가했다. 에디터 실행 상태, 프로젝트 경로, 엔진 버전, 플러그인 버전, 현재 월드의 액터 목록, 특정 액터의 트랜스폼과 컴포넌트, 에셋 목록과 상세 정보, 선택 항목, 최근 로그를 읽을 수 있게 했다.

읽기 기능이 안정된 뒤에는 변경 기능을 추가했다. 액터의 트랜스폼을 바꾸고, 액터를 생성하거나 삭제하고, UObject 프로퍼티를 변경하고, 새 에셋을 생성하고, 패키지를 저장하는 도구를 만들었다.

마지막으로 Blueprint와 Material Graph 조작 기능을 분리했다. 그래프는 일반 액터/에셋 변경보다 구조가 복잡하다. 노드, 핀, 링크, 그래프 종류, 노드별 속성, Material root property 같은 개념을 다뤄야 한다. 그래서 그래프 목록 조회, 그래프 내용 조회, 노드 카탈로그 조회, 노드 생성, 노드 업데이트, 핀 연결, 배치 변경을 별도 도구로 뒀다.

빌드와 실행은 다음 순서로 검증했다.

Ampliare build commands
cd C:\unrealworkspace\ActionCombat\Plugins\Ampliare\McpServer npm install npm run build cd C:\unrealworkspace\ActionCombat & "C:\Program Files\Epic Games\UE_5.6\Engine\Build\BatchFiles\Build.bat" ActionCombatEditor Win64 Development -Project="C:\unrealworkspace\ActionCombat\ActionCombat.uproject" -WaitMutex

로딩 중...build compile

MCP 서버 빌드와 Unreal 플러그인 컴파일 검증
Ampliare MCP 도구 그룹
펼치기

실제 검증 사례

실행 중인 Unreal Editor와 실제 MCP 도구를 연결해서 검증했다.

로딩 중...mcp runtime validation

Codex에서 MCP 도구를 호출해 Unreal Editor 브리지 상태를 확인한 결과

액터 생성과 피직스 적용

첫 번째 검증은 선택된 BP_TestActor를 기준으로 같은 Blueprint 클래스를 여러 개 생성하고, 서로 다른 크기와 위치를 적용하는 작업이었다.

이 작업은 먼저 선택 항목을 읽고, 대상 액터와 컴포넌트를 확인하는 순서로 진행했다. 그 다음 드라이런으로 변경 가능성을 확인한 뒤, 실제 실행 옵션을 붙여 액터를 생성하고 프로퍼티를 적용했다.

로딩 중...actor prompt

여기서는 .uasset 파일을 직접 수정하지 않았다. MCP 도구로 현재 에디터 상태를 읽고, Unreal Editor API를 통해 액터를 생성했다. 월드 액터 수가 증가하는지, 생성된 액터가 의도한 위치에 배치되는지, 에디터 화면에서 결과가 보이는지까지 확인했다.

Material Graph로 디졸브 머티리얼 만들기

두 번째 검증은 Material Graph 편집이었다. M_Test_Dissolve 머티리얼을 만들고, NoiseTexture, DissolveAmount, EdgeWidth, EdgeColor를 사용하는 디졸브 그래프를 구성한 뒤 선택 액터에 적용하는 작업이었다.

로딩 중...material prompt

이 사례에서는 ampliare_asset_create로 머티리얼 에셋을 만들고, ampliare_graph_node_create, ampliare_graph_node_update, ampliare_graph_pin_connect로 Material Graph를 구성했다. 마지막에는 생성된 머티리얼을 액터의 SphereMesh에 적용하고 패키지를 저장했다.