XGEN 15일 무료 체험 — 설치 없이 브라우저에서 바로 시작하세요체험 신청
PlateerAI Labs
·Reading Time | 5 min
작성 | 김진수

고정된 실행 계약을 하나의 MCP 도구로 내보내기(3편)

워크플로우마다 다른 캔버스 그래프와 달리 하네스의 고정된 제어 골격을 컴파일하고, 외부에서 호출할 단일 도구 계약으로 만든 과정을 다룹니다.

고정된 실행 계약을 하나의 MCP 도구로 내보내기(3편) — 커버 일러스트Series · 하네스 개발기시리즈 3번째 글 · 전체 10편 보기

에이전트 하네스 설계 · 3/10

제품과 엔진의 경계를 나누자 하네스는 XGEN Agentic AI Platform(이하 XGEN) 안에서만 실행되는 기능일 필요가 없어졌습니다. 같은 QA 실행을 로컬 AI 클라이언트와 자동화 도구에서도 쓰려면 캔버스 전체를 다시 구현하게 할 것이 아니라, 설치하고 호출할 수 있는 하나의 도구로 내보내야 했습니다. 그래서 컴파일과 MCP(Model Context Protocol) 공개를 같은 문제로 다루기 시작했습니다.

4월 20일 만든 첫 컴파일러는 워크플로우와 실행 설정을 pip install할 수 있는 Python wheel로 묶었습니다. 4월 29일에는 npm 패키지와 spec.json 중심으로 방향을 바꾸면서 wheel 생성 경로를 걷어냈고, 5월 중순에는 같은 실행 규격을 사용하는 Python 컴파일러가 다시 돌아왔습니다. 이후 HTTP·RAG·MCP 도구와 하위 워크플로우, 캔버스 그래프까지 독립 실행 범위에 들어왔습니다.

이 왕복에서 풀어야 했던 문제는 패키지 형식이 아니었습니다. 무엇을 고정하면 복잡한 에이전트 실행을 하나의 안정된 도구 계약으로 공개할 수 있는가가 핵심이었습니다.

고정된 하네스 실행 흐름을 하나의 MCP 도구로 공개했습니다

일반 캔버스 워크플로우도 저장 시점에는 노드와 간선으로 구성된 방향성 비순환 그래프(DAG)입니다. 다만 워크플로우마다 그래프 모양이 달라지고, 실행기는 그때마다 간선에서 실행 순서를 계산해야 합니다. 라우터, 반복, 하위 워크플로우가 포함되면 분기 의미와 노드 구현, 포트 매핑도 함께 알아야 합니다. 캔버스 그래프 전체를 그대로 MCP 도구로 감싸려면 제품 실행기와 노드 레지스트리까지 외부 환경에 옮겨야 하는 이유입니다.

하네스는 다른 쪽을 고정했습니다. 입력 정리, 대화 이력, 프롬프트, 도구 공개, 정책, 컨텍스트, 도구 실행, 판정, 결과 정리의 순서는 모든 실행에서 같은 상태 머신을 따릅니다. 달라지는 것은 각 단계에서 쓸 전략과 도구, 판정 기준, 반복 상한입니다. 여기서 ‘정적 파이프라인’은 답이나 도구 순서까지 미리 정한다는 뜻이 아니라, 실행 제어의 골격과 단계별 입출력 규약이 고정돼 있다는 뜻입니다.

컴파일러는 이 차이를 이용합니다. WorkflowSnapshot에 하네스 설정, 도구 정의, 의존성, 외부 입력 이름을 동결하면 외부 런타임은 캔버스의 임의 그래프를 다시 해석할 필요가 없습니다. 생성된 MCP 서버는 다음처럼 하나의 인터페이스만 공개합니다.

run_workflow(input)
  → 고정된 하네스 상태 머신 실행
  → final_output 반환

MCP가 하네스를 정적으로 만드는 것은 아닙니다. 컴파일러가 실행 계약을 먼저 고정하고, MCP는 그 실행을 외부 클라이언트가 호출할 수 있게 노출하는 인터페이스입니다. 캔버스 그래프도 나중에는 별도 그래프 규격과 인터프리터를 함께 동결해 지원했지만, 고정된 하네스 실행 흐름을 공개하는 경우보다 필요한 정보가 많았습니다.

첫 wheel은 코드를 옮겼지만 실행 환경까지 옮기지는 못했습니다

첫 컴파일러는 캔버스 그래프와 하네스 설정을 WorkflowSnapshot으로 만들고, 이를 포함한 wheel을 생성했습니다. 별도 환경에서 설치와 import가 되는지 확인했기 때문에 코드 복사보다 한 걸음 나아갔습니다. 하지만 실제 도구를 호출하면 제품 안에서만 성립하던 전제가 다시 드러났습니다.

제품 프로세스에서는 설정 서비스가 모델과 반복 횟수를 채우고, 도구 레지스트리가 이름을 실행 함수에 연결하며, 데이터베이스가 세션을 이어 줍니다. 스냅샷에 노드 ID가 남아 있어도 독립 실행기에는 그 노드를 불러올 클래스와 호출 경로가 없습니다. 패키지는 설치됐지만 같은 실행이라고 부르기 어려웠습니다.

반대로 제품 패키지와 설정 서비스를 모두 wheel에 넣으면 실행은 쉬워집니다. 대신 독립 산출물이 제품의 축소 복제본이 됩니다. 앞 편에서 끊어 낸 의존성이 배포 파일 안에서 다시 이어지는 셈입니다.

여기서 컴파일의 목표를 바꿨습니다. 현재 코드를 그대로 포장하는 것이 아니라, 다른 런타임이 같은 실행 결정을 복원할 수 있는 규격을 만드는 것입니다.

npm 전환은 패키지보다 규격을 먼저 보게 했습니다

4월 말에는 Python wheel 경로를 제거하고 npm 패키지 안의 spec.json을 기준으로 실행하는 방향을 택했습니다. Python 코드를 생성하는 데 집중하면 다른 런타임에서 계약을 읽기 어렵고, 패키지 안에 무엇이 보존됐는지도 확인하기 힘들었기 때문입니다.

spec.json에는 그래프뿐 아니라 단계와 전략, 품질 문턱, 반복 상한, 입력·출력 스키마, 필요한 엔진 호환 범위가 들어갑니다. 실행기가 달라도 이 규격을 같은 의미로 해석하는 것이 목표였습니다. 워크플로우 전체를 stdio MCP 서버로 내보내면 외부에서는 하나의 도구처럼 보이므로, 입력과 출력 스키마도 공개 API가 됩니다.

다만 규격만으로 모든 문제를 풀 수는 없었습니다. Python 환경에서 이미 쓰고 있던 공급자와 도구 어댑터를 다시 활용하기 어렵고, spec.json에 적힌 도구가 실제 호출 가능한지 보장하는 계층도 필요했습니다. Node 패키지는 이식 가능한 표현을 분명하게 했지만 Python 실행 경로를 대체하지는 못했습니다.

그래서 5월 중순 Python 컴파일러를 되살렸습니다. 처음으로 되돌아간 것이 아니라, npm 전환에서 얻은 실행 규격을 Python 산출물에도 적용했습니다. 포장 형식은 둘이지만 동결해야 할 의미는 하나로 맞췄습니다.

PyPI와 stdio는 서로 대체하는 선택지가 아니었습니다

컴파일 화면에는 wheel, PyPI, npm, stdio MCP, 원격 MCP가 함께 나타납니다. 이 용어를 모두 ‘배포 방식’으로 보면 왜 여러 경로가 필요한지 설명하기 어렵습니다. 실제로는 서로 다른 질문에 답합니다.

구분 선택지 결정하는 것
산출물 형식 Python wheel, npm tarball 어떤 런타임이 패키지를 설치하고 실행할 것인가
전달 경로 wheel 직접 다운로드, PyPI 등록, npm registry·tarball 산출물을 어디서 받아 버전별로 보관할 것인가
실행 진입점 Python API, 명령행(CLI), MCP 서버 설치한 워크플로우를 누가 어떤 형태로 호출할 것인가
MCP 전송 표준 입출력(stdio), Streamable HTTP 실행 중인 MCP 서버와 클라이언트가 어떤 연결로 메시지를 주고받을 것인가

PyPI는 Python 패키지를 배포하고 버전을 고정하는 저장소입니다. stdio는 설치된 프로세스가 표준 입력과 출력으로 MCP 메시지를 주고받는 전송 방식입니다. 따라서 PyPI에 등록한 워크플로우를 pip install한 뒤, 그 패키지의 MCP 진입점을 stdio로 실행할 수 있습니다.

PyPI에 워크플로우 패키지 등록
  → pip install
  → python -m <workflow>.mcp
  → 로컬 클라이언트와 stdio로 MCP 통신

둘을 함께 지원한 이유도 여기에 있습니다. PyPI가 없으면 wheel 파일을 사용자마다 전달하고 의존성과 버전을 따로 관리해야 합니다. stdio가 없으면 Claude Desktop이나 로컬 개발 도구가 설치된 패키지를 별도 서버 없이 자식 프로세스로 실행하기 어렵습니다. 하나는 설치 이전의 유통 문제, 다른 하나는 설치 이후의 통신 문제를 풉니다.

