untappd-mcp 2.3.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for Untappd — beers, breweries, venues, check-ins, wishlists, and your friend feed",
10
- "version": "2.3.1"
10
+ "version": "2.4.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Untappd",
16
16
  "source": "./",
17
17
  "description": "MCP server for Untappd — search beers/breweries/venues, read profiles/check-ins/wishlists, and post check-ins, toasts, and comments",
18
- "version": "2.3.1",
18
+ "version": "2.4.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "untappd-mcp",
3
3
  "displayName": "Untappd",
4
- "version": "2.3.1",
4
+ "version": "2.4.0",
5
5
  "description": "MCP server for Untappd — search beers/breweries/venues, read check-ins and wishlists, and post check-ins, toasts, and comments",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/.mcp.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "mcpServers": {
3
3
  "untappd": {
4
- "command": "node",
5
- "args": ["${CLAUDE_PLUGIN_ROOT}/dist/index.js"],
4
+ "command": "npx",
5
+ "args": ["-y", "untappd-mcp"],
6
6
  "env": {
7
7
  "UNTAPPD_ACCESS_TOKEN": "${UNTAPPD_ACCESS_TOKEN}",
8
8
  "UNTAPPD_USERNAME": "${UNTAPPD_USERNAME}",
package/README.md CHANGED
@@ -37,7 +37,8 @@ it goes stale.
37
37
  | `UNTAPPD_USER_AGENT` | no | Override the User-Agent (default mimics the app). |
38
38
  | `UNTAPPD_TIMEZONE` | no | IANA timezone (e.g. `America/New_York`) that check-ins are stamped with when the call doesn't pass `timezone`. Set it when the server runs somewhere other than the drinker's zone (e.g. a hosted connector, usually UTC). Defaults to the server process's zone. |
39
39
  | `UNTAPPD_PHOTO_DIR` | no | The directory `untappd_checkin`'s `photo_path` must be inside (several allowed, separated by `:`). Defaults to `~/Downloads/untappd-mcp` — put a photo there to attach it. Any other file is refused, so a prompt-injected shout or comment can't publish an arbitrary image from your disk. |
40
- | `UNTAPPD_CACHE_DB` | no | Path to the local check-in cache SQLite file (default `~/.untappd-mcp/checkins.db`; created owner-only — dir `0700`, file `0600`). Local/stdio only. |
40
+ | `UNTAPPD_CACHE_DB` | no | Path to the local check-in cache SQLite file (default `~/.untappd-mcp/checkins.db`). A relative path or leading `~` resolves against your **home** directory, never the working directory, so every host (Claude Desktop, Claude Code, …) finds the same file; the resolved path is logged to stderr at startup. The db file is always made owner-only (`0600`), and so are any directories the server creates plus the default `~/.untappd-mcp`; a directory you point this into that already exists keeps its permissions. Local/stdio only. |
41
+ | `UNTAPPD_CACHE_DIR` | no | Directory for the cache instead of `~/.untappd-mcp` (the file is `checkins.db` inside it). `UNTAPPD_CACHE_DB` wins if both are set. |
41
42
 
42
43
  Copy `.env.example` to `.env` and fill it in for local use.
43
44
 
@@ -88,7 +89,105 @@ Writes (each asks you to confirm first — see [Confirmations](#confirmations)):
88
89
 
89
90
  Check-in cache: `untappd_sync_checkins`, `untappd_sync_user_beers`,
90
91
  `untappd_cache_has_had`, `untappd_cache_has_had_many`, `untappd_cache_not_had`,
91
- `untappd_cache_query`, `untappd_top_not_had`, `untappd_cache_forget`.
92
+ `untappd_cache_query`, `untappd_top_not_had`, `untappd_cache_status`, `untappd_cache_forget`.
93
+
94
+ Menu picks: `untappd_menu_pick` (tap-list lines in, ranked not-had beers out),
95
+ `untappd_venue_pick` (same, from a venue's Untappd menu).
96
+
97
+ ## Picking a beer off a menu
98
+
99
+ `untappd_menu_pick` turns "find Mer a beer" from a menu photo into one call.
100
+ Give it the lines as read off the board:
101
+
102
+ ```json
103
+ {
104
+ "username": "mer",
105
+ "items": [
106
+ "HAP & HARRY'S TENNESSEE LAGER $6",
107
+ "I BELIEVE IN NASHVILLE BLONDE",
108
+ "JACKALOPE FENNARIO IPA",
109
+ "BEARDED IRIS HOMESTYLE",
110
+ "BLACK ABBEY SEASONAL",
111
+ "ROTATING CIDER"
112
+ ],
113
+ "venue_city": "Nashville",
114
+ "top_n": 2
115
+ }
116
+ ```
117
+
118
+ In one call it:
119
+
120
+ 1. **Freshens the cache.** If the user's distinct-beers list is already complete
121
+ it does an incremental refresh, stopping at the first beer that's cached and
122
+ unchanged (usually one page). If the list is empty or partial it backfills
123
+ in the same call until done or `api_budget` (default 60 calls, shared by
124
+ every step) runs out. You never need to call a sync tool first.
125
+ 2. **Resolves each line to a beer.** It strips prices, sizes, ABV and
126
+ punctuation, searches Untappd (falling back to a query without the style
127
+ word, because Untappd search matches every term), and scores the candidates
128
+ on brewery match, beer-name match (so "Homestyle" isn't read as "Double
129
+ Homestyle"), check-in volume (the canonical entry wins over a 3-check-in
130
+ duplicate) and `venue_city`. Up to 4 searches run at once, and a 429 is
131
+ retried with backoff. Resolutions are cached on disk, so the next visit to
132
+ the same venue costs no searches.
133
+ 3. **Handles generic lines.** `<Brewery> SEASONAL` / `ROTATING` becomes a
134
+ `possible` list of that brewery's seasonal-looking beers, marked
135
+ `needs_confirmation`. A line with no brewery (`ROTATING CIDER`) comes back
136
+ `unresolved: "ask bartender"`.
137
+ 4. **Checks every beer against the cache** (both sources, as `has_had_many` does).
138
+ 5. **Ranks the not-had beers** by global rating, applies `style_filter`, and
139
+ returns the top `top_n` as `picks`.
140
+
141
+ Output for the call above, with Mer having had Fennario (abridged: `not_had`
142
+ also lists the Tennessee Lager at 3.33, `possible` lists six candidates, and a
143
+ `freshness` block is appended):
144
+
145
+ ```json
146
+ {
147
+ "picks": [
148
+ { "item": "BEARDED IRIS HOMESTYLE", "bid": 1502190, "name": "Homestyle", "brewery": "Bearded Iris Brewing",
149
+ "style": "IPA - New England / Hazy", "abv": 6, "global_rating": 3.98,
150
+ "reason": "not had · 3.98 global rating · #1 of 3 not-had beers on this list" },
151
+ { "item": "I BELIEVE IN NASHVILLE BLONDE", "bid": 3780741, "name": "I Believe In Nashville",
152
+ "brewery": "I Believe Brewing Company", "style": "Blonde / Golden Ale - American", "abv": 4.5,
153
+ "global_rating": 3.54, "reason": "not had · 3.54 global rating · #2 of 3 not-had beers on this list" }
154
+ ],
155
+ "had": [
156
+ { "item": "JACKALOPE FENNARIO IPA", "bid": 1893601, "name": "Fennario", "brewery": "Jackalope Brewing Company",
157
+ "count": 3, "last_date": "2025-08-01T20:00:00.000Z", "user_rating": 3.75 }
158
+ ],
159
+ "possible": [
160
+ { "item": "BLACK ABBEY SEASONAL", "brewery": "The Black Abbey Brewing Company",
161
+ "reason": "seasonal-looking beers from this brewery",
162
+ "candidates": [
163
+ { "bid": 1223176, "name": "Chronicles Festbier", "style": "Festbier", "abv": 4.9, "global_rating": 3.672, "had": false }
164
+ ],
165
+ "needs_confirmation": true }
166
+ ],
167
+ "unresolved": [{ "item": "ROTATING CIDER", "reason": "ask bartender" }],
168
+ "sync": { "mode": "backfill", "api_calls": 1 },
169
+ "api_calls": 11,
170
+ "api_budget": 60,
171
+ "another_run_needed": false
172
+ }
173
+ ```
174
+
175
+ `sync` says what the refresh did; coverage lives in `freshness`. If the budget
176
+ runs out before the backfill finishes, `freshness.coverage_complete` is `false`,
177
+ `another_run_needed` is `true`, and the `freshness` caveat warns that a "not
178
+ had" may be a false negative. Calling again continues from where it stopped.
179
+
180
+ **Quota.** When an Untappd response carries `X-Ratelimit-Remaining`, the server
181
+ remembers it for the hour, and every budgeted tool (`menu_pick`, `venue_pick`,
182
+ `top_not_had`, both syncs) caps its budget to what's left. The result then
183
+ includes `quota_remaining`, plus `budget_clamped_to_quota: true` when the cap
184
+ applied. Untappd's public API documents the header; if the private API this
185
+ server uses doesn't send it, budgets work exactly as before.
186
+
187
+ `untappd_venue_pick` takes an Untappd venue URL (`https://untappd.com/v/<slug>/<id>`)
188
+ or id instead of `items`. It pages through the venue's whole verified menu, takes
189
+ the venue's city from it, and runs the same check and ranking. Menu items already
190
+ carry beer ids, so there is no search step.
92
191
 
93
192
  ## Check-in cache
94
193
 
@@ -96,10 +195,19 @@ The Untappd API only exposes paged lists (50 per page) and has **no** "has this
96
195
  user ever had beer X?" lookup — answering that from the API alone means paging an
97
196
  entire history (often 11k+ check-ins) against a tight ~100-calls/hour rate limit.
98
197
  These tools maintain a SQLite mirror so the question is answered instantly,
99
- offline, with zero API calls. The mirror is a local file
100
- (`node:sqlite`, path via `UNTAPPD_CACHE_DB`); the store is injectable, so another
198
+ offline, with zero API calls. The mirror is a local SQLite file
199
+ (`node:sqlite`, default `~/.untappd-mcp/checkins.db`, overridable with
200
+ `UNTAPPD_CACHE_DB` / `UNTAPPD_CACHE_DIR`). The store is injectable, so another
101
201
  deployment can back it differently without the tools changing.
102
202
 
203
+ **Persistence.** The file survives restarts, and a backfill that was cut off
204
+ (rate limit, crash, host quit) resumes from its saved offset on the next sync.
205
+ The file runs in WAL mode with a busy timeout and does every write in a
206
+ transaction, so two hosts open at the same time can share it. The schema is
207
+ versioned (`PRAGMA user_version`) and only ever migrated forward, adding
208
+ columns and tables, so an upgrade never wipes it. `untappd_cache_status` shows
209
+ the resolved path, size, schema version and per-user coverage.
210
+
103
211
  Two sync sources fill the cache:
104
212
 
105
213
  - **`untappd_sync_user_beers`** pages `user/beers` — the user's **complete