[Discord Decision MCP] 아키텍처 설계문서

Discord Decision MCP — 아키텍처 설계문서

1. 프로젝트 개요

1.1 목적

Claude Code가 tmux Teammate 모드로 자율 작업 중, 사용자 결정이 필요한 시점에 Discord를 통해 질문하고 응답을 받아 작업을 재개하는 MCP (Model Context Protocol) 서버입니다.

1.2 핵심 특징

특징설명
프로젝트당 Bot 1개각 프로젝트는 독립된 Discord Bot 사용
무한 대기 기본값Timeout 없이 사용자 응답 대기 (Claude가 독단 진행 금지)
상태 영속화프로세스 재시작 후에도 대기 상태 복원
한국어 친화적한글 선택지, Yes/No 응답 지원
MCP 기반 통신모든 Discord 통신은 MCP Tool을 통해 이루어짐

1.3 버전 정보

  • 버전: 1.0.0
  • Python 요구사항: >= 3.11
  • MCP 프레임워크: FastMCP >= 0.1.0

2. 디렉토리 구조

discord-decision/
├── discord_mcp/              # 메인 패키지
   ├── __init__.py
   ├── server.py             # MCP 서버 진입점
   ├── config.py             # 환경변수 설정 관리
   
   ├── bot/                  # Discord API 클라이언트
      ├── __init__.py
      ├── client.py         # REST API 클라이언트 (httpx)
      └── gateway.py        # WebSocket 게이트웨이 (수신 전용)
   
   ├── decision/             # 결정 요청 관리
      ├── __init__.py
      ├── manager.py        # 결정 요청 생명주기 관리
      ├── poller.py         # Discord Polling 및 응답 대기
      ├── parser.py         # 사용자 응답 파싱
      └── state.py          # 상태 영속화 (JSON 파일)
   
   ├── tools/                # MCP Tools 구현
      ├── __init__.py
      ├── ask.py            # discord_ask_decision
      ├── notify.py         # discord_notify
      ├── report.py         # discord_report_progress
      ├── status.py         # discord_check_pending
      ├── inbox.py          # discord_read_inbox, discord_clear_inbox
      └── _templates.py     # Discord 메시지 템플릿
   
   └── daemon/               # 감시 데몬
       ├── __init__.py
       ├── watcher.py        # Discord 채널 감시 (discord-watch CLI)
       └── inbox.py          # Inbox 파일 관리

├── scripts/                  # 유틸리티 스크립트
   └── start-discord-watch.sh

├── tests/                    # 테스트 스위트
   ├── __init__.py
   ├── test_parser.py
   └── test_state.py

├── CLAUDE.md                 # Claude Code 프로젝트 지침
├── README.md                 # 프로젝트 설명
├── pyproject.toml            # 프로젝트 설정 및 의존성
└── mcp.json.example          # MCP 설정 예시

3. 주요 모듈 설명

3.1 서버 진입점 (server.py)

역할: FastMCP 서버 생성 및 MCP Tool 등록

mcp = FastMCP(name="discord-decision-mcp")
# Tool 등록
mcp.tool()(discord_ask_decision)
mcp.tool()(discord_notify)
mcp.tool()(discord_report_progress)
mcp.tool()(discord_check_pending)
mcp.tool()(discord_read_inbox)
mcp.tool()(discord_clear_inbox)

실행 방법:

discord-mcp  # 또는 python -m discord_mcp.server

3.2 설정 관리 (config.py)

역할: 환경변수에서 설정값 로드

환경변수설명기본값
DISCORD_BOT_TOKENDiscord Bot Token (“Bot " 접두사 포함)필수
DISCORD_CHANNEL_ID기본 질문 채널 ID필수
PROJECT_NAMEquestion_id 생성에 사용“project”
PENDING_DIR상태 파일 저장 경로~/.claude/pending_decisions
POLL_INTERVAL_SECONDSDiscord polling 간격 (초)5

3.3 Discord API 클라이언트 (bot/)

3.3.1 REST API 클라이언트 (client.py)

역할: Discord REST API 호출 (httpx 기반)

주요 메서드:

class DiscordClient:
    async def send_message(channel_id, content, embeds=None)
    async def get_messages(channel_id, after=None, limit=50)
    async def create_thread(channel_id, name, first_message)
    async def archive_thread(thread_id)

특징:

  • Rate limit 자동 처리 (429 응답 시 재시도)
  • 싱글턴 패턴 (get_client())

3.3.2 WebSocket 게이트웨이 (gateway.py)

역할: Discord Gateway WebSocket 연결 (수신 전용)

특징:

  • MESSAGE_CREATE 이벤트 실시간 수신
  • Polling 방식의 지연을 5초 → 즉시로 단축
  • Gateway 없이도 시스템 정상 동작 (Polling fallback)
  • 자동 재연결 (Exponential backoff)
class GatewayClient:
    async def run()     # 이벤트 수신 시작
    async def stop()    # 연결 종료

3.4 결정 요청 관리 (decision/)

3.4.1 DecisionManager (manager.py)

역할: 결정 요청의 전체 생명주기 조율

class DecisionManager:
    async def ask(question, context, options, timeout_seconds, thread_id) -> PollResult
    async def restore_pending() -> list[DecisionState]

흐름:

  1. 중복 질문 확인
  2. Thread 생성 (또는 기존 Thread 재사용)
  3. 상태 파일 생성 (~/.claude/pending_decisions/{question_id}.json)
  4. Poller 실행하여 응답 대기
  5. SIGHUP 시그널 핸들러로 disconnected 상태 표시

3.4.2 DecisionPoller (poller.py)

역할: Discord 채널을 polling하여 응답 대기

class DecisionPoller:
    async def wait(state: DecisionState) -> PollResult

특징:

  • 기본값: 무한 대기 (timeout_seconds=None)
  • tmux pane에 실시간 대기 상태 표시 (ANSI escape)
  • 모호한 응답 시 최대 2회 재질문

3.4.3 응답 파서 (parser.py)

역할: 사용자 Discord 응답 파싱

지원 패턴:

  • 선택지 매칭: “A”, “a”, “A번”, “A로 해줘”, “1”, “1번”
  • 긍정/부정: “yes”, “네”, “예”, “no”, “아니요”
  • 자연어: 15자 이상 또는 한글 3자 이상
def parse_response(text: str, options: list[str]) -> ParseResult
def build_clarify_message(...) -> str  # 재질문 메시지 생성

3.4.4 상태 저장소 (state.py)

역할: 결정 요청 상태를 JSON 파일로 영속화

상태 파일 위치: ~/.claude/pending_decisions/{question_id}.json

class DecisionState(BaseModel):
    question_id: str
    project: str
    question: str
    context: str
    options: list[str]
    timeout_seconds: float | None
    thread_id: str
    message_id: str
    asked_at: str
    status: Literal["pending", "disconnected", "resolved", "aborted", "timeout"]
    clarify_attempts: int
    resolved_at: str | None
    resolution: str | None
    selected_option: str | None

class StateStore:
    def save(state: DecisionState)
    def load(question_id: str) -> DecisionState | None
    def load_all_pending() -> list[DecisionState]
    def resolve(question_id, resolution, selected_option)
    def is_duplicate(question: str) -> bool

3.5 MCP Tools (tools/)

Tool블로킹설명
discord_ask_decision사용자 결정 요청
discord_notify진행 상황 알림
discord_report_progress작업 완료 리포트
discord_check_pending미해결 질문 확인
discord_read_inboxInbox 메시지 조회
discord_clear_inboxInbox 메시지 삭제

3.6 감시 데몬 (daemon/)

3.6.1 DiscordWatcher (watcher.py)

역할: Discord 채널을 감시하여 새 메시지를 inbox 파일에 기록

실행 방법:

discord-watch --interval 10
# 또는
tmux new-session -d -s discord-watch 'uv run discord-watch'

특징:

  • 지정된 채널(들)을 주기적으로 polling
  • 새로운 사용자 메시지 감지 시 inbox에 기록
  • tmux pane에 상태 표시

3.6.2 InboxStore (inbox.py)

역할: Discord 메시지를 저장하는 JSON 파일 기반 저장소

Inbox 파일 위치: ~/.claude/discord_inbox.json

class InboxMessage:
    message_id: str
    channel_id: str
    thread_id: str | None
    author: str
    author_id: str
    content: str
    timestamp: str
    read: bool

class InboxStore:
    def add_message(msg: InboxMessage)
    def get_unread() -> list[InboxMessage]
    def mark_read(message_id: str)
    def clear_read()  # 읽은 메시지 삭제

4. 데이터 흐름도

4.1 결정 요청 흐름

┌─────────────────┐
│  Claude Code    │
└────────┬────────┘
         │ discord_ask_decision()
┌─────────────────────────────────────┐
│  MCP Server (server.py)             │
│  ┌───────────────────────────────┐  │
│  │ DecisionManager.ask()         │  │
│  │  1. 중복 확인                  │  │
│  │  2. Thread 생성               │  │
│  │  3. 상태 파일 저장             │  │
│  └───────────────┬───────────────┘  │
└──────────────────┼──────────────────┘
┌─────────────────────────────────────┐
│  DecisionPoller.wait()              │
│  ┌───────────────────────────────┐  │
│  │ Polling 루프                   │  │
│  │  1. tmux 상태 업데이트         │  │
│  │  2. Discord API 호출          │  │
│  │  3. 응답 파싱                  │  │
│  │  4. 모호하면 재질문            │  │
│  └───────────────┬───────────────┘  │
└──────────────────┼──────────────────┘
         ┌─────────┴─────────┐
         ▼                   ▼
┌───────────────────┐  ┌──────────────────┐
│  Discord REST API │  │  Discord Thread  │
│    (client.py)    │  │                  │
└───────────────────┘  └────────┬─────────┘
                        ┌────────▼────────┐
                        │  사용자 응답    │
                        └────────┬────────┘
                        ┌─────────────────┐
                        │  parser.py      │
                        │  - 선택지 매칭  │
                        │  - 모호성 감지  │
                        └────────┬────────┘
                        ┌─────────────────┐
                        │  store.resolve()│
                        └────────┬────────┘
                        ┌─────────────────┐
                        │  PollResult     │
                        │  반환           │
                        └─────────────────┘

4.2 감시 데몬 흐름

┌─────────────────────────────────────────────┐
│  DiscordWatcher (daemon/watcher.py)         │
│  ┌───────────────────────────────────────┐  │
│  │ 1. 초기 메시지 ID 설정                 │  │
│  │ 2. 주기적 Polling (기본 10초)         │  │
│  │ 3. 새 메시지 감지                     │  │
│  │ 4. InboxStore.add_message()           │  │
│  └───────────────────┬───────────────────┘  │
└──────────────────────┼──────────────────────┘
┌─────────────────────────────────────────────┐
│  ~/.claude/discord_inbox.json               │
│  {                                          │
│    "last_message_id": "...",                │
│    "messages": [                            │
│      { "message_id": "...", ... }           │
│    ]                                        │
│  }                                          │
└──────────────────────┬──────────────────────┘
                       │ discord_read_inbox()
┌──────────────────────┴──────────────────────┐
│  Claude Code                                │
└─────────────────────────────────────────────┘

4.3 세션 복원 흐름

┌─────────────────────────────────────────────┐
│  Claude Code 세션 시작                      │
└──────────────────────┬──────────────────────┘
┌─────────────────────────────────────────────┐
│  discord_check_pending()                    │
└──────────────────────┬──────────────────────┘
┌─────────────────────────────────────────────┐
│  DecisionManager.restore_pending()          │
│  ┌───────────────────────────────────────┐  │
│  │ 1. ~/.claude/pending_decisions/*.json │  │
│  │ 2. Discord API로 Thread 확인          │  │
│  │ 3. 답변 있으면 → 자동 해결            │  │
│  │ 4. 답변 없으면 → 재시작 알림          │  │
│  └───────────────────────────────────────┘  │
└─────────────────────────────────────────────┘

5. MCP 도구 목록

5.1 discord_ask_decision

설명: 사용자 결정이 필요할 때 Discord Thread에 질문 전송 후 응답 대기 (블로킹)

파라미터:

이름타입필수설명
questionstring질문 내용
contextstring현재 작업 상황
optionsstring[]선택지 목록 (빈 배열이면 자유 응답)
timeout_secondsfloat대기 Timeout (null이면 무한 대기)
thread_idstring기존 Thread ID (null이면 새 Thread 생성)

반환값:

{
  "success": true,
  "answer": "A) 지금 실행",
  "selected_option": "A) 지금 실행",
  "question_id": "project_20260301_abc123",
  "timed_out": false,
  "aborted": false
}

5.2 discord_notify

설명: 진행 상황을 Discord에 알림 (논블로킹)

파라미터:

이름타입필수설명
messagestring알림 메시지
levelstringinfo/warning/success/error (기본값: info)
thread_idstring전송할 Thread ID (null이면 기본 채널)

5.3 discord_report_progress

설명: 작업 완료 또는 단계 완료 시 결과 리포트 (논블로킹)

파라미터:

이름타입필수설명
titlestring리포트 제목
summarystring작업 결과 요약
detailsstring[]세부 항목 목록
thread_idstring전송할 Thread ID

5.4 discord_check_pending

설명: 세션 시작 시 미해결 질문 확인 (논블로킹)

파라미터: 없음

반환값:

{
  "has_pending": true,
  "pending_questions": [
    {
      "question_id": "project_20260301_abc123",
      "question": "배포할까요?",
      "thread_id": "1234567890",
      "asked_at": "2026-03-01T10:30:00Z",
      "status": "pending"
    }
  ]
}

5.5 discord_read_inbox

설명: Inbox에 저장된 메시지 조회 (논블로킹)

파라미터:

이름타입기본값설명
unread_onlybooleantrue읽지 않은 메시지만 반환
mark_readbooleanfalse조회한 메시지를 읽음으로 표시

5.6 discord_clear_inbox

설명: Inbox 메시지 삭제 (논블로킹)

파라미터:

이름타입기본값설명
read_onlybooleantrue읽은 메시지만 삭제

6. 의존성 정보

6.1 핵심 의존성

패키지버전용도
fastmcp>= 0.1.0MCP 서버 프레임워크
httpx>= 0.27.0비동기 HTTP 클라이언트
websockets>= 12.0WebSocket 클라이언트
python-dotenv>= 1.0.0환경변수 로드
pydantic>= 2.0.0데이터 모델 검증
anyio>= 4.0.0비동기 실행

6.2 개발 의존성

패키지용도
pytest테스트 프레임워크
pytest-asyncio비동기 테스트 지원
pytest-mockMock 지원
respxhttpx mocking

6.3 CLI 명령어

discord-mcp      # MCP 서버 실행
discord-watch    # 감시 데몬 실행

7. 상태 파일 형식

7.1 결정 상태 파일

위치: ~/.claude/pending_decisions/{question_id}.json

{
  "question_id": "project_20260301_abc123",
  "project": "my-project",
  "question": "DB 마이그레이션을 실행할까요?",
  "context": "v1→v2 스키마 변경",
  "options": ["A) 지금 실행", "B) 스테이징 먼저", "C) 보류"],
  "timeout_seconds": null,
  "thread_id": "1234567890",
  "message_id": "0987654321",
  "asked_at": "2026-03-01T10:30:00Z",
  "status": "pending",
  "clarify_attempts": 0,
  "resolved_at": null,
  "resolution": null,
  "selected_option": null
}

7.2 Inbox 파일

위치: ~/.claude/discord_inbox.json

{
  "last_message_id": "1234567890",
  "messages": [
    {
      "message_id": "1234567890",
      "channel_id": "1234567890",
      "thread_id": null,
      "author": "username",
      "author_id": "1234567890",
      "content": "메시지 내용",
      "timestamp": "2026-03-01T00:00:00Z",
      "read": false
    }
  ]
}

8. 아키텍처 원칙

8.1 설계 원칙

  1. 싱글톤 패턴: DiscordClient, StateStore, InboxStore는 모듈 레벨 싱글턴
  2. 상태 영속화: 모든 결정 요청 상태는 파일로 영속화
  3. 무한 대기 기본값: Claude가 독단적으로 진행하지 않도록 timeout 기본값은 None
  4. 재질문 제한: 모호한 응답은 최대 2회까지만 재질문
  5. Rate Limit 처리: Discord API 429 응답 시 자동 대기 후 재시도

8.2 에러 handling

상황처리 방법
Discord API 429자동 대기 후 재시도 (최대 5회)
WebSocket 연결 끊김Exponential backoff로 재연결
응답 파싱 실패최대 2회 재질문 후 중단
Timeout 발생작업 중단 알림 후 aborted 상태
세션 끊김 (SIGHUP)disconnected 상태로 저장

문서 버전: 1.0.0 마지막 수정: 2026-03-02


영어 버전: English Version

Hugo로 만듦
JimmyStack 테마 사용 중