PyPI라는 이름도 두 역할로 등장합니다. xgen-harness는 상태 머신과 도구 실행기를 제공하는 엔진 패키지입니다. 컴파일된 워크플로우 패키지는 동결한 설정과 도구 정의를 담고, 자신을 만든 엔진 버전을 정확히 의존합니다. 엔진만 설치하면 L사 QA 도구가 생기는 것이 아니고, 워크플로우 패키지를 설치하면 필요한 엔진 버전이 함께 해석되는 구조입니다.

wheel 직접 다운로드와 PyPI 등록도 결과물의 의미는 같습니다. 검증이나 폐쇄망 반입처럼 파일을 직접 통제해야 하면 wheel을 전달하고, 여러 사용자가 같은 이름과 버전으로 설치해야 하면 PyPI를 사용합니다. PyPI에 올린 버전은 같은 번호로 덮어쓸 수 없으므로 수정된 실행 계약은 새 버전으로 배포하고, 설치 후에는 실제 wheel의 스냅샷과 엔진 핀을 다시 확인해야 합니다.

표준 입출력과 원격 HTTP는 실행 위치가 달랐습니다

stdio에서는 클라이언트가 MCP 서버 프로세스를 직접 시작합니다. 별도 포트를 열지 않고 환경 변수도 로컬 프로세스에 줄 수 있어 개인 개발 도구와 단일 사용자 실행에 단순합니다. 대신 클라이언트마다 프로세스가 생기고, 수명과 업데이트도 각 설치 환경이 관리해야 합니다.

원격 Streamable HTTP 경로에서는 XGEN이나 MCP Station이 패키지를 실행하고 여러 클라이언트가 URL로 호출합니다. 중앙에서 새 버전을 배포하고 인증과 실행 기록을 관리하기 쉽지만, 서버 수명주기와 네트워크, OAuth 같은 운영 책임이 추가됩니다. 로컬 프로세스를 직접 시작할 수 없는 웹 기반 자동화 도구에는 이 경로가 필요했습니다.

원격 MCP를 직접 지원하는 클라이언트는 HTTP URL을 등록할 수 있습니다. stdio 설정만 받는 로컬 클라이언트에는 npx 런처를 두고, 런처가 stdio 요청을 원격 HTTP MCP로 중계하는 방식도 사용했습니다. 겉으로는 모두 MCP 도구 하나지만 실제 실행 위치와 인증 책임은 다릅니다.

로컬 실행:  client ──stdio──> 설치된 Python·Node 워크플로우 프로세스
원격 실행:  client ──HTTP───> XGEN gateway ──> 중앙 워크플로우 실행
중계 실행:  client ──stdio──> npx launcher ──HTTP──> XGEN gateway

Python API와 CLI도 별도 엔진이 아닙니다. Python API는 다른 애플리케이션 프로세스 안에 워크플로우를 포함할 때, CLI는 셸과 배치 작업에서 한 번 실행할 때, MCP는 도구 호출 규약이 필요한 외부 클라이언트에서 사용합니다. 세 진입점이 같은 build_pipeline()과 스냅샷을 읽어야 호출 표면이 달라도 실행 의미가 유지됩니다.

실행 결정을 바꾸는 값과 환경 값을 나눴습니다

무엇을 동결할지 정하는 기준은 두 가지였습니다. 값이 달라질 때 에이전트의 의사결정 방식이 바뀌는지, 다른 환경으로 복사해도 안전한지를 봤습니다.

단계 구성, 품질 기준, 반복과 종료 조건, 도구 입력 스키마는 실행 결과를 바꾸므로 산출물에 남깁니다. API 키와 접근 토큰, 배포 환경별 주소는 복사해서는 안 되므로 외부 입력으로 선언합니다. 설정의 ${VAR} 참조는 컴파일할 때 필요한 변수 이름만 수집하고 실제 값은 실행 시점에 읽습니다.

동결: 단계·전이·판정 기준·도구 규격·입출력 스키마
주입: 자격 정보·환경별 주소·실행 시점의 세션

너무 적게 동결하면 같은 산출물도 환경마다 다른 에이전트가 됩니다. 너무 많이 동결하면 비밀값이 새거나 운영 주소가 개발 패키지에 박힙니다. 컴파일러는 파일 생성기보다 이 경계를 검사하는 도구에 가까워졌습니다.

