콘텐츠로 이동

evo-dispatch 클라이언트 연동 규격 v1

원천 · framework/evomind/web/workflow-project-evo-dispatch기준 · 릴리스 v0.1.0커밋 · 734af815파일 · docs/protocol/client-protocol.md기준선 · 2026-11-12

  • 규격 버전: v1 (schema_version: "1")
  • 상태: 승인 — 2026-10-18 Human 승인 (r4)
  • 원본 위치: 이 저장소의 docs/protocol/. 다른 프로젝트는 이 문서를 복사하지 않고, 규격 버전과 커밋을 지정해 참조한다.
  • 흐름도: design-flow.md
  • 종단간 암호화(선택 확장, 초안): e2e.md
  • 워크그룹(선택 확장, 초안): workgroup.md
  • 이 규격은 세션 요청 공간(/mcp)이다. 별개 규격: 버그 신고 공간(/mcp/bugs, evo 제품 사용자) bugs.md, 개발 요청 공간(/mcp/dev, 프로젝트 개발 에이전트 전용, 옛 이름 공용 요청 공간, 옛 주소 /mcp/board는 에서 제거) dev-requests.md
  • 대상 클라이언트: Evo CLI, evo Mac 앱, evo Windows 앱, evo-service(웹)
  • 근거 커밋: Evo CLI develop 9efab1a

활성 세션끼리 요청을 보내고 받는다. 그것만 한다.

세션 A ── 요청 보내기 ──▶ evo-dispatch ◀── 활성 신호(20초) ── 세션 B
│
세션 B ◀── 받은 요청 확인·읽음 표시 ──┘
세션 B: 받은 요청을 자기 방식대로 처리한다 (dispatch는 관여하지 않는다)
세션 B ── (원하면) 답장 보내기 ──▶ 세션 A (A가 아직 활성일 때만)

다음 원칙을 지킨다.

  • 여러 사용자가 각자 여러 세션을 가진다. 한 사용자는 여러 기기에서 여러 클라이언트(Evo CLI 여러 개, Mac 앱, Windows 앱 등)를 동시에 열 수 있다. 서버 하나가 모든 사용자를 받는다.
  • 요청은 내 세션 사이에서만 오간다. 다른 사용자의 세션에는 보낼 수 없고, 다른 사용자의 세션·요청은 보이지 않는다. 사용자 사이 전송은 v1 범위가 아니다.
  • 활성 세션에만 보낸다. 보내는 순간 받을 활성 세션이 없으면 보내기를 거부한다. 종료된 세션은 요청을 받을 수 없다.
  • dispatch는 전달만 한다. 받은 쪽이 요청을 어떻게 처리하는지 추적하지 않는다. 결과를 알리고 싶으면 답장을 보낸다.
  • 특정 플랫폼을 전제하지 않는다. GitLab, git 저장소, 이슈, MR 같은 것은 필수 필드나 흐름의 전제가 아니다. 링크가 필요하면 선택 항목인 links에 자유 URL로 넣는다.
  • 쓰고 남은 데이터는 지운다. 활성 신호는 이력을 쌓지 않고, 종료된 세션과 끝난 요청은 보존 기간 뒤 삭제한다(§7).
항목 필수 값 바뀌는가 출처
사용자 필수 Keycloak sub(+iss) 안 바뀜 서버가 Bearer 토큰에서 꺼낸다. 클라이언트가 직접 보내지 않는다.
기기 필수 device_id (UUIDv4) 안 바뀜 클라이언트가 설치 단위로 한 번 만들어 영구 저장한다.
세션 필수 client_kind + session_id 자주 바뀜 evo는 세션 UUID, evo-service는 대화 UUID를 쓴다.
작업 담당 이름 선택 agent (예: CD01) 사람이 정할 때만 세션마다 정한다. 시작할 때 실행 옵션으로 정하고, 세션 중에 명령으로 바꾸거나 지울 수 있다(evo: evo --dispatch-name <이름>, /dispatch name <이름>, /dispatch name -). 바꿔도 session_id는 그대로이고, 이미 받은 요청은 그 세션에 남는다. 다음 활성 신호부터 새 이름으로 받는다.
작업 공간 이름 선택 workspace (예: evo, my-app) 사람이 정할 때만 사용자가 지정하거나, 클라이언트가 작업 폴더 이름을 기본값으로 쓴다.
담당(persona) 선택 persona (예: tester, developer-backend-go) 사람이 정할 때만 워크그룹(workgroup.md)에서 이 세션이 맡은 일을 다른 멤버에게 알리는 표시 정보다. 형식은 ^[a-z0-9][a-z0-9-]{0,63}$이고 다른 값은 invalid_request(400)다. 마지막 활성 신호의 값이 현재 값이며 없거나 빈 값이면 담당 없음이다(dispatch_sessions·dispatch_workgroups에서 필드를 뺀다). 평문 표시 정보이고 E2E 대상이 아니며, 서버는 값으로 권한·주소를 가르지 않는다.
  • client_kind 값: evo-cli, evo-mac, evo-windows, evo-web
  • 세션은 사용자 + client_kind + session_id 로 구분한다. 서로 다른 사용자가 같은 session_id를 보내도 다른 세션이다. 기기(device_id)와 agent·workspace 이름도 사용자마다 따로 본다.
  • 클라이언트 종류가 달라도(예: Evo CLI ↔ evo-service 웹) 같은 사용자(iss + sub) 일 때만 서로 주고받는다. 로그인 서버(issuer)가 다르면 아이디가 같아도 다른 사용자다. evo-service 로그인이 Evo CLI와 같은 사용자로 이어지는 방법은 evo-service #82에서 정한다.
  • 이름 형식: 영문·숫자로 시작하고 영문·숫자·.·_·-만 쓴다. 최대 64자.
  • presence에는 같은 이름의 작업 공간을 구분하기 위한 workspace.path_hash(작업 폴더 절대경로의 sha256)를 선택으로 붙일 수 있다. 경로 원문은 보내지 않는다.

