Skip to main content
Glama
Joshlucpoll

umami-recipes-mcp

by Joshlucpoll
README.md
# Umami Recipes MCP

An MCP server for [Umami Recipes](https://www.umami.recipes/) that can:

- list the recipe books available to your account;
- find recipes by name, ingredient, direction, note, URL, time, or tag;
- read a complete recipe;
- create recipes, including ingredients, directions, notes, tags, times, source URL, visibility, and nutrition fields;
- edit selected recipe fields while preserving everything omitted.
- upload JPEG, PNG, or WebP photos to existing recipes from an HTTPS URL or base64 image data.

It supports two deployment modes:

- a remote, stateless Streamable HTTP server on Cloudflare Workers at `/mcp`;
- a local stdio server for desktop MCP clients.

The implementation uses the same Firebase project, Firestore collections, document shapes, validation limits, and atomic recipe/tag bookkeeping as Umami's current web client. It never exposes a delete tool.

## Install and verify

```bash
npm install
npm test
npm run build
npm run deploy:dry-run
```

## Deploy to Cloudflare

The Worker uses OAuth 2.1 authorization code flow with PKCE. Its
`MCP_ACCOUNT_MAP` secret contains named account entries, and each entry maps a
unique browser login code to one Umami Firebase refresh token:

```json
{
  "personal": {
    "loginCode": "a-long-random-code-for-personal",
    "umamiRefreshToken": "the-personal-firebase-refresh-token"
  },
  "family": {
    "loginCode": "a-different-long-random-code-for-family",
    "umamiRefreshToken": "the-family-firebase-refresh-token"
  }
}
```

The names are only labels for maintaining the configuration. During OAuth
authorization, entering the `personal` login code creates a grant that can only
operate as the mapped personal Umami account. Neither the login code nor the
Umami refresh token is included in the OAuth grant.

Copy the ignored account-map template, replace every placeholder, validate it,
store the complete map as a Cloudflare secret, and deploy:

```bash
cp .mcp-accounts.example.json .mcp-accounts.json
npx wrangler login
npm run accounts:validate
npm run accounts:deploy
npm run deploy
```

`accounts:deploy` reads `.mcp-accounts.json`, validates it, and sends it to
`wrangler secret put MCP_ACCOUNT_MAP` over standard input without printing the
credentials. Pass a different file after `--` if needed, for example
`npm run accounts:deploy -- .mcp-accounts.production.json`. Account-map files
are ignored by Git. Cloudflare limits an individual secret to 5 KiB, so this
mechanism is intended for a small account registry.

The `OAUTH_KV` binding in `wrangler.jsonc` stores OAuth clients, grants, and
tokens. Wrangler provisions this KV namespace during deployment. If the Worker
was previously deployed with the fixed bearer-token version, the old
`mcpToken` field is accepted as a login code during migration; running
`accounts:deploy` rewrites it to `loginCode`.

Wrangler prints the deployed `workers.dev` hostname. The MCP endpoint is:

```text
https://umami-recipes-mcp.<your-subdomain>.workers.dev/mcp
```

The Worker configuration in `wrangler.jsonc` uses the current compatibility date, `nodejs_compat`, generated binding types, required-secret validation, and Workers observability.

### Obtain an Umami refresh token

If you have an Umami email/password login, the included helper exchanges those credentials directly with Firebase and prints a refresh token. To keep the password out of shell history in zsh:

```zsh
read "UMAMI_EMAIL?Umami email: "
read -s "UMAMI_PASSWORD?Umami password: "
export UMAMI_EMAIL UMAMI_PASSWORD
npm run auth:refresh-token
unset UMAMI_EMAIL UMAMI_PASSWORD
```

Repeat this for each Umami login and copy each printed value into the matching
`umamiRefreshToken` field in `.mcp-accounts.json`. Treat refresh tokens like
passwords and clear them from terminal scrollback when practical. Generate a
different long random `loginCode` for every account using a password manager.

### Connect a remote MCP client

Configure a Streamable HTTP MCP client with the deployed URL. The client opens
the OAuth authorization page in your browser; enter the `loginCode` for the
Umami account that client should use.

Claude Code:

```bash
claude mcp remove umami-recipes 2>/dev/null || true
claude mcp add --transport http --scope user umami-recipes \
  https://umami-recipes-mcp.<your-subdomain>.workers.dev/mcp
```

Run `/mcp` inside Claude Code and choose `umami-recipes` to authenticate.

Codex CLI:

```bash
codex mcp remove umami-recipes 2>/dev/null || true
codex mcp add umami-recipes \
  --url https://umami-recipes-mcp.<your-subdomain>.workers.dev/mcp
codex mcp login umami-recipes
```

For another OAuth-capable MCP client, the configuration contains only the URL:

```json
{
  "mcpServers": {
    "umami-recipes": {
      "url": "https://umami-recipes-mcp.<your-subdomain>.workers.dev/mcp"
    }
  }
}
```

Clients that only support local stdio MCP servers can use the local stdio mode
below. OAuth-capable clients manage their own access and refresh tokens; do not
put the account login code in an `Authorization` header.

### Local Worker development

Copy the example secrets file and replace the JSON placeholders. Add more named
entries to test multiple accounts locally:

```bash
cp .dev.vars.example .dev.vars
npm run dev
```

The local endpoint is normally `http://localhost:8787/mcp`. `.dev.vars` is ignored by Git.

## Local stdio mode

For local use, choose one of these authentication configurations:

```bash
export UMAMI_REFRESH_TOKEN='your Firebase refresh token'

# Or sign in when the MCP first makes a request:
export UMAMI_EMAIL='you@example.com'
export UMAMI_PASSWORD='your Umami password'

# Or use a short-lived Firebase ID token:
export UMAMI_ID_TOKEN='your token'
export UMAMI_USER_ID='your Firebase user id' # only if absent from the token
```

Build and configure the stdio entrypoint in your MCP host:

```json
{
  "mcpServers": {
    "umami-recipes": {
      "command": "node",
      "args": ["/absolute/path/to/umami-mcp/dist/index.js"],
      "env": {
        "UMAMI_REFRESH_TOKEN": "your-refresh-token"
      }
    }
  }
}
```

Run `npm run dev:stdio` for an unbuilt local development server.

## Tools

- `list_recipe_books` — returns book ids and names.
- `list_recipe_tags` — returns exact tag names and recipe counts across every accessible book, or one selected book.
- `find_recipes` — searches a bounded set of recently updated candidates across all accessible books, or one selected book.
- `get_recipe` — returns ingredients, directions, and notes as editable Markdown strings.
- `create_recipe` — creates a recipe and atomically updates the selected book's recipe/tag metadata.
- `edit_recipe` — patches only supplied fields; when `tags` is supplied, it replaces the full tag list and atomically updates tag counts.
- `add_recipe_photo` — uploads one JPEG, PNG, or WebP image (up to 10 MB) from a public HTTPS URL or base64 data, then asks Umami to attach and process it.

Ingredients, directions, and notes are Markdown strings. Supported formatting is
paragraphs, headings, bold, italic, `<u>underline</u>`, HTTP(S) links, and flat
ordered or unordered lists. For example:

```json
{
  "name": "Tomato soup",
  "ingredients": "- **500g** tomatoes\n- 1 onion",
  "directions": "## Method\n\n1. Chop the vegetables.\n2. [Simmer gently](https://example.com/simmering).",
  "notes": "Serve <u>hot</u>."
}
```

Raw HTML other than `<u>`, images, tables, code blocks, task lists, and nested or
multi-paragraph lists are rejected because Umami has no equivalent rich-text
node. Omitting a field during an edit preserves it; supplying an empty string
clears it. Each Markdown field is limited to 50,000 characters.

## Security model

The remote deployment supports a small registry of independent Umami accounts:

- Clients use OAuth dynamic client registration and authorization code flow
  with S256 PKCE; a fixed login code is never accepted as an MCP bearer token.
- Every login code maps to exactly one Umami refresh token, and duplicate login
  codes are rejected as invalid configuration. Code comparisons are designed
  to avoid timing leaks.
- The browser authorization form is CSRF-protected and sends restrictive CSP,
  framing, caching, referrer, and permissions headers.
- OAuth grant properties contain only the account label. Umami credentials stay
  in the Cloudflare secret and are looked up for each authenticated request.
- OAuth clients, grants, access tokens, and refresh tokens are stored in
  `OAUTH_KV`. Access tokens expire after one hour, while refresh tokens expire
  after 90 days; after the latter, the client authorizes again in the browser.
- Removing an account from `MCP_ACCOUNT_MAP` immediately prevents its existing
  grants from reaching Umami, even if an OAuth token remains in KV.
- Each MCP request creates fresh server and Umami authentication state; no
  request-specific state is stored globally.

Possession of an account's login code allows a person to authorize new clients
with all exposed MCP capabilities for that mapped Umami account. Store each
code in a password manager and rotate it if disclosed.

## Development commands

```bash
npm test              # Node protocol, OAuth, and Worker HTTP tests
npm run build          # stdio build plus Worker type-check
npm run accounts:validate # validate the ignored account map
npm run accounts:deploy   # publish it as the MCP_ACCOUNT_MAP secret
npm run types          # regenerate Cloudflare binding types
npm run types:check    # verify generated types are current
npm run deploy:dry-run # validate and bundle without deploying
```

The test suite covers OAuth login-code mapping and consent hardening, Umami
authentication and rich-text conversion, Firestore values, recipe
search/create/edit behavior, atomic tag bookkeeping, MCP v2 protocol
discovery/invocation, and the Worker OAuth boundary.

TDQS

A3.9/5.0

Scored across 5 tools

Disambiguation5/5

Each tool targets a distinct action: listing books, searching recipes, retrieving a recipe, creating, and editing. There is no overlap; get_recipe retrieves a full recipe while find_recipes returns search candidates, and create/edit are clearly separated.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern with lowercase and underscores: list_recipe_books, find_recipes, get_recipe, create_recipe, edit_recipe. The pattern is uniform and predictable.

Tool Count5/5

With 5 tools, the server is well-scoped for a recipe management domain. Each tool covers a necessary operation without redundancy, fitting comfortably within the ideal 3-15 range.

Completeness3/5

The set covers listing books, searching, retrieving, creating, and editing recipes, but lacks a delete_recipe operation, which is a core CRUD gap. Additionally, find_recipes is search-focused rather than a straightforward list-all tool, which may limit listing by book.

Maintenance

ActivitySlowing
ResponsivenessNo issues