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
develop9efab1a
1. 이 규격이 하는 일
섹션 제목: “1. 이 규격이 하는 일”활성 세션끼리 요청을 보내고 받는다. 그것만 한다.
세션 A ── 요청 보내기 ──▶ evo-dispatch ◀── 활성 신호(20초) ── 세션 B │세션 B ◀── 받은 요청 확인·읽음 표시 ──┘세션 B: 받은 요청을 자기 방식대로 처리한다 (dispatch는 관여하지 않는다)세션 B ── (원하면) 답장 보내기 ──▶ 세션 A (A가 아직 활성일 때만)다음 원칙을 지킨다.
- 여러 사용자가 각자 여러 세션을 가진다. 한 사용자는 여러 기기에서 여러 클라이언트(Evo CLI 여러 개, Mac 앱, Windows 앱 등)를 동시에 열 수 있다. 서버 하나가 모든 사용자를 받는다.
- 요청은 내 세션 사이에서만 오간다. 다른 사용자의 세션에는 보낼 수 없고, 다른 사용자의 세션·요청은 보이지 않는다. 사용자 사이 전송은 v1 범위가 아니다.
- 활성 세션에만 보낸다. 보내는 순간 받을 활성 세션이 없으면 보내기를 거부한다. 종료된 세션은 요청을 받을 수 없다.
- dispatch는 전달만 한다. 받은 쪽이 요청을 어떻게 처리하는지 추적하지 않는다. 결과를 알리고 싶으면 답장을 보낸다.
- 특정 플랫폼을 전제하지 않는다. GitLab, git 저장소, 이슈, MR 같은 것은 필수 필드나 흐름의 전제가 아니다. 링크가 필요하면 선택 항목인
links에 자유 URL로 넣는다. - 쓰고 남은 데이터는 지운다. 활성 신호는 이력을 쌓지 않고, 종료된 세션과 끝난 요청은 보존 기간 뒤 삭제한다(§7).
2. 신원
섹션 제목: “2. 신원”| 항목 | 필수 | 값 | 바뀌는가 | 출처 |
|---|---|---|---|---|
| 사용자 | 필수 | 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)를 선택으로 붙일 수 있다. 경로 원문은 보내지 않는다.
3. 요청 주소와 받는 세션
섹션 제목: “3. 요청 주소와 받는 세션”새 요청은 바뀌지 않는 이름으로 주소를 정한다. 서버는 보내는 순간 그 주소에 맞는 활성 세션을 찾아 받는 세션 목록(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가 있어야 한다.
4. 활성 세션(presence)
섹션 제목: “4. 활성 세션(presence)”클라이언트는 세션이 열려 있는 동안 활성 신호를 보낸다. 형식은 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 만료로 종료를 판정한다.- 활성 세션 목록은 같은 사용자의 세션끼리만 보인다.
5. 세션 교체와 종료
섹션 제목: “5. 세션 교체와 종료”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).
6. 요청 형식과 상태
섹션 제목: “6. 요청 형식과 상태”형식은 schemas/message.schema.json을 따른다.
필수 항목:
schema_version,idkind:request(요청) 또는reply(답장)from,torecipients: 받는 세션 목록. 서버가 채우며 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→unreadread→cancelled: 이미 읽은 요청은 보낸 쪽이 취소할 수 없다. 필요하면 새 요청으로 알린다.cancelled·expired에서 다른 상태로recipients에 없는 세션이나 종료된 세션의 읽음 표시
7. 데이터 저장과 정리
섹션 제목: “7. 데이터 저장과 정리”활성 신호는 20초마다 오므로 이력을 쌓으면 세션 하나가 하루에 4,320건을 만든다. 그래서 서버는 다음 원칙으로 저장하고 정리한다. 저장 구조와 정리 작업의 구현은 에서 정한다.
- 활성 신호는 세션당 1행만 둔다. 신호가 오면 마지막 신호 시각만 갱신하고, 신호 이력은 남기지 않는다.
- 정리 작업은 삭제만 맡는다. 활성·만료 판정은 조회 시점에 계산하므로, 정리 작업이 멈추거나 늦어도 동작은 틀리지 않는다.
| 대상 | 보존 기간 | 처리 |
|---|---|---|
| 종료된 세션 | 종료 후 24시간 | 삭제 |
끝난 요청(read·cancelled·expired) |
closed_at 후 7일 |
본문 포함 삭제 |
| 기기 정보 | 그 기기의 마지막 세션 종료 후 30일 | 삭제 |
개발 요청 공간에서 가져가거나 취소한 요청(dev-requests.md §7) |
그 상태가 된 뒤 30일 | 넘기기 이력 포함 삭제 |
버그 신고 공간에서 닫은 버그(bugs.md §6) |
닫힌 뒤 30일 | 삭제 |
- 받는 세션이 모두 끝나 만료된 안 읽은 요청은, 받는 세션 중 가장 늦게 끝난 시각을
closed_at으로 보고 같은 7일 기준으로 지운다. - 종료된 세션이 아직 남아 있는 안 읽은 요청의 받는 세션이면, 만료 시각을 계산할 수 있도록 그 요청이 지워질 때까지 세션 행을 남긴다.
클라이언트는 삭제된 요청을 다시 조회할 수 없다. 오래 보관해야 하는 내용은 받은 쪽이 자기 방식으로 따로 남긴다.
8. 클라이언트별 의무
섹션 제목: “8. 클라이언트별 의무”표기: 필수 = 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로 다시 검토한다.
9. 서버 인터페이스
섹션 제목: “9. 서버 인터페이스”세부는 서버 구현(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, 토큰 종류를 검증한다. claimtyp가 있으면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-Idheader로 지금 세션을 알린다. 서버는 이 세션을 토큰 사용자 안에서만 찾고, 활성 신호를 보내는 활성 세션일 때만 tool을 처리한다. - 런타임 직접 호출: 클라이언트 런타임은 모델을 거치지 않고 MCP tool을 직접 부를 수 있다(예: 받은 요청 목록 화면). 서버는 모델 호출과 런타임 호출을 구분하지 않으며, 둘 다 같은 Bearer 토큰과
X-Dispatch-Client-Kind·X-Dispatch-Session-Idheader가 있어야 한다.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이다.
- JSON으로 읽을 수 없는 본문은 HTTP 400과
- 본문 크기: 요청 본문은 서버 설정 상한(기본 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. |
10. 보안 규칙
섹션 제목: “10. 보안 규칙”- 사용자 격리: 서버는 모든 조회·변경을 토큰의 사용자 안에서만 한다. 다른 사용자의 세션 목록, 요청, 안 읽은 수는 어떤 tool·endpoint로도 보이지 않는다.
- 요청
body에는 비밀정보(토큰, 비밀번호, 내부 IP)를 넣지 않는다. - v1의 요청은 실행 명령이 아니다.
- 받는 세션은 요청을 읽고, 사람이나 자기 규칙에 따라 처리한다.
- 자동 실행(2·3단계)은
extensions.execution과 별도 보안 검토 Issue로 정한다.
- 클라이언트는 작업 폴더 경로 원문을 보내지 않는다.
11. 검증 방법
섹션 제목: “11. 검증 방법”python3 - <<'EOF'import json, glob, jsonschemaS = {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)EOFexamples/valid/는 모두 통과해야 하고, examples/invalid/는 모두 거부돼야 한다.
12. 변경 관리
섹션 제목: “12. 변경 관리”- 하위 호환이 깨지는 변경은
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·서버 동작 변경 없음 |
답변은 문서를 근거로 만들지만 틀릴 수 있습니다. 출처 문서를 함께 확인하세요.