새 요청은 바뀌지 않는 이름으로 주소를 정한다. 서버는 보내는 순간 그 주소에 맞는 활성 세션을 찾아 받는 세션 목록(recipients)으로 고정한다.

to.kind 필수 값 받는 세션 (보내는 순간 활성인 것만)
user — 같은 사용자의 모든 활성 세션 (보내는 세션 제외)
agent agent 같은 사용자의 활성 세션 중 agent가 같은 세션. client_kind는 보지 않는다.
workspace workspace 같은 사용자의 활성 세션 중 workspace.name이 같은 세션. client_kind는 보지 않는다.
session client_kind, session_id 지정한 세션 하나. 그 세션이 활성일 때만. 처음 요청과 답장 모두 쓸 수 있다.
  • v1은 같은 사용자 소유 세션끼리만 주고받는다. 주소에는 사용자를 지정하는 항목이 없으며, 서버는 모든 주소를 보내는 사용자(토큰의 sub) 안에서만 찾는다.
  • 다른 사용자의 세션을 session 주소나 reply_to로 지정하면 그 세션이 없는 것으로 처리한다(no_active_recipient 또는 not_found).
  • 받는 세션이 0개면 보내기를 거부한다(오류 no_active_recipient). 요청은 저장되지 않는다.
  • 받는 세션 목록은 보낸 뒤 늘어나지 않는다. 나중에 켜진 세션은 그 요청을 받지 않는다.
  • 보내기 응답에는 받는 세션 목록이 들어간다. 보낸 쪽은 누가 받는지 바로 알 수 있다.
  • 보내기 전에 dispatch_sessions로 내 활성 세션 목록을 확인할 수 있다.
  • session 주소의 session_id는 /clear·/compact(세션 교체, §5) 때 바뀐다. 목록에서 골라 보낼 때는 보내기 직전에 dispatch_sessions로 본 값을 쓴다. 그 사이 바뀌었거나 끝났으면 no_active_recipient다.
  • 답장(kind: "reply")에는 주소 종류와 상관없이 reply_to가 있어야 한다.

클라이언트는 세션이 열려 있는 동안 활성 신호를 보낸다. 형식은 schemas/presence.schema.json을 따른다.

항목 값
전송 주기 20초 (허용 범위 15~30초)
비활성 판정(TTL) 마지막 신호 후 90초
이벤트 heartbeat(정기 신호), rotate(세션 교체), end(정상 종료, 가능할 때만)
실행 방식 mode: interactive(TUI·앱 화면) 또는 headless(-p 실행)
지원 기능 capabilities: inbox(받은 요청 확인), notify(화면 알림), context_inject(대화에 요청 자동 주입), push(워크그룹 푸시 채널 지원, push.md)
신호 응답 그 세션의 안 읽은 요청 수. 클라이언트는 이 값으로 새 요청을 알아챈다.
  • 활성 판정은 조회 시점에 계산한다. 서버는 “마지막 신호 시각 + 90초”가 지금보다 이후인 세션만 활성으로 본다. 정리 작업이 늦어도 판정은 틀리지 않는다.
  • end를 받으면 그 세션은 즉시 종료다. evo에는 종료 hook이 없어서(internal/hooks/hooks.go, 이벤트 8종) quit이나 터미널 종료 때 end가 오지 않을 수 있다. 이때는 TTL 만료로 종료를 판정한다.
  • 활성 세션 목록은 같은 사용자의 세션끼리만 보인다.

evo 세션 ID는 다음 표처럼 바뀐다. 근거는 evo 9efab1a 기준 코드 위치다.