도구 설명을 호출 가능한 소스로 바꿨습니다

모델에게 도구 이름과 JSON 스키마를 보여 주는 것만으로는 독립 실행이 되지 않습니다. 모델이 올바른 인자를 만들어도 산출물은 요청을 어디로 보낼지 모릅니다. 5월 중순 이후에는 도구를 call_kindcall_spec으로 정규화해 이 간격을 메웠습니다.

HTTP 도구에는 메서드와 URL 템플릿, 헤더와 본문 매핑이 필요합니다. RAG 도구는 검색 서비스 연결과 결과 형식을, MCP 도구는 서버 실행 또는 중계 방식을 알아야 합니다. 이 정의를 FrozenToolSource에 담아 제품 레지스트리가 없는 곳에서도 이름 검색부터 실제 호출까지 이어지게 했습니다.

호출 가능성만 동결하지는 않았습니다. HTTP 도구가 로컬·메타데이터 주소로 향하지 못하게 막고, 오류에 자격 정보가 섞이지 않게 했습니다. 제품이 대신 보장하던 안전 경계도 독립 실행 계약에 포함해야 했습니다.

Python 산출물과 Node 산출물은 같은 코드를 쓰지 않습니다. 대신 같은 스냅샷과 도구 정의를 입력으로 받고, 같은 이름이 같은 호출 의미를 갖게 했습니다. 한쪽은 생성 모듈과 wheel에, 다른 쪽은 spec.json과 실행 래퍼에 담더라도 단계 선택과 도구 결과의 표준 형식은 일치해야 했습니다. 두 런타임을 지원하는 만큼 스냅샷 동등성 회귀 테스트와 엔진 버전 고정을 계속 관리해야 하는 비용도 생겼습니다.

하위 워크플로우와 캔버스 그래프가 마지막 경계였습니다

단일 도구를 동결한 뒤에는 워크플로우 자체를 도구로 호출하는 경우가 남았습니다. 상위 에이전트가 하위 에이전트를 선택하면 하위 설정과 도구도 함께 얼려야 합니다. 이름만 남기면 독립 환경에서 다시 제품 레지스트리를 찾게 됩니다.

5월 말에는 선택된 도구와 하위 파이프라인을 재귀적으로 수집해 산출물에 포함했습니다. 하위 워크플로우가 또 다른 워크플로우를 부를 수 있으므로 깊이 제한과 순환 검사가 필요했습니다. Node 실행기에도 같은 하위 파이프라인 의미를 맞췄습니다.

캔버스 그래프는 더 까다로웠습니다. 제품의 노드 클래스를 그대로 import하는 대신 노드와 간선, 포트 매핑을 실행 가능한 그래프 규격으로 바꾸고 독립 인터프리터가 읽게 했습니다. 이 단계까지 와서야 단일 도구, 하위 파이프라인, 캔버스 워크플로우가 같은 산출물 모델 안에서 실행될 수 있었습니다.

깨끗한 환경에서 경계를 확인했습니다

검증은 제품 프로세스 밖에서 해야 했습니다. 제품 안에서 컴파일 함수를 실행하면 설정 서비스와 도구 레지스트리가 빠진 정보를 우연히 채워 줄 수 있습니다.

먼저 스냅샷을 저장했다 다시 읽어 단계와 설정, 외부 입력 선언이 보존되는지 확인했습니다. 그다음 생성된 wheel과 npm tarball을 빈 환경에 설치하고 CLI와 MCP 서버를 시작했습니다. 도구 목록 조회에서 끝내지 않고 HTTP·RAG·MCP 대표 도구와 하위 워크플로우를 실제로 호출했습니다. 캔버스 실행과 독립 실행에서는 최종 문장보다 사용한 도구, 적용한 판정 기준, 종료 이유를 비교했습니다.

컴파일러는 wheel을 만들었다가 npm으로 갈아타고 다시 Python으로 돌아왔습니다. 그러나 같은 자리를 맴돈 것은 아닙니다. 첫 wheel은 코드 이동의 한계를 보여 줬고, npm 규격은 실행 의미를 분리해 보게 했으며, 돌아온 Python 컴파일러는 그 계약을 실제 도구 호출까지 완성했습니다. 다음 편에서는 이렇게 동결된 실행 안에서 답을 만드는 역할과 그 답을 채택하는 역할을 왜 따로 두었는지 살펴봅니다.


이전 편 → 엔진 코어와 제품 통합 계층을 분리한 이유(2편) 다음 편 → 생성과 판정을 별도 실행 단계로 나눈 이유(4편)

#하네스#컴파일#독립 실행
블로그 목록으로