hardcover-oauth-mcp
A remote MCP server for Hardcover, the book tracking site. It lets Claude and other MCP clients read and manage your library, reading progress, reviews, journal, lists, goals and more.
Public instance: https://hardcover.benjuntilla.com/mcp. Anyone with a Hardcover account can connect; see Connect a client and the privacy notice. Or deploy your own.
- Sign in with Hardcover (OAuth). Nobody pastes an API key. Each person connects their own account through Hardcover’s OAuth, with PKCE, and picks which permissions to grant. They can revoke access any time from Hardcover’s Authorized apps page.
- Reads and writes. About 47 task-shaped tools cover the library, progress, reviews, journal, lists, goals, follows, notifications and vibes. Raw GraphQL tools reach anything else in the Hardcover API.
- Paid and librarian features are labelled. A tool that needs a Supporter membership or a librarian role says so in its title, description and
_meta. - Runs on Cloudflare Workers. A Durable Object per connection holds the tokens and serves MCP; KV holds OAuth state. Alchemy deploys it in one command, and
alchemy devruns the same code locally.
Built with Effect 4 (including Effect’s own MCP server), TypeScript 7, Bun, mise and Astro (for this page and the privacy notice).
How it works
┌──────────────┐ MCP + OAuth 2.1 ┌──────────────────────────────┐
│ MCP client │ ─────────────────▶ │ Worker │
│ (Claude, …) │ ◀───────────────── │ OAuthProvider, consent page │
└──────────────┘ MCP access token │ auth + rate limits on /mcp │
└──────────────┬───────────────┘
│ one per connection
┌──────────────▼───────────────┐
│ HardcoverConnection (DO) │
│ Hardcover tokens │
│ Effect McpServer + tools │
└──────────────┬───────────────┘
│ OAuth + GraphQL
┌──────────────▼───────────────┐
│ Hardcover API │
└──────────────────────────────┘
The Worker is an OAuth authorization server for MCP clients, built on @cloudflare/workers-oauth-provider. It supports dynamic client registration and Client ID Metadata Documents.
Connecting works like this:
- Your MCP client opens
/authorize, which shows a consent page for choosing permissions. - The page hands you to Hardcover to sign in.
- Hardcover returns to
/callback. - The MCP client receives its own short-lived token for
/mcp.
Hardcover’s tokens never reach the client. Each connection’s Hardcover tokens live in a HardcoverConnection Durable Object. It refreshes them single-flight, because Hardcover rotates refresh tokens and revokes the whole chain if one is reused. It also revokes them after 150 days without use.
The Worker checks each /mcp request (token, allowlist, rate limit, scopes) and forwards it to the connection’s Durable Object. The object runs Effect’s MCP server, which speaks the sessionless 2026-07-28 revision as well as 2025-11-25, 2025-06-18 and 2025-03-26. Sessions for the 2025 revisions live in the object’s memory. When Cloudflare evicts an idle object, its sessions end: the client gets a 404 and, as the protocol requires, starts a new session.
The landing page (this README) and the privacy notice (PRIVACY.md) are prerendered by Astro and served as static assets.
Tools
Kinds: read, write (adds or changes data), delete. Each tool only appears if your connection was granted its scopes.
All 53 tools
| Tool | What it does | Kind | Scopes | Needs |
|---|---|---|---|---|
hardcover_me |
Who am I on Hardcover | read | read:me:content |
|
hardcover_search |
Search Hardcover | read | read:catalog |
|
hardcover_get_book |
Get a book | read | read:catalog |
|
hardcover_get_editions |
Get editions | read | read:catalog |
|
hardcover_get_author |
Get an author | read | read:catalog |
|
hardcover_get_series |
Get a series | read | read:catalog |
|
hardcover_trending_books |
Trending books | read | read:catalog |
|
hardcover_list_library |
List my library | read | read:library |
|
hardcover_currently_reading |
Currently reading | read | read:library |
|
hardcover_list_reviews |
List my reviews | read | read:library |
|
hardcover_get_library_entry |
Get my entry for a book | read | read:library |
|
hardcover_set_status |
Set reading status | write | write:library, read:library |
|
hardcover_update_progress |
Update reading progress | write | write:library, read:library |
|
hardcover_mark_finished |
Mark a book finished | write | write:library, read:library |
|
hardcover_rate_book |
Rate a book | write | write:library, read:library |
|
hardcover_write_review |
Write a review | write | write:library, write:reviews, read:library |
|
hardcover_delete_review |
Delete a review | delete | write:library, write:reviews, read:library |
|
hardcover_update_library_entry |
Edit my entry for a book | write | write:library, read:library |
|
hardcover_set_owned |
Mark an edition owned | write | write:library, read:lists, write:lists |
|
hardcover_remove_from_library |
Remove a book from my library | delete | write:library, read:library |
|
hardcover_add_read |
Add a reading session | write | write:library, read:library |
|
hardcover_update_read |
Edit a reading session | write | write:library, read:library |
|
hardcover_delete_read |
Delete a reading session | delete | write:library, read:library |
|
hardcover_list_journal |
List journal entries | read | read:journal |
|
hardcover_add_journal_entry |
Add a journal entry | write | write:library |
|
hardcover_update_journal_entry |
Edit a journal entry | write | write:library |
|
hardcover_delete_journal_entry |
Delete a journal entry | delete | write:library |
|
hardcover_list_lists |
List my lists | read | read:lists |
|
hardcover_get_list |
Get a list | read | read:lists, read:catalog |
|
hardcover_create_list |
Create a list | write | write:lists |
|
hardcover_update_list |
Edit a list | write | write:lists, read:lists |
|
hardcover_delete_list |
Delete a list | delete | write:lists, read:lists |
|
hardcover_add_to_list |
Add a book to a list | write | write:lists |
|
hardcover_remove_from_list |
Remove a book from a list | delete | write:lists, read:lists |
|
hardcover_reorder_list |
Reorder a list | write | write:lists, read:lists |
|
hardcover_list_goals |
List my reading goals | read | read:goals |
|
hardcover_create_goal |
Create a reading goal | write | write:goals |
|
hardcover_update_goal |
Edit a reading goal | write | write:goals, read:goals |
|
hardcover_delete_goal |
Delete a reading goal | delete | write:goals, read:goals |
|
hardcover_get_user |
Look up a reader | read | read:users |
|
hardcover_list_follows |
List follows | read | read:social |
|
hardcover_follow_user |
Follow a reader | write | write:social, read:users |
|
hardcover_unfollow_user |
Unfollow a reader | delete | write:social, read:users |
|
hardcover_list_notifications |
List notifications | read | read:notifications |
|
hardcover_mark_notifications_read |
Mark notifications read | write | write:notifications |
|
hardcover_list_vibes |
List my vibes | read | read:vibes |
|
hardcover_create_vibe |
Create a vibe | write | write:vibes |
Supporter |
hardcover_find_operations |
Find Hardcover API operations | read | none | |
hardcover_describe_type |
Describe a Hardcover GraphQL type | read | none | |
hardcover_query |
Run a Hardcover GraphQL query | read | none | |
hardcover_mutate |
Run a Hardcover GraphQL mutation | write | any write:* scope |
Supporter for write:vibes |
hardcover_delete |
Run a Hardcover GraphQL deletion | delete | any write:* scope |
Supporter for write:vibes |
hardcover_catalog_edit |
Add to or edit the Hardcover catalog | write | any write:* catalog scope |
Librarian for write:catalog:edit, Librarian (book_mapper) for write:catalog:map |
Supporter means a paid Hardcover membership. Librarian means a Hardcover librarian role: write:catalog:edit needs one, and book mappings (write:catalog:map) need book_mapper. Anyone may add catalog records and fill empty fields with write:catalog:append. Hardcover enforces all of this itself. This server labels the requirements (hardcover-oauth-mcp/requires and hardcover-oauth-mcp/scope-requires in each tool’s _meta), and drops edit and map at sign-in when your account lacks the role. Updating a vibe’s signals also needs Supporter; for non-Supporters Hardcover ignores those changes silently.
The raw tools (hardcover_query, hardcover_mutate, hardcover_delete, hardcover_catalog_edit) check each document before sending it, so mistakes come back as clear errors instead of costing rate limit. The checks are:
- the operation kind and the root fields API tokens may use;
- at most 10 top-level fields;
- none of the text operators Hardcover disables (
_like,_ilike,_regex…); - the scopes each root field, gated column and gated argument needs.
hardcover_find_operations and hardcover_describe_type read an index of the schema generated from Hardcover’s API docs (mise run schema:sync).
Hardcover’s rate limits are per user and shared across every app and token: 60 requests a minute, a burst of 10 (15 for Supporters), and 5,000 a day (50,000). Every top-level field costs one request.
Deploy your own
You need a Cloudflare account, a Hardcover account and mise (or Bun 1.3.10). A custom domain is optional; without one the server lives at https://hardcover-oauth-mcp.<your-subdomain>.workers.dev.
1. Install
git clone https://github.com/benjuntilla/hardcover-oauth-mcp
cd hardcover-oauth-mcp
mise install # Bun, pinned in mise.toml
mise run install # bun install
cp .env.example .env
2. Register a Hardcover app
Create one at hardcover.app/account/developer-apps/new:
- Application type: Web app (server-side). This can’t be changed later.
- Redirect URIs:
https://<your-domain>/callbackonly. For local runs, register a second app instead of adding localhost here. - Scopes this app may request:
read:me:content read:me:roles read:catalog read:library write:library write:reviews read:journal read:lists write:lists read:goals write:goals read:users read:social write:social read:notifications write:notifications read:prompts write:prompts read:vibes write:vibes read:account write:catalog:append write:catalog:edit write:catalog:map openid offline_access. The catalog scopes are unticked on the consent page by default, andeditandmapare only granted to librarians.offline_accesslets the server refresh Hardcover tokens while you’re away, and Hardcover requiresopenidwith it; MCP clients never see either. Leaveallunticked, since it would also cover scopes Hardcover adds later. - Device Authorization Grant: off.
Put the client id and secret into .env as HARDCOVER_CLIENT_ID and HARDCOVER_CLIENT_SECRET. The secret is shown only once.
3. Configure
In .env:
ALLOWED_HARDCOVER_USERS: your Hardcover username (or several, or numeric ids). Use*to let anyone connect, as the public instance does. Every person uses their own account and Hardcover rate limit, but your Worker pays for the CPU. Empty locks everyone out.HARDCOVER_MCP_DOMAIN: a hostname on a zone in your Cloudflare account, such ashardcover.example.com. Leave it empty for workers.dev.
4. Sign Alchemy in to Cloudflare
bun alchemy profile edit --add Cloudflare # opens a browser to authorize; pick the account
Or set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID in .env. The token needs Workers Scripts, Workers KV Storage and Workers Routes edit permissions, plus DNS edit on the zone if you use a custom domain.
5. Deploy
mise run deploy --stage prod
This builds the static pages with Astro and creates the KV namespace, the Durable Object, the Worker with its secrets and static assets, the rate limiters, the daily cleanup cron and the custom domain. It then prints the URL. Run it again after changes. mise run logs --stage prod tails logs. mise run destroy --stage prod removes everything.
Alchemy keeps deployment state in .alchemy/. Set ALCHEMY_STATE=cloudflare so the state lives in your Cloudflare account instead; then any machine, including CI, can redeploy or destroy.
- Plan: use Workers Paid. A connection’s Durable Object builds its MCP server when it wakes, which takes about 100 ms of CPU, ten times the Free plan’s per-request limit.
- Rate limits: per client IP, 10 registrations, 30 sign-in steps and 60 token requests a minute; per Hardcover account, 120 MCP requests a minute.
- Observability: Workers Logs, Traces and Issues are on. Unexpected failures (defects) are logged with no tokens or request data and grouped in Issues. Expected ones, such as rate limits, revoked access or bad queries, go back to the agent as tool errors.
6. Connect a client
The MCP URL is https://<your-domain>/mcp.
- Claude (web, desktop, mobile): Settings → Connectors → Add custom connector, then paste the URL.
- Claude Code:
claude mcp add --transport http hardcover https://<your-domain>/mcp, then run/mcpto sign in. - Cursor and others: add a remote server with that URL. The client discovers OAuth by itself.
- MCP Inspector:
bunx @modelcontextprotocol/inspector, choose Streamable HTTP, and enter the URL.
Your client opens the consent page, then Hardcover. Approve both and the tools appear.
Run locally
Hardcover accepts http://localhost redirect URIs, so the whole flow works on your machine:
- Register a separate Hardcover app for development, the same as above but with the redirect URI
http://localhost:1337/callback. - Fill in
.envwith that app’s credentials.HARDCOVER_MCP_DOMAINis ignored locally. - Run
mise run dev. It serves the Worker in workerd athttp://localhost:1337, with local KV and Durable Objects, and reloads on code changes. The static pages (README, PRIVACY.md,site/) are built once at start, so restart it to see changes to them. No Cloudflare account is needed. - Connect a client on the same machine to
http://localhost:1337/mcp, for exampleclaude mcp add --transport http hardcover-local http://localhost:1337/mcp.
Remote clients such as Claude on the web can’t reach localhost; they need a deployment.
Development
mise run check # TypeScript 7 typecheck + unit tests
mise run test:e2e # the Worker in workerd, a mock Hardcover, and a scripted MCP client
- Unit tests (
bun test) cover the GraphQL guard, the executor’s refresh and retry rules, scope handling, and every tool. Each tool runs through the real Effect MCP server, and its GraphQL is validated against Hardcover’s published schema (test/fixtures/hardcover-schema.graphql). - The end-to-end test walks the full OAuth flow and calls tools over 2026-07-28 and in a 2025-06-18 session. It also checks:
- concurrent calls share one Hardcover refresh;
- revoked access sends the client back to sign in;
- a missing scope triggers step-up;
- the static pages are served;
- the rate limits apply.
- The README tool table is generated from the toolkit (
mise run docs:tools), and a unit test fails when it’s stale.
Layout:
| Path | What |
|---|---|
alchemy.run.ts |
Infrastructure: Worker, KV, Durable Object, rate limiters, static site build, secrets, cron, domain |
src/worker.ts |
Entry point and per-IP rate limits |
src/oauth/ |
MCP-facing authorization server: provider config, consent page, Hardcover callback |
src/connection/ |
HardcoverConnection Durable Object: token storage and refresh, and the connection’s MCP server |
src/hardcover/ |
Hardcover OAuth client, GraphQL executor, guard, scopes, schema index |
src/mcp/ |
Effect MCP server, the /mcp auth wrapper, tools |
site/, astro.config.mjs |
Astro pages for README.md and PRIVACY.md, and the shared stylesheet |
scripts/ |
Schema sync and the README tool table |
Security notes
- Hardcover tokens are stored only in the connection’s Durable Object and are never sent to MCP clients. Refresh tokens rotate, and the old one is replaced before the new access token is used.
- The consent page escapes everything a client supplies. It shows the redirect host, warns about local redirects, and can’t be framed. It is bound to your browser by the OAuth library’s
__Host-cookies, and remembered approvals use a key derived from the Hardcover client secret. - The callback checks Hardcover’s
iss(RFC 9207) and the PKCE verifier.ALLOWED_HARDCOVER_USERSis checked at sign-in and on every MCP request. - Unauthenticated endpoints are rate-limited per IP, and
/mcpper Hardcover account. - Revoking the app on Hardcover, or 150 days without use, ends the connection. The client then has to sign in again.
License
MIT, © 2026 Ben Juntilla. src/generated/hardcover-api.json and test/fixtures/hardcover-schema.graphql are derived from hardcoverapp/hardcover-docs (MIT, © Hardcover LLC). This project isn’t affiliated with Hardcover.