Skip to main content
Glama
enstw

simplenote-mcp

by enstw
README.md
# simplenote-mcp

![simplenote-mcp — persistent memory for Claude.ai projects](docs/hero.png)

A **remote MCP connector that gives Claude.ai *projects* a persistent,
cross-conversation file store**, backed by Simplenote and deployed free on
Cloudflare Workers.

## The problem

A Claude.ai project conversation has **no writable storage that survives across
conversations** — confirmed by probing the project sandbox from inside it:

| Surface | Agent can write? | Crosses conversations? |
|---|---|---|
| `/mnt/project` | no (only the user adds files) | yes |
| `/mnt/user-data/outputs` | yes | **no — keyed per conversation** |
| VM (`/home`, `/tmp`) | yes | no (ephemeral) |

`/mnt/user-data/outputs` is durable *within* a conversation, but its storage
namespace is the conversation id — a file written in one conversation is
invisible in the next, even inside the same project. The only cross-conversation
surface, `/mnt/project`, is read-only to the agent. So there is no
writable **and** cross-conversation surface — and because the sandbox's network
egress is allow-listed, a script running inside it can't reach Simplenote
directly either.

This connector supplies the missing surface from *outside* the sandbox: each
conversation pulls its files at the start and pushes changes back before ending.
The pull→work→push protocol the in-conversation agent follows lives in
[`BOOTSTRAP.md`](./BOOTSTRAP.md) — add that file to your Claude project.

## Why Cloudflare Workers

A connector must answer **instantly** when Claude calls a tool, which rules out
free hosts that sleep. Workers never sleep (~5 ms cold start), the free tier
needs **no credit card**, HTTPS is built in, and it's Anthropic's reference path
for remote MCP **with OAuth** (`workers-oauth-provider` + `McpAgent`). SQLite
Durable Objects — which `McpAgent` uses for session state — are now on the free
plan, so the whole thing runs for $0. (The sandbox's allow-listed egress can't
reach Simplenote, so the connector has to be remote anyway.)

The Simplenote/Simperium client is a faithful TypeScript port of the public
[`simplenote.py`](https://github.com/mrtazz/simplenote.py) library (token auth,
the first-line-heading→filename convention, version-aware writes, `markdown`
system tag), so notes round-trip with the Simplenote apps.

## Model

One Simplenote **note per file**, scoped to a Claude.ai project by a **tag**
(`claude-project-<key>`) that isolates that project's notes from the rest of the
account. A file's `path` is the slug of its first-line heading
(`# Design Notes` → `Design-Notes.md`).

One connector serves the whole account, but Claude.ai sends no project id to a
connector, so the `<key>` is supplied **in-band**: every tool takes a required
`project` argument. Each project declares its key on a `PROJECT ID:` line in
its **custom instructions** (see `docs/project-instructions.md`), and the
assistant passes it on every call — that's what keeps projects separate.

| Tool | Purpose |
|---|---|
| `list_files(project)` | discover persisted files |
| `read_file(project, path)` | pull one file |
| `write_file(project, path, content)` | persist/overwrite a file |
| `delete_file(project, path)` | trash a file |

Single-user by design: one Simplenote account (token stored as a Worker secret);
OAuth just gates access to you with a shared password.

## Deploy

Prereqs: a free Cloudflare account, [pnpm](https://pnpm.io) + Node, and a
**Simplenote API token**. The connector's runtime auth is token-only
(`X-Simperium-Token` — see `src/simperium.ts`); your email/password are never
part of the deployment. To obtain a token:

- **Extract it from the web app** (recommended — no extra tooling, and your
  password never leaves the login form): log in at
  [app.simplenote.com](https://app.simplenote.com), open DevTools → Network,
  click any `api.simperium.com` request, and copy the `X-Simperium-Token`
  request header value.
- **Or mint one** with the public
  [`simplenote`](https://pypi.org/project/simplenote/) library — a one-time
  local call that exchanges your email/password for a token:

  ```bash
  uv run --with simplenote python -c \
    "from simplenote import Simplenote; print(Simplenote('EMAIL', 'PASSWORD').get_token())"
  ```

```bash
pnpm install

# 1. Log in to Cloudflare (opens a browser):
pnpm exec wrangler login

# 2. Create the KV namespace the OAuth provider uses, then paste the printed id
#    into wrangler.jsonc (kv_namespaces[0].id).
pnpm exec wrangler kv namespace create OAUTH_KV

# 3. (project scope is per-call, not configured here — each Claude.ai project
#    sets its own PROJECT ID in its custom instructions; nothing in wrangler.jsonc.)

# 4. Set secrets (not in any file):
pnpm exec wrangler secret put SIMPLENOTE_TOKEN     # your Simperium token
pnpm exec wrangler secret put ACCESS_PASSWORD      # a password you choose

# 5. Ship it:
pnpm run deploy
```

You'll get `https://simplenote-mcp.<your-subdomain>.workers.dev`.

**Connect in Claude.ai** (Max/Team/Enterprise): Settings → Connectors → *Add
custom connector* → URL `https://simplenote-mcp.<subdomain>.workers.dev/mcp`.
Claude runs the OAuth flow; enter your `ACCESS_PASSWORD` on the login screen.

Finally, **upload [`BOOTSTRAP.md`](./BOOTSTRAP.md) unedited into the project's
files** (open your project → *Add files*) so every conversation picks up the
protocol, and **paste the snippet from
[`docs/project-instructions.md`](./docs/project-instructions.md) into the
project's custom instructions**, setting its `PROJECT ID:` line to a unique
slug — that key is what scopes this project's notes.
Then, in a chat, ask Claude to *load my project files from Simplenote* to
rehydrate at the start, and *save these back to Simplenote* to persist before
you finish.

## Local development

```bash
cp .dev.vars.example .dev.vars     # fill in SIMPLENOTE_TOKEN + ACCESS_PASSWORD
pnpm dev                           # wrangler dev (local Worker)
pnpm dlx @modelcontextprotocol/inspector   # point at http://localhost:8787/mcp
```

```bash
pnpm run typecheck   # tsc --noEmit
pnpm test            # vitest: store/simperium logic (in-memory, no network)
```

## Layout

- `src/index.ts` — OAuthProvider wiring (the entrypoint)
- `src/mcp.ts` — `McpAgent` Durable Object + the four tools
- `src/store.ts` — file↔note mapping, tag scoping (port of `store.py`)
- `src/simperium.ts` — Simperium HTTP client (port of `simplenote.py`)
- `src/auth.ts` — single-user OAuth login gate
- `BOOTSTRAP.md` — the doc you add to the Claude project
- `python/` — optional local **stdio** variant for Claude Desktop / Claude Code