Skip to main content
Glama

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 audit

R2 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

l4_list

List note paths, optionally under a folder prefix

l4_read

Read one note's text by its R2 path

l4_write

Overwrite an existing note, or create it if absent

l4_create

Create a note; errors if the path already exists

l4_move

Copy a note to a new path and delete the old one (wikilinks are plain text and are not rewritten)

l4_trash

Move a note to _Trash/ instead of deleting it -- nothing is ever hard-deleted through MCP

l4_search

FTS5 full-text search, multi-word AND matching, ranked snippets

l4_semantic

Vector search over Workers AI embeddings -- finds related notes even with no shared keywords

l4_backlinks

List notes that [[wikilink]] to a given note

l4_reindex

Rebuild the D1 index and (optionally) Vectorize embeddings from R2

l4_put_image

Store an image (base64) under attachments/

l4_get_image

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 as l4_reindex, callable outside the MCP/OAuth surface

  • POST /admin/audit -- runs the weekly vault audit immediately

  • POST /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/reindex after 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 a Vault audit YYYY-MM-DD.md note back into R2:

    • MOC drift -- notes under Projects/ not linked from Projects/Projects.md

    • Archive drift -- resolved/closed/done-status project notes still outside Archive/

    • Stale actives -- status: active notes not updated in 21+ days

    • Broken wikilinks -- [[links]] with no matching note title

    • Trash age -- _Trash/ objects older than 30 days (reported, never auto-purged)

    • Malformed frontmatter -- missing frontmatter, or tags: as a bare string instead of a list

    • Unregistered deadlines -- notes with a deadline: date not linked from Deadlines.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 of src/audit.ts to 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 if RESEND_API_KEY isn'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 -- edit TAG_COLORS in 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 the station / expected / confidence / review_by / outcome_recorded / retro frontmatter convention this template ships with (edit DECISION_FOLDER in 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 at FORECASTS_PATH (default Forecasts.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 (edit LOG_PATHS in 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's Daily/YYYY-MM-DD.md note (- [ ] HH:MM — ...), after stripping signature blocks. Evening captures (9pm-4am local time, see VAULT_TZ) get an #late-night tag and no implied deadline until you revisit them in a later review. Edit ALLOWED_SENDERS and FALLBACK_INBOX in src/email.ts before 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 (frontmatter review_by in the past, outcome_recorded still unset) and emails it to you via the Resend API, then appends your plain-text reply verbatim as that note's ## Outcome section 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 the RESEND_API_KEY secret (optional -- the drip cron no-ops gracefully without it); edit DRIP_TO, DRIP_FROM, OUTCOME_ADDRESS, and DECISION_PREFIXES in src/drip.ts to 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.ts for the cost accounting.

  • Nodal clustering (src/cluster.ts), monthly: groups the entries of two source notes (NODAL_PATH and DREAMS_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.5 embedding model used in src/indexer.ts

  • Workers 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_NAME

d1 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 created

  • d1_databases[0].database_id -- the ID printed by wrangler d1 create

  • vectorize[0].index_name -- the Vectorize index name you created

  • kv_namespaces[0].id -- the ID printed by wrangler kv namespace create

  • account_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.sql

3. 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:

  1. In the Cloudflare dashboard, go to Zero Trust → Access → Applications and add a SaaS application (OIDC), pointed at your Worker's /callback route as the redirect URI.

  2. 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.

  3. 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 string

For 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 deploy

7. 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/mcp

Limitations

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_write overwrites 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 format
A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • F
    license
    A
    quality
    D
    maintenance
    Enables 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.
    11
    1
  • A
    license
    -
    quality
    C
    maintenance
    Provides a shared MCP memory layer for AI clients, backed by Cloudflare Workers and D1, enabling personal Markdown notes management.
    MIT
  • A
    license
    -
    quality
    B
    maintenance
    Exposes a personal markdown-based second brain (Obsidian-style) as an MCP server, enabling agents to search, read, and write notes with privacy controls.
    MIT
  • A
    license
    -
    quality
    B
    maintenance
    Enables 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.
    4
    MIT

View all related MCP servers

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.

View all MCP Connectors

Latest Blog Posts

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