Barkeeper
by NickM-27
README.md
# Barkeeper
An MCP server that tracks a home bar: what's on the shelf, and the cocktail
recipes you want to keep.
It is deliberately small — 7 tools, roughly 900 tokens of tool schema and
instructions combined — so it stays cheap to run against local models.
## Design
**Inventory is presence-only.** An item is either on the shelf or it isn't —
there are no quantities or units to keep up to date. Each item carries a
category so the listing can be grouped.
**Recipes are a name, ingredient lines, and a method.** Ingredients are plain
text (`"2 oz bourbon"`), so there is no per-ingredient schema for a model to
get wrong.
**The model does the cross-referencing.** There is no `what_can_i_make` tool.
To answer that, the model calls `list_inventory` and `list_recipes` and compares
the two — it already knows that a recipe calling for bourbon is satisfied by a
bottle named "Buffalo Trace". The server's instructions tell it to match
generously.
## Tools
| Tool | Arguments | Purpose |
| --- | --- | --- |
| `list_inventory` | — | Everything on the shelf |
| `add_item` | `name`, `category` | Put one item on the shelf |
| `remove_item` | `name` | Take one item off |
| `list_recipes` | — | Every recipe with ingredients, without methods |
| `get_recipe` | `name` | One recipe in full |
| `save_recipe` | `name`, `ingredients`, `instructions?` | Save or replace a recipe |
| `delete_recipe` | `name` | Delete a recipe |
`list_recipes` omits the method so the common "what can I make?" question stays
cheap; `get_recipe` fills in the detail for the one drink you settle on.
### Categories
`list_inventory` groups by category, in shelf order, skipping any that are empty:
```text
spirit: Buffalo Trace bourbon, Tanqueray gin
liqueur: Campari
bitters: Angostura
mixer: Fever-Tree tonic water
```
| Category | What belongs there |
| --- | --- |
| `spirit` | Base liquor — gin, vodka, whiskey, rum, tequila, brandy |
| `liqueur` | Sweetened or fortified — Campari, Cointreau, amaro, vermouth, sherry |
| `wine` | Still or sparkling — prosecco, Champagne |
| `bitters` | Cocktail bitters, dashed rather than poured — Angostura, Peychaud's |
| `mixer` | Non-alcoholic liquids — tonic, soda, juice, cola |
| `syrup` | Sweeteners — simple, orgeat, grenadine, honey |
| `garnish` | Citrus, herbs, olives, cherries |
| `other` | Anything else |
Bitters gets its own category because it fits neither of the obvious two: it's
alcoholic, so it isn't a mixer, but it's measured in dashes rather than poured,
so grouping it with the spirits misrepresents the shelf.
The enum says `spirit` rather than `liquor` deliberately — `liquor` and
`liqueur` differ by one letter, and asking a small model to choose between two
near-identical strings invites silent miscategorisation.
Re-adding an item that's already on the shelf updates its category, so a wrong
guess is corrected by just adding it again.
Names are matched case- and punctuation-insensitively, and partial names work
when they're unambiguous — `get_recipe("old fash")` finds "Old Fashioned".
An ambiguous partial returns an error listing the candidates rather than
guessing.
## Run it
The server speaks two transports. **HTTP** is the default in Docker: one port,
plugged in by URL. **stdio** is the default for a local install, since that's
how a client spawns it as a subprocess.
### Docker (HTTP)
```sh
docker run -d --name barkeeper -p 8000:8000 -v barkeeper-data:/data \
ghcr.io/nickmowen/mcp-barkeeper
```
Then point any MCP client at `http://localhost:8000/mcp`:
```json
{
"mcpServers": {
"barkeeper": {
"type": "http",
"url": "http://localhost:8000/mcp"
}
}
}
```
Or with Claude Code:
```sh
claude mcp add --transport http barkeeper http://localhost:8000/mcp
```
Compose is included — `docker compose up -d` gives the same thing with the
volume and port already wired.
The image runs as a non-root user (uid 1000), exposes `8000`, and keeps its
database in the `/data` volume. Prefer a **named volume**, as above: a bind
mount to a host directory has to be writable by uid 1000 or the server can't
create its database. A healthcheck confirms the port is bound.
### Local (stdio)
```sh
uv sync
```
```sh
claude mcp add barkeeper -- uv --directory /path/to/mcp-barkeeper run barkeeper
```
### Which HTTP transport?
`--transport http` serves **Streamable HTTP** at `/mcp` — the current MCP
transport, which POSTs requests and streams replies back over SSE. This is what
you want.
`--transport sse` serves the older **HTTP+SSE** transport, a separate `/sse`
stream plus a `/messages/` endpoint. It was deprecated in MCP 2025-03-26 and is
here only for clients that haven't moved yet.
### Configuration
Flags beat environment variables, which beat defaults.
| Variable | Flag | Default | Purpose |
| --- | --- | --- | --- |
| `BARKEEPER_TRANSPORT` | `--transport` | `stdio` (`http` in Docker) | `stdio`, `http`, or `sse` |
| `BARKEEPER_HOST` | `--host` | `127.0.0.1` (`0.0.0.0` in Docker) | Interface to bind |
| `BARKEEPER_PORT` | `--port` | `8000` | Port to bind |
| `BARKEEPER_DB_PATH` | — | platform data dir | SQLite file; `:memory:` for throwaway |
| `BARKEEPER_ALLOWED_HOSTS` | — | unset | Comma-separated; enables DNS-rebinding protection |
| `BARKEEPER_ALLOWED_ORIGINS` | — | unset | Comma-separated; enables DNS-rebinding protection |
Binding to `0.0.0.0` puts the server on every interface the container can reach,
with no authentication — keep it on a private network or bound to `127.0.0.1`.
Setting either allow-list switches on DNS-rebinding protection, which rejects
requests whose `Host` or `Origin` header isn't named:
```sh
BARKEEPER_ALLOWED_HOSTS="localhost:*,127.0.0.1:*"
```
Both are left unset by default because enabling protection with an empty
allow-list rejects every request.
## Storage
A single SQLite file. In Docker it lives at `/data/barkeeper.db`; locally it is
created on first use at:
- macOS: `~/Library/Application Support/barkeeper/barkeeper.db`
- Linux: `~/.local/share/barkeeper/barkeeper.db`
- Windows: `%APPDATA%\barkeeper\barkeeper.db`
Override with `BARKEEPER_DB_PATH`. Use `:memory:` for a throwaway session.
## Development
```sh
uv run pytest
```
`create_server(conn)` accepts a connection, so tests run against an in-memory
database with no global state.
TDQS
A4.1/5.0
Scored across 7 tools
Disambiguation5/5
Each tool targets a distinct resource and action: inventory items (add, remove, list) and recipes (list, get, save, delete). There is no overlap or confusion between tools.
Naming Consistency5/5
All tools follow a consistent verb_noun pattern using snake_case (remove_item, list_recipes, get_recipe, etc.). The verb clearly indicates the operation and the noun the target resource.
Tool Count5/5
Seven tools is well-scoped for a bar management server covering both inventory and recipe management. Each tool has a clear purpose and none are redundant.
Completeness5/5
The surface covers full CRUD for recipes (list, get, save, delete) and essential operations for inventory (add, remove, list). The add_item tool also handles updates via category refresh, so no major gaps exist.
Maintenance
ActivitySlowing
ResponsivenessNo issues