시나리오 세션 ID evo 근거 클라이언트가 보낼 신호 서버 처리
새로 시작 새 ID cmd/evo/run_stages.go:83 session.New 새 ID로 heartbeat 새 세션으로 등록한다. 이전 세션 앞 요청은 받지 않는다.
/clear 새 ID internal/ui/commands.go:156 rotate, previous_session_id=이전 ID, rotation_cause: "clear" 같은 세션이 이어지는 것으로 본다. 이전 세션이 받는 세션으로 들어 있는 안 읽은 요청에서 이전 ID를 새 ID로 바꾼다(같은 device_id일 때만). 이전 세션은 종료(교체) 처리한다.
/compact 새 ID internal/ui/session.go:35 (oldID 보관) rotate, rotation_cause: "compact" /clear와 같다.
--fork 새 ID(복사본) internal/session/session.go Fork 새 ID로 heartbeat, forked_from=원본 ID 새 세션으로 등록한다. 원본의 요청을 옮기지 않는다.
--resume, --continue 같은 ID session.Open, session.Latest 같은 ID로 heartbeat TTL 안이면 같은 세션이 계속 활성이다. TTL이 지나 이미 종료됐으면 새 활성 기간으로 등록하고, 종료 전에 받았던 요청은 되살리지 않는다.
quit, 터미널 닫기, 비정상 종료 — 종료 hook 없음 end(보낼 수 있을 때만) 또는 신호 끊김 end를 받으면 즉시, 아니면 90초 뒤 종료로 판정한다.

세션이 종료되면 서버는 다음과 같이 처리한다.

  • 그 세션이 받는 세션으로 들어 있는 안 읽은 요청에서, 받는 세션이 모두 종료됐으면 그 요청을 expired로 바꾼다. 다른 받는 세션이 아직 활성이면 그대로 둔다.
  • 종료된 세션 앞으로 오는 답장(to.kind: "session")은 거부한다(오류 no_active_recipient).

형식은 schemas/message.schema.json을 따른다.

필수 항목:

  • schema_version, id
  • kind: request(요청) 또는 reply(답장)
  • from, to
  • recipients: 받는 세션 목록. 서버가 채우며 1개 이상이다.
  • title(200자 이내), body(16KB 이내, Markdown)
  • status, created_at

선택 항목:

  • reply_to: 답장일 때 필수
  • links: 자유 URL과 이름, 최대 20개
  • read_by: 읽음 상태일 때 필수
  • closed_at: 읽음·취소·만료가 된 시각. 이 세 상태일 때 필수
  • extensions.execution: 2·3단계 실행 요청용 예약 공간. v1 클라이언트는 무시한다.

v1에는 처리 상태(수락, 진행, 완료 등)와 처리 결과 필드가 없다. Schema에 없는 필드는 거부한다.

현재 → 다음 누가 조건
unread → read 받는 세션 recipients에 든 활성 세션이 읽음 표시를 한다. read_by와 closed_at을 기록한다.
unread → cancelled 보낸 쪽 —
unread → expired 서버 recipients의 세션이 모두 종료됐다.
  • 요청 1건의 읽음 상태는 1개다. 받는 세션이 여럿이면 먼저 읽음 표시한 세션이 기록되고, 다른 받는 세션의 받은 요청 목록에서는 빠진다.
  • 만료 판정도 조회 시점에 계산한다. 받는 세션이 모두 비활성인 안 읽은 요청은 조회될 때 expired로 처리된다.

금지 전이:

  • read → unread
  • read → cancelled: 이미 읽은 요청은 보낸 쪽이 취소할 수 없다. 필요하면 새 요청으로 알린다.
  • cancelled·expired에서 다른 상태로
  • recipients에 없는 세션이나 종료된 세션의 읽음 표시

활성 신호는 20초마다 오므로 이력을 쌓으면 세션 하나가 하루에 4,320건을 만든다. 그래서 서버는 다음 원칙으로 저장하고 정리한다. 저장 구조와 정리 작업의 구현은 에서 정한다.

  • 활성 신호는 세션당 1행만 둔다. 신호가 오면 마지막 신호 시각만 갱신하고, 신호 이력은 남기지 않는다.
  • 정리 작업은 삭제만 맡는다. 활성·만료 판정은 조회 시점에 계산하므로, 정리 작업이 멈추거나 늦어도 동작은 틀리지 않는다.
대상 보존 기간 처리
종료된 세션 종료 후 24시간 삭제
끝난 요청(read·cancelled·expired) closed_at 후 7일 본문 포함 삭제
기기 정보 그 기기의 마지막 세션 종료 후 30일 삭제
개발 요청 공간에서 가져가거나 취소한 요청(dev-requests.md §7) 그 상태가 된 뒤 30일 넘기기 이력 포함 삭제
버그 신고 공간에서 닫은 버그(bugs.md §6) 닫힌 뒤 30일 삭제
  • 받는 세션이 모두 끝나 만료된 안 읽은 요청은, 받는 세션 중 가장 늦게 끝난 시각을 closed_at으로 보고 같은 7일 기준으로 지운다.
  • 종료된 세션이 아직 남아 있는 안 읽은 요청의 받는 세션이면, 만료 시각을 계산할 수 있도록 그 요청이 지워질 때까지 세션 행을 남긴다.

클라이언트는 삭제된 요청을 다시 조회할 수 없다. 오래 보관해야 하는 내용은 받은 쪽이 자기 방식으로 따로 남긴다.

표기: 필수 = v1 연동에 반드시 필요 · 상속 = evo 엔진 구현을 그대로 씀 · 직접 구현 = evo 엔진을 쓰지 않고 앱이 직접 구현 · 선택 · 후속 = 별도 Issue에서 정함

