Ampliare는 Unreal Editor와 MCP 클라이언트를 연결하기 위해 만든 에디터 플러그인이다.
Codex 같은 MCP 클라이언트가 실행 중인 Unreal Editor의 월드, 액터, 에셋, Blueprint, Material Graph 상태를 읽고,
필요한 편집 작업을 요청할 수 있게 하는 중간 계층이다.
LLM은 텍스트 파일은 잘 읽고 고치지만, Unreal Engine 프로젝트의 핵심 작업 단위인 .uasset은
같은 방식으로 다루기 어렵다.
.uasset을 일반 텍스트 파일처럼 직접 수정하면 에셋 손상, 참조 관계 깨짐, 에디터 크래시가 생길 수 있다.
그래서 Ampliare는 파일을 직접 패치하는 대신 실행 중인 Unreal Editor 안에서 Unreal API를 호출하는 구조로 설계했다.
로딩 중...
AI 에이전트가 Unreal 프로젝트를 도와주려면 현재 에디터가 어떤 상태인지 알아야 한다. 현재 열린 레벨이 무엇인지, 어떤 액터가 선택되어 있는지, 콘텐츠 브라우저의 에셋이 어떤 참조를 갖는지, Blueprint Graph의 노드와 핀이 어떻게 연결되어 있는지를 읽을 수 있어야 한다.
반대로 변경은 별도 경로가 필요했다. 파일을 직접 고치는 방식이 아니라, Unreal Editor가 제공하는 API와 패키지 저장 흐름을 지나야 한다. 그래서 요구사항을 다음처럼 잡았다.
이 요구사항을 만족시키기 위해 Unreal 플러그인과 MCP 서버를 분리했다. Unreal 쪽은 에디터 내부 상태와 실제 편집 작업을 맡고, MCP 서버는 Codex가 사용할 도구 스키마와 요청 중계를 맡는다.
Ampliare는 크게 네 계층으로 나뉜다.
GEditor, Asset Registry, Blueprint/Material Graph API를 통해 실제 상태를 읽거나 변경한다.로딩 중...
구조를 파일 단위로 보면 다음과 같다.
이 구조에서는 MCP 클라이언트가 Unreal 내부 구현을 몰라도 된다.
클라이언트는 ampliare_world_actors, ampliare_actor_set_transform, ampliare_graph_get 같은 도구를 호출한다.
MCP 서버는 이 요청을 HTTP API로 바꾸고, Unreal 플러그인은 실제 에디터 API를 호출한다.
Unreal 플러그인은 Editor 전용 모듈이다.
Ampliare.uplugin에서는 모듈 타입을 Editor로 두고, PostEngineInit 시점에 로드되도록 했다.
에디터가 열린 뒤 브리지가 뜨는 구조가 필요했기 때문이다.
핵심 진입점은 FAmpliareModule::StartupModule()이다.
여기서 FHttpServerModule을 통해 로컬 HTTP 서버를 시작하고, 읽기/변경/그래프 라우트를 등록한다.
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 서버는 TypeScript로 작성했다.
@modelcontextprotocol/sdk로 stdio 서버를 열고, zod로 각 도구 입력 스키마를 정의한다.
Codex가 도구를 호출하면 MCP 서버가 Unreal 브리지로 HTTP 요청을 보낸다.
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 도구의 외부 인터페이스는 안정적으로 유지할 수 있다.
예를 들어 읽기 도구는 다음처럼 구성된다.
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: false와 confirm: true가 함께 있어야 한다.
Unreal 플러그인 쪽에서는 모든 요청이 루프백 주소에서 왔는지 확인한다.
비루프백 요청은 바로 403 Forbidden 응답을 돌려준다.
외부 네트워크에서 에디터 브리지에 접근하는 것을 막기 위한 장치다.
변경 실행 여부는 작은 구조체 하나로 통일했다.
struct FMutationGuard
{
bool bDryRun = true;
bool bConfirm = false;
bool ShouldExecute() const
{
return !bDryRun && bConfirm;
}
};MCP 서버 쪽 입력 스키마도 같은 규칙을 따른다.
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 같은 개념을 다뤄야 한다. 그래서 그래프 목록 조회, 그래프 내용 조회, 노드 카탈로그 조회, 노드 생성, 노드 업데이트, 핀 연결, 배치 변경을 별도 도구로 뒀다.
빌드와 실행은 다음 순서로 검증했다.
로딩 중...
실행 중인 Unreal Editor와 실제 MCP 도구를 연결해서 검증했다.
로딩 중...
첫 번째 검증은 선택된 BP_TestActor를 기준으로 같은 Blueprint 클래스를 여러 개 생성하고,
서로 다른 크기와 위치를 적용하는 작업이었다.
이 작업은 먼저 선택 항목을 읽고, 대상 액터와 컴포넌트를 확인하는 순서로 진행했다. 그 다음 드라이런으로 변경 가능성을 확인한 뒤, 실제 실행 옵션을 붙여 액터를 생성하고 프로퍼티를 적용했다.
로딩 중...
여기서는 .uasset 파일을 직접 수정하지 않았다.
MCP 도구로 현재 에디터 상태를 읽고, Unreal Editor API를 통해 액터를 생성했다.
월드 액터 수가 증가하는지, 생성된 액터가 의도한 위치에 배치되는지, 에디터 화면에서 결과가 보이는지까지 확인했다.
두 번째 검증은 Material Graph 편집이었다.
M_Test_Dissolve 머티리얼을 만들고, NoiseTexture, DissolveAmount, EdgeWidth, EdgeColor를 사용하는 디졸브 그래프를 구성한 뒤
선택 액터에 적용하는 작업이었다.
로딩 중...
이 사례에서는 ampliare_asset_create로 머티리얼 에셋을 만들고,
ampliare_graph_node_create, ampliare_graph_node_update, ampliare_graph_pin_connect로 Material Graph를 구성했다.
마지막에는 생성된 머티리얼을 액터의 SphereMesh에 적용하고 패키지를 저장했다.