Skip to main content
Glama
README.md
# dueday-mcp

개인 할 일을 **ChatGPT / Claude 챗에서 MCP로** 관리하는 작은 서버입니다. 마감일과 함께 *준비 시작일*(`due - lead_days`)을 계산해서, 일 단위 예약 작업이 "지금부터 준비해야 하는 일"을 알려줄 수 있게 설계했습니다.

- **MCP 도구 13개**: 할 일 8개(`add_todo`, `list_todos`, `update_todo`, `complete_todo`, `cancel_todo`, `delete_todo`, `upcoming`, `list_tags`) + 목표 5개(`list_goals`, `add_goal`, `update_goal`, `log_progress`, `goal_progress`)
- **장기 목표 관리**: 인생 목표 → 연간·단기·장기 목표, 목표당 지표 여러 개(누적·측정값·달성 여부), 체크인 기록, 할 일을 목표에 연결(연결 없으면 일상)
- **REST API**: 같은 서비스 계층을 `/api/*`로 노출 (웹 UI용)
- **저장소**: SQLite (`node:sqlite` 내장, 네이티브 빌드 불필요)
- **인증**: 내장 OAuth 2.1(PKCE, 사전 등록 공개 클라이언트, DCR 없음) 또는 정적 Bearer 토큰, 클라이언트별 요청 제한, 64KB 본문 제한
- **배포**: Docker Compose, 선택적으로 Cloudflare Tunnel 프로필
- **선택 기능**: [gbrain](https://github.com/garrytan/gbrain) 지식 브레인의 프로젝트 페이지에 항목을 선별 동기화

> 개인용으로 만든 도구라 시간대는 **Asia/Seoul 고정**이고, 도구 설명과 오류 메시지는 한국어입니다.

## 빠른 시작 (Docker)

```bash
cp .env.example .env
# .env 의 API_TOKEN 을 채웁니다: openssl rand -hex 32
mkdir -p data
docker compose up -d --build
curl localhost:3080/health
```

컨테이너는 3000번에서 듣고, 호스트에는 `HOST_PORT`(기본 3080)로 `127.0.0.1`에만 공개됩니다. Linux에서 bind mount 권한 문제가 나면 `chown 1000:1000 data`를 실행하세요(컨테이너는 `node` 사용자로 동작).

### 외부 공개 (ChatGPT 커넥터용)

ChatGPT 커스텀 커넥터는 공개 HTTPS 주소가 필요합니다. Cloudflare Zero Trust에서 터널을 만들고, 퍼블릭 호스트명을 `http://todo:3000`으로 연결한 뒤:

```bash
# .env 에 TUNNEL_TOKEN 추가
docker compose --profile tunnel up -d
```

ChatGPT의 커스텀 커넥터는 인증 방식으로 **OAuth**만 제공하므로(정적 키 옵션 없음) `.env`에 `PUBLIC_URL`과 `OWNER_PASSWORD`를 설정해 내장 OAuth 2.1 서버를 켭니다. 그 뒤 ChatGPT → 설정 → Apps & Connectors → 개발자 모드 → Create 에서:

- 연결: 서버 URL `https://<host>/mcp`
- 인증: OAuth. 메타데이터는 `/.well-known/oauth-authorization-server`에서 자동 발견됩니다. 클라이언트 ID는 `OAUTH_CLIENT_ID`(기본 `chatgpt`), 시크릿 없음(PKCE 공개 클라이언트).
- 연결 버튼을 누르면 `/authorize` 승인 페이지가 열리고 `OWNER_PASSWORD`를 입력하면 완료됩니다.

### Claude.ai / Claude Code

- **claude.ai 커스텀 커넥터**: `.env`의 `OAUTH_CLIENTS`에 confidential 클라이언트를 추가합니다(예: `claude|https://claude.ai/api/mcp/auth_callback,https://claude.com/api/mcp/auth_callback|<secret>`). claude.ai → 설정 → Connectors → Add custom connector에서 URL `https://<host>/mcp`, Advanced settings에 OAuth Client ID `claude`와 Client Secret을 입력합니다. 연결 시 `/authorize` 승인 페이지에서 `OWNER_PASSWORD`를 입력합니다.
- **Claude Code**: 헤더 방식이 가장 단순합니다.

```bash
claude mcp add --transport http -s user dueday https://<host>/mcp --header "Authorization: Bearer <API_TOKEN>"
```

Claude Code나 스크립트처럼 헤더를 직접 넣을 수 있는 클라이언트는 `Authorization: Bearer <API_TOKEN>`도 계속 쓸 수 있습니다.

## 환경 변수

| 변수 | 기본 | 설명 |
|---|---|---|
| `API_TOKEN` | 필수 | `/api`, `/mcp` 보호용 Bearer 토큰, 16자 이상 |
| `PORT` | 3000 | 컨테이너 내부 포트 |
| `HOST_PORT` | 3080 | compose가 호스트에 여는 포트 |
| `DB_PATH` | `./data/todo.db` | SQLite 파일 (WAL) |
| `RATE_LIMIT_PER_MIN` | 240 | 클라이언트(IP 또는 토큰)별 분당 요청 수 (웹 UI는 한 번 열 때 요청 5~6개) |
| `PUBLIC_URL` | 비움 | 터널이 노출하는 공개 origin. `OWNER_PASSWORD`와 함께 설정하면 OAuth 활성 |
| `OWNER_PASSWORD` | 비움 | `/authorize` 승인 페이지 비밀번호, 12자 이상. `WEB_PASSWORD`가 없으면 웹 로그인에도 사용 |
| `WEB_PASSWORD` | 비움 | 웹 UI 로그인 전용 비밀번호(4자 이상, PIN 가능). 실패 5회/15분 잠금, 전체 30회/15분 잠금 |
| `OAUTH_CLIENT_ID` | `chatgpt` | 사전 등록 공개 클라이언트 ID |
| `OAUTH_REDIRECT_URIS` | ChatGPT 기본 | 허용 리다이렉트 URI(쉼표 구분). `https://chatgpt.com/connector/oauth/<id>`는 항상 허용 |
| `OAUTH_CLIENTS` | 비움 | 여러 클라이언트: `id\|redirect1,redirect2[\|secret];...`. 설정 시 위 두 값을 대체 |
| `GBRAIN_URL` | 비움 | gbrain MCP 엔드포인트. 아래 자격증명 중 하나와 함께 설정하면 동기화 활성 |
| `GBRAIN_TOKEN` | 비움 | 정적 bearer 토큰 |
| `GBRAIN_CLIENT_ID`, `GBRAIN_CLIENT_SECRET` | 비움 | OAuth client_credentials (권장). 토큰은 자동 발급·캐시 |
| `TUNNEL_TOKEN` | 비움 | `tunnel` 프로필용 cloudflared 토큰 |
| `ANTHROPIC_API_KEY` | 비움 | 설정하면 새 할 일을 Claude로 자동 분류(아래 참고). 비우면 기능 꺼짐 |
| `ENRICH_MODEL` | `claude-opus-5` | 분류에 쓰는 모델 |
| `ENRICH_DAILY_CAP` | 200 | 하루(Asia/Seoul) 최대 분류 호출 수. 넘으면 분류 없이 저장 |

## MCP 도구

모든 응답은 `{ success, data, meta: { today, total? } }` 형태이고 `meta.today`는 Asia/Seoul 기준 오늘입니다. 상대 날짜("이번주 금요일")는 클라이언트(LLM)가 `meta.today`로 변환해서 `YYYY-MM-DD`로 보냅니다. 날짜만 오면 그날 18:00으로 저장됩니다.

| 도구 | 역할 |
|---|---|
| `add_todo` | 등록. `title`, `due?`, `tags?`, `lead_days?`(기본 3), `note?`, `brain_ref?` |
| `list_todos` | 조회. `status`, `tag`, `due_before`, `due_after`, `q`, `limit`, `offset` |
| `update_todo` | 부분 수정. 바꿀 필드만 최상위에. `due: null`이면 마감 제거 |
| `complete_todo` | 완료 / `reopen: true`로 되돌리기 |
| `cancel_todo` | 취소(목록·알림에서 제외, 기록 유지) / `reopen: true`로 되살리기 |
| `delete_todo` | 영구 삭제. 명시적 요청 시에만 |
| `upcoming` | 알림용. `overdue`, `start_now`, `later`, `no_due` 그룹 + 한 줄 `summary` |
| `list_tags` | 태그와 미완료 개수 |
| `list_goals` | 인생·연간·단기·장기 목표와 진행률(달성률, 기간 경과율, 상태) |
| `add_goal` / `update_goal` | 목표와 지표 정의. 지표 kind: count(권·회 누적) / value(kg·명·BTC 측정값) / boolean |
| `log_progress` | 체크인. "책 한 권 끝냈어" → reading +1, "몸무게 75.8" → weight 75.8 |
| `goal_progress` | 목표 상세: 지표별 진행, 최근 체크인, 열린 할 일 |

할 일은 `add_todo`의 `goal`(목표 태그)로 목표에 연결됩니다. 연결되지 않은 할 일은 **일상**입니다. 진행률은 지표 달성률의 평균(지표가 없으면 할 일 완료율)이고, 연간 목표는 기간 경과율과 비교해 앞섬 / 순항 / 뒤처짐 / 달성으로 표시됩니다.

상세 명세는 [`docs/mcp-tools.json`](docs/mcp-tools.json), 아키텍처는 [`docs/architecture.html`](docs/architecture.html)에 있습니다.

### ChatGPT 프로젝트 지침 예시

개발자 모드 커넥터는 대화마다 `+` 도구 메뉴에서 켜야 하고, 명시하지 않으면 ChatGPT가 자체 예약·메모 기능으로 처리해 버립니다. 전용 프로젝트를 만들어 아래 지침을 넣어 두면 안정적으로 동작합니다.

```text
너는 내 할 일 비서다. 할 일 저장소는 dueday 커넥터(MCP) 하나뿐이다.

[절대 규칙]
- 할 일 등록·조회·수정·완료·취소는 반드시 dueday 도구(add_todo, list_todos, update_todo, complete_todo, cancel_todo, upcoming, list_tags)로 처리한다.
- ChatGPT 자체 예약(리마인더)·메모·캔버스 기능으로 대신하지 않는다. 도구를 쓸 수 없으면 "dueday 커넥터가 이 대화에 켜져 있지 않다"고 알리고 멈춘다.
- "안 해도 된다/없던 일로"는 cancel_todo(되돌릴 수 있음). delete_todo는 내가 "삭제"라고 명시했을 때만 쓰고, 실행 전에 제목을 확인받는다.

[날짜]
- 모든 날짜는 Asia/Seoul. 기준일은 도구 응답의 meta.today를 쓴다.
- 상대 표현은 YYYY-MM-DD로 바꿔 넘긴다. "이번주"는 오늘이 속한 월~일, "다음주"는 그 다음 월~일. 요일이 없으면 그 주 금요일로 잡고 답변에 요일을 같이 적어 확인받는다.
- 시각이 없으면 due에 날짜만 넘긴다.

[등록]
- 서버가 비어 있는 태그·준비 기간·목표 연결을 자동으로 채워 응답에 돌려준다. 내가 직접 말한 값(태그, "며칠 전부터", 목표)만 넘기고 나머지는 비운다.
- 한 메시지에 할 일이 여러 개면 각각 따로 등록한다.
- 응답의 suggestion이 null이 아니면 "목표로 올릴까?"를 한 줄 제안하고, 승낙하면 add_goal(같은 tag) 후 update_todo(goal)로 연결한다.

[대화 처리]
- "뭐 남았어": upcoming(days=7) → 마감 지남 / 지금 준비 시작 / 예정 순으로 요약.
- "~했어": list_todos(q=키워드)로 찾아 complete_todo. 후보가 둘 이상이면 고르게 한다.
- "~미뤄줘": update_todo로 due 변경.
- 등록 후 답변은 한 줄: 제목 · 마감(요일) · 준비 시작일 · 태그 · 목표(또는 일상). 응답에 온 값을 그대로 쓴다.
```

### 예약 작업 프롬프트 예시 (매일 아침)

```text
dueday 커넥터의 upcoming 도구를 days=7로 호출해라. 결과를 이렇게 정리해서 알려줘:
1) 마감 지남 — 제목, 마감일 (있을 때만, 굵게)
2) 오늘 준비 시작 — 제목, 마감일, 남은 일수. 맨 위에 눈에 띄게.
3) 이번 주 예정 — 제목, 마감일, 준비 시작일
4) 마지막 줄에 도구 응답의 summary를 그대로 인용.
아무것도 없으면 "오늘은 준비 시작할 일이 없음" 한 줄만.

오늘이 월요일이면 마지막에 "주간 목표 리뷰" 블록을 추가한다: list_goals(status=active)를 호출해
연간 목표를 뒤처짐 → 순항 → 앞섬 → 달성 순으로 나열하고, 각 줄에 제목 · 달성률 vs 기간 경과율 ·
지표 현재값/목표값을 적는다. 뒤처짐 목표에는 이번 주에 할 만한 행동 하나를 제안하되 제안임을 밝힌다.
이번 주 체크인이 없는 주기 목표(cadence_met=false)는 "이번 달 아직"으로 따로 표시한다.
```

## 개발

```bash
pnpm install
API_TOKEN=devtokendevtokendevtoken pnpm dev   # tsx watch
pnpm test            # vitest
pnpm test:coverage   # 80% 이상 강제
pnpm typecheck && pnpm build
```

구조: `src/todos`(도메인·저장소·날짜), `src/mcp`(MCP 어댑터), `src/api`(REST), `src/auth`(Bearer·요청 제한), `src/oauth`(OAuth 2.1 서버), `src/brain`(gbrain 동기화), `src/db`(마이그레이션).

## 자동 분류 (선택, Claude API)

`ANTHROPIC_API_KEY`를 설정하면 어느 경로(웹·REST·MCP)로 들어오든 새 할 일을 저장 직후 백그라운드에서 한 번 분류합니다.

- **비어 있는 필드만 채웁니다.** 사용자가 준 `tags`, `lead_days`, `due`, `goal`은 절대 덮어쓰지 않습니다.
- 태그(기존 태그 우선)·준비 기간(`lead_days`)·제목 속 날짜(`due`)는 바로 적용하고, 목표 연결은 확신이 높을 때만 적용합니다.
- "매주 두 번 달리기"처럼 목표에 가까운 항목은 **목표 제안**으로만 남깁니다. `GET /api/suggestions`에서 보고 `POST /api/suggestions/:id/accept`(인생 목표 아래 목표 생성 + 할 일 연결) 또는 `/dismiss`로 처리합니다. 웹 UI의 목표 페이지에도 `AI 제안` 카드로 나옵니다.
- 자동으로 채운 필드는 `todo.enrichment`에 기록되고 웹에서 `AI` 칩으로 표시됩니다. 사용자가 직접 수정하면 표시가 사라집니다.
- 실패하면 할 일은 그대로 두고 `enrichment_log`에 남깁니다. 하루 호출 상한(`ENRICH_DAILY_CAP`)을 넘으면 `skipped`로 기록합니다.
- MCP `add_todo`는 분류를 최대 8초 기다렸다가 채워진 할 일과 `suggestion`(목표 제안, 없으면 null)을 함께 돌려줍니다. 챗 클라이언트는 사용자가 직접 말한 값만 넘기면 됩니다. 웹·REST 등록은 기다리지 않고 백그라운드로 처리합니다.
- 분류 호출은 직렬로 처리되며 요청당 약 1KB 입력(시스템 프롬프트는 캐시)이라 비용은 하루 수십 건 기준 매우 낮습니다.

## gbrain 동기화 (선택)

`brain_ref`에 gbrain 프로젝트 페이지 슬러그를 주면 생성·완료 시 그 페이지의 `## 남은 일` 표에 `todo:<id>` 행을 추가·갱신합니다. 없는 페이지는 만들지 않으며 결과는 `brain_sync_log`에 남습니다. 시간 제한은 요청당 5초입니다.

gbrain 쪽에는 `projects/`만 쓸 수 있는 client_credentials 클라이언트를 하나 등록하는 것을 권합니다:

```bash
gbrain auth register-client dueday --grant-types client_credentials \
  --scopes "read write" --token-endpoint-auth-method client_secret_post \
  --bound-slug-prefixes projects/
```

## 라이선스

MIT