의무 Evo CLI evo Mac evo Windows evo-service(웹)
1. 활성 등록(§4) 필수. evo 코드 변경 필요(주기 신호, device_id 저장, agent·workspace 설정, 종료 시 end 시도) 상속(evo sidecar). 앱은 sidecar가 실행 중일 때만 활성 직접 구현(evo 엔진 미사용, evo-windows #12). client_kind evo-windows, device_id는 앱 데이터 폴더의 device-id(UUIDv4), 20초 주기 신호. 로그아웃·종료·업데이트 재시작 때 end(최대 6초 대기). 창을 닫으면 앱이 끝나며 트레이 상주는 없다 직접 구현(evo-service DispatchService, client_kind evo-web). 브라우저 탭이 대화를 20초마다 임대·갱신하고(POST /v1/presence), 탭이 모두 닫히거나 서비스가 끝날 때 end를 보낸다. session_id는 대화 UUID, device_id는 브라우저가 만든 UUID이며, 신호에는 SSO 토큰이 있어야 한다
2. 세션 교체 보고(§5) 필수. /clear·/compact·--fork 지점에 신호 추가 상속 직접 구현. /clear·compact 때 교체 보고 후속(대화 전환 규칙)
3. 받은 요청 확인 필수. 신호 응답의 안 읽은 요청 수로 알아채고 MCP tool로 확인한다. 입력 때 자동 주입(UserPromptSubmit hook의 additionalContext)은 선택. 사용자가 받은 요청 목록에서 하나를 골라 작업 지시로 진행하는 화면(예: 슬래시 커맨드)도 선택이며 evo 후속 Issue에서 한다. 고른 요청은 자동 실행하지 않고, 사용자가 작업 지시를 입력해야 진행한다. 상속. 앱 화면 알림은 후속 직접 구현(evo-windows #12). 앱 화면 알림은 후속 직접 구현(런타임 직접 호출, §9). 서버가 dispatch_inbox를 대신 불러 대화 화면에 안 읽은 요청을 보여 준다. 모델에는 열지 않는다
4. 읽음 표시 필수. MCP tool 상속 직접 구현(evo-windows #12) 직접 구현. 런타임이 사용자가 고른 요청에만 dispatch_read를 부른다(§9 런타임 직접 호출). 모델 도구로는 열지 않는다
5. 인증 필수. Keycloak 로그인 토큰을 Bearer로 첨부(evo#222) 상속 직접 구현. 데스크톱 로그인 클라이언트 evo-desktop 토큰을 Bearer로 첨부(서버 허용은) 직접 구현. SSO access token을 Bearer로 붙인다. 재배포로 서버 메모리의 토큰이 사라지면 다시 로그인할 때까지 활성 신호를 보내지 않는다(evo-service)
6. 호출 세션 표시(§9) 필수. evo 코드 변경 필요: MCP 요청마다 X-Dispatch-Client-Kind·X-Dispatch-Session-Id header에 지금 세션 값을 붙이고, 세션이 바뀌면 새 값으로 바꾼다. 지금의 고정 header 설정으로는 세션 교체를 따라가지 못한다. 상속 직접 구현. MCP 요청마다 X-Dispatch-* header 직접 구현. MCP 요청마다 X-Dispatch-Client-Kind(evo-web)·X-Dispatch-Session-Id(대화 UUID) header
7. 세션 주소 표시와 이름 정하기(§2) 필수. 자기 세션의 주소(agent·workspace)를 사용자에게 늘 보여 주고(예: 상태줄), 세션마다 agent를 정하고 바꿀 수 있게 한다. evo#263·#264·#265에서 구현 상속 직접 구현. 상태 줄에 주소 표시, 세션마다 이름 바꾸기 필수(연동 시작 때). 대화마다 주소를 보여 주고 agent를 정하게 한다
  • evo-service(웹) 열의 근거: evo-service backend/app/src/main/java/com/evomind/chat/dispatch/(DispatchService·DispatchClient·DispatchModelTools). E2E r2는 evo-service ·, 상대 기기 키 변경 경고는 이다. 2(세션 교체 보고)는 대화 전환 규칙이 정해지지 않아 아직 없다.
  • 권고(선택): 세션 목록을 보여 줄 때 agent·workspace·실행 방식이 같은 세션이 여럿이면 dispatch_sessions의 active_since(활성 시작 시각)를 함께 보여 사람이 구분하게 한다.
  • 각 프로젝트의 구현은 이 표를 근거로 각 저장소에 등록되는 요청 Issue에서 진행한다. 게시는 Human이 지시할 때 한다.
  • 꺼져 있는 세션에는 요청이 가지 않으므로, 1단계에는 PC 상주 수신 프로그램이 필요 없다. 상주 프로그램은 2026-10-26 취소했고, 워크플로우 그룹 실행을 만들 때 새 Issue로 다시 검토한다.

세부는 서버 구현(backend/)에서 정했다.

경로 쓰는 쪽 내용
POST /mcp 모델(MCP tool), 클라이언트 런타임 MCP Streamable HTTP. 서버에 MCP 세션 상태를 두지 않는다(stateless).
POST /v1/presence 클라이언트 런타임 활성 신호. 본문은 presence.schema.json, 응답은 {"schema_version":"1","unread":<안 읽은 요청 수>}. 모델이 호출하지 않는다.
GET /v1/push 클라이언트 런타임 (선택) 워크그룹 메시지 푸시 채널. push.md
GET /actuator/health 운영 상태 확인. 토큰 없이 열린다(/** 포함).
GET /actuator/info 운영 빌드·버전 정보. 토큰 없이 열린다.
  • 인증: /actuator/health(/** 포함)와 /actuator/info 말고는 모든 요청에 Keycloak access token을 Authorization: Bearer로 붙인다. 서버는 JWKS로 서명, iss, exp, 허용 azp, 토큰 종류를 검증한다. claim typ가 있으면 Bearer(access token)만 받는다. ID·refresh 토큰은 unauthorized(401)다. 서버에 받을 aud 값이 설정돼 있으면 토큰 aud에 그 값이 있어야 하고(다른 값과 함께여도 된다), 없으면 unauthorized(401)다. 설정이 없으면 aud를 보지 않는다. 값은 Keycloak 클라이언트 설정으로 들어가므로 클라이언트가 할 일은 없다. 서명 알고리즘은 서버 설정 목록(기본 RS256)만 받는다. JWKS는 캐시하고, 모르는 kid가 와도 issuer마다 30초에 최대 2번만 다시 받는다. 그 사이 모르는 kid 토큰은 unauthorized(401)다.
  • 호출 세션 표시: MCP 요청에는 X-Dispatch-Client-Kind와 X-Dispatch-Session-Id header로 지금 세션을 알린다. 서버는 이 세션을 토큰 사용자 안에서만 찾고, 활성 신호를 보내는 활성 세션일 때만 tool을 처리한다.
  • 런타임 직접 호출: 클라이언트 런타임은 모델을 거치지 않고 MCP tool을 직접 부를 수 있다(예: 받은 요청 목록 화면). 서버는 모델 호출과 런타임 호출을 구분하지 않으며, 둘 다 같은 Bearer 토큰과 X-Dispatch-Client-Kind·X-Dispatch-Session-Id header가 있어야 한다.
    • dispatch_read는 받는 세션이 부르는 순간 읽음이 되고, 같은 요청을 받은 다른 세션의 목록에서 빠진다(§6). 목록 화면에서는 dispatch_inbox만 부르고, 사용자가 고른 요청에만 dispatch_read를 부른다. 목록에서 고르지 않고 나가면 아무것도 읽음이 되지 않는다.
  • 로그: 요청 body와 토큰을 서버 로그에 남기지 않는다.

MCP tool:

tool 인자 결과
dispatch_send to(§3 주소), title, body, 선택 kind(기본 request), reply_to, links. E2E 확장(e2e.md)에서는 title·body·links 대신 enc(봉투)를 쓴다. 입력 형식에서 필수는 to뿐이고, 평문일 때 title·body가 없으면 서버가 invalid_request로 답한다 저장된 요청 전체(message.schema.json 형식). from은 서버가 호출 세션으로 채운다. recipients가 받는 세션 목록이다.
dispatch_inbox 선택 limit(1~50, 기본 20) 안 읽은 수와 받은 요청 목록. 오래된 순이며 본문은 빠진다.
dispatch_read id 받는 세션이 부르면 읽음 표시하고 본문을 돌려준다. 보낸 세션이 부르면 상태를 바꾸지 않고 지금 상태(안 읽음·읽음·만료)를 돌려준다.
dispatch_sessions 없음 내 활성 세션 목록(작업 담당 이름, 담당(persona, 있을 때만), 작업 공간 이름, 실행 방식, 지원 기능, 활성 시작·마지막 신호 시각, 자기 세션 여부). E2E 확장에서는 device_id와 (있으면) e2e 공개키가 더해진다(e2e.md)
  • 확장 tool: 워크그룹을 쓰는 클라이언트에는 tool 3종(dispatch_workgroup_create·dispatch_workgroups·dispatch_workgroup_leave)이 더 열린다(workgroup.md §6). 푸시는 MCP tool이 아니라 GET /v1/push(SSE)다(push.md).
  • 오류 응답: HTTP는 상태 코드와 {"error":"<코드>","message":"..."}로 답한다. MCP tool은 isError: true와 같은 모양의 결과로 답한다.
  • 결과 담기: tool 결과(정상·오류)는 content의 text 블록 하나에 JSON 문자열로 담는다. structuredContent와 outputSchema는 없다. 클라이언트는 content[0].text를 JSON으로 푼다.
  • MCP 프로토콜 오류: 서버가 모르는 JSON-RPC 메서드(예: server/discover)에는 HTTP 200과 JSON-RPC 오류 {"jsonrpc":"2.0","id":<요청 id>,"error":{"code":-32601,"message":"..."}}로 답한다. 처리 중 예기치 않은 오류는 -32603이다. 오류에는 code·message만 있다. 클라이언트는 -32601을 “지원하지 않음”으로 보고 다음 단계로 넘어간다.
    • JSON으로 읽을 수 없는 본문은 HTTP 400과 {"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error"}}다. JSON이지만 JSON-RPC 요청이 아니면 HTTP 400과 -32600이다.
  • 본문 크기: 요청 본문은 서버 설정 상한(기본 256KiB) 안이어야 한다. 넘으면 HTTP 413과 {"error":"invalid_request",...}다. 규격 최대 길이의 요청은 상한 안이다.
  • 요청 수: 서버는 인증된 사용자마다 1분 요청 수를 센다(/mcp·/v1/presence 합계, 기본 300). 넘으면 HTTP 429, Retry-After(초), {"error":"rate_limited",...}다. 클라이언트는 Retry-After만큼 기다렸다가 다시 보낸다. 20초 활성 신호와 보통 tool 호출은 한도에 닿지 않는다.
  • id 인자: 전체 UUID 또는 대시 없는 소문자 16진수 8~32자 접두사다. 접두사는 호출자가 볼 수 있는 범위 안에서 하나일 때만 해석되고, 결과의 id는 항상 전체 UUID다. 없으면 not_found, 2건 이상이면 ambiguous(409)다. 형식이 아니면 invalid_request다.
  • tool 설명·입력 형식: 모델 요청마다 붙으므로 짧게 둔다. 입력 형식에는 인자 이름·타입·필수·enum·limit 범위만 있다. 글자 수, 이름 형식, 정의되지 않은 필드, links 형식은 서버가 검사해 invalid_request로 답한다. 인자와 결과는 위 표 그대로다.
  • 보낸 쪽 취소(unread → cancelled)는 v1 tool에 없다. 필요해지면 후속 Issue로 정한다.
코드 언제
no_active_recipient 보낼 때 주소에 맞는 활성 세션이 없다. 답장 대상 세션이 종료됐다.
not_recipient recipients에 없는 세션이나 종료된 세션이 읽음 표시를 했다.
invalid_transition 금지된 상태 전이를 요청했다(예: 읽은 요청 취소).
not_found 없거나 보존 기간이 지나 삭제된 요청이다. 다른 사용자의 요청도 같은 코드로 답한다(있는지 알리지 않는다).
unauthorized 토큰이 없거나 검증에 실패했다.
inactive_session MCP tool을 부른 세션이 활성이 아니다(활성 신호를 보내지 않았거나 끝났다).
invalid_request 형식이 규격과 맞지 않는다(Schema 위반, 호출 세션 header 없음 등). 본문이 상한을 넘으면 HTTP 413이다.
ambiguous id 접두사에 해당하는 항목이 권한 범위 안에서 2건 이상이다. 더 긴 접두사를 쓴다. 후보 ID는 돌려주지 않는다.
rate_limited 요청 수 한도를 넘었다. HTTP 429와 Retry-After.
  • 사용자 격리: 서버는 모든 조회·변경을 토큰의 사용자 안에서만 한다. 다른 사용자의 세션 목록, 요청, 안 읽은 수는 어떤 tool·endpoint로도 보이지 않는다.
  • 요청 body에는 비밀정보(토큰, 비밀번호, 내부 IP)를 넣지 않는다.
  • v1의 요청은 실행 명령이 아니다.
    • 받는 세션은 요청을 읽고, 사람이나 자기 규칙에 따라 처리한다.
    • 자동 실행(2·3단계)은 extensions.execution과 별도 보안 검토 Issue로 정한다.
  • 클라이언트는 작업 폴더 경로 원문을 보내지 않는다.
터미널 창
python3 - <<'EOF'
import json, glob, jsonschema
S = {n: json.load(open(f"docs/protocol/schemas/{n}.schema.json")) for n in ("message", "presence")}
for kind, expect_ok in (("valid", True), ("invalid", False)):
for f in sorted(glob.glob(f"docs/protocol/examples/{kind}/*.json")):
s = S["presence" if "/presence-" in f else "message"]
ok = not list(jsonschema.Draft202012Validator(s).iter_errors(json.load(open(f))))
print("OK " if ok == expect_ok else "BAD", f)
EOF

examples/valid/는 모두 통과해야 하고, examples/invalid/는 모두 거부돼야 한다.

  • 하위 호환이 깨지는 변경은 schema_version을 올린다. 각 프로젝트 요청 Issue에는 새 버전을 다시 게시한다.
  • 선택 필드 추가처럼 하위 호환 변경은 변경 이력에만 기록한다.
버전 날짜 내용
v1 (초안 r2) 2026-10-17 최초 작성. 처리 상태·맡기·GitLab 경로 주소 포함
v1 (초안 r3) 2026-10-17 요청 전달만 남기고 처리 상태·맡기·결과 필드 삭제. GitLab·git 전제 삭제, 주소를 작업 담당·작업 공간 이름으로 변경. request.schema.json을 message.schema.json으로 대체
v1 2026-10-18 r3 내용 그대로 Human 승인
v1 (초안 r4) 2026-10-18 활성 세션에만 보내기, 받는 세션 목록(recipients)을 보낼 때 고정, 받는 세션이 모두 종료되면 만료, 7일 기한·답장 넓히기 삭제. 활성 신호 세션당 1행·보존 기간·오류 코드 추가. 여러 사용자·여러 세션 전제와 사용자 격리(내 세션 사이만) 명시. 아직 구현·게시한 곳이 없어 v1 유지
v1 (r4) 2026-10-18 r4 내용 그대로 Human 승인 (4df379a)
v1 (r4 보충) 2026-10-18 서버 구현에 맞춰 §9 서버 인터페이스 세부(경로, 호출 세션 header, tool 인자·결과, 오류 코드 inactive_session·invalid_request), §8 의무 6(호출 세션 표시), §7 만료 요청 정리 기준을 채웠다. Schema 변경 없음
v1 (r4 보충 2) 2026-10-19 . §9에 클라이언트 런타임의 MCP tool 직접 호출(같은 토큰·호출 세션 header 필요)과 dispatch_read 읽음 시점 주의, §8 의무 3에 받은 요청을 골라 사용자 지시로 진행하는 화면(선택)을 적었다. Schema·서버 동작 변경 없음
v1 (r4 보충 3) 2026-10-19 (evo#267). §3 session 주소를 답장 전용에서 처음 요청에도 쓸 수 있게 넓혔다. session_id가 세션 교체 때 바뀐다는 주의를 적었다. message.schema.json의 “세션 주소면 답장 필수” 조건을 뺐고, 해당 무효 샘플을 유효 샘플 message-request-session.json으로 옮겼다. 하위 호환
v1 (r4 보충 4) 2026-10-19 . 별개 규격인 공용 요청 공간 board.md 링크만 더했다. 이 규격의 내용·Schema 변경 없음
v1 (r4 보충 5) 2026-10-19 (evo#264). §2 작업 담당 이름을 세션마다 정하고 바꿀 수 있다(바꿔도 session_id·받은 요청 유지)로 고치고, 클라이언트 종류가 달라도 같은 사용자(iss+sub)일 때만 주고받는다고 적었다. §3 이름 주소는 client_kind와 관계없다. §8 의무 7(세션 주소 표시와 이름 정하기)과 같은 이름 세션 구분 권고를 더했다. 지금 서버 동작을 적은 것이라 Schema·서버 변경 없음
v1 (r4 보충 6) 2026-10-23 (evo-cli 요청). §9에 MCP 프로토콜 오류 응답(모르는 메서드 -32601, HTTP 200, code·message만)을 적었다. Schema 변경 없음
v1 (r4 보충 7) 2026-10-26 (evo-cli 요청). §9 tool 설명·입력 형식을 짧게 두고 형식 검사는 서버가 한다고 적었다. tool·인자 이름, 결과, 서버 검사, Schema 변경 없음
v1 (r4 보충 8) 2026-10-26 (evo-windows 요청 cf9ba764). §8 evo Windows 열을 ’상속’에서 ’직접 구현’으로 고쳤다(evo-windows #12). 표기에 ’직접 구현’을 더했다. 다른 열·Schema·서버 동작 변경 없음
v1 (r4 보충 9) 2026-10-27 (A-1). §9 인증에 서명 알고리즘 목록(기본 RS256)과 JWKS 다시 받기 제한을 적었다. Keycloak 기본 토큰은 그대로 통한다. Schema·tool 변경 없음
v1 (r4 보충 10) 2026-10-27 (D-1·E-1). §9에 본문 크기 상한(413 invalid_request)과 읽을 수 없는 MCP 본문의 JSON-RPC 오류(-32700·-32600, code·message만)를 적었다. 정상 요청·Schema·tool 변경 없음
v1 (r4 보충 11) 2026-10-27 (D-2). §9에 사용자별 요청 수 한도와 오류 코드 rate_limited(HTTP 429, Retry-After)를 더했다. 하위 호환: 정상 사용은 한도에 닿지 않는다. Schema·tool 변경 없음
v1 (r4 보충 12) 2026-10-27 (A-2 일부·A-3). §9 인증에 토큰 종류(typ) 검사를 적었다. Keycloak access token은 그대로 통한다. Schema·tool 변경 없음
v1 (r4 보충 13) 2026-10-27 (D-3). §7 보존 표에 요청 공간의 끝난 요청(30일)을 더했다. /mcp 동작·Schema·tool 변경 없음
v1 (r4 보충 14) 2026-10-28 (O6). §9 결과 담기: tool 결과를 text 블록 하나에만 담고 structuredContent를 뺐다(같은 JSON을 두 번 싣던 것). 결과 JSON의 필드·값은 그대로다. 클라이언트 4곳(evo-cli·evo-service·evo-mac·evo-windows)이 text를 읽는다고 확인했다
v1 (r4 보충 15) 2026-10-28 (A-2 나머지). §9 인증에 aud 검사를 적었다. 서버 설정이 있을 때만 검사하며 기본은 꺼짐이다. 켜는 값은 evo-dispatch이고 Keycloak 클라이언트 3개(evo-cli·evo-web·evo-desktop)에 Audience mapper를 붙인 뒤 켠다(evo-infra). 클라이언트 코드·Schema·tool 변경 없음
v1 (r4 보충 16) 2026-10-28 ·. 머리말에 세 공간(세션 요청 공간 /mcp, 버그 신고 공간 /mcp/bugs, 개발 요청 공간 /mcp/dev)과 규격 링크, §7 보존 표에 닫은 버그(30일)와 개발 요청 공간 이름. /mcp 동작·Schema·tool 변경 없음
v1 (r4 보충 17) 2026-11-03 (D1~D5). 선택 확장 e2e.md(종단간 암호화 초안 r1) 추가: message.schema.json에 선택 필드 enc·from_keys(있으면 title·body·links 없음), presence.schema.json에 선택 필드 e2e, 오류 코드 recipient_mismatch·e2e_required(문서에만, 서버는), 시험 벡터 examples/e2e/. enc·e2e를 쓰지 않는 기존 평문 메시지·클라이언트는 그대로 유효하다(하위 호환). 서버 동작은 변하지 않았다
v1 (r4 보충 18) 2026-11-03 . E2E 확장을 서버에 구현했다: dispatch_send의 enc, 활성 신호의 e2e, dispatch_sessions의 device_id·e2e, 오류 코드 recipient_mismatch(409)·e2e_required(400), 설정 REQUIRE_E2E(기본 꺼짐). dispatch_send 입력 형식의 필수가 to뿐으로 바뀌었다(평문 필수 여부는 서버가 검사). 기존 평문 동작은 그대로다
v1 (r4 보충 19) 2026-11-03 (evo-cli 요청 6c29ee93). 선택 확장 workgroup.md(워크그룹 초안 r1) 추가: message.schema.json의 to.kind에 workgroup·workgroup_member, extensions.execution에 auto·workgroup_id·hop 모양, tool 3종(dispatch_workgroup_create·dispatch_workgroups·dispatch_workgroup_leave), 오류 코드 not_group_member(403). 그룹 주소를 쓰지 않는 기존 클라이언트는 영향이 없다(하위 호환)
v1 (r4 보충 20) 2026-11-03 (evo-cli 요청 a3daca60·e88ecc0d). e2e.md §ctx에 워크그룹 주소의 to 정규 문자열(workgroup:<id>·workgroup_member:<id>:<alias>)과 받을 기기 고르는 법을 적었다. 서버 동작·봉투 모양은 그대로다
v1 (r4 보충 21) 2026-11-03 (evo-cli 요청 e88ecc0d, 사용자 결정). E2E r2: 봉투 v 2는 extensions.execution을 서명 범위(ctx 끝 execution 필드, VER evo-dispatch/e2e/2)에 넣는다. 서버는 enc와 extensions.execution이 함께 오면 enc.v 2를 요구한다. v 1은 그대로 통한다(하위 호환)
v1 (r4 보충 22) 2026-11-03 (evo-cli 요청 35b4ebc2). 선택 확장 push.md(워크그룹 푸시 채널 초안 r1, 규격만) 추가: GET /v1/push(SSE), 워크그룹 메시지에만 message.created 알림, 재연결 시 읽지 않은 워크그룹 메시지 재전송, presence.capabilities에 push. 서버 구현은 후속이다. 기존 클라이언트와 서버 동작은 그대로다
v1 (r4 보충 23) 2026-11-03 . 푸시 채널 서버 구현: GET /v1/push(SSE)가 열렸다. 규격은 push.md 그대로이며 클라이언트 쪽 변경은 없다
v1 (r4 보충 24) 2026-11-03 (evo-cli 요청 a42e5027). E2E r2 공유 시험 벡터 examples/e2e/vector-r2.json과 생성기(GenerateVector.java r2)·검증기(verify.mjs vector-r2.json)를 더했다. 규격·서버 동작은 그대로다
v1 (r4 보충 25) 2026-11-03 (evo-cli 요청 a5ba6b17). 활성 신호에 선택 필드 persona(담당)를 더했고, dispatch_sessions와 dispatch_workgroups의 멤버 행이 있을 때 persona를 돌려준다. 보내지 않는 클라이언트는 그대로 동작한다. 서버 배포 전에는 모르는 필드로 invalid_request(400)다
v1 (r4 보충 26) 2026-11-05 (evo-cli 요청 39cd3255). id를 받는 도구(dispatch_read, /mcp/dev의 dev_cancel·dev_release·dev_forward·bug_release·bug_close·bug_image)가 전체 UUID 대신 대시 없는 소문자 16진수 8~32자 접두사도 받는다. 권한 범위 안에서 하나면 그 항목으로 해석하고, 0건은 not_found, 2건 이상은 새 오류 코드 ambiguous(409)다. 응답 id와 기존 전체 UUID 동작은 그대로다(하위 호환)
v1 (r4 보충 27) 2026-11-05 . 구현과 어긋난 서술만 고쳤다: §8 evo-service 열의 의무 1·3·4·5·6을 구현 완료로(2만 후속), §9에서 토큰 없이 열리는 경로에 /actuator/info를 더하고 확장 tool 3종·GET /v1/push 포인터를 적었다. 규범 문장·Schema·서버 동작 변경 없음