icloud-mcp
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", tsconfigNodeNext)주요 라이브러리:
@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 라이브러리를 |
|
ESM + | MCP SDK가 ESM 우선이다. |
반복 일정은 | CalDAV 서버측 |
이벤트 식별자는 | CalDAV 리소스 URL은 서버가 바꿀 수 있어 대화 중 재사용에 부적합하다. |
이벤트 조회는 tsdav |
|
uid → 리소스 URL 캐시 + | iCloud는 UID |
모든 로그는 stderr 로만 출력 | stdio transport에서 stdout은 JSON-RPC 전용 채널이다. stdout에 한 줄이라도 찍으면 프로토콜이 깨진다. |
종일 일정의 | RFC 5545의 |
모듈 구조 및 소유권
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 | 설명 |
| 사용 가능한 캘린더 목록 조회 |
| 기간(시작~종료)으로 이벤트 조회 |
| 키워드로 이벤트 검색 |
| 이벤트 생성 (제목, 시작/종료, 종일 여부, 위치, 메모, 알림, 반복) |
| 이벤트 수정 |
| 이벤트 삭제 |
| 단일 이벤트 상세 조회 |
작업 보드
이 섹션을 Jira처럼 사용한다. 상태:
[ ]TODO /[~]IN PROGRESS /[x]DONE 작업 착수 시[~]로, 완료 시[x]로 갱신하고 커밋한다.
Phase 1 — 프로젝트 셋업
ICMCP-20: 공용 계약 정의 (
src/types.ts,src/errors.ts) — Phase 2/3 병렬 구현의 전제ICMCP-1: Node.js + TypeScript 프로젝트 초기화 (
package.json,tsconfig.json,.gitignore)ICMCP-2: 의존성 설치 (
@modelcontextprotocol/sdk,tsdav,ical.js,zod)ICMCP-3: 빌드/실행 스크립트 구성 (
build,dev,start) +src/config.ts
Phase 2 — iCloud CalDAV 연동
ICMCP-5: CalDAV 클라이언트 모듈 작성 — 로그인, principal/calendar-home 디스커버리
ICMCP-6: 캘린더 목록 조회 구현 (
current-user-privilege-set으로 readOnly 판별)ICMCP-7: 이벤트 조회(기간 필터) 구현 — VEVENT 파싱, 타임존 처리
ICMCP-8: 이벤트 생성 구현 — VEVENT 생성, UID 관리
ICMCP-9: 이벤트 수정/삭제 구현 — etag
If-Match기반 충돌 처리ICMCP-13: 반복 이벤트(RRULE) 지원 —
ICAL.RecurExpansion전개, EXDATE/RECURRENCE-ID 반영, 500회 상한
Phase 3 — MCP 서버
ICMCP-10: MCP 서버 뼈대 작성 (stdio transport, 서버 메타데이터)
ICMCP-11: Tool 스키마 정의 (zod v4) 및 7개 tool 등록
ICMCP-12: 에러 처리 — 인증 실패, 네트워크 오류, 잘못된 입력을 사용자 친화적 메시지로 변환
Phase 4 — 통합 및 검증
ICMCP-21: 통합 — 전체 타입체크/빌드 통과, stdout 누수 감사, 자격증명 없는 기동 동작 확인
ICMCP-4: 앱 암호 발급 절차 문서화 (appleid.apple.com → 앱 암호)
ICMCP-14: Claude Code에 MCP 서버 등록 (
claude mcp add) 및 실제 캘린더로 E2E 테스트 — 등록·연결·tools/list(7개)·list_calendars(캘린더 8개)에 더해 생성 → 조회 → 수정 → 삭제 왕복, 종일 일정 왕복, 조회 구간 경계까지 실 계정으로 확인했다. 아래 미결 사항 2만 남았다ICMCP-15: Claude Desktop 설정 방법 문서화 (
claude_desktop_config.json) — GUI 최소 PATH 환경에서 래퍼가/usr/local/bin/node로 폴백해 기동하는 것까지 검증ICMCP-26: 프로젝트를 TCC 보호 폴더 밖(
~/dev/icloud-mcp)으로 이전 — Claude Desktop이 GUI 프로세스라~/Documents내 파일을 실행조차 못 하고Operation not permitted로 죽었다. 앱에 문서 폴더 접근 권한을 주는 대신 경로를 옮겨 해결. 기존 위치엔 심볼릭 링크를 남겨 폴더 구성을 유지했고, Claude Code/Desktop 양쪽 등록 경로를 갱신했다.ICMCP-27: 다른 Mac에서 한 번에 설치·등록하는 셋업 스크립트 (
scripts/setup.sh) — 빌드, 키체인 저장, Claude Code/Desktop 등록, TCC 보호 폴더 검사, GUI 최소 PATH 기동 검증까지ICMCP-16: README 사용법 최종 정리 (설치, 설정, tool 사용 예시)
ICMCP-22:
getEvent/updateEvent/deleteEvent의 UID 조회가 캘린더 전체 스캔이었다. UIDprop-filterREPORT는 iCloud가 412로 거부해서 쓸 수 없었고, 대신 uid→URL 캐시와<UID>.ics규약 직행으로 해결했다(둘 다 빗나갈 때만 전체 스캔). 실측 970ms → 262~362ms.ICMCP-29: 전반 최적화 및 중복 코드 정리 — 이벤트 조회를 REPORT 1왕복으로, 캘린더별 조회를 병렬로,
current-user-privilege-set을 캘린더 홈 PROPFIND에 합쳐 기동 시 요청 8건 제거. 중복 구현(eventDateTimeToMillis/eventDateTimeToTimestamp,wrapHandler/wrapZeroArgHandler)과 사용처 없는 코드(extractUid, 쓰이지 않던 export/tsconfig 옵션) 제거.noUnusedLocals/noUnusedParameters로 재발 방지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: 캐싱으로 조회 속도 개선
실제 계정으로 확인이 필요한 미결 사항
조회 구간end의 배타성— 실 계정으로 확인했다. 2026-08-01T00:00Z에 시작하는 이벤트에 대해[07-31, 08-01)은 미포함,[08-01, 08-02)는 포함. 문서/tool 설명대로start는 포함,end는 배타적이다.readOnly판별 —current-user-privilege-set을 덕타이핑으로 해석한다. 실 계정 응답에write/writeContent/writeProperties가 실제로 담겨 오는 것과 그것을 우리 판별식이 읽는 것까지는 확인했으나, 테스트 계정에 읽기 전용 캘린더(구독 캘린더 등)가 없어 "읽기 전용으로 판별하는" 반대 방향은 여전히 미검증이다. 구독 캘린더를 하나 추가해 재확인 필요.생성 직후 재조회— 실 계정에서 생성 → 재조회 → 수정 → 삭제 왕복이 모두 성공했다. 반영 지연으로 인한 재시도는 필요하지 않았다.
빠른 설치 — 다른 Mac에서 (ICMCP-27)
새 Mac에서는 셋업 스크립트 하나로 끝난다. ~/Documents·~/Desktop·~/Downloads 밖에 clone해야 한다 (이유는 Claude Desktop 등록 참고).
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 # 인자로 넘겨도 된다스크립트가 하는 일:
프로젝트가 TCC 보호 폴더 안에 있으면 중단하고 옮기는 방법을 안내한다
Node.js 18 이상 확인 →
npm install→npm run buildApple ID의 Latin1 범위 검사 (한글이 섞이면 기동 시점에
btoa()에서 죽으므로 미리 막는다)앱 암호를 키체인에 저장 (이미 있으면 유지할지 묻는다). 화면·셸 히스토리 어디에도 남지 않는다
Claude Code 등록 (
claudeCLI가 있으면)Claude Desktop 설정 병합 (
jq없이 node로 처리, 기존 키 보존,.bak생성)GUI 최소 PATH 환경을 재현해 실제로 iCloud 로그인까지 되는지 검증
끝나면 Claude Code는 재시작, Claude Desktop은 ⌘Q 후 재실행하면 된다.
앱 암호는 기기마다 따로 발급하는 편이 낫다. 한 대를 분실하거나 정리할 때 그 기기 것만 폐기하면 되고, 다른 기기는 영향을 받지 않는다. (Apple 계정당 앱 암호는 여러 개 만들 수 있다.)
개발 환경 설정
npm install
npm run build # dist/ 생성
npm run typecheck # 타입만 검사
npm run dev # watch 모드수동으로 설정하려면 아래 절들을 따른다.
앱 암호 발급
로그인 및 보안 > 앱 전용 암호 > + 로 새 암호 생성 (이름은
icloud-mcp등 아무거나)xxxx-xxxx-xxxx-xxxx형태의 16자 암호가 한 번만 표시된다. 창을 닫으면 다시 볼 수 없으니 바로 복사한다.
Apple ID 본 비밀번호로는 CalDAV 로그인이 되지 않는다. 2단계 인증이 켜져 있어야 앱 암호 메뉴가 나타난다.
Claude Code 등록 — 권장: 키체인 방식 (ICMCP-25)
MCP 호스트 설정 파일(~/.claude.json)에 앱 암호를 평문으로 적지 않는 방식이다. 암호는 macOS 키체인에 두고, 실행 래퍼 bin/icloud-mcp.sh가 기동 시점에 꺼내 환경변수로 주입한다.
# 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 등록 — 간단(비권장): 환경변수 직접 주입
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 등)를 지우지 않도록 병합해서 쓴다.
{
"mcpServers": {
"icloud": {
"command": "/절대/경로/icloud-mcp/bin/icloud-mcp.sh",
"env": { "APPLE_ID": "you@icloud.com" }
}
}
}명령줄로 안전하게 병합하려면:
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에 걸리지 않는다:
mv ~/Documents/어딘가/icloud-mcp ~/dev/icloud-mcp ln -s ~/dev/icloud-mcp ~/Documents/어딘가/icloud-mcpdist/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으로 절대경로를 지정한다:"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 본 비밀번호나 다른 기기 로그인에는 영향이 없다.
키체인 방식이면 교체는 항목 갱신 한 번으로 끝난다:
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 전후 실측:
동작 | 이전 | 이후 |
| 5,335ms | 517ms |
| 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에 아무것도 쓰지 않는다.