mise-mcp
by LinusU
README.md
# mise-mcp
**Mise** is a tiny shared source of truth for a household's food inventory, meal
plan, recipes and preferences, exposed as a remote [MCP](https://modelcontextprotocol.io)
server. Connect ChatGPT, Claude or any other MCP-capable assistant and talk to it:
- "I bought these, add them to the inventory."
- "We used the chicken thighs and half the broccoli for dinner."
- "What's in the freezer?" / "That cream expires tomorrow, can we use it?"
- "Plan dinners for next week." / "Ella wants noodles tonight, rearrange the plan."
- "We cooked the planned wok, update the inventory."
The assistant does the fuzzy reasoning. Mise stores reliable structured state
and offers coarse, atomic operations to read and change it. There is no LLM
inside Mise.
Runs on Cloudflare Workers + D1 and fits comfortably in the free tier.
## How it works
```
ChatGPT / Claude / Claude Code / MCP Inspector
│ HTTPS, Streamable HTTP MCP, Authorization: Bearer <token>
▼
Cloudflare Worker (src/index.ts)
├─ /mcp MCP endpoint (stateless, one server per request)
├─ /.well-known/* OAuth discovery metadata
├─ /oauth/* tiny OAuth 2.1 authorization server (login = access key)
└─ D1 (SQLite) households, members, inventory, meals, recipes,
preferences, audit log, tokens
```
- **Inventory** rows are physical lots. "500 g chicken expiring tomorrow" and
"1 kg chicken in the freezer" are two rows; nothing is merged by name.
Quantities are `quantity` + `unit`, or a free-text `quantityText` ("some").
- **Meals** live on a calendar: date, meal type, title, status
(planned / cooked / skipped / cancelled), optional recipe link.
- **Recipes** are lightweight: name, description, ingredient list with rough
amounts, free-text instructions, tags, servings.
- **Preferences** are short natural-language statements ("Ella likes noodles").
- **Changes** is an append-only audit log with the acting member, the client
(ChatGPT, Claude Code, ...) and a readable summary.
Every mutation is one D1 `batch()`, which runs as a single transaction: either
all of "consume 450 g chicken, use up the noodles, mark dinner cooked" happens
or none of it does.
## MCP tools
| Tool | Purpose |
| --- | --- |
| `get_household_context` | Members, today's date in the household timezone, default servings, all preferences. Call first. |
| `get_inventory` | Current items; filter by `location`, `search`, `expiringWithinDays`. |
| `get_expiring_inventory` | Items expiring within N days (default 3). |
| `add_inventory_items` | Add one or more lots. |
| `update_inventory_items` | Change fields on existing lots (`null` clears). |
| `consume_inventory` | Record usage per item (`amount`, `usedAll`, or `remaining`/`remainingText`); optional `mealId` marks that meal cooked atomically. |
| `remove_inventory_items` | Delete lots (thrown away, mistakes). |
| `get_meal_plan` | Meals in a date range (default next 7 days). |
| `plan_meals` | Create or replace meals for several days at once (by id or by date + type). |
| `update_meals` | Move, rename, change status, swap (two updates in one call). |
| `delete_meals` | Hard delete. |
| `search_recipes`, `get_recipe`, `save_recipe`, `update_recipe`, `delete_recipe` | Recipe library. |
| `set_preference`, `remove_preference` | Durable planning preferences. |
| `get_recent_changes` | Audit log, newest first. |
Responses are compact JSON with stable ids (`inv_…`, `meal_…`, `rcp_…`, `pref_…`),
returned both as text and as `structuredContent`. Validation problems come back
as tool errors with a hint, and nothing is written when any part of a batch is
invalid.
## Authentication
Every request to `/mcp` needs `Authorization: Bearer <token>`. Tokens are random
strings; only their SHA-256 hash is stored in D1, bound to a household member.
There are two ways to get one, and both end up as the same kind of token:
1. **Static API token** (Claude Code, MCP Inspector, curl, scripts):
`npm run token:create -- Linus --label "Claude Code" --remote`.
2. **OAuth 2.1** (ChatGPT, Claude.ai, Claude Desktop): the Worker is also a
minimal authorization server. The assistant discovers it through the
standard `401` + `/.well-known/oauth-protected-resource` handshake, registers
itself (dynamic client registration or a client-ID metadata document), and
sends the user to a sign-in page where they pick who they are (Linus / Ella)
and type the household **access key** (the `MISE_ACCESS_KEY` secret). PKCE
S256 is required; refresh tokens rotate.
Why both? As of September 2026, ChatGPT only connects to authenticated MCP
servers via OAuth, Claude.ai supports OAuth (static headers are in a limited
beta), while Claude Code and the MCP Inspector send a plain bearer header.
Secrets never go in query strings and nothing secret is committed.
Because each token belongs to a member, the audit log records who made a change
and through which client.
## Setup
### 1. Prerequisites
- A Cloudflare account (free plan is enough: Workers + D1).
- Node.js 20+ and npm.
- `npx wrangler login` once, so Wrangler can deploy.
```bash
npm install
```
### 2. Create the D1 database
```bash
npx wrangler d1 create mise
```
Copy the printed `database_id` into `wrangler.jsonc` (replace the zeros).
### 3. Apply migrations
Local (used by `npm run dev` and the tests):
```bash
npm run db:migrate
```
Remote (production):
```bash
npm run db:migrate:remote
```
### 4. Seed the household
`seed/household.sql` creates the household "Linus & Ella", its two members and
a few starting preferences. It is idempotent and contains no fake inventory.
```bash
npm run seed:remote
```
For local development, `npm run seed` runs the household seed **and**
`seed/dev.sql`, which adds a known dev token (`mise-dev-token`, belongs to
Linus) plus sample inventory, a recipe and a planned meal. Never run `dev.sql`
against production.
To rename the household or members, edit `seed/household.sql` before running
it (or update the rows with `wrangler d1 execute`).
### 5. Set the access key secret
The access key is what a person types on the OAuth sign-in page.
```bash
npx wrangler secret put MISE_ACCESS_KEY
```
Locally, copy `.dev.vars.example` to `.dev.vars` (already ignored by git) and
set the value there.
### 6. Local development
```bash
npm run dev
```
The Worker listens on `http://localhost:8787`. Try it:
```bash
curl -s localhost:8787/mcp -X POST \
-H "content-type: application/json" \
-H "accept: application/json, text/event-stream" \
-H "authorization: Bearer mise-dev-token" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_inventory","arguments":{"location":"fridge"}}}'
```
Checks:
```bash
npm run check # typecheck + lint + tests
npm test # vitest, runs inside the Workers runtime against a local D1
```
### 7. Deploy
```bash
npm run deploy
```
### 8. Find the public MCP URL
Wrangler prints the Worker URL on deploy, e.g.
`https://mise-mcp.<your-subdomain>.workers.dev`. The MCP endpoint is that URL
plus `/mcp`. `GET /` on the Worker echoes it back.
### 9. Create a token (for bearer-header clients)
```bash
npm run token:create -- Linus --label "Claude Code" --remote
```
The token is printed once. Drop `--remote` to create one in the local database.
### 10. Connect an MCP client
**Claude Code**
```bash
claude mcp add --transport http mise https://mise-mcp.<subdomain>.workers.dev/mcp \
--header "Authorization: Bearer <token>"
```
**ChatGPT** (Settings → Apps & Connectors → Create, requires developer mode):
enter `https://mise-mcp.<subdomain>.workers.dev/mcp`, choose OAuth. ChatGPT
discovers the authorization server, opens the Mise sign-in page, and you pick
your name and enter the access key. Each person connects from their own
ChatGPT account and gets their own token.
**Claude.ai / Claude Desktop** (Customize → Connectors → Add custom connector):
enter the same URL and leave the OAuth fields empty; Claude detects the OAuth
server and runs the same sign-in flow. If your organization has the
"Request headers" beta you can instead add `Authorization: Bearer <token>`.
**MCP Inspector**
```bash
npx @modelcontextprotocol/inspector
```
Transport "Streamable HTTP", URL `…/mcp`, add an `Authorization` header with
`Bearer <token>`.
### 11. Test that it works
Ask the assistant:
1. "What food is in the fridge?"
2. "Add 1.2 kg chicken thighs to the fridge, expiring in 3 days, and a package of egg noodles to the pantry."
3. "Plan chicken noodle wok for tomorrow's dinner."
4. "Show the meal plan for the next week."
5. "We cooked the wok: used 450 g chicken and the whole package of noodles."
6. "How much chicken is left?"
7. "What changed recently?"
The same sequence runs automatically in `test/mise.test.ts`.
## Project layout
```
src/index.ts fetch router: health, OAuth, /mcp
src/mcp.ts builds the MCP server, registers tools
src/tools/ inventory.ts, meals.ts, recipes.ts, household.ts, shared.ts
src/auth.ts bearer token verification and issuance
src/oauth.ts OAuth 2.1 authorization server + sign-in page
src/db.ts ids, dates, hashing, audit-row helper
migrations/ D1 SQL migrations (wrangler d1 migrations)
seed/ household.sql (safe for prod), dev.sql (local only)
scripts/ create-token.mjs
test/ vitest suites running inside workerd
```
## Design notes
- **No grocery ontology.** An item is what a human would call it plus a few
structured fields. The assistant knows "ICA chicken thighs" is chicken.
- **Lots, not aggregates.** Separate rows per purchase keep expiry tracking
honest. Merging is a future decision, not a schema constraint.
- **Coarse, atomic tools.** Every mutation takes a list and runs as one D1
batch. This keeps conversational multi-step changes to a single call.
- **Stateless MCP.** One server instance per HTTP request, no sessions, no
Durable Objects. The SDK's Workers-safe JSON-schema validator is used
because the default one relies on `new Function`.
- **Two protocol eras.** Requests from 2025-era clients (today's ChatGPT and
Claude) are answered with plain JSON; 2026-07-28 clients go through the
SDK's modern handler.
- **One household per deployment for now.** The schema carries `household_id`
everywhere and tokens are household-scoped, so adding a second household is
a data change plus a small sign-in page tweak, not a rewrite.
- **Timezone.** Stored per household (seeded as `Europe/Stockholm`) so
"tonight" resolves to the right date.
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues