L4
Provides email sending capabilities via Resend's API, used to deliver daily review drip reminders for overdue decision notes.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@L4search my notes for anything about the R2 migration"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
L4
L4 is a personal memory / knowledge-base server: a plain markdown vault (Obsidian-style, but nothing here depends on Obsidian) stored in Cloudflare R2, exposed to any MCP-capable AI client over the Model Context Protocol. Point Claude, or any other MCP client, at your own notes -- list them, read them, write them, search them by keyword or by meaning, follow the backlink graph -- all served from a single Cloudflare Worker you own and run.
It's model-agnostic: the server speaks MCP over Streamable HTTP, so anything that can be an MCP client can connect, not just Claude.
Why this exists
Most "AI + your notes" setups either upload your notes into someone else's product, or require a local process you have to keep running. L4 is neither: it's a few hundred lines of TypeScript on Cloudflare's edge, backed entirely by Cloudflare-managed storage, that you deploy once and then just... use, from any device, through any MCP client, over standard OAuth.
Related MCP server: SkyBrain
Architecture
MCP client (Claude, etc.)
| Streamable HTTP + OAuth 2.1 / PKCE
v
Cloudflare Worker (this repo)
|-- @cloudflare/workers-oauth-provider issues MCP-facing OAuth tokens,
| handles dynamic client registration
|-- Cloudflare Access (OIDC) upstream login: one-time-PIN email auth,
| gated by an email allow-list policy
|-- R2 bucket (source of truth) your notes, as plain markdown + images
|-- D1 (SQLite) (derived index) FTS5 full-text index, frontmatter
| metadata, wikilink graph
|-- Vectorize + Workers AI (derived) embeddings for semantic search
|-- KV OAuth state/grant storage
`-- Cron Triggers nightly reindex + weekly vault auditR2 is the only source of truth. D1 and Vectorize are both fully derived from R2 and can be
rebuilt from scratch at any time (the l4_reindex tool, or /admin/reindex) -- there is no
data in the index that isn't recoverable by re-reading the bucket.
The 12 tools
Tool | What it does |
| List note paths, optionally under a folder prefix |
| Read one note's text by its R2 path |
| Overwrite an existing note, or create it if absent |
| Create a note; errors if the path already exists |
| Copy a note to a new path and delete the old one (wikilinks are plain text and are not rewritten) |
| Move a note to |
| FTS5 full-text search, multi-word AND matching, ranked snippets |
| Vector search over Workers AI embeddings -- finds related notes even with no shared keywords |
| List notes that |
| Rebuild the D1 index and (optionally) Vectorize embeddings from R2 |
| Store an image (base64) under |
| Retrieve an image as base64 |
Two maintenance actions exist as plain REST endpoints instead of MCP tools, since they're infra operations rather than something an AI client should trigger mid-conversation:
POST /admin/reindex-- same asl4_reindex, callable outside the MCP/OAuth surfacePOST /admin/audit-- runs the weekly vault audit immediatelyPOST /admin/semantic-- ad hoc semantic query, useful for debugging embeddings
All three require an x-admin-token header matching the ADMIN_TOKEN secret.
Scheduled jobs
Four Cron Triggers, configured in wrangler.jsonc (triggers.crons) and dispatched in the
Worker's scheduled handler in src/index.ts:
Nightly reindex (
0 7 * * *, 07:00 UTC daily) -- rebuilds the D1 full-text index, frontmatter metadata, and wikilink graph from R2. Keeps the derived index in sync without anyone remembering to call/admin/reindexafter bulk changes made outside the tools.Weekly vault audit (
0 10 * * 1, Monday 10:00 UTC) -- runs seven integrity/hygiene checks (src/audit.ts) against the D1 index and writes the results as aVault audit YYYY-MM-DD.mdnote back into R2:MOC drift -- notes under
Projects/not linked fromProjects/Projects.mdArchive drift -- resolved/closed/done-status project notes still outside
Archive/Stale actives --
status: activenotes not updated in 21+ daysBroken wikilinks --
[[links]]with no matching note titleTrash age --
_Trash/objects older than 30 days (reported, never auto-purged)Malformed frontmatter -- missing frontmatter, or
tags:as a bare string instead of a listUnregistered deadlines -- notes with a
deadline:date not linked fromDeadlines.md
Each check runs in its own try/catch, so one broken check reports an error in its own section instead of blanking the whole report. The folder names above (
Projects/,Archive/, etc.) are just the convention this template ships with -- edit the constants at the top ofsrc/audit.tsto match your own vault's layout.The same run also appends three night shift sections from
src/nightshift.ts(read-only, cost-bounded -- see that file's header for the exact per-run cost caps): a what-changed-this-week summary (optionally compressed into one paragraph by a small Workers AI model, using only path/type/change-kind, never full note bodies), semantically close note pairs with no[[wikilink]]between them ("suggested wikilinks"), and near-identical note pairs ("near-duplicate candidates," e.g. conflict-fork copies). Suggestions only -- nothing is ever added, merged, or deleted automatically.Daily review drip (
0 11 * * *, 11:00 UTC daily) -- optional; see "Inbound email capture and review drip" below. No-ops gracefully ifRESEND_API_KEYisn't set.Monthly nodal clustering (
0 12 1 * *, 1st of the month, 12:00 UTC) -- optional; see "Night shift and nodal clustering" below.
Additional views
Three optional read-only pages, each gated by its own tiny login rather than sitting behind the MCP OAuth flow (see "Auth for the extra views" below):
/map-- a live 3D force-graph of your vault (src/map.ts): notes are colored dots (by first tag -- editTAG_COLORSin that file to match your own tags),[[wikilinks]]are connecting lines with traffic particles, and link targets with no matching note ("ghost stars") show dim. A timeline slider at the bottom replays the vault's growth using each note's earliest frontmatter date (created>date>updated); notes with no date at all are treated as present from the beginning. Click a node to read the note in a side panel./calibration-- a decision-calibration dashboard (src/calibration.ts) over notes tracked with thestation/expected/confidence/review_by/outcome_recorded/retrofrontmatter convention this template ships with (editDECISION_FOLDERin that file if your own convention uses a different folder or field names). Shows open/partial/resolved counts, a station breakdown, overdue reviews, a confidence-vs-outcome scatter, and -- once any resolved decision carries both a numeric confidence and a machine-checkable hit/miss field -- a running Brier score. Also does a best-effort extraction of prediction line items from an optional note atFORECASTS_PATH(defaultForecasts.md). Degrades gracefully: an empty vault or missing note just shows empty-state messaging, not a broken page./health-- a read-only chart dashboard (src/health.ts) over four append-only log notes (editLOG_PATHSin that file to point at your own logs, or leave any of them missing -- a missing log just shows up as "missing" rather than breaking the page). Parsing is tolerant of drifting formats: entries it can't parse are counted and kept as a truncated sample rather than silently dropped.
Auth for the extra views
Cloudflare Access cannot reliably gate an individual path (like /map) at the edge on a
*.workers.dev-style hostname -- confirmed against a manually-created Self-hosted Access
Application, and consistent with reports on Cloudflare's own community forum. So each of
these three views runs its own minimal browser login instead, reusing the same
Access-for-SaaS app and ACCESS_*/COOKIE_ENCRYPTION_KEY secrets the MCP flow already
trusts: redirect to Access, verify the returned id_token, set our own signed session
cookie scoped to that one path. All three share the Access app's single registered
/callback redirect URI and tell their login traffic apart from each other and from the
MCP flow's own callback traffic by a state-parameter prefix (map_, cal_, health_).
If you deploy on a custom domain instead, you likely don't need this workaround and could
gate these paths with a normal Access application instead -- the code here just doesn't
assume you have one.
Inbound email capture and review drip
Two optional features that turn email into vault input:
Capture (
src/email.ts) -- point a Cloudflare Email Routing rule at this Worker for an address on a domain you control, and anything you mail or forward to it lands as an unchecked item in the day'sDaily/YYYY-MM-DD.mdnote (- [ ] HH:MM — ...), after stripping signature blocks. Evening captures (9pm-4am local time, seeVAULT_TZ) get an#late-nighttag and no implied deadline until you revisit them in a later review. EditALLOWED_SENDERSandFALLBACK_INBOXinsrc/email.tsbefore deploying -- only senders on that allow-list are accepted, and a capture that fails after acceptance is forwarded to the fallback inbox rather than silently lost.Review drip (
src/drip.ts) -- once a day, the cron finds the single most-overdue decision note (frontmatterreview_byin the past,outcome_recordedstill unset) and emails it to you via the Resend API, then appends your plain-text reply verbatim as that note's## Outcomesection when the reply comes back through Email Routing. It never infers, scores, or paraphrases your reply -- recording is mechanical; scoring, if any, is a separate step you do yourself. Requires a Resend account and theRESEND_API_KEYsecret (optional -- the drip cron no-ops gracefully without it); editDRIP_TO,DRIP_FROM,OUTCOME_ADDRESS, andDECISION_PREFIXESinsrc/drip.tsto your own addresses and folder before deploying.
Both require a domain added to your Cloudflare account with Email Routing turned on,
and a routing rule (dashboard or API -- not part of wrangler.jsonc) forwarding the
relevant address(es) to this Worker.
Night shift and nodal clustering
Night shift rides the weekly vault-audit note -- see "Scheduled jobs" above for what it adds and
src/nightshift.tsfor the cost accounting.Nodal clustering (
src/cluster.ts), monthly: groups the entries of two source notes (NODAL_PATHandDREAMS_PATH-- point these at your own free-form, log-style notes, or adapt the two parsers if your entries use a different shape) by embedding similarity and writes up which entries land near each other. It's a neighbor-finder, not an interpreter: it lists cluster membership and nothing more, never a proposed meaning.
Required Cloudflare resources
You need a Workers Paid plan -- Vectorize and cron triggers on the scale used here aren't available on the free tier. You also need:
A Workers project (this repo)
An R2 bucket (the vault)
A D1 database (the derived index)
A Vectorize index (semantic search), 768 dimensions / cosine metric to match the
@cf/baai/bge-base-en-v1.5embedding model used insrc/indexer.tsWorkers AI binding (runs the embedding model, no separate resource to create)
A KV namespace (OAuth state/grant storage)
A Cloudflare Access application (login gate in front of the OAuth flow)
Optional, only if you use the corresponding feature (see "Inbound email capture and review drip" above):
A domain on your Cloudflare account with Email Routing enabled, plus a routing rule pointing at this Worker
A Resend account and API key (
RESEND_API_KEY), for the review drip's outbound mail
Setup
1. Create the resources
npm install
npx wrangler login
npx wrangler r2 bucket create your-vault-bucket
npx wrangler d1 create your-notes-db
npx wrangler vectorize create your-notes-index --dimensions=768 --metric=cosine
npx wrangler kv namespace create YOUR_KV_NAMESPACE_NAMEd1 create and kv namespace create print an ID -- you need those for the next step.
2. Fill in wrangler.jsonc
Open wrangler.jsonc and replace the placeholders:
r2_buckets[0].bucket_name-- the R2 bucket name you createdd1_databases[0].database_id-- the ID printed bywrangler d1 createvectorize[0].index_name-- the Vectorize index name you createdkv_namespaces[0].id-- the ID printed bywrangler kv namespace createaccount_id(add it if you belong to more than one Cloudflare account)
Apply the schema to your new D1 database:
npx wrangler d1 execute your-notes-db --remote --file=schema.sql3. Set up Cloudflare Access
L4 does not do its own username/password auth -- login is entirely delegated to Cloudflare Access, which for a personal vault is usually configured as one-time-PIN email login gated by an allow-list:
In the Cloudflare dashboard, go to Zero Trust → Access → Applications and add a SaaS application (OIDC), pointed at your Worker's
/callbackroute as the redirect URI.Add a policy on that application restricting access to specific email addresses (an allow-list of exactly the people who should be able to log in). This policy is the entire access-control surface -- see "Limitations" below.
From the Access application's OIDC settings, copy the Client ID, Client Secret, Token URL, Authorization URL, and JWKS URL. Those map directly to the
ACCESS_*secrets below.
4. Set secrets
The Worker expects these secrets to exist (see the secrets block in wrangler.jsonc,
and interface Env in worker-configuration.d.ts):
npx wrangler secret put ACCESS_CLIENT_ID
npx wrangler secret put ACCESS_CLIENT_SECRET
npx wrangler secret put ACCESS_TOKEN_URL
npx wrangler secret put ACCESS_AUTHORIZATION_URL
npx wrangler secret put ACCESS_JWKS_URL
npx wrangler secret put COOKIE_ENCRYPTION_KEY # any long random string, e.g. openssl rand -hex 32
npx wrangler secret put ADMIN_TOKEN # gates /admin/*; any long random stringFor local development with wrangler dev, put the same values in a .dev.vars file
instead (already gitignored) rather than using wrangler secret put.
5. The workers.dev gotcha (HTTP 1042)
If you deploy to a *.workers.dev subdomain rather than a custom domain, the OAuth
provider's internal self-fetch (it calls back into its own Worker as part of the token
exchange) can get blocked with a Cloudflare error 1042 ("blocked: request looked like it
came from Cloudflare's own network"). The fix is to add the
global_fetch_strictly_public compatibility flag alongside nodejs_compat in
wrangler.jsonc's compatibility_flags. Custom domains generally don't hit this.
6. Deploy
npx wrangler deploy7. Upload notes and build the index
Copy your markdown files into the R2 bucket (via wrangler r2 object put, the R2 dashboard,
rclone, or any S3-compatible tool -- R2 speaks the S3 API), then build the index:
curl -X POST https://<your-worker>.workers.dev/admin/reindex \
-H "x-admin-token: <your ADMIN_TOKEN>"8. Connect a client
Most MCP clients (Claude, etc.) support OAuth natively -- just add the server URL
(https://<your-worker>.workers.dev/mcp) and follow the login flow, which will bounce
through Cloudflare Access.
For clients that don't speak OAuth directly, use the mcp-remote proxy:
npx -y mcp-remote https://<your-worker>.workers.dev/mcpLimitations
Be honest with yourself about these before treating this as a production system:
No read-only scope. Every authorized client gets the full tool set -- list, read, write, move, trash. There is no way to grant a client read-only access.
No per-client revocation. The Cloudflare Access email allow-list is the only kill switch. Revoking a single MCP client's token isn't supported; removing an email from the Access policy is how you cut someone off.
No R2 object versioning configured.
l4_writeoverwrites in place. If you want version history or accidental-overwrite protection, turn on R2 bucket versioning yourself -- it isn't wired into the tool logic either way.Trust model is "anyone who can log into your Access app has full read/write." That's appropriate for a single-user personal vault; it is not a multi-tenant system.
Local development
npm run dev # wrangler dev, local
npm run type-check
npm run lint:fix
npm run formatThis server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- FlicenseAqualityDmaintenanceEnables AI assistants to manage a personal markdown-based knowledge base with natural language interactions. Supports creating, searching, updating, and organizing notes across categories like people, recipes, meetings, and procedures.111
- Alicense-qualityCmaintenanceProvides a shared MCP memory layer for AI clients, backed by Cloudflare Workers and D1, enabling personal Markdown notes management.MIT
- Alicense-qualityBmaintenanceExposes a personal markdown-based second brain (Obsidian-style) as an MCP server, enabling agents to search, read, and write notes with privacy controls.MIT
- Alicense-qualityBmaintenanceEnables interaction with a local knowledge repository via MCP, providing tools for capturing, indexing, and searching Markdown notes with Git version control and optional semantic search.4MIT
Related MCP Connectors
MCP-native open-source Notion alternative: read & write pages, databases and kanban boards.
Serve a folder of Markdown notes as an MCP server: hybrid search, reading, and sourced answers.
User-owned memory for AI agents, Copilot, Claude, IDEs, CLIs, and chat apps over remote MCP.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/geographerj/l4-template'
If you have feedback or need assistance with the MCP directory API, please join our Discord server