Skip to main content
Glama
Aephir

cocktail-recipes-mcp

by Aephir

cocktail-recipes-mcp

Dockerized Python 3.11 MCP server for the Aephir/cocktail-recipes REST API.

This server is designed for Claude Desktop and other MCP clients. It authenticates against the cocktail app via session-cookie login (POST /api/auth/login) and uses REST-only calls.

Features

  • Python 3.11 MCP server with typed models and validation

  • Self-contained OAuth 2.1 authorization server for remote Claude connectors

  • Session-cookie auth with automatic re-login on 401

  • Safe retries for idempotent calls (read-only and dry-run operations)

  • No blind retries for non-idempotent apply calls

  • Dry-run safety defaults for destructive tools

  • Structured response and error schema

  • Capability probing tool for backend endpoint readiness

  • Operation log tool for recent MCP actions

Related MCP server: mealie-mcp

Implemented Tools

  • list_recipes

  • get_recipe

  • create_recipe (defaults dry_run=true)

  • update_recipe (defaults dry_run=true)

  • delete_recipe (defaults dry_run=true)

  • list_ingredients

  • create_ingredient (defaults dry_run=true)

  • update_ingredient (defaults dry_run=true)

  • delete_ingredient (defaults dry_run=true, supports force=true)

  • list_tools

  • create_tool (defaults dry_run=true)

  • update_tool (defaults dry_run=true)

  • delete_tool (defaults dry_run=true, supports force=true)

  • merge_ingredients (dry_run=true default)

  • merge_tools (dry_run=true default)

  • recategorize_recipes (dry_run=true default)

  • bulk_update_recipes (dry_run=true default)

  • update_tags_bulk (dry_run=true default)

  • operation_log_recent

  • api_capabilities

Safety Behavior

All destructive tools default to dry_run=true (create/update/delete/merge/recategorize/tag bulk).

To apply, pass explicit dry_run=false.

Each destructive tool returns:

  • preview (affected IDs and count)

  • apply_executed boolean

  • backend result payload

Apply integrity guard:

  • If a tool is called with dry_run=false but the backend responds with dry_run=true, MCP returns an error (apply_not_executed) instead of success.

  • If the backend explicitly reports zero changed rows/ids for an apply request, MCP returns an error (apply_not_executed) so silent no-ops are surfaced.

If backend admin endpoints are not available yet, tool responses return structured not_implemented with an actionable message.

Recipe update safety:

  • update_recipe now uses fetch-merge-send behavior on apply: MCP first reads the current recipe, merges your provided fields, and sends a full update payload.

  • This prevents accidental clearing of tools or garnishes when those fields are omitted from the request.

  • To intentionally clear a list field, pass it explicitly as an empty list.

Delete safeguards for ingredients/tools:

  • delete_ingredient and delete_tool perform a recipe-reference precheck.

  • If references are found, apply mode is blocked unless force=true.

  • Dry-run output includes reference count and sample impacted recipes for review.

Tool Permission Hints

This server publishes MCP tool annotations so clients can distinguish read-only tools from mutating tools for permission workflows.

  • Read-only tools (readOnlyHint=true): list_recipes, get_recipe, list_ingredients, list_tools, operation_log_recent, api_capabilities

  • Additive write tools (readOnlyHint=false, destructiveHint=false): create_recipe, create_ingredient, create_tool

  • Mutating/destructive tools (readOnlyHint=false, destructiveHint=true): update_recipe, delete_recipe, update_ingredient, delete_ingredient, update_tool, delete_tool, merge_ingredients, merge_tools, recategorize_recipes, bulk_update_recipes, update_tags_bulk

In clients that support bulk approvals, this enables "always allow" for the read-only set while keeping mutating tools gated.

Required Environment Variables

  • COCKTAIL_API_BASE_URL

  • COCKTAIL_API_USERNAME

  • COCKTAIL_API_PASSWORD

  • PUBLIC_BASE_URL when using streamable-http

  • AUTH_USERNAME when using streamable-http

  • AUTH_PASSWORD when using streamable-http

  • OAUTH_STORAGE_HOST_DIR when using a host-path mount for OAuth state

