
Keepfor.me
io.github.melcutzPublished by GitHub account melcutzv0.2.0Updated Nov 6, 2026
Your personal library, readable by AI agents: durable snapshots, hybrid search, 46 MCP tools.
Overview
Remote MCP server for Keepfor.me, a read-it-later library, letting an assistant save, search, read, tag and organize your saved pages and notes.
- What you can do
- Save webpages, batches of URLs and personal notes to the library, with optional provenance fields
- Search the library by keyword and meaning, at item or passage level, and find related items
- Read items in paginated form, browse lists, and check reading history and library digests
- Manage tags: create, rename, merge, delete, suggest, search and bulk-tag items, with dry-run previews
- Create and manage highlights and collections, pin or trash items, and read account plan and quota
- Expose read-only resources for single items and tags, plus research brief and weekly review prompts
- When to use it
- Useful when you want an assistant to work from your own saved reading instead of the open web: capturing links during research, retrieving what you already saved on a topic, or tidying tags and duplicates across a personal library.
- Requirements
- A remote Streamable HTTP endpoint; no local install. An OAuth 2.1 sign-in or a personal access token created in Keepfor.me Settings, sent as an Authorization Bearer header. Read-only and read-write scopes are issued separately.
Installation
In SourceWeft
- Open Keepfor.me in the dashboard and add it to a workspace.
- Connect the required credentials. They are stored privately and never shown on public pages.
- Enable the server for the chats that should use its tools.
Web executable via Streamable HTTP. Remote servers run from the web runtime once configured in a workspace.
Use with an agent
Paste this prompt into Claude Code, Codex, Cursor or another agent — or into SourceWeft chat. The agent reads this server's install guide, shows you its publisher, endpoint and sign-in, and adds it to your client once you agree. Keys never go into a command.
Read https://sourceweft.com/mcp/io.github.melcutz%2Fkeepfor.me/install.md and connect the MCP server it describes to my client. Before changing any config, show me its publisher, endpoint and sign-in method and wait for my OK. Never put an API key in a command or in a file I would commit; ask me where it should come from.Other MCP clients
Add this to your client's mcpServers config. The client completes OAuth sign-in on first connect.
{
"mcpServers": {
"keepfor-me": {
"type": "http",
"url": "https://app.keepfor.me/api/mcp"
}
}
}README
Keepfor.me
[Release] [Tests] [Security] [Runtime] [FastAPI] [Protocol] [License] [Support this project]
A thin, fast read-it-later and personal library application running entirely on Cloudflare Python Workers, D1 SQLite with FTS5, Vectorize, R2, and the Model Context Protocol (MCP).
Features
- Four Core Jobs: Capture, Store, Search, and Share with AI Agents.
- Python Edge Runtime: Powered by FastAPI on Cloudflare Python Workers (Pyodide).
- Installable PWA & Native Sharing: Add to Home screen on Android and iOS. On Android, native Web Share Target integrates directly into the system share sheet. On iOS, 1-tap Apple Shortcuts integrate with the system share sheet using your Safari session, paired with smart clipboard detection in the PWA.
- Distraction-Free Reader: Customizable themes (Light, Sepia, Dark), fonts (Sans, Serif, Mono), and font sizes.
- Hybrid Search: Reciprocal Rank Fusion (RRF) combining D1 FTS5 BM25 keyword matching and Vectorize semantic embeddings (
bge-base-en-v1.5). FTS and embedding calls run concurrently, and the vector path is time-bounded so a slow AI binding degrades to keyword-only instead of hanging. - Resilient Content Extraction: Automated body parsing via
trafilaturawith OpenGraph metadata fallback so bookmarks are never lost. Browser-identical request headers plus an optional reader-proxy fallback (configurable viaREADER_PROXY_BASE, off by default) recover many bot-walled (HTTP 403) origins. - Visible Extraction State: Every item shows
Extracting…, content, or a failed reason (e.g.HTTP 403) rather than silently stalling. Bulk imports fan out through the queue, never blocking the request. - Dual Snapshots in R2: Raw original HTML snapshot (
raw.html) and sanitized reader HTML (clean.html). - Trash, Not Data Loss: Deleting (from the UI or from an agent) moves an item to the Trash instead of destroying it. The row, the snapshots and the vectors stay, so Restore is instant; Delete forever in the Trash view is the only permanent path, and a scheduled purge clears the Trash after 30 days.
- Zero-Config Single-Tenant Lock: First registration automatically claims admin ownership and locks out external signups.
- Model Context Protocol (MCP): Remote Streamable HTTP server at
/api/mcp(stateless, Bearer PAT auth) for opencode, Claude Desktop, and Cursor — no local install needed. - Instant Browser Capture: Drag-and-drop JavaScript bookmarklet and unpacked Manifest V3 extension.
- Import & Export: Ingest Pocket, Instapaper, Omnivore, and browser bookmark exports (Netscape HTML & CSV). Chrome folder names are preserved as tags. Notes import from Google Keep Takeout zips, Evernote
.enexfiles, and zips of Markdown/text files (Obsidian vaults, Apple Notes via Exporter, Simplenote): tags and original dates are kept, links inside notes become regular saves, attachments are listed but not stored, and re-imports never duplicate (50 MB cap for zips and.enex, 10 MB for HTML/CSV; inside a zip: 2 MB per file, 200 MB total and 20,000 files, so decompression bombs are refused). Every notes upload keeps a status record with its progress and final counts (notes imported, duplicates skipped, links queued, attachments not imported, parse errors, plan-limit skips) and a capped list of error messages, so the result survives a refresh. - Mobile-First UI: Bottom tab bar and tag chip strip on phones, ≥32px touch targets, no horizontal overflow down to 360px.
Architecture Overview
Extraction lifecycle
Saving a URL inserts the row with status='queued' and enqueues a job; the queue
consumer fetches, extracts, stores snapshots, and flips it to ok or failed with
a reason. Failures are always visible in the UI and reader.
Two deliberate design choices:
- No queue binding → extract inline. Local dev and tests have no queue
consumer, so
save_itemextracts synchronously rather than leaving items stuck inqueuedforever. - Bulk imports never run inline. A 900-bookmark import would issue thousands of D1 writes inside one request; instead it fans out as queue messages of 25 bookmarks, or a background task when no queue is bound.
If messages are ever lost, Settings → Re-queue stuck items re-sends jobs for rows
stuck in queued (skips fresh rows so live messages aren't duplicated).
Trash and restore
Deleting never destroys. MCP delete_item and DELETE /api/items/{id} set
items.deleted_at and stop there: the row, the R2 snapshots and the vector
embeddings stay, so an agent cannot destroy library data. Trashed items are
invisible everywhere (library, search, MCP reads, counts, exports) until they
are restored.
The Trash view (/trash) lists them with two actions: Restore brings
an item back into the library, Delete forever is the only permanent
delete. Hosts can also run the scheduled cleanup themselves:
Nothing is deleted behind the user's back: without a scheduler the Trash just grows.
Reader Proxy Fallback (READER_PROXY_BASE)
By default, Keepfor.me does not route failed fetches to any third-party service. When an origin rejects a direct fetch (HTTP 403, 429, 530), the item is recorded as failed.
To opt in to reader proxy fallback (e.g. using Jina Reader or a self-hosted readability proxy):
- Configure
READER_PROXY_BASEinwrangler.jsonc(vars.READER_PROXY_BASE = "https://r.jina.ai/") or in.dev.vars. - Privacy Notice: When enabled, blocked article URLs will be transmitted to the configured proxy service. Private IP ranges (e.g.,
127.0.0.1,10.0.0.0/8,192.168.0.0/16) and local domains are strictly blocked by SSRF egress policy and are never forwarded to any reader proxy.
For full guidance on private / offline deployments and egress policies, see docs/self-hosting.md.
Deployment
1. Prerequisites
- Node.js 20 or newer and npm
- uv 0.12.3 or newer
- Cloudflare account with Workers, D1, R2, Vectorize, and Queues enabled
- Custom domain:
keepfor.me(or standard*.workers.devsubdomain)
2. Provision Cloudflare Resources
Run the following commands once to create your D1 database, R2 bucket, Vectorize index, and Queue:
3. Apply Database Migrations
Apply the initial schema (tables, FTS5 virtual table, indexes):
For local testing:
4. Deploy
Deploys are manual: self-hosters deploy to their own Cloudflare account via pywrangler deploy or GitHub Actions workflow_dispatch (with input confirm: "deploy"). Pushing to main does not auto-deploy.
[!WARNING]
wrangler.jsonchas"remote": trueon D1, R2, and Vectorize bindings. Never run local dev againstwrangler.jsoncas it interacts directly with remote resources. For local simulation, copywrangler.local.example.jsonctowrangler.local.jsoncand runuvx --from workers-py pywrangler dev --config wrangler.local.jsonc.
PyWrangler bundles the Python dependencies declared in pyproject.toml.
A healthy bundle is roughly 52,000 KiB / ~4,500 modules. If .venv-workers/
paths appear in the module table, the build venv is being bundled and the deploy is
one dependency bump away from failing.
Once deployed, visit your domain (e.g., https://app.keepfor.me or
https://keepfor-me.<your-subdomain>.workers.dev). The first user to register
automatically becomes the admin and locks public registration.
5. Custom Domain
wrangler.jsonc declares a route for app.keepfor.me. Create the DNS record once
(Cloudflare dashboard → DNS → Add record):
The Worker route matches before the IP is ever contacted, so the placeholder
address is fine. workers_dev is left enabled so the *.workers.dev URL keeps
working.
Connecting AI Agents (Remote MCP)
Keepfor.me hosts a remote Streamable HTTP MCP server at https://app.keepfor.me/api/mcp,
exposing 42 tools for capturing, finding, reading and tagging your library:
save_url: Save any webpage to your library.save_urls: Save many webpages to your library in one call, with a status for each URL.save_note: Save a note, snippet or prompt template.search_library: Hybrid search across text and meaning.search_passages: Passage-level search with permanent snapshot URLs.find_item: Look up an item id by title or url (no search, no embeddings).get_item: Read metadata and excerpt by id, url or exact title.read_item: Paginated full-text reading.list_items: Compact paginated browse with sorting and filters.reading_history: What you saved and opened, with date filters.related_items: Items similar to a given item or text.list_tags/create_tag/describe_tag/update_tag/rename_tag/merge_tags/delete_tag: Full tag management.tag_item/tag_items/set_item_tags/retag_by_query: Tagging, in bulk and safely (dry-run by default).bulk_update: Update status, pin and tags on every item matching a filter (dry-run by default).suggest_tags/search_tags/tag_stats/related_tags/tag_cleanup: Tag vocabulary tools.find_cleanup: Read-only cleanup report (duplicate urls, dead links, never-opened old items, untagged items, near-duplicate tags).delete_item/pin_item: Remove or pin an item.library_digest: A compact recap — saves, opens, top tags, unread highlights.create_highlight/list_highlights/delete_highlight: Quote a passage of an item, anchored by character offsets.create_collection/list_collections/add_to_collection/remove_from_collection/rename_collection/delete_collection: Named buckets of library items.get_account: Your plan, the end of the trial and the quota left — free, so it still answers when the quota is exhausted.
The server is stateless (no sessions, plain JSON responses, no SSE stream) and authenticates with the same Personal Access Tokens as the REST API.
Setup
- Go to Settings & API in your Keepfor.me UI.
- Click Generate Token to create a Personal Access Token (
kfm_live_...). Pick Read-only in the scope picker if the agent should only be able to browse and search the library. - Point your client at the endpoint with the token as a Bearer header:
opencode (opencode.json):
Then export KEEPFOR_ME_TOKEN=kfm_live_... before launching opencode, and
restart opencode after changing the config.
Cursor / Claude Desktop: add a custom remote MCP server with URL
https://app.keepfor.me/api/mcp and header
Authorization: Bearer kfm_live_YOUR_TOKEN_HERE.
Handshake check without any client:
Available MCP Tools (Server 0.2.0)
The MCP server exposes 42 tools. The tools that change many items at once take a dry_run flag: it is only accepted as a JSON boolean (omitted means true, a preview), and anything else is rejected rather than applied.
save_url— Save any webpage to your library. Accepts optionalagent_summary,why_saved,source_context,saved_byfor agent research provenance.save_urls— Save up to 50 webpages to your library in one call, with a status per URL (queued,exists,invalid,over_limit) and the totals. Needs the write scope. The URLs are deduped by canonical URL, every new URL goes through the same save check assave_url(the ones the save limit refuses come back asover_limit), and the extraction is queued, so the call does not wait for the pages.save_note— Save a personal note, code snippet, or prompt template. Accepts the same optional provenance fields.search_library— Hybrid search across text and meaning (item-level results with snippets).search_passages— Passage-level hybrid search over semantic chunks withmax_charsbudget. Returnschunk_id, char offsets, and permanentsnapshot_url(R2 archive) that survives link rot.read_item— Token-efficient paginated reading of full content. Returnstext,next_cursor,total_chunks,outline.get_item— Retrieve metadata and excerpt, addressed byitem_id,urlor exacttitle. By default capscontent_textto excerpt and reportstruncatedandcontent_length; useinclude_content=truefor full text.list_items— Compact paginated browse (id,title,url,saved_at,tags— no body). Sorts bysaved,openedortitle; filters bysince/until,domain,pinned,typeand tags.find_item— Find item ids by exact/prefix title or exact/normalized url. No search, no embeddings: the cheap path from a title you know to anitem_idyou can read.reading_history— Reading history with date filtering (since/until), event types (saved/opened), and optional query/tag filters.related_items— Nearest neighbors by vector similarity from anitem_idor rawtext, with one-line reasons.list_tags— Every tag with its item count and last-used date. Sorts bycount,nameorrecent; filters byprefixandmin_count.create_tag— Create a tag with optionaldescription,colorandparent(hierarchy, e.g.work/research).describe_tag— A tag's metadata, parent, children and item count.update_tag— Edit a tag's description, color or parent.rename_tag— Rename a tag; every item carrying it is updated.merge_tags— Fold several tags into one target tag (at most 20 source tags per call).delete_tag— Remove a tag from items. Items are never deleted.dry_run=trueby default: returns the affected count and a sample;dry_run=falseapplies.tag_item— Add or remove tags on a single item.tag_items— Add/remove tags across many items.dry_run=trueby default. At most 100 items per call; the result reportstotal_matchedandtruncated.retag_by_query— Tag items matching a title/url query (plus tag filters).dry_run=trueby default. At most 100 items per call; the result reportstotal_matchedandtruncated.bulk_update— Update the status, the pin state and the tags of every item matching a filter (status group, saved date range, domain, pinned, type, tags).dry_run=trueby default: it reportstotal_matched,affectedandtruncatedwithout writing anything. At most 200 items are updated per call, and an empty filter or an empty change is rejected. The whole update is applied in one transaction, and tag names follow thecreate_tagrules (2-64 characters ofa-z,0-9, space,_or-).set_item_tags— Replace the whole tag set of one item.suggest_tags— Suggest tags for an item or for free text, drawn from your existing vocabulary first.search_tags— Fuzzy search over tag names and descriptions (machinelearningfindsmachine-learning).tag_stats— Tag usage statistics for the library.related_tags— Tags that co-occur with a tag on the same items.tag_cleanup— Near-duplicate tags, unused tags and untagged items worth a look.find_cleanup— Read-only cleanup report: duplicate items (same normalized url), dead links (recorded extraction failures — no network requests), never-opened old items, untagged items and near-duplicate tags. Takescategories,older_than_daysand a per-categorylimit(max 100); each category reports the candidates, a count and atruncatedflag. The item categories list one id with a reason; the group categories list one entry per group —duplicatesgives the group's ids (capped at 10), its size and whether the id list was truncated,near_duplicate_tagsgives the group's tag names — solimitcaps groups and one url saved 300 times stays a small response.delete_item— Move an item to the Trash. The item leaves the library but is not destroyed: it stays restorable for 30 days, and only the user can delete it for good from the Trash view.pin_item— Pin or unpin an item to/from the top shelf.library_digest— Read-only recap of the library for a period: saves, opens, top tags and unread highlights. Takes optionalsince,untilandtag; every list in the response is capped.create_highlight— Quote a passage of an item and anchor it with the character offsets it covers insidecontent_text.start_offsetdefaults to0,end_offsettostart_offset + len(text); an end offset past the end of the item's content is refused.list_highlights— The highlights of one item, ordered by their position in the text, with the quoted text, offsets, optional note and color.delete_highlight— Remove one highlight. The item it belongs to is never deleted.create_collection— Create a named bucket of library items, with an optional description. Creating one with an existing name returns it unchanged.list_collections— Your collections with their item count and item ids (at most 20 per collection, withitems_truncatedwhen there are more).add_to_collection— Put items in a collection, addressed by name or id. At most 100 items per call; the result reportsadded,skippedandtruncated.remove_from_collection— Take items out of a collection. The items themselves are never deleted.rename_collection— Rename a collection. Renaming onto a name that is already taken is refused, never merged.delete_collection— Delete a collection and its links. The items it held are never deleted; useremove_from_collectionto empty it.get_account— Read-only report of your account: the plan,trial_ends_atand the quota of every metered action (limit,used,remaining,resets_at). The call is free — it costs no quota unit, so it still answers when the quota is exhausted.
All tools declare MCP annotations (readOnlyHint, destructiveHint, idempotentHint). They are advisory hints for the client, and the server uses them to enforce the scopes of a token (see below). A token is never rejected for missing scope information: no scope information means full access. Each description says when to use a tool versus its siblings. List-type responses include limit, offset/cursor, and a fields projection; the default list_items page stays under 4 KB.
Tag filters (tags_all, tags_any, tags_none, untagged) work on list_items, search_library, search_passages, find_item, reading_history and bulk_update, and count child tags: filtering on work also matches work/research.
MCP Resources and Prompts
The server also speaks the resources and prompts parts of the MCP spec, with the same auth and tenant scoping as the tools.
Resources — read-only views of your own library:
resources/list returns your most recent items and most used tags, resources/templates/list returns the two URI templates, and resources/read returns the payload as JSON.
Prompts — reusable templates that read your own data:
Token scopes (read-only tokens)
A Personal Access Token can be limited to read-only. Create it on Settings & API with the Scope picker next to the token name (Full access is the default):
*(all) — the default. Every tool works, exactly as before.read— over MCP the token only sees the read tools intools/list, and any write or destructive tool (save_url,save_note,tag_item,create_tag,update_tag,rename_tag,merge_tags,delete_tag,tag_items,retag_by_query,bulk_update,set_item_tags,delete_item,pin_item,create_highlight,delete_highlight,create_collection,add_to_collection,remove_from_collection,rename_collection,delete_collection) is refused with a JSON-RPC error (error.data.type=insufficient_scope) before it runs — nothing is written.
Tokens created before scopes existed keep their full access: their stored
scope is NULL and means "all". Scopes are enforced everywhere a token works:
over MCP each tool needs the scope its annotations declare, and over REST a
read-only token gets 403 on every write route. The exception is the
read-only POST routes /api/search and /search: a search is a read, so a
read-only token may use them. /api/mcp enforces scopes per tool. PATs are
refused on /settings/* entirely, so a token can never mint a broader one.
Self-hosters: the scope column comes from migration 0012_pat_scopes.sql, so
apply the migrations (step 3 above) before deploying.
Browser & Mobile Capture
1. Android (PWA + System Share Sheet)
- Open your domain in Chrome and tap Add to Home screen (or Install app).
- To save from anywhere, tap Share → Keepfor.me in any app — the native Web Share Target opens a prefilled save sheet, and swiping back returns you seamlessly to your app.
2. iPhone & iPad (Apple Shortcut & Smart Clipboard)
iOS WebKit sandboxes PWAs from the system share sheet. Keepfor.me provides two zero-friction capture methods for iOS:
- 1-Tap Apple Shortcut (Share Sheet): Go to Settings & API → iPhone & iPad Quick Sharing and tap Install Keepfor.me Shortcut. This adds a native action to your iOS Share Sheet that sends URLs directly to Keepfor.me using your existing Safari login session (no API tokens or technical configuration required).
- PWA Smart Clipboard: Add the app to your Home Screen from Safari (Share → Add to Home Screen). When you copy a link in any app and switch to the Keepfor.me PWA, a floating toast detects the clipboard URL and lets you save it with a single tap.
3. Drag-and-Drop Bookmarklet (desktop)
Go to Settings and drag the Keepfor.me button to your browser's bookmarks bar. Click it on any page to open a quick-save dialog.
Bookmarklets always show a generic globe icon. To get the app icon: bookmark any page on your domain, edit that bookmark, paste the copied code (Settings → Copy code) as its URL, and name it
Keepfor.me.
4. Browser Extension (Manifest V3)
- Open Chrome/Brave/Edge and navigate to
chrome://extensions/. - Enable Developer mode (top right).
- Click Load unpacked and select the
keepfor.me/browser-extensionfolder. - Click the extension icon, enter your Worker URL and PAT token once in Settings
(
⚙️), then save any tab withAlt+S.
Local Development & Testing
Install the project and its test dependencies, then run the pytest suite:
Run pytest from the repo root — keepfor resolves as a namespace package only
when the root is on sys.path.
Lint and format exactly as CI does (bare ruff check . uses different rules and
will pass on things CI rejects):
To run the local Worker preview with its Python dependencies:
Local dev has no queue consumer, so saved links extract inline and show content immediately.
Performance notes
Cold starts dominate perceived latency on Python Workers: page loads sit around 250ms warm but can spike past 1.5s when an isolate boots.
- Import graph hygiene: Heavy article parsers (
trafilatura,bs4,lxml) are imported lazily inside the extraction code. A regression test asserts they stay out of the request import path. - Concurrent search & I/O: Hybrid search runs D1 FTS and Workers AI embedding
lookups concurrently (
asyncio.gather), bounded by a 5s timeout. - D1 query optimization: Composite indexes (
(user_id, created_at DESC)and(user_id, status, created_at DESC)) eliminate SQLite in-memory temporary B-tree filesorts (USE TEMP B-TREE FOR ORDER BY). Status counts, feed items, and tag aggregations are parallelized viaasyncio.gather, and multi-statement tag/deletion mutations are batched viaexecute_batchto minimize RPC roundtrips.
Support this project
If you find Keepfor useful, you can support its development:
Support Keepfor with a donation on Stripe (preset $5, or choose your amount).
Contributing
Contributions are welcome! Please read CONTRIBUTING.md for details on our development workflow, testing, and pull request process.
All contributors must sign our Contributor License Agreement (CLA). You retain copyright ownership of your work; the agreement grants us the license necessary to operate the hosted service and offer commercial licenses.
License
Keepfor.me is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only).
- Self-Hosting: You can self-host freely for personal or organizational use without restriction.
- Network Services: If you modify Keepfor.me and offer it as a service over a network, you must publish your modified source code under the AGPL-3.0.
- Commercial Licensing: Commercial licenses and alternative licensing agreements are available upon request to support proprietary, enterprise, or closed-source deployments. See LICENSING.md for details.
- Prior Releases: Previous releases prior to this license change remain under the MIT License for anyone who already has them.
- Trademark: The project name and marks are governed by TRADEMARK.md.
Source: README.md at commit da6a1b8
Tools
0Version history
1- v0.2.0LatestNov 6, 2026
Score
85% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 daysRequired25 pts
- Auth declaredRequired10 pts
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts
