lunchmoney-readonly-mcp
Read-only Lunch Money MCP for ChatGPT
A single-user remote MCP server for Lunch Money, designed for ChatGPT Business and deployed on Cloudflare Workers. It uses Google OAuth only to identify an allowlisted user; the Lunch Money API token is stored as a Cloudflare encrypted secret and is never returned to ChatGPT or an MCP tool.
This project was originally based on the bm1549/lunchmoney-mcp-cloudflare
project and has been adapted into a read-only Lunch Money MCP template for
Cloudflare Workers and ChatGPT. The bundled upstream implementation is retained
under vendor-lunchmoney-mcp, including its original MIT license and
attribution. Thanks to Brian Marks for the original work.
This repository is a template. It contains no live deployment URL, API token, Google OAuth secret, email address, or Cloudflare resource ID.
Default security model
The default Worker remains strictly read-only and enforces that boundary in two independent layers:
createReadonlyServer()exposes a positive allowlist of 25 retrieval tools. Create, update, delete, upload, sync, refresh, split, grouping, and budget mutation tools are not registered.runWithReadonlyConfig()rejects any non-GETrequest to the Lunch Money API before a network request is made.
All retrieval tools declare readOnlyHint: true, destructiveHint: false, and
openWorldHint: false.
The optional categorization mode described below does not replace or weaken
the default deployment. If ACCESS_MODE is absent, the Worker is read-only.
Optional category-only sibling Worker
For workflows where an assistant should help categorize transactions without receiving general write access, the same code can be deployed as a second Worker with:
ACCESS_MODE=categorizeThat mode adds exactly one mutation tool:
categorize_transaction(transaction_id, category_id)The category-only server has two independent safety layers:
Its MCP surface is a positive allowlist containing the same 25 retrieval tools plus only
categorize_transaction.Its API policy permits
GET, plus aPUTto/transactions/{numeric-id}only when the JSON body contains exactly one key:category_id. All otherPOST,PUT,DELETE, and upload requests are rejected beforefetch.
category_id may be a positive integer or null to remove a category. The
categorization tool cannot change amount, date, payee, notes, tags, account,
status, recurring linkage, metadata, splits, groups, or attachments.
Why use a sibling Worker?
Keeping two endpoints makes the trust boundary obvious:
your existing endpoint stays strictly read-only;
the second endpoint is connected only when you intentionally want categorization assistance;
a configuration mistake in the categorization deployment does not silently convert the read-only endpoint into a writer.
Requirements
A Lunch Money API token
A Cloudflare account with Workers and KV available
A Google Cloud OAuth 2.0 web client
A ChatGPT workspace that supports custom MCP apps
Install and verify
This repository tracks pnpm-lock.yaml, so use pnpm:
corepack enable
corepack prepare pnpm@10.15.0 --activate
pnpm install --frozen-lockfile
pnpm run typecheck
pnpm run lint
pnpm testFocused access-boundary tests are also available:
pnpm run test:readonly
pnpm run test:categorizeDeploy the read-only Worker
In
wrangler.jsonc, choose a unique Workernameand create the OAuth KV namespace:pnpm exec wrangler kv namespace create OAUTH_KVCopy the returned namespace ID into
REPLACE_WITH_OAUTH_KV_NAMESPACE_IDinwrangler.jsonc.Deploy once to obtain your Worker URL:
pnpm exec wrangler deployCreate a Google OAuth 2.0 Web application. Add this redirect URI, replacing the host with your Worker URL:
https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev/authorize/callbackSet the production secrets interactively. Never put their values in Git:
pnpm exec wrangler secret put GOOGLE_CLIENT_ID pnpm exec wrangler secret put GOOGLE_CLIENT_SECRET pnpm exec wrangler secret put ALLOWED_EMAILS pnpm exec wrangler secret put STATE_SECRET pnpm exec wrangler secret put LUNCHMONEY_API_TOKENDeploy again. Your MCP endpoint is:
https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev/mcp
Deploy the category-only sibling
Do not put personal Worker names, account IDs, KV IDs, or deployment URLs in
the public repository. Keep the sibling configuration in the gitignored
wrangler.categorize.personal.jsonc.
A practical setup is:
Copy your private read-only deployment configuration to
wrangler.categorize.personal.jsonc.Give it a different Worker
name.Add:
"vars": { "ACCESS_MODE": "categorize" }Prefer a separate OAuth KV namespace for the sibling so its OAuth grants are isolated from the read-only Worker.
Add the sibling Worker's
/authorize/callbackURL to the Google OAuth web client, or create a separate OAuth client if you want stronger separation.Set the same required secrets against the sibling configuration:
pnpm exec wrangler secret put GOOGLE_CLIENT_ID --config wrangler.categorize.personal.jsonc pnpm exec wrangler secret put GOOGLE_CLIENT_SECRET --config wrangler.categorize.personal.jsonc pnpm exec wrangler secret put ALLOWED_EMAILS --config wrangler.categorize.personal.jsonc pnpm exec wrangler secret put STATE_SECRET --config wrangler.categorize.personal.jsonc pnpm exec wrangler secret put LUNCHMONEY_API_TOKEN --config wrangler.categorize.personal.jsoncDeploy it:
pnpm exec wrangler deploy --config wrangler.categorize.personal.jsonc
Connect the read-only and category-only endpoints as separate ChatGPT apps so you can tell which capability is active.
Connect ChatGPT
In ChatGPT, create a custom app using the Worker's
/mcpendpoint and choose OAuth authentication.Review the custom-app warning, create the draft, then publish and enable it.
Connect with an allowlisted Google account.
Start a fresh chat with the app enabled and verify the tool list before asking it to inspect or categorize Lunch Money data.
Before making the repository public
Run:
pnpm run check:public-releaseAlso review git status, confirm .dev.vars and both personal Wrangler config
files are untracked, and ensure the Worker name, endpoint, Cloudflare resource
IDs, and example documentation do not identify your personal deployment.
Verification
The access tests verify:
the exact strict read-only tool allowlist;
the exact category-only tool allowlist;
annotations for read and category-write tools;
absence of representative broader mutation tools;
rejection of arbitrary writes before network access; and
successful passage of only the exact category-only transaction
PUT.
See THIRD_PARTY_NOTICES.md for attribution details.