MCP server that proxies an AppSheet app holding notes, with per-user JWT auth. Works locally over stdio for a single user and as a multi-user HTTP service when deployed. List, read, add, update notes and list attachments from any MCP client.
Built on mcp-app.
pipx install git+https://github.com/krisrowe/notes.gitThis installs three commands:
notes— shell shortcuts (list,read,add,update,attachments list)notes-mcp— MCP server (stdio,serve)notes-admin— admin CLI (connect,users,tokens,health,probe,register)
Prereqs: Python 3.10+, an AppSheet app with the expected schema.
For a single-user setup on your own machine: register a local profile
holding your AppSheet API key, then point an MCP client at notes-mcp stdio.
notes-admin connect local
notes-admin users add local \
--datastore appsheet \
--app-id <your-appsheet-app-guid> \
--api-key <your-appsheet-application-access-key>The datastore field selects the backend implementation. appsheet
is currently the only supported value. Future versions may add
others (e.g. google sheets, firestore). If a profile names a
datastore the running version doesn't know about, the SDK raises a
clear error citing the running notes version and the supported set.
The app_id and api_key together define which AppSheet app this
profile reads from. See docs/APPSHEET.md for
where to find both.
If your AppSheet app uses table names other than Note and
Attachment, set:
export APPSHEET_NOTE_TABLE=<your-notes-table>
export APPSHEET_ATTACHMENT_TABLE=<your-attachments-table>Claude Code:
claude mcp add --scope user notes -- notes-mcp stdio --user localGemini CLI:
gemini mcp add notes --command notes-mcp --args "stdio --user local" --scope usernotes list --limit 3Or, with stdio registered, ask the MCP client: "list my recent notes."
The server is a standard ASGI Python app with these requirements:
| Var | Required | Default | Purpose |
|---|---|---|---|
SIGNING_KEY |
Yes | — | JWT signing key. Generate with python3 -c 'import secrets; print(secrets.token_urlsafe(32))' |
APP_USERS_PATH |
No | ~/.local/share/notes/users/ |
Per-user profile data directory. Must be persistent on serverless platforms — defaults to ephemeral container storage. |
APPSHEET_NOTE_TABLE |
No | Note |
Override only if your AppSheet app uses a non-default notes table name |
APPSHEET_ATTACHMENT_TABLE |
No | Attachment |
Override only if your AppSheet app uses a non-default attachments table name |
JWT_AUD |
No | not validated | Expected JWT audience claim |
TOKEN_DURATION_SECONDS |
No | 315360000 (~10y) | New token lifetime in seconds |
The AppSheet app_id and api_key are NOT env vars — they live in
each user's profile (per-user, rotatable independently). See "Manage
users" below.
- Start command:
notes-mcp serve --host 0.0.0.0 --port $PORT - MCP endpoint:
/(root, not/mcp) - Health:
GET /health(no auth) - Admin:
POST/GET/DELETE/PATCH /admin/...(signing-key auth) - Auth model: the app handles JWT auth itself; the platform must allow unauthenticated traffic through to the app — every request is gated by mcp-app's middleware.
SIGNING_KEY is a secret. Inject it via your platform's secret manager
(GCP Secret Manager, AWS Secrets Manager, GitHub Actions secrets, etc.)
— never check it into the repo.
docker build -t notes .
docker run -p 8080:8080 \
-e SIGNING_KEY=$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))') \
-v /path/on/host:/data \
-e APP_USERS_PATH=/data/users \
notesA one-line Procfile ships with the repo:
web: notes-mcp serve --host 0.0.0.0 --port $PORT
Set SIGNING_KEY and a persistent APP_USERS_PATH through the
platform's env-var or secret mechanism, then deploy from source.
The admin CLI persists per-app config under ~/.config/notes/setup.json
so subsequent commands don't repeat --url or --signing-key. connect
and the deploy itself are independent — deploying does not auto-connect
the admin CLI.
notes-admin connect https://<your-deployment-url> --signing-key <key><key> is whatever value SIGNING_KEY was set to at deploy time.
Retrieve it from wherever your deployment stored the secret (cloud
secret manager, CI/CD secret, deployment-tool-managed value).
For local-store admin (the local profile used in stdio mode):
notes-admin connect localEach authorized user gets a stored profile holding their AppSheet
target — the app_id of the app they read, and the api_key to
access it:
notes-admin users add alice@example.com \
--datastore appsheet \
--app-id <her-appsheet-app-guid> \
--api-key <her-appsheet-key>
notes-admin users list
notes-admin users get-profile alice@example.comusers add rejects existing users to prevent accidental overwrites.
Use update-profile to rotate either field:
notes-admin users update-profile alice@example.com api_key <new-appsheet-key>
notes-admin users update-profile alice@example.com app_id <different-appsheet-app>The CLI is self-documenting — --help prints field names and
descriptions:
notes-admin users add --helpnotes-admin users revoke alice@example.comprobe is the single end-to-end check — confirms the server is up,
auth works, and the MCP layer responds with the expected tools:
notes-admin probeFor a deeper check that exercises the user's backend credential too,
run the safe tool — count_notes with limit=10, capped server-side:
notes-admin safe-tool --invoke --user alice@example.comReturns {"notes": {"found": N, "limit": 10}} and reveals no note
content. Use this to confirm a deployed user's backend is reachable
and authenticated end-to-end.
register mints a token for a user and emits ready-to-paste registration
commands for Claude Code, Gemini CLI, and the Claude.ai web client:
notes-admin register --user alice@example.comManual registration if register isn't available:
# Claude Code (HTTP transport)
claude mcp add --scope user notes-remote \
--transport http https://<your-deployment-url>/ \
--header "Authorization: Bearer <user-token>"
# Gemini CLI (HTTP transport)
gemini mcp add notes-remote https://<your-deployment-url>/ \
--transport http \
--header "Authorization=Bearer <user-token>" \
--scope userFor Claude.ai (web), use the URL https://<your-deployment-url>/ with
the user token as a bearer credential.
| Tool | Description |
|---|---|
list_notes |
Search/list notes with Gmail-style query |
read_note |
Read a single note by ID |
add_note |
Create a note |
update_note |
Update a note's title/content/labels |
list_attachments |
List attachments for a note |
count_notes |
Count notes (capped); identity-only, no note content. Doubles as the deployment smoke test. |
Tool descriptions and parameter schemas come from the docstrings and
type hints in notes/mcp/tools.py — that's the canonical reference.
| Query | Meaning |
|---|---|
meeting |
Text search in title and content |
"exact phrase" |
Phrase search |
label:work |
Filter by label |
-label:archived |
Exclude label |
meeting label:work |
Text AND label (implicit AND) |
label:work OR label:home |
Either label |
(label:a OR label:b) meeting |
Grouping with parentheses |
The notes command is a thin wrapper for ad-hoc queries from the shell.
Each command loads the local user from the store unless --user is
passed.
notes list # All notes (default 50)
notes list "label:work" # Filter
notes list "meeting -label:archived" # Search + exclude
notes list --sort=-modified --limit 10 # Sort and limit
notes read <id> # Read one note
notes add "Title" -c "Body" -l "Work" # Create
notes update <id> -t "New title" # Update
notes attachments list <id> # List attachments- docs/APPSHEET.md — AppSheet credentials, table schema, troubleshooting
- CONTRIBUTING.md — architecture, testing, development
MIT