ouroboros · MCP 서버: 도구 정의·노출과 Subagent 디스패치

한 줄 요약

ouroboros는 자기 기능(인터뷰·실행·평가·진화)을 AI 모델이 버튼처럼 눌러 쓰는 “도구(tool)“로, MCP 서버를 통해 바깥에 내놓는다. 왜 배우나: 이 층을 알면 “모델이 어떻게 ouroboros를 직접 운전하는가”와 “무거운 작업을 누가 대신 처리하는가”가 보인다.

그림

flowchart TD
  A[".mcp.json: uvx ouroboros mcp serve 등록"] --> B["서버 기동: get_ouroboros_tools 가 핸들러를 ToolRegistry에 일괄 등록"]
  B --> C["tools/list: 도구 명세를 모델에게 광고"]
  C --> D["모델이 tools/call(도구이름, 인자) 호출"]
  D --> E["보안 검사: 인증→레이트리밋→인가→입력검증"]
  E --> F["registry.call 이 이름으로 핸들러를 찾아 handle 실행"]
  F --> G{"플러그인으로 위임할까?"}
  G -->|아니오 in-process| H["서버가 직접 실행"]
  H --> H2{"오래 걸리는 작업인가?"}
  H2 -->|예| I["JobManager가 백그라운드 잡 생성 → job_id만 즉시 반환"]
  H2 -->|아니오| J["바로 결과 반환"]
  I --> K["job_status / job_wait / job_result 로 진행 폴링"]
  G -->|예 OpenCode plugin| L["역할 프롬프트(agents/*.md) 조립"]
  L --> M["_subagent 봉투를 결과 meta에 실어 반환"]
  M --> N["OpenCode 브리지가 자식 세션을 띄워 LLM 작업 위임 → 결과 회수"]

쉽게 풀기

1) MCP 서버 = ouroboros의 리모컨 ouroboros 안에는 “인터뷰·실행·평가·Ralph 루프” 같은 기능이 많다. 이를 AI 모델(클로드 코드 등)이 직접 쓰게 리모컨 버튼처럼 내놓는 창구가 MCP 서버다. .mcp.jsonuvx ... ouroboros mcp serve 한 줄만 넣으면, 모델 화면에 ouroboros_execute_seed, ouroboros_interview, ouroboros_evaluate, ouroboros_ralph 같은 버튼 수십 개가 한꺼번에 뜬다.

2) 버튼 하나 = 핸들러 클래스 하나 각 버튼 뒤에는 파이썬 핸들러 클래스가 한 개씩 붙는다(예: ExecuteSeedHandler). 핸들러는 두 가지만 갖추면 된다 — (a) definition(이름·설명·입력칸을 적은 이름표), (b) handle()(눌렀을 때 도는 함수). 모델은 이름표만 보고 “언제 누를지”를 판단한다.

3) 안내데스크 — ToolRegistry 모델이 “이 이름의 도구를 실행해줘” 하면 ToolRegistry가 이름으로 핸들러를 찾아 handle()을 호출한다(호텔 프런트가 객실 번호로 방을 찾듯). 같은 이름이 두 번 등록되면 헷갈리므로 등록 단계에서 오류로 막는다.

4) 오래 걸리는 일은 번호표부터 — JobManager 인터뷰·실행처럼 몇 분 걸리는 작업을 끝까지 기다리면 모델이 멈춘다. 그래서 진동벨처럼 번호표(job_id)를 즉시 돌려주고 뒤에서 작업을 돌린다. 모델은 ouroboros_job_status / wait / result로 폴링해 결과를 회수한다. 진행 상태는 이벤트로 기록돼 중간에 끊겨도 복원된다.

5) 무거운 일은 자식에게 — _subagent 봉투 OpenCode 환경에서 돌 때는 LLM을 직접 부르지 않는다. 대신 “너는 소크라테스식 면접관이야” 같은 역할 설명서(agents/socratic-interviewer.md)를 봉투에 담아 돌려준다. OpenCode 브리지가 봉투를 받아 자식 세션(서브에이전트)을 띄우고 대화를 맡긴다. 부모는 짐을 내려놓고, 작업은 보이는 자식 창에서 진행돼 관측·병렬이 쉽다.

flowchart LR
  M["모델 tools/call"] --> R["ToolRegistry: 이름→핸들러"]
  R --> H["handle()"]
  H --> Q{"should_dispatch_via_plugin?"}
  Q -->|in-process| J["JobManager 번호표 or 즉시 결과"]
  Q -->|plugin| S["_subagent 봉투 → 자식 세션"]

핵심 정리

구성요소한 줄 역할비유
MCP 서버기능을 도구로 노출리모컨
핸들러(definition+handle)도구 1개 이름표 + 실행버튼
ToolRegistry이름으로 핸들러 찾아 실행호텔 프런트
JobManager오래 걸리는 작업을 백그라운드로진동벨/번호표
_subagent 봉투역할 프롬프트를 자식에게 위임외주 의뢰서

도구 호출은 두 길로 갈린다. 분기 판단은 should_dispatch_via_plugin()이 전담하며, 봉투를 잘못 내보내면 받을 플러그인이 없어 흐름이 깨진다.

flowchart TD
  C["tools/call"] --> G{"runtime + opencode_mode"}
  G -->|"claude / codex / None"| P1["in-process: 직접 실행, 길면 JobManager"]
  G -->|"OpenCode + plugin"| P2["plugin: _subagent 봉투만 반환, LLM은 자식이 호출"]

실제 예시

핸들러가 도구 이름표를 만드는 방식, 레지스트리가 이름으로 찾아 실행하는 방식, 봉투 생성과 게이트 판정까지 — 전체 코드는 아래에 접어둔다.

새 도구 1개를 추가하려면 핸들러 한 개를 만들어 두 경로(in-process / plugin)를 분기하고, get_ouroboros_tools() 반환 튜플에 MyHandler(...)를 추가하면 서버 기동 시 자동 등록·노출된다.

요약 & 셀프체크

3줄 요약:

  1. MCP 서버는 기능을 도구(버튼)로 노출하고, 핸들러 1개 = 도구 1개이며 ToolRegistry가 이름으로 찾아 실행한다.
  2. 오래 걸리는 작업은 JobManagerjob_id(번호표)를 즉시 돌려주고 백그라운드로 처리하며, 상태는 이벤트로 영속된다.
  3. OpenCode plugin 환경에서는 LLM을 직접 부르지 않고 _subagent 봉투(역할 프롬프트 = agents/*.md)를 자식 세션에 위임한다.

스스로 답해보기:

  • 모델이 도구를 부르면 어떤 검사를 거쳐 어느 함수가 실제로 실행되는가?
  • 같은 도구라도 in-process 경로와 plugin 경로로 갈리는 기준은 무엇인가?
  • 인터뷰처럼 몇 분 걸리는 작업에서 모델이 멈추지 않게 하는 장치는 무엇이고, 결과는 어떻게 회수하는가?

연결

OB_개요 · _분석축_루브릭 · OB_40_orchestrator-execution-loop · OB_50_provider-adapters-and-backend-neutral-runtime · OB_70_extension-points-hooks-skills-commands