Optional variables are in .env.example.

Transport Modes

  • stdio (default in .env.example): process-local MCP over stdin/stdout, no published port.

  • streamable-http: network-reachable MCP over HTTP with embedded OAuth 2.1, requires a published port or reverse proxy.

For Claude running on a different machine, use streamable-http.

Embedded OAuth

When MCP_TRANSPORT=streamable-http, this container acts as both:

  • the OAuth authorization server

  • the OAuth-protected MCP resource server

Implemented endpoints:

  • GET /.well-known/oauth-authorization-server

  • GET /.well-known/oauth-protected-resource

  • GET /.well-known/jwks.json

  • POST /register

  • GET|POST /authorize

  • POST /token

  • POST|GET /mcp protected by Authorization: Bearer <token>

OAuth details:

  • Authorization Code flow with PKCE (S256 required)

  • Dynamic Client Registration for public clients (token_endpoint_auth_method: none)

  • Built-in login form and consent screen using AUTH_USERNAME and AUTH_PASSWORD

  • JWT access tokens signed by a persisted RSA key

  • Rotating refresh tokens for public clients

Persisted OAuth state:

  • signing key

  • registered clients

  • authorization codes

  • refresh tokens

If you want this state to live on the host filesystem, mount a directory such as:

  • /mnt/storage_1/docker/cocktail-recipes-mcp/oauth

Keep OAUTH_STORAGE_DIR=/data/oauth inside the container and set OAUTH_STORAGE_HOST_DIR to the host path above in Portainer or compose.

Local Run

  1. Create env file:

cp .env.example .env
  1. Install dependencies and run:

python3.11 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -e .[test]
python -m cocktail_recipes_mcp.main

Docker Run

Build image:

docker build -t cocktail-recipes-mcp:latest .

Run container:

docker run --rm -i --env-file .env cocktail-recipes-mcp:latest

Compose example:

docker compose up --build cocktail-recipes-mcp

Note: docker-compose.yml reads values from environment variables. For local CLI usage, export them or place them in a local .env file before running docker compose.

For remote clients, set MCP_TRANSPORT=streamable-http, configure PUBLIC_BASE_URL, AUTH_USERNAME, AUTH_PASSWORD, and publish MCP_HTTP_PUBLISH_PORT.

Use this when you run both the cocktail app and this MCP server as containers.

For Portainer "Create stack from repository":

  • Keep all compose YAML in this repository.

  • Do not add manual YAML overrides in the Portainer UI.

  • Set Stack environment variables in the UI using the table below.

Portainer Stack Environment Variables

When both stacks share the same external Docker network (cocktail_net), use the app service name in COCKTAIL_API_BASE_URL.

Key

Example value

Required

Notes

COCKTAIL_API_BASE_URL

http://cocktail-app:3000

Yes

Use service-name URL on shared Docker network.

COCKTAIL_API_USERNAME

admin

Yes

Cocktail app login username.

COCKTAIL_API_PASSWORD

change-me

Yes

Cocktail app login password.

MCP_TRANSPORT

streamable-http

Yes (remote Claude)

Use stdio only for local process clients.

MCP_HTTP_HOST

0.0.0.0

Yes (remote Claude)

Container bind address.

MCP_HTTP_PORT

8000

Yes (remote Claude)

Internal container port.

MCP_HTTP_PATH

/mcp

Yes (remote Claude)

MCP endpoint path.

MCP_HTTP_PUBLISH_PORT

8011

Yes (remote Claude)

Host-published port.

PUBLIC_BASE_URL

https://cocktail-mcp.example.com

Yes (remote Claude)

Public HTTPS origin used by OAuth metadata.

AUTH_USERNAME

connector-admin

Yes (remote Claude)

Login used on OAuth consent screen. Set anything, then use the same when logging in from Claude

AUTH_PASSWORD

change-me

Yes (remote Claude)

Password for the connector login above. Set anything, then use the same when logging in from Claude

OAUTH_STORAGE_DIR

/data/oauth

Recommended

Keep as shown unless you changed image internals.

OAUTH_STORAGE_HOST_DIR

/mnt/storage_1/docker/cocktail-recipes-mcp/oauth

Recommended

Host path for persisted OAuth keys/tokens.

Then:

  1. Ensure the host path in OAUTH_STORAGE_HOST_DIR exists and is writable by Docker.

  2. Deploy/redeploy the stack in Portainer.

  3. In Claude, add a custom connector pointing to https://<your-fqdn>/mcp.

Portainer Error: PermissionError: [Errno 13] Permission denied: '/data/oauth/jwt_signing_key.pem'

Cause:

  • The container runs as a non-root user (appuser).

  • The bind-mounted host directory from OAUTH_STORAGE_HOST_DIR is not writable by that user.

Fix on the Docker host:

  1. Create the directory if needed:

sudo mkdir -p /mnt/storage_1/docker/cocktail-recipes-mcp/oauth
  1. Confirm container UID/GID for appuser:

docker run --rm --entrypoint sh cocktail-recipes-mcp:latest -c 'id -u appuser; id -g appuser'
  1. Set ownership using that output. Example if UID is 100 and GID is 101:

sudo chown -R 100:101 /mnt/storage_1/docker/cocktail-recipes-mcp/oauth
sudo chmod -R u+rwX,g+rwX /mnt/storage_1/docker/cocktail-recipes-mcp/oauth
  1. Redeploy the stack.

Quick test fallback (not for long-term use):

sudo chmod -R 777 /mnt/storage_1/docker/cocktail-recipes-mcp/oauth

Portainer Error: .env not found

If you saw an error like env file /data/compose/<id>/.env not found, the stack was expecting a physical .env file inside Portainer's compose directory.

This repo now avoids that requirement by reading COCKTAIL_API_* from stack environment variables directly.

In Portainer Stack deployment:

  • Add these variables in the Stack Environment variables UI:

    • COCKTAIL_API_BASE_URL

    • COCKTAIL_API_USERNAME

    • COCKTAIL_API_PASSWORD

    • PUBLIC_BASE_URL

    • AUTH_USERNAME

    • AUTH_PASSWORD

    • OAUTH_STORAGE_HOST_DIR

  • Redeploy the stack.

Your example values are valid for this setup.

Portainer Error: pull access denied for cocktail-recipes-mcp

This happens when Portainer tries to pull cocktail-recipes-mcp:latest from a registry. This project is intended to build from source in the stack, not pull from Docker Hub.

Resolution:

  • Use the current compose file from this repo (it uses build and pull_policy: never).

  • In Portainer stack deployment, disable any "pull latest image" behavior for this stack.

  • Redeploy the stack so the image is built locally on the Docker host.

FQDN vs Internal URL

  • If MCP and cocktail app are in the same Docker network, prefer internal URL (http://service-name:port).

  • If MCP is outside that network, use a reachable external URL/FQDN (for example https://cocktails.example.com).

For MCP client access:

  • Same LAN/VPN: http://<host-ip>:<published-port><MCP_HTTP_PATH>

  • Internet-facing: put SWAG/Nginx in front and use https://<fqdn><MCP_HTTP_PATH>

SWAG Nginx Config (Redacted Template)

Use this as a template for SWAG when proxying to an MCP host IP and published port. Replace placeholder values with your own environment values.

# /config/nginx/site-confs/cocktail-mcp.conf

server {
  listen 443 ssl http2;
  server_name mcp.example.com;

  include /config/nginx/ssl.conf;
  include /config/nginx/proxy.conf;

  client_max_body_size 0;

  add_header X-Content-Type-Options "nosniff" always;
  add_header X-Frame-Options "DENY" always;
  add_header Referrer-Policy "no-referrer" always;

  # SWAG not on shared Docker network: use MCP host IP + published port.
  set $mcp_upstream http://192.168.1.50:8011;

  # OAuth discovery endpoints + JWKS
  location ~ ^/\.well-known/(oauth-authorization-server|oauth-protected-resource|jwks\.json)$ {
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Authorization $http_authorization;
    proxy_buffering off;
    proxy_request_buffering off;
    proxy_read_timeout 3600;
    proxy_send_timeout 3600;
    proxy_pass $mcp_upstream;
  }

  # MCP and OAuth flow endpoints
  location ~ ^/(mcp|register|authorize|token|oauth/login|oauth/consent)$ {
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Authorization $http_authorization;
    proxy_buffering off;
    proxy_request_buffering off;
    proxy_read_timeout 3600;
    proxy_send_timeout 3600;
    proxy_pass $mcp_upstream;
  }

  location = /healthz {
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass $mcp_upstream/healthz;
  }

  location / {
    return 404;
  }
}

Expected quick checks:

  • GET /.well-known/oauth-authorization-server returns 200

  • GET /.well-known/oauth-protected-resource returns 200

  • GET /mcp without token returns 401

Go-Live Checklist

  1. MCP and app containers share a network.

  2. MCP env vars are set in Portainer, including PUBLIC_BASE_URL, AUTH_USERNAME, and AUTH_PASSWORD.

  3. Login endpoint is reachable from MCP (POST /api/auth/login).

  4. OAuth state host path is mounted to /data/oauth.

  5. GET /.well-known/oauth-authorization-server succeeds.

  6. GET /.well-known/oauth-protected-resource succeeds.

  7. Unauthenticated GET /mcp returns 401 with WWW-Authenticate pointing at protected-resource metadata.

  8. For admin operations, run dry_run=true first and apply only with dry_run=false.

Claude Desktop Connection

Add an MCP server entry to your Claude Desktop config.

Local Python command

{
  "mcpServers": {
    "cocktail-recipes": {
      "command": "/absolute/path/to/cocktail-recipes-mcp/.venv/bin/python",
      "args": ["-m", "cocktail_recipes_mcp.main"],
      "env": {
        "COCKTAIL_API_BASE_URL": "http://localhost:3000",
        "COCKTAIL_API_USERNAME": "admin",
        "COCKTAIL_API_PASSWORD": "change-me"
      }
    }
  }
}

Docker command

{
  "mcpServers": {
    "cocktail-recipes": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "--env-file",
        "/absolute/path/to/cocktail-recipes-mcp/.env",
        "cocktail-recipes-mcp:latest"
      ]
    }
  }
}

Remote HTTP MCP (different machine)

When running in Portainer with MCP_TRANSPORT=streamable-http, use your reachable URL:

  • Direct host/port example: http://10.0.30.51:8000/mcp

  • SWAG/FQDN example: https://mcp.example.com/mcp

For Claude custom connector setup:

  • URL: https://mcp.example.com/mcp

  • Leave OAuth Client ID empty.

  • Leave OAuth Client Secret empty.

  • Claude should dynamically register as a public client and complete OAuth against this server.

Do not put COCKTAIL_API_USERNAME or COCKTAIL_API_PASSWORD into Claude. Those remain server-side only.

Tests

Run minimal tests:

pytest

Manual Verification

Discovery endpoints:

curl -i https://cocktail-mcp.example.com/.well-known/oauth-authorization-server
curl -i https://cocktail-mcp.example.com/.well-known/oauth-protected-resource
curl -i https://cocktail-mcp.example.com/.well-known/jwks.json

Protected MCP endpoint without a token:

curl -i https://cocktail-mcp.example.com/mcp

Expected result:

  • 401 Unauthorized

  • WWW-Authenticate: Bearer ... resource_metadata="https://cocktail-mcp.example.com/.well-known/oauth-protected-resource"

Automated tests now cover:

  • metadata discovery

  • dynamic client registration

  • authorization code flow with PKCE

  • refresh token rotation

  • /mcp bearer protection

Example Transcript

See docs/example_transcripts.md for a dry_run/apply merge example (Cucumber vs cucumber).

Maintenance

ActivitySlowing
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    B
    maintenance
    Docker-hosted MCP server for Mealie that enables AI agents to read cookbooks, extract recipes, translate them if needed, store them in Mealie, and verify the results.
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    A read-only MCP server for Mealie that enables searching recipes, managing shopping lists, meal plans, and retrieving household/instance info via tools. Supports secure per-user authentication and multiple Mealie instances.
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Read-only MCP server exposing Brewfather brewing data—batches, recipes, fermentation readings, and inventory—as tools for natural language queries.
    9
    MIT