icloud-mcp
by jinho-waah
README.md
# icloud-mcp
iCloud Calendar와 Claude를 연결하는 MCP(Model Context Protocol) 서버.
Claude(Claude Code / Claude Desktop)에서 자연어로 iCloud 캘린더를 조회·생성·수정·삭제할 수 있게 한다.
---
## 아키텍처
```
Claude (Claude Code / Desktop)
│ MCP (stdio)
▼
icloud-mcp 서버 (TypeScript, @modelcontextprotocol/sdk)
│ CalDAV (HTTPS)
▼
iCloud CalDAV 서버 (caldav.icloud.com)
```
- **연결 방식**: CalDAV + Apple **앱 암호(app-specific password)** 인증
- macOS EventKit 방식 대비 플랫폼 독립적이고, 권한 팝업 없이 동작
- Apple ID 2FA 환경에서도 앱 암호로 접근 가능
- **언어/런타임**: TypeScript + Node.js 18+, **ESM** (`"type": "module"`, tsconfig `NodeNext`)
- **주요 라이브러리**:
- `@modelcontextprotocol/sdk` — MCP 서버 프레임워크 (stdio transport)
- `tsdav` — CalDAV 클라이언트
- `ical.js` — iCalendar(VEVENT) 파싱 **및** 생성
- `zod` — tool 입력 스키마 검증
- **인증 정보 관리**: 환경변수(`APPLE_ID`, `APPLE_APP_PASSWORD`) — 코드/저장소에 절대 커밋 금지. 앱 암호는 macOS 키체인에 두고 실행 래퍼(`bin/icloud-mcp.sh`)가 주입하는 것을 권장한다(ICMCP-25).
### 확정된 기술 결정
| 결정 | 이유 |
|------|------|
| iCalendar 라이브러리를 `ical.js` **하나로** 통일 | `update_event`는 기존 VEVENT를 파싱 → 수정 → 재직렬화하는 왕복이 필수다. 생성 전용 라이브러리(`ical-generator`)를 함께 쓰면 왕복 과정에서 필드 유실·포맷 불일치가 발생한다. |
| ESM + `NodeNext` | MCP SDK가 ESM 우선이다. |
| 반복 일정은 `ICAL.RecurExpansion`으로 **클라이언트 측 전개** | CalDAV 서버측 `<C:expand>` REPORT는 iCloud 지원이 불균일하다. 조회 구간 내 개별 발생을 라이브러리로 전개하는 편이 안정적이다. |
| 이벤트 식별자는 `uid` + `calendarUrl` | CalDAV 리소스 URL은 서버가 바꿀 수 있어 대화 중 재사용에 부적합하다. |
| 이벤트 조회는 tsdav `fetchCalendarObjects()` 대신 `calendarQuery()` REPORT **한 번** | `fetchCalendarObjects()`는 etag 목록 REPORT + multiget REPORT로 2왕복이다. iCloud는 첫 REPORT에서 곧바로 `calendar-data`를 돌려주므로 한 번이면 된다 (ICMCP-29). |
| uid → 리소스 URL 캐시 + `<UID>.ics` 규약 직행 | iCloud는 UID `prop-filter` REPORT를 **412 Precondition Failed로 거부한다**(실측). 서버가 걸러줄 수 없으므로 클라이언트가 위치를 기억하고, 모르면 iCloud의 저장 규약인 `<UID>.ics`를 직접 친다. 둘 다 빗나가야 전체 스캔이다 (ICMCP-22). |
| 모든 로그는 **stderr** 로만 출력 | stdio transport에서 stdout은 JSON-RPC 전용 채널이다. stdout에 한 줄이라도 찍으면 프로토콜이 깨진다. |
| 종일 일정의 `end.date`는 **포함적**(마지막 날) | RFC 5545의 `DTEND`는 배타적이지만("8/1 하루" → `DTEND:20260802`), LLM이 매번 +1일을 정확히 계산하기를 기대하는 건 위험하다. "8월 1~3일" → `start=08-01, end=08-03`으로 직관적으로 매핑되게 하고, ±1일 변환은 `ical.ts` 한 곳에서만 처리한다. |
### 모듈 구조 및 소유권
```
bin/
└── icloud-mcp.sh # 실행 래퍼: 키체인에서 앱 암호를 꺼내 주입 후 서버 exec
scripts/
└── setup.sh # 새 Mac 셋업: 빌드 + 키체인 + 호스트 등록 + 기동 검증
src/
├── types.ts # 공용 계약: CalendarEvent, EventDraft, ICloudCalendarClient …
├── errors.ts # ICloudError + 에러 코드
├── config.ts # 환경변수 로딩/검증
├── datetime.ts # EventDateTime 순수 헬퍼 (양쪽 레이어가 공유)
├── caldav/
│ ├── client.ts # createICloudClient() — ICloudCalendarClient 구현체
│ └── ical.ts # VEVENT ↔ CalendarEvent 변환, RRULE 전개
├── tools/ # MCP tool 등록 (7종)
├── schemas.ts # zod 입력 스키마
└── index.ts # 서버 엔트리포인트 (stdio)
```
`src/types.ts`가 CalDAV 레이어와 MCP 레이어 사이의 **고정된 계약**이다. 양쪽은 이 파일을 통해서만 통신하며, 계약 변경은 양쪽 동시 수정을 뜻하므로 함부로 바꾸지 않는다.
## 제공할 MCP Tools
| Tool | 설명 |
|------|------|
| `list_calendars` | 사용 가능한 캘린더 목록 조회 |
| `list_events` | 기간(시작~종료)으로 이벤트 조회 |
| `search_events` | 키워드로 이벤트 검색 |
| `create_event` | 이벤트 생성 (제목, 시작/종료, 종일 여부, 위치, 메모, 알림, 반복) |
| `update_event` | 이벤트 수정 |
| `delete_event` | 이벤트 삭제 |
| `get_event` | 단일 이벤트 상세 조회 |
---
## 작업 보드
> 이 섹션을 Jira처럼 사용한다. 상태: `[ ]` TODO / `[~]` IN PROGRESS / `[x]` DONE
> 작업 착수 시 `[~]`로, 완료 시 `[x]`로 갱신하고 커밋한다.
### Phase 1 — 프로젝트 셋업
- [x] ICMCP-20: 공용 계약 정의 (`src/types.ts`, `src/errors.ts`) — Phase 2/3 병렬 구현의 전제
- [x] ICMCP-1: Node.js + TypeScript 프로젝트 초기화 (`package.json`, `tsconfig.json`, `.gitignore`)
- [x] ICMCP-2: 의존성 설치 (`@modelcontextprotocol/sdk`, `tsdav`, `ical.js`, `zod`)
- [x] ICMCP-3: 빌드/실행 스크립트 구성 (`build`, `dev`, `start`) + `src/config.ts`
### Phase 2 — iCloud CalDAV 연동
- [x] ICMCP-5: CalDAV 클라이언트 모듈 작성 — 로그인, principal/calendar-home 디스커버리
- [x] ICMCP-6: 캘린더 목록 조회 구현 (`current-user-privilege-set`으로 readOnly 판별)
- [x] ICMCP-7: 이벤트 조회(기간 필터) 구현 — VEVENT 파싱, 타임존 처리
- [x] ICMCP-8: 이벤트 생성 구현 — VEVENT 생성, UID 관리
- [x] ICMCP-9: 이벤트 수정/삭제 구현 — etag `If-Match` 기반 충돌 처리
- [x] ICMCP-13: 반복 이벤트(RRULE) 지원 — `ICAL.RecurExpansion` 전개, EXDATE/RECURRENCE-ID 반영, 500회 상한
### Phase 3 — MCP 서버
- [x] ICMCP-10: MCP 서버 뼈대 작성 (stdio transport, 서버 메타데이터)
- [x] ICMCP-11: Tool 스키마 정의 (zod v4) 및 7개 tool 등록
- [x] ICMCP-12: 에러 처리 — 인증 실패, 네트워크 오류, 잘못된 입력을 사용자 친화적 메시지로 변환
### Phase 4 — 통합 및 검증
- [x] ICMCP-21: 통합 — 전체 타입체크/빌드 통과, stdout 누수 감사, 자격증명 없는 기동 동작 확인
- [x] ICMCP-4: 앱 암호 발급 절차 문서화 (appleid.apple.com → 앱 암호)
- [x] ICMCP-14: Claude Code에 MCP 서버 등록 (`claude mcp add`) 및 실제 캘린더로 E2E 테스트 — 등록·연결·`tools/list`(7개)·`list_calendars`(캘린더 8개)에 더해 생성 → 조회 → 수정 → 삭제 왕복, 종일 일정 왕복, 조회 구간 경계까지 실 계정으로 확인했다. 아래 미결 사항 2만 남았다
- [x] ICMCP-15: Claude Desktop 설정 방법 문서화 (`claude_desktop_config.json`) — GUI 최소 PATH 환경에서 래퍼가 `/usr/local/bin/node`로 폴백해 기동하는 것까지 검증
- [x] ICMCP-26: 프로젝트를 TCC 보호 폴더 밖(`~/dev/icloud-mcp`)으로 이전 — Claude Desktop이 GUI 프로세스라 `~/Documents` 내 파일을 실행조차 못 하고 `Operation not permitted`로 죽었다. 앱에 문서 폴더 접근 권한을 주는 대신 경로를 옮겨 해결. 기존 위치엔 심볼릭 링크를 남겨 폴더 구성을 유지했고, Claude Code/Desktop 양쪽 등록 경로를 갱신했다.
- [x] ICMCP-27: 다른 Mac에서 한 번에 설치·등록하는 셋업 스크립트 (`scripts/setup.sh`) — 빌드, 키체인 저장, Claude Code/Desktop 등록, TCC 보호 폴더 검사, GUI 최소 PATH 기동 검증까지
- [ ] ICMCP-16: README 사용법 최종 정리 (설치, 설정, tool 사용 예시)
- [x] ICMCP-22: `getEvent`/`updateEvent`/`deleteEvent`의 UID 조회가 캘린더 전체 스캔이었다. **UID `prop-filter` REPORT는 iCloud가 412로 거부**해서 쓸 수 없었고, 대신 uid→URL 캐시와 `<UID>.ics` 규약 직행으로 해결했다(둘 다 빗나갈 때만 전체 스캔). 실측 970ms → 262~362ms.
- [x] ICMCP-29: 전반 최적화 및 중복 코드 정리 — 이벤트 조회를 REPORT 1왕복으로, 캘린더별 조회를 병렬로, `current-user-privilege-set`을 캘린더 홈 PROPFIND에 합쳐 기동 시 요청 8건 제거. 중복 구현(`eventDateTimeToMillis`/`eventDateTimeToTimestamp`, `wrapHandler`/`wrapZeroArgHandler`)과 사용처 없는 코드(`extractUid`, 쓰이지 않던 export/tsconfig 옵션) 제거. `noUnusedLocals`/`noUnusedParameters`로 재발 방지
- [x] ICMCP-25: 앱 암호를 MCP 호스트 설정(`~/.claude.json`)에 평문으로 두지 않도록 macOS 키체인 연동 — 실행 래퍼 `bin/icloud-mcp.sh`가 기동 시 키체인에서 꺼내 환경변수로 주입
### Backlog (추후)
- [ ] ICMCP-23: 쓰기 시 VTIMEZONE을 고정 오프셋 단일 observance로 합성한다(tzdata 미번들). DST가 있는 지역에서 전환을 걸치는 반복 일정은 전환 이후 발생의 오프셋이 틀어질 수 있다. 기본값 `Asia/Seoul`은 DST가 없어 영향 없음.
- [ ] ICMCP-24: tsdav의 디스커버리/조회 계열은 상태 코드 없는 평범한 `Error`를 던져서, 에러 분류가 메시지 정규식 추정에 의존한다. CRUD 계열만 상태 코드 기반으로 정확히 분류됨.
- [ ] ICMCP-28: 기동 시 디스커버리가 간헐적으로 실패한다. 실제로 Claude Desktop에서 principal 조회는 성공하고 `calendar-home-set` 조회만 `cannot find homeUrl`로 실패한 뒤, 재시작하니 정상 동작한 사례가 있다. home url은 `p60-caldav.icloud.com` 같은 별도 샤드 호스트로 리디렉트되는데 이쪽이 일시적으로 실패하는 것으로 보인다. 디스커버리 단계에 짧은 백오프 재시도를 넣어야 한다. 지금은 실패하면 서버가 그대로 죽어서 호스트에는 `Server disconnected`로만 보인다.
- [ ] ICMCP-17: iCloud Reminders(미리알림) 지원
- [ ] ICMCP-18: 초대/참석자(ATTENDEE) 지원
- [ ] ICMCP-19: 캐싱으로 조회 속도 개선
### 실제 계정으로 확인이 필요한 미결 사항
1. ~~**조회 구간 `end`의 배타성**~~ — 실 계정으로 확인했다. 2026-08-01T00:00Z에 시작하는 이벤트에 대해 `[07-31, 08-01)`은 **미포함**, `[08-01, 08-02)`는 **포함**. 문서/tool 설명대로 `start`는 포함, `end`는 배타적이다.
2. **`readOnly` 판별** — `current-user-privilege-set`을 덕타이핑으로 해석한다. 실 계정 응답에 `write`/`writeContent`/`writeProperties`가 실제로 담겨 오는 것과 그것을 우리 판별식이 읽는 것까지는 확인했으나, **테스트 계정에 읽기 전용 캘린더(구독 캘린더 등)가 없어 "읽기 전용으로 판별하는" 반대 방향은 여전히 미검증이다.** 구독 캘린더를 하나 추가해 재확인 필요.
3. ~~**생성 직후 재조회**~~ — 실 계정에서 생성 → 재조회 → 수정 → 삭제 왕복이 모두 성공했다. 반영 지연으로 인한 재시도는 필요하지 않았다.
---
## 빠른 설치 — 다른 Mac에서 (ICMCP-27)
새 Mac에서는 셋업 스크립트 하나로 끝난다. **`~/Documents`·`~/Desktop`·`~/Downloads` 밖에 clone해야 한다** (이유는 [Claude Desktop 등록](#claude-desktop-등록-icmcp-15) 참고).
```bash
git clone https://github.com/jinho-waah/icloud-mcp.git ~/dev/icloud-mcp
cd ~/dev/icloud-mcp
./scripts/setup.sh # Apple ID를 대화형으로 물어본다
./scripts/setup.sh you@icloud.com # 인자로 넘겨도 된다
```
스크립트가 하는 일:
1. 프로젝트가 TCC 보호 폴더 안에 있으면 **중단하고 옮기는 방법을 안내**한다
2. Node.js 18 이상 확인 → `npm install` → `npm run build`
3. Apple ID의 Latin1 범위 검사 (한글이 섞이면 기동 시점에 `btoa()`에서 죽으므로 미리 막는다)
4. 앱 암호를 **키체인에 저장** (이미 있으면 유지할지 묻는다). 화면·셸 히스토리 어디에도 남지 않는다
5. Claude Code 등록 (`claude` CLI가 있으면)
6. Claude Desktop 설정 병합 (`jq` 없이 node로 처리, 기존 키 보존, `.bak` 생성)
7. **GUI 최소 PATH 환경을 재현해 실제로 iCloud 로그인까지 되는지 검증**
끝나면 Claude Code는 재시작, Claude Desktop은 ⌘Q 후 재실행하면 된다.
앱 암호는 **기기마다 따로 발급**하는 편이 낫다. 한 대를 분실하거나 정리할 때 그 기기 것만 폐기하면 되고, 다른 기기는 영향을 받지 않는다. (Apple 계정당 앱 암호는 여러 개 만들 수 있다.)
## 개발 환경 설정
```bash
npm install
npm run build # dist/ 생성
npm run typecheck # 타입만 검사
npm run dev # watch 모드
```
수동으로 설정하려면 아래 절들을 따른다.
### 앱 암호 발급
1. [appleid.apple.com](https://appleid.apple.com) 로그인
2. **로그인 및 보안** > **앱 전용 암호** > **+** 로 새 암호 생성 (이름은 `icloud-mcp` 등 아무거나)
3. `xxxx-xxxx-xxxx-xxxx` 형태의 16자 암호가 한 번만 표시된다. 창을 닫으면 다시 볼 수 없으니 바로 복사한다.
Apple ID 본 비밀번호로는 CalDAV 로그인이 되지 않는다. 2단계 인증이 켜져 있어야 앱 암호 메뉴가 나타난다.
### Claude Code 등록 — 권장: 키체인 방식 (ICMCP-25)
MCP 호스트 설정 파일(`~/.claude.json`)에 앱 암호를 평문으로 적지 않는 방식이다. 암호는 macOS 키체인에 두고, 실행 래퍼 `bin/icloud-mcp.sh`가 기동 시점에 꺼내 환경변수로 주입한다.
```bash
# 1) 앱 암호를 키체인에 저장 (한 번만)
# '-w' 뒤에 값을 쓰지 않으면 대화형으로 입력받는다 → 셸 히스토리에 남지 않는다
security add-generic-password -s icloud-mcp -a "you@icloud.com" -T /usr/bin/security -w
# 2) 래퍼를 command로 등록. env에는 APPLE_ID만 들어간다
claude mcp add icloud --scope user \
-e APPLE_ID=you@icloud.com \
-- /절대/경로/icloud-mcp/bin/icloud-mcp.sh
```
- `-T /usr/bin/security`는 키체인 항목의 ACL에 `security` 명령을 신뢰 앱으로 등록한다. 이게 없으면 서버가 뜰 때마다 키체인 접근 허용 팝업이 뜬다.
- 키체인 항목의 **account**는 `APPLE_ID` 값과 같아야 한다. 래퍼가 `-a "$APPLE_ID"`로 조회한다.
- 서비스 이름을 바꾸려면 `ICLOUD_MCP_KEYCHAIN_SERVICE` 환경변수로 덮어쓴다 (기본 `icloud-mcp`).
- `APPLE_APP_PASSWORD`가 환경변수로 이미 들어와 있으면 래퍼는 키체인을 건너뛴다 (CI·수동 테스트용).
- GUI 앱(Claude Desktop)은 PATH가 최소한이라 nvm/homebrew의 `node`를 못 찾을 수 있다. 그런 경우 `NODE_BIN`에 `node` 절대경로를 지정한다.
**이 방식이 막아주는 것과 못 막는 것**: 평문 유출 경로(설정 파일 백업·클라우드 동기화·스크린샷·실수 커밋)를 막고, 폐기/교체 지점을 한 곳으로 모은다. 반면 키체인은 로그인 시 잠금이 풀려 있으므로 **이미 내 계정 권한을 획득한 악성 프로세스**는 여전히 꺼낼 수 있다. 로컬 침해에 대한 방어가 아니다.
### Claude Code 등록 — 간단(비권장): 환경변수 직접 주입
```bash
claude mcp add icloud --scope user \
-e APPLE_ID=you@icloud.com \
-e APPLE_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx \
-- node /절대/경로/icloud-mcp/dist/index.js
```
앱 암호가 `~/.claude.json`에 **평문으로** 남는다(파일 권한은 600). 편의상 쓸 수는 있지만 위 키체인 방식을 권한다.
### Claude Desktop 등록 (ICMCP-15)
설정 파일: `~/Library/Application Support/Claude/claude_desktop_config.json`
Claude Desktop에는 `claude mcp add` 같은 CLI가 없으므로 이 파일을 직접 편집한다. **기존 키(`preferences` 등)를 지우지 않도록** 병합해서 쓴다.
```json
{
"mcpServers": {
"icloud": {
"command": "/절대/경로/icloud-mcp/bin/icloud-mcp.sh",
"env": { "APPLE_ID": "you@icloud.com" }
}
}
}
```
명령줄로 안전하게 병합하려면:
```bash
CFG=~/Library/Application\ Support/Claude/claude_desktop_config.json
cp "$CFG" "$CFG.bak"
jq '.mcpServers.icloud = {
"command": "/절대/경로/icloud-mcp/bin/icloud-mcp.sh",
"env": { "APPLE_ID": "you@icloud.com" }
}' "$CFG" > "$CFG.tmp" && mv "$CFG.tmp" "$CFG"
```
**Claude Code 설정과 완전히 독립이다.** 두 앱은 서로 다른 파일을 보므로 양쪽 다 쓰려면 양쪽에 등록해야 한다. 키체인 항목은 하나를 공유한다.
주의할 점:
- **프로젝트를 `~/Documents`·`~/Desktop`·`~/Downloads` 안에 두면 안 된다 (ICMCP-26).** 이 세 폴더는 macOS TCC 보호 대상이다. Claude Desktop은 GUI 앱이라 기본적으로 접근 권한이 없어서, 그 안의 스크립트를 **실행조차 못 하고** 이렇게 죽는다:
```
/bin/sh: /Users/이름/Documents/.../bin/icloud-mcp.sh: Operation not permitted
[icloud] Server transport closed unexpectedly
```
파일 권한(`chmod +x`)이나 quarantine 문제가 아니다. 터미널에서 실행되는 Claude Code는 이미 권한이 있으므로 **같은 설정이 Claude Code에선 되고 Desktop에서만 실패한다.** 앱에 문서 폴더 접근 권한을 줘도 해결되지만(시스템 설정 > 개인정보 보호 및 보안 > 파일 및 폴더), 서버 하나 돌리려고 GUI 앱에 문서 폴더 전체를 여는 건 과하다. **보호 폴더 밖(`~/dev` 등)에 두는 편이 낫다.**
기존 폴더 구성을 유지하고 싶으면 원래 위치에 심볼릭 링크를 남긴다. 링크는 사람이 탐색할 때만 쓰이고 MCP 설정에는 실제 경로를 적으므로 TCC에 걸리지 않는다:
```bash
mv ~/Documents/어딘가/icloud-mcp ~/dev/icloud-mcp
ln -s ~/dev/icloud-mcp ~/Documents/어딘가/icloud-mcp
```
`dist/index.js`를 읽는 node도 같은 TCC 컨텍스트에서 돌기 때문에, 래퍼만 밖으로 빼는 것으로는 부족하고 **프로젝트 전체**가 밖에 있어야 한다.
- **`command`는 반드시 절대경로**로 쓴다. 상대경로는 앱의 작업 디렉터리 기준이라 맞지 않는다.
- **GUI 앱은 PATH가 최소한(`/usr/bin:/bin:/usr/sbin:/sbin`)이다.** 로그인 셸이 아니라 launchd가 띄우기 때문에 `~/.zshrc`가 로드되지 않고, nvm이나 homebrew로 설치한 `node`는 PATH에 없다. 그래서 `node dist/index.js`를 직접 `command`로 지정하면 데스크톱 앱에서만 `spawn node ENOENT`로 실패한다.
래퍼는 이 상황에서 `/usr/local/bin/node` → `/opt/homebrew/bin/node` 순으로 폴백한다. 둘 다 없으면 `env`에 `NODE_BIN`으로 절대경로를 지정한다:
```json
"env": { "APPLE_ID": "you@icloud.com", "NODE_BIN": "/Users/이름/.nvm/versions/node/v22.14.0/bin/node" }
```
- **앱을 완전히 종료(⌘Q)한 뒤 다시 실행**해야 설정이 반영된다. 창만 닫는 것으로는 부족하다.
- 로그: `~/Library/Logs/Claude/mcp-server-icloud.log` (서버 stderr), `~/Library/Logs/Claude/mcp.log` (호스트 측 연결 로그). 연결이 안 되면 여기부터 본다.
- 키체인 항목을 만들 때 `-T /usr/bin/security`를 빠뜨리면, 데스크톱 앱이 서버를 띄울 때마다 키체인 접근 허용 팝업이 뜬다.
### 공통 주의사항
- **`--scope project`를 쓰지 말 것.** 저장소 루트에 `.mcp.json`을 만들고 거기에 앱 암호를 평문으로 적는다. 이 저장소는 GitHub에 푸시되므로 그대로 유출된다. (`.gitignore`에 방어선을 넣어 두긴 했다.)
- `--scope user`는 `~/.claude.json`에 저장되어 어느 디렉터리에서든 쓸 수 있다.
- 등록 명령은 셸 히스토리에 남는다. 신경 쓰인다면 실행 후 `history -d` 로 지우거나, 앞에 공백을 한 칸 넣어 히스토리에 남지 않게 한다.
- **`APPLE_ID`에 한글 등 Latin1 범위 밖 문자가 들어가면 안 된다.** CalDAV Basic 인증 헤더를 만드는 `btoa()` 단계에서 `The string to be encoded contains characters outside of the Latin1 range.`로 죽고, 호스트에는 `Connection closed`로만 보인다.
`.env` 파일은 읽지 않는다. 자격증명은 MCP 호스트가 환경변수로 주입한다.
### 암호 교체 / 폐기
앱 암호가 새어 나갔다고 판단되면 appleid.apple.com에서 해당 암호만 **취소**하면 된다. Apple ID 본 비밀번호나 다른 기기 로그인에는 영향이 없다.
키체인 방식이면 교체는 항목 갱신 한 번으로 끝난다:
```bash
security delete-generic-password -s icloud-mcp -a "you@icloud.com"
security add-generic-password -s icloud-mcp -a "you@icloud.com" -T /usr/bin/security -w
```
선택 환경변수: `ICLOUD_DEFAULT_CALENDAR_URL`(생성 시 기본 캘린더), `ICLOUD_DEFAULT_TIMEZONE`(기본 `Asia/Seoul`).
### 현재 상태
Phase 1~3 구현 완료. 타입체크·빌드 통과, 전 소스에 stdout 출력 없음(프로토콜 안전), 자격증명 누락 시 stderr로 안내 후 exit 1 확인.
**실 계정 연동 확인됨** — Claude Code에 키체인 래퍼 방식으로 등록(ICMCP-25), CalDAV 로그인·principal 디스커버리 성공, `initialize` + `tools/list`(7개) 정상, `list_calendars`로 실제 캘린더 8개 조회 성공. 이벤트 생성 → 조회 → 수정 → 삭제 왕복, 종일 일정 왕복, 조회 구간 경계까지 실 계정에서 확인했다(ICMCP-14). 남은 미검증 항목은 읽기 전용 캘린더 판별 하나뿐이다.
**성능** — 같은 계정(캘린더 8개, 1년치 이벤트 230건)에서 ICMCP-29 전후 실측:
| 동작 | 이전 | 이후 |
|------|------|------|
| `list_events` (전체 캘린더, 1년) | 5,335ms | **517ms** |
| `get_event` | 970ms | **262\~362ms** |
| 기동 시 디스커버리 | 2,809ms | 2,533ms (요청 8건 감소) |
## 보안 원칙
- Apple ID 본 비밀번호는 절대 사용하지 않는다. **앱 암호만** 사용한다.
- `.env`는 `.gitignore`에 포함하며, 자격증명은 어떤 파일로도 커밋하지 않는다.
- 앱 암호는 MCP 호스트 설정 파일에 평문으로 두지 않는다. macOS 키체인 + `bin/icloud-mcp.sh` 조합을 쓴다(ICMCP-25).
- 로그는 전부 stderr로 나가며, `src/errors.ts`의 `redact()`가 진단 메시지에서 Apple ID·앱 암호를 마스킹한다. 래퍼 스크립트도 stdout에 아무것도 쓰지 않는다.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues