Zotero를 헤르메스에 붙였다 — 심사 초대 논문 36권이 자동으로 서재에 들어온다
저널에서 오는 심사 초대 메일이 매일 쌓입니다. 작년부터 Gmail을 감시하는 크론을 돌려서 초대 편지들을 JSON 파일 하나로 뽑아두기만 했는데, 오늘 실측으로 추적 중인 건이 75건이었습니다. 정작 그 논문들을 Zotero 서지관리 컬렉션으로 옮기는 일은 계속 손으로 하고 있었습니다. 감시는 자동이고 분류는 수동이라, 하루에 10분씩 새는 구멍이 이번 작업의 대상이었습니다.

Zotero에는 공식 CLI가 없습니다
시작 전에 생태계를 조사해 봤는데, Zotero 공식 CLI는 존재하지 않습니다. 없어서 없는 걸 만든 셈이고, 결국 커뮤니티 도구인 54yyyu/zotero-mcp를 고르게 됐습니다. PyPI 패키지명은 zotero-mcp-server이고, 버전은 0.15.1로 고정했습니다. 고정해두지 않으면 남이 무엇을 올렸는지 모르는 채로 잠들게 되거든요.
설치에서 작은 사고가 하나 있었습니다. 이 도구가 기본 Python에서는 pydantic 호환 문제로 돌아가지 않다가, Python 3.12로 재설치하니 멀쩡해졌습니다. 오류 메시지는 제 코드의 문제를 알려주는 게 아니라 제 환경의 버전을 알려주고 있었습니다.
핵심 결정 — 로컬 DB 직독을 버리고 Web API로 통일
가장 유혹이 큰 길은 데스크톱에 놓인 zotero.sqlite 파일을 직접 읽는 것입니다. 저도 한동안 그쪽을 봤고, 결국 기각했습니다. 로컬 DB는 데스크톱의 sync 로그인을 거쳐야만 채워지는데 이 환경은 화면 없는 Linux 워크스테이션이고, 데이터의 진짜 원본은 서버 쪽 라이브러리에 있습니다. 남의 집 냉장고 문을 열고 제 반찬을 세는 격이라, 반쯤 비어 있는 사본을 붙들고 있을 이유가 없었습니다.
대신 Zotero Web API로 나가서 들어오는 왕복을 택했습니다. 이 결정은 나중에 빛이 났습니다. 증분 동기화 크론과 대량 적재 스크립트가 같은 API를 공유하게 돼서, 읽기 로직이 두 갈래로 갈라지지 않았습니다.
서재를 사서에게 맡기되, 통째로 불태울 권한은 주지 않습니다
데스크톱은 10.0.6을 설치했습니다. 자격증명은 ~/.config/zotero-mcp 아래 config.json 한 곳에 권한 600으로 모았습니다. Hermes와 Claude Code가 이 파일 하나를 같이 쓰면서 헤드리스 검증을 통과했습니다. 읽기 확인은 명령 한 줄이면 됩니다. --json을 빼면 markdown이 출력돼서 기계 해석이 불안정해지므로, 읽기에는 --json을 붙이는 게 습관이 됐습니다.
zotero-cli --json get collection-items <key>
보안 규칙은 기능보다 먼저 그었습니다. 봇의 쓰기 범위를 Inbox/Hermes와 lab_paperreview 컬렉션, 그리고 hermes-added 태그가 붙은 항목으로 제한했습니다. 기존 항목을 수정하거나 삭제하거나 병합하는 일은 금지입니다. 클라우드 임베딩도 금지했는데, 메모가 밖으로 나가는 길 자체를 막아두자는 취지입니다. 사서에게 서재를 정리시키는 것은 좋아도, 서재를 통째로 태울 권한까지 줄 필요는 없습니다.
Zotero 업무만 전담하는 헤르메스 프로필 librarian도 분리했습니다. 스킬 12개를 압축해서 싣고, SOUL.md에 Zotero 하드룰을 명시해 뒀습니다. default 프로필은 Zotero 요청을 직접 처리하지 않고 @librarian으로 위임 라우팅합니다. 여기서 부수 발견이 하나 나왔습니다. 다른 프로필의 profile.yaml에 description: 키가 빠져 있었는데, YAML 파싱이 조용히 실패해서 계속 기본 설정으로 돌아가고 있었습니다. 설정은 저장돼서 살아 있는 게 아니라, 파싱돼야 비로소 살아 있습니다.
적재에서 저를 문 것은 중복 20건
첫 배치로 심사 초대 논문 35건을 lab_paperreview에 올렸습니다. 증분 스크립트가 그 뒤 1건을 더 올려서 현재 36건입니다. state 파일에도 synced 1건이 남아 있어서 개수는 서로 대조할 수 있습니다.
첫 배치는 깨끗하지 않았습니다. 초기 테스트에서 요청을 limit=100 한 번으로만 보내고 다음 페이지를 순회하지 않아, 중복 20건이 그대로 적재됐습니다. 전체 페이지를 순회하고 dedup 단계를 넣어서 고쳤습니다. 고치고 나서의 판단 기준은 멱등성이었습니다. 같은 것을 두 번 돌렸을 때 개수가 늘지 않아야 진짜 자동화이고, 늘면 그냥 타이머입니다.
함정은 두 개 더 있었습니다. 크론 등록 때 스크립트를 심볼릭 링크로 두니 realpath가 허용 디렉터리 바깥을 가리켜서 실행이 거부됐습니다. 실행 사본을 스크립트 디렉터리에 실제 파일로 카피해서 해결했습니다. CrossRef에 DOI가 미등록인 논문(일부 대형 출판사 계열)은 add가 실패하는 구멍도 있었습니다. 파이프라인이 CrossRef에 기록이 있는지 먼저 확인하고 나서 진행하도록 로직을 고쳤습니다.
성공보다 먼저 적어두는 한계들
증분 동기화 크론(매일 08:55, 18:55)은 등록해 놓고 현재 일시정지 상태입니다. 위임 실측과 읽기/쓰기 키 분리가 끝날 때까지는 손으로 돌리는 편이 안전하다고 판단했습니다. 지금은 읽기와 쓰기 키가 합쳐진 키를 쓰고 있어서, 검증이 끝날 때까지는 믿지 않는 쪽이 맞습니다.
Better BibTeX는 아직 미설치입니다. citation key 고정과 .bib 자동 내보내기는 논문 집필 단계의 진짜 본진이라서 다음 단계로 미뤘습니다. 새 세션에서 default→librarian 실제 위임이 DM으로 정상 전달되는지 실측도 남아 있습니다. 운영 중 별개의 버그도 하나 만났습니다. 한국어 생성이 길어지면 토큰 반복 붕괴가 가끔 발생하는데, 해당 문단만 재전달해서 복구하며 쓰고 있습니다.
그래도 서재의 개수는 36건입니다. 감시, 추출, 적재, 대조까지 사슬은 한 바퀴 돌아갔지만, 마지막으로 알아서 돌리라는 링크는 아직 앞쪽에 놓여 있습니다. Zotero와 에이전트를 갖고 있고 하루 10분도 아까우시다면, --json 읽기 명령 하나로만 시작해 보셔도 좋습니다. 키 분리를 끝내고 크론을 다시 켜면 그 이야기를 또 쓰러 오겠습니다.