hatebucli MCP server
2026년 11월 13일 커밋Hatena Bookmark 기록을 로컬 캐시에 동기화하여 AI 어시스턴트가 북마크 아카이브를 검색, 분석 및 쿼리할 수 있도록 합니다.
다른 이름 yuiseki/hatebucli
- 스타
- 10
- Tools
- 14
- 다운로드, 30일
- —
- 최신 버전
- —
- 마지막 커밋
- 2026년 11월 13일
- 라이선스
- —
hatebucli MCP란?
hatebucli은(는) MCP server입니다. Hatena Bookmark 기록을 로컬 캐시에 동기화하여 AI 어시스턴트가 북마크 아카이브를 검색, 분석 및 쿼리할 수 있도록 합니다. 14개 도구를 제공합니다. 예: hatebu_search, hatebu_list, hatebu_domains.
프로젝트 README에서 생성된 요약
hatebucli MCP server 정보
이 MCP server를 사용하면 AI 어시스턴트를 통해 개인 Hatena Bookmark 기록을 분석할 수 있습니다. 북마크 피드를 로컬 JSON 캐시로 동기화하여 수십만 개의 항목에 대해 빠르고 오프라인으로 쿼리할 수 있도록 지원합니다. 데이터가 로컬에 저장되므로 AI는 외부 서버에서 데이터를 지속적으로 가져올 필요 없이 전체 아카이브를 검색, 분석 및 쿼리할 수 있습니다.
사용자는 Hatena ID를 설정하고 동기화 명령을 실행하여 로컬 캐시를 생성함으로써 서버를 구축합니다. 설정이 완료되면 AI 클라이언트는 도구를 사용하여 특정 용어 검색, 특정 URL의 기존 북마크 여부 확인, 도메인 및 태그 순위 생성과 같은 복잡한 작업을 수행할 수 있습니다. 이 서버는 대규모 아카이브를 효율적으로 처리하도록 설계되었으며, 타임라인 생성 및 단어 빈도 분석과 같은 특화된 명령을 통해 독서 습관에 대한 통찰을 제공합니다.
활용 사례
- 1이전 북마크 확인특정 URL을 기록에 이미 저장했는지 어시스턴트에게 확인 요청을 합니다.
- 2독서 트렌드 파악특정 월 또는 연도에 관심을 가졌던 가장 특징적인 주제나 단어의 요약을 요청합니다.
- 3주제 변화 분석어시스턴트에게 두 시기를 비교하여 북마크 내 주제가 어떻게 증가하거나 감소했는지 분석하게 합니다.
- 4독서 맥락 발견특정 기사와 비슷한 시기에 어떤 다른 페이지들이 북마크되었는지 어시스턴트에게 찾아달라고 요청합니다.
프로젝트의 README를 바탕으로 작성되었습니다. 모든 활용 사례는 해당 내용의 인용문과 대조하여 확인합니다.
hatebucli MCP 도구 14
README에서 감지됨hatebu_searchhatebu_listhatebu_domainshatebu_tagshatebu_wordshatebu_statshatebu_lookuphatebu_timelinehatebu_taggedhatebu_randomhatebu_comparehatebu_context
| hatebu_search | — | |
| hatebu_list | — | |
| hatebu_domains | — | |
| hatebu_tags | — | |
| hatebu_words | — | |
| hatebu_stats | — | |
| hatebu_lookup | — | |
| hatebu_timeline | — | |
| hatebu_tagged | — | |
| hatebu_random | — | |
| hatebu_compare | — | |
| hatebu_context | — |
도구 14개 모두 보기도구 수 줄이기
hatebu_burstshatebu_resurface
| hatebu_bursts | — | |
| hatebu_resurface | — |
구성
HATENA_USER
| 이름 | 설명 | 유형 |
|---|---|---|
| HATENA_USER | — |
자동 검사
2026년 11월 14일에 실행됨전체 보고서 →아직 인증되지 않았습니다. 서버는 80점 이상의 점수, 심각한 결함 없음, 그리고 검사에 충분한 데이터를 갖추어야 합니다. 검사 방식
- 소스 저장소 연결 및 접근 가능yuiseki/hatebucli is public and reachablegithub.com/yuiseki/hatebucli ↗통과
- 패키지가 이 저장소를 소스로 지정검사되지 않음
- 빌드 출처(Sigstore 증명)검사되지 않음
- 오픈 소스 라이선스 (OSI)No license결함 발견
- 최근 1년 내 커밋 또는 릴리스Last commit or release 2026-11-13github.com/yuiseki/hatebucli ↗통과
- OpenSSF Scorecard검사되지 않음
- 패키지 이름이 인기 패키지와 유사하지 않음검사되지 않음
- 패키지와 의존성에 심각한 보안 권고 없음검사되지 않음
- 모델에 대한 숨겨진 지침 없음14 tool descriptions and the README read; no override phrases, credential paths or hidden characters통과
- 파괴적인 도구에 라벨 표시됨No tool named like delete, drop or exec통과
- 원격 endpoint가 MCP 핸드셰이크에 응답검사되지 않음
README
GitHub에서 보기@yuiseki/hatebucli
Your Hatena Bookmark history, on your machine and answerable.
Hatena Bookmark gives you a feed of what you bookmarked, one day at a time, and
not much else. hatebu syncs those days into a local cache and then answers
questions over the whole of it: what you were reading in 2019, which sites you
keep coming back to, when a subject first showed up, whether you have already
bookmarked the page in front of you. Everything except today is answered from
the cache, so it works offline and it stays fast on an archive of hundreds of
thousands of bookmarks.
It also runs as an MCP server, so an assistant can ask the same questions without you translating each one into flags.
Install
Not on npm yet. Install it from a checkout:
git clone https://github.com/yuiseki/hatebucli.git
cd hatebucli
npm install && npm run build
npm link # puts `hatebu` on your PATH
Needs Node 22.23.4, 24.22.1, or 26.12.1 or newer within those release lines.
The security fixes in those releases cover the supported Node.js lines. Run it
on an older one and it says so rather than failing from inside node_modules.
Getting started
hatebu config set username <your_hatena_id>
hatebu sync --days 30
hatebu words
sync is what fills the cache, one request per day, spaced out because the
feed is somebody else's server. Nothing else here fetches anything, apart from
today.
Commands
Every command that reports bookmarks or rankings takes -j, --json.
One day
hatebu list # today, fresh from the feed
hatebu list --date 2026-02-18 # an earlier day, from the cache
| Option | |
|---|---|
-d, --date <yyyy-mm-dd> |
the day. Defaults to today |
-j, --json |
Search
hatebu search 地図 # every cached day, best match first
hatebu search zenn.dev --field url
hatebu search 地図 --date 2026-02-18 -l 50
A query is one or more terms separated by whitespace, and every term has to
appear. A term of plain letters and digits has to appear as a whole word, so
ai means AI and not mai, aichi or maplibre. Anything else is matched as
written: 地図 finds 地図帳 and 白地図, and arxiv.org is a host. 地図 AI
is two terms and needs both. Results are ordered by score, then newest first,
and the last line says how many matched when the limit cut them.
--match substring looks inside words, which is what finds LiteLLM when you
search llm. See ADR 012 for the measurements
behind the default.
To count what came from a site, use hatebu lookup instead: it matches the
hostname, where a URL search matches the text of the URL anywhere in it.
| Option | |
|---|---|
-f, --field <all|title|url> |
where to match. Default all |
-m, --match <word|substring> |
how a plain term is matched. Default word |
-d, --date <yyyy-mm-dd> |
one day only |
-l, --limit <number> |
default 10 |
-j, --json |
Rankings over a range
hatebu domains --date 2026 # the sites of a year
hatebu tags --date 2026-02 # the tags of a month
hatebu words # what last week was about
hatebu words --today
hatebu stats --days 30 --top 20 # a window as one summary
domains, tags and words take the same range options. With none of them
the range is the week ending yesterday.
| Option | |
|---|---|
--date <yyyy|yyyy-mm|yyyy-mm-dd> |
a day, a month or a year |
--today |
today only. Cannot be combined with --date |
-l, --limit <number> |
rows. Default 10, capped at 10 for domains and tags, 30 for words |
--by <distinctive|count> |
words only. Default distinctive |
-j, --json |
words runs the titles through a Japanese morphological analyser and ranks by
how much more the range used each word than the archive usually does, so the
answer is what the stretch was about rather than which words it contained. A
word counts once per bookmark however often the title repeats it.
hatebu words --rebuild-background # once, about a minute
hatebu words --date 2013
The background is one file of word counts over the whole archive, in total and
by year, about 4MB.
Building it tokenizes every title, which takes a minute, so it is a command you
run rather than something that happens during sync; a day of new bookmarks
moves a background of 374,000 by nothing. Without it, words falls back to raw
counts and says so. --by count asks for those on purpose.
Ranking this way is what makes a year legible:
2013 js node grunt javascript fluentd css backbone sass angularjs chef
2019 nhk ニュース 朝日新聞 ライブドアニュース 放火 yahoo pay 香港 文春
Frequency alone answers ai github for and the https for every year of this
archive. See ADR 010.
stats puts a window together: how much, at what hours, on what weekdays, and
the sites and tags that led it. Markdown to read, --json to compute from; the
two are cut the same way, so a bucket nobody bookmarked in is absent from both
rather than reported as a zero.
| Option | |
|---|---|
--date <yyyy|yyyy-mm|yyyy-mm-dd> |
the day the window ends on. Defaults to yesterday |
--days <number> |
window length. Default 7 |
--top <number> |
rows per section. Default 10, capped at 20 |
-j, --json |
Each of these reports the days of the range the cache does not hold, so a low count can be told apart from a gap in the sync.
The whole archive
hatebu lookup https://example.com/article # have I bookmarked this page?
hatebu lookup example.com # what have I read from this site?
hatebu timeline --query maplibre --bars # when was I reading about this?
hatebu timeline --by month --tag 地図
hatebu tagged 地図 # what did I file under this tag?
hatebu random -n 5 --from 2010-01-01 # dig something out of the archive
lookup answers about one page when given a full URL, and about a site when
given a bare hostname. A subdomain counts as part of the site, and the scheme
and a trailing slash are ignored, because neither makes it a different page. A
URL that was never bookmarked still reports how much came from the same site.
timeline counts per year or per month, oldest first, and takes --tag,
--domain, --query, --from and --to in any combination. --bars draws
each row. random takes the same filters.
These read every cached day rather than an index. Over an archive of a few hundred thousand bookmarks that is under a second.
Choosing a best of the day
hatebu pick # yesterday's bookmarks, numbered
hatebu pick 12 # the entry page for number 12
hatebu pick --weekly # the week's daily_best
hatebu pick --monthly --date 2026-09
pick writes nothing to Hatena. It lays out the candidates and gives you the
entry page for the one you chose; you add the daily_best tag there, the way
you would add any tag. The tag comes back into the cache on the next sync,
which is what makes the weekly round able to read what the daily round chose:
| round | candidates | tag to add |
|---|---|---|
pick |
the day's bookmarks | daily_best |
pick --weekly |
daily_best of the last seven days |
weekly_best |
pick --monthly |
weekly_best of the month |
monthly_best |
pick fetches the days of its window first, unless the cache already shows the
round as decided, because a tag added on the entry page is invisible until that
day is fetched again. A decided round costs no requests; an undecided week
costs its seven days. --no-sync reads the cache as it stands.
A round that is already decided shows what was chosen and stops, rather than
asking you to read the day again. A day may have more than one best; they are
all shown, and all of them go forward as candidates for the weekly round. --all lists the candidates anyway, with the
chosen one marked, and a number still resolves against that list if you change
your mind. See ADR 011 for why this does not
go through the write API.
Reading the archive against itself
hatebu compare 2023 2026 # what rose, and what fell away
hatebu compare 2025 2026 --by domains
hatebu context https://example.com/article # what it was read alongside
hatebu timeline --query maplibre --bars # count and share, per period
hatebu bursts --query maplibre # when it flared, and how far above usual
hatebu resurface --peaked-after 2015 # what you cared about and stopped mentioning
compare scores each period against the other, so a word common to both says
nothing and a word rare in both can be the whole of the difference. Every row
carries both counts and both rates per thousand, because two periods are
rarely the same size.
More in 2026
claude: 18 → 260 (0.2 → 3.8 per 1000)
エージェント: 15 → 155 (0.2 → 2.3 per 1000)
More in 2023
chatgpt: 510 → 62 (5.9 → 0.9 per 1000)
langchain: 109 → 5 (1.3 → 0.1 per 1000)
bursts names the episodes a timeline only draws. A month counts when it runs
at three times the subject's own rate over the whole archive and holds at least
three matches, so a subject is bursting because it is unusual for itself, not
because the month was busy:
maplibre: 123 bookmarks, usually 0.3 per 1000
2022-11..2023-01 15 bookmarks, peak 2023-01 at 5.6 per 1000 (17x)
2024-10..2024-12 17 bookmarks, peak 2024-11 at 7.4 per 1000 (22.5x)
2026-05..2026-09 28 bookmarks, peak 2026-08 at 12.2 per 1000 (37.1x)
resurface takes no subject at all: it reads the vocabulary year by year for
the shape of something that burned and went out, scoring how loud it got, how
gone it is, how long it has been dormant and whether there was ever enough of
it. It needs the background model that words uses, rebuilt with the version
that carries a breakdown by year. See
ADR 013.
word peak year then now dormant
ニュース 2020 125.22 30.78 6y (17259 bookmarks)
android 2015 56.42 4.92 11y (4411 bookmarks)
コロナ 2020 77.82 0.94 6y (1687 bookmarks)
vr 2018 42.69 0.39 8y (1090 bookmarks)
context answers why a page was bookmarked, by showing what was bookmarked
around it, grouped into sittings by the quiet between them. Give it a URL or a
time as 2026-04-10T21:15.
Filling the cache
hatebu sync # yesterday
hatebu sync --days 30 # yesterday back thirty days
hatebu sync --date 2019-07-04 # one day
hatebu import ./old-bookmarks # a legacy <dir>/YYYY/MM/*.json tree
sync never fetches today by default: today is still changing, and a cached
copy of a half-finished day would be wrong from the moment it was written.
--days and --date cannot be combined.
Configuration
hatebu config set username <your_hatena_id>
hatebu config get username
Where things live
| Bookmarks | ${XDG_CACHE_HOME:-~/.cache}/hatebucli/YYYY/MM/DD.json |
| Username | ~/.config/hatebu/credentials.json |
HATENA_USER |
the user ID, and it wins over the stored one |
HATENA_BOOKMARK_RSS_URL |
override the feed URL. %s is the user ID |
The cache is plain JSON per day and safe to copy, diff or back up. Earlier
versions also kept a search index under hatebucli/index; nothing writes there
any more and it can be deleted (see
ADR 005).
What the archive can and cannot tell you
- A day that was never synced is not a day with nothing in it, and the commands say which is which rather than reporting a zero.
- Tags come from the feed as it stood on the day. A stretch from before you
started tagging has no tags, which is a gap in the data and not a gap in your
reading;
wordsis the tool for those years. - Comments are only present where you left one, which for most people is almost nowhere.
- Re-syncing an old day is safe and sometimes an improvement: a legacy import can carry a URL where the title should be, and the feed still has the title.
MCP server
hatebu --mcp-server runs the CLI as a Model Context Protocol server over
stdio, so an MCP client can read your bookmarks. --mcp, mcp-server and
mcp start the same thing.
hatebu --mcp-server
It needs a username before it starts, from hatebu config set username or from
HATENA_USER in the client's environment. stdout carries only JSON-RPC;
anything meant for a human goes to stderr.
Configured in a client:
{
"mcpServers": {
"hatebu": {
"command": "node",
"args": ["/path/to/hatebucli/bin/hatebu.js", "--mcp-server"],
"env": { "HATENA_USER": "your_username" }
}
}
}
Once this is on npm the command becomes
npx -y @yuiseki/hatebucli --mcp-server.
Everything except today is answered from the local cache, so run hatebu sync
before pointing a client at it.
Tools
Fourteen, all read-only. Nothing writes to your account, and there is no sync tool: fetching is a decision about somebody else's server, so it stays a command you run.
| Tool | Arguments |
|---|---|
hatebu_search |
query (required, up to 200 characters), field, match, date, limit (default 10, max 100). Returns match_count and returned_count |
hatebu_list |
date (defaults to today) |
hatebu_domains |
date, today, limit |
hatebu_tags |
date, today, limit |
hatebu_words |
date, today, by, limit (default 30) |
hatebu_stats |
date, days, top |
hatebu_lookup |
url_or_domain (required), limit |
hatebu_timeline |
by, tag, domain, query, from, to |
hatebu_tagged |
tag (required), limit |
hatebu_random |
count, tag, domain, query, from, to |
hatebu_compare |
a, b (required), by (words, domains, tags), limit |
hatebu_context |
url_or_time (required), minutes, gap |
hatebu_bursts |
one of query, tag, domain (required), from, to, lift, min_count |
hatebu_resurface |
limit, min_total, recent_years, peaked_after, peaked_before |
They answer the same numbers as the matching command, down to the defaults, and a test holds them to it. What differs is what a model needs and a terminal does not:
- Every ranking returns
missing_datesalongside the numbers, so a model can tell a quiet week from an unsynced one. hatebu_listsays whether the day was in the cache at all.hatebu_statsreturns the Markdown and the JSON as two blocks, so a model gets both without a second call. The JSON is byte for byte whatstats --jsonprints.hatebu_searchreportsmatch_countas well asreturned_count, so a question about how many were bookmarked is not answered by counting rows the limit allowed through,--jsonreturns, since a title is matched per character and they are single letters;matchedInstays, because which field matched is worth knowing.
A bad argument comes back as an error on that call. The server keeps answering.
Development
npm install
npm run build
npm test
CI runs the same on Node 22, 24 and 26. npm install wants npm 11 or newer:
npm 10.9.3 cannot resolve this dependency set and dies in arborist with
Cannot read properties of null (reading 'edgesOut'). npm ci against the
committed lockfile is fine on either.
The tests spawn the built CLI and assert on its output, and the MCP tests speak JSON-RPC to it over a pipe, so they cover the framing too. Nothing in them touches your real cache or reaches Hatena: the fetching paths run against a stub feed served from a second process.
To unlink a local build again:
npm unlink -g @yuiseki/hatebucli
The version in package.json is the only way to tell two builds apart, so
bump it whenever a copy goes anywhere: a global install, a directory a service
runs from, a tarball. Three copies all claiming the same version is how you end
up debugging the wrong one.
The decisions behind the shape of this thing are in docs/ADR, including why search scans instead of keeping an index and why the MCP server is the CLI rather than a second program.
hatebucli MCP 질문
hatebucli MCP는 어떤 도구를 제공하나요?
14개 도구를 제공합니다. 예: hatebu_search, hatebu_list, hatebu_domains, hatebu_tags, hatebu_words.
AI 어시스턴트와 hatebucli를 어떻게 사용하나요?
MCP server로 실행되므로, 사용자가 직접 CLI 플래그를 입력할 필요 없이 어시스턴트가 북마크 기록에 대해 질문할 수 있습니다.
오프라인에서도 작동하나요?
네, 오늘 저장한 북마크를 제외한 모든 데이터는 로컬 캐시에서 답변하므로 오프라인 사용이 가능합니다.
북마크에서 특정 용어를 어떻게 검색하나요?
공백으로 구분된 용어를 사용하여 search 명령어를 사용할 수 있습니다. --match 플래그를 통해 완전 일치 또는 부분 일치 검색을 지원합니다.
설치를 위한 시스템 요구 사항은 무엇인가요?
Node.js 버전 22.23.4, 24.22.1, 26.12.1 또는 해당 릴리스 라인의 최신 버전이 필요합니다.