untappd-mcp 2.3.2 → 2.5.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +185 -5
- package/dist/bundle.js +6579 -1752
- package/dist/cache/brewery-beers.js +116 -0
- package/dist/cache/db.js +100 -10
- package/dist/cache/favorites.js +244 -0
- package/dist/cache/multi-user.js +61 -0
- package/dist/cache/response-cache.js +23 -0
- package/dist/cache/store.js +1424 -24
- package/dist/cache/sync-beers.js +51 -7
- package/dist/cache/sync-brewery-likes.js +124 -0
- package/dist/cache/sync-venues.js +200 -0
- package/dist/cache/sync-wishlist.js +126 -0
- package/dist/cache/sync.js +61 -12
- package/dist/cache/venue-drinks.js +101 -0
- package/dist/client.js +38 -0
- package/dist/compact.js +47 -42
- package/dist/index.js +27 -4
- package/dist/menu/match.js +178 -0
- package/dist/menu/pick.js +520 -0
- package/dist/menu/venue-menu.js +108 -0
- package/dist/quota.js +14 -0
- package/dist/ratelimit.js +236 -0
- package/dist/tools/beer.js +4 -1
- package/dist/tools/brewery.js +49 -11
- package/dist/tools/cache-optional.js +22 -0
- package/dist/tools/cache.js +506 -62
- package/dist/tools/favorites.js +110 -0
- package/dist/tools/menu.js +104 -0
- package/dist/tools/trip.js +384 -0
- package/dist/tools/user.js +38 -7
- package/dist/tools/venue.js +91 -115
- package/dist/version.js +1 -1
- package/mint.yaml +17 -0
- package/package.json +5 -1
- package/server.json +16 -2
- package/skills/untappd-mcp/SKILL.md +30 -0
|
@@ -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.
|
|
10
|
+
"version": "2.5.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.
|
|
18
|
+
"version": "2.5.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.
|
|
4
|
+
"version": "2.5.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/README.md
CHANGED
|
@@ -37,7 +37,9 @@ 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`). 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. |
|
|
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. |
|
|
42
|
+
| `UNTAPPD_RATE_LIMIT_PER_HOUR` | no | The hourly call limit the syncs' stop-before-429 guard enforces (default `100`, Untappd's documented per-token limit). Only change it if Untappd gives your token a different limit. |
|
|
41
43
|
|
|
42
44
|
Copy `.env.example` to `.env` and fill it in for local use.
|
|
43
45
|
|
|
@@ -88,7 +90,163 @@ Writes (each asks you to confirm first — see [Confirmations](#confirmations)):
|
|
|
88
90
|
|
|
89
91
|
Check-in cache: `untappd_sync_checkins`, `untappd_sync_user_beers`,
|
|
90
92
|
`untappd_cache_has_had`, `untappd_cache_has_had_many`, `untappd_cache_not_had`,
|
|
91
|
-
`untappd_cache_query`, `untappd_top_not_had`, `untappd_cache_forget
|
|
93
|
+
`untappd_cache_query`, `untappd_top_not_had`, `untappd_cache_status`, `untappd_cache_forget`,
|
|
94
|
+
`untappd_sync_wishlist` (mirrors a user's wishlist locally so picks can boost wishlisted beers),
|
|
95
|
+
`untappd_sync_brewery_likes` (mirrors the breweries a user likes/follows on Untappd, as "untappd" favorites),
|
|
96
|
+
`untappd_sync_user_venues` + `untappd_cache_has_visited` (a user's full venue history — "has this person
|
|
97
|
+
been here?" — which pages fully for any public/friend account, unlike their check-ins).
|
|
98
|
+
|
|
99
|
+
Trip planning: `untappd_trip_check` takes a user (or `usernames` + `mode`) and up to 25 stops — each a
|
|
100
|
+
`venue_id`, a `name` (+ `city`/`state`), a `lat`/`lng`, and/or a `brewery_id` — and returns per stop:
|
|
101
|
+
whether they've been there (first/recent visit, total visits, the matched venues incl. Untappd's
|
|
102
|
+
"(duplicate of …)" copies; a brewery stop is matched by the brewery's name), how many of the brewery's
|
|
103
|
+
beers they've had, and the top N (default 4) **not-had** beers by Untappd global rating, with an
|
|
104
|
+
optional `style` filter (out-of-production beers skipped unless asked). Visits and has-had are answered
|
|
105
|
+
from the cache only; the one API call it may make is `brewery/beer_list` for a brewery whose list isn't
|
|
106
|
+
cached or is past its 7-day TTL — at most `api_budget` (default 5; `0` = strictly cache-only, using a
|
|
107
|
+
stale list flagged `stale`), under the rate-limit guard. Favorite venues and breweries are flagged,
|
|
108
|
+
`prefer_favorites` (default true) boosts wishlisted and favorite-brewery beers (liked on Untappd, derived or
|
|
109
|
+
manual), and a stop within
|
|
110
|
+
`near_favorite_radius_m` (default 2 km) of a favorite venue gets `near_favorites` and a "Near a
|
|
111
|
+
favorite: …" hint.
|
|
112
|
+
|
|
113
|
+
Group checks: `untappd_cache_has_had_many` and `untappd_top_not_had` accept `usernames` (up to 10)
|
|
114
|
+
with `mode: "none_had"` (default — beers new to everyone) or `"any_not_had"` (beers at least one of
|
|
115
|
+
them still needs); each beer says who has and hasn't had it.
|
|
116
|
+
|
|
117
|
+
What they drank at a venue (opt-in, costs calls): `untappd_sync_venue_drinks` fetches the beer from
|
|
118
|
+
each cached venue's first and most-recent visit check-in (`checkin/view`, budgeted by `max_calls`,
|
|
119
|
+
stops before the rate limit). It's a partial picture — two check-ins per venue at most — shown in
|
|
120
|
+
`untappd_cache_has_visited` as `drinks`.
|
|
121
|
+
|
|
122
|
+
Favorites (cache only, never written to Untappd): `untappd_favorites_list` lists a user's favorite
|
|
123
|
+
beers, breweries and venues, each once with its `sources` and a `reason`. **Derived** favorites come
|
|
124
|
+
from the synced history — venues with ≥5 check-ins or visits spanning ≥3 distinct months; breweries
|
|
125
|
+
with ≥5 beers had and an average rating ≥3.75, or ≥15 beers; beers rated ≥4.25 or had ≥3 times —
|
|
126
|
+
and every threshold can be overridden per call. **Manual** favorites are added and removed with
|
|
127
|
+
`untappd_favorite_add` / `untappd_favorite_remove` (local table, optional note). Results from
|
|
128
|
+
`untappd_cache_has_had`, `untappd_cache_has_had_many`, `untappd_cache_has_visited`, `untappd_brewery_beers`, `untappd_venue_menu`
|
|
129
|
+
and `untappd_top_not_had` carry a `favorite` field `{is_favorite, sources, reason}`, and
|
|
130
|
+
`untappd_top_not_had` ranks wishlisted beers and beers from favorite breweries higher by default
|
|
131
|
+
(`prefer_favorites: false` turns that off); `untappd_trip_check` carries the same flags and boost.
|
|
132
|
+
|
|
133
|
+
**Untappd** favorites (breweries only): `untappd_sync_brewery_likes` mirrors the breweries a user
|
|
134
|
+
"likes" on Untappd (`user/brewerylikes` — the app calls them "followings"; works for another user's
|
|
135
|
+
public/friend account, ~50 per call). Like the wishlist sync it's a full pass from the top, resumable
|
|
136
|
+
(cursor saved every page, `another_run_needed`, `percent`), stops before the rate limit with
|
|
137
|
+
`stopped_for_rate_limit` + resume info, and only drops unliked breweries once a pass finishes. Liked
|
|
138
|
+
breweries then carry the source `untappd` everywhere favorites appear — merged with `derived` and
|
|
139
|
+
`manual`, each source listed once — and count for the `prefer_favorites` boost and trip_check's flags.
|
|
140
|
+
`untappd_cache_status` reports each user's `brewery_likes` coverage, and `untappd_cache_forget` removes them.
|
|
141
|
+
|
|
142
|
+
Untappd has **no followed or favorite *venue* list** at all (no web or API route; `venue/info` has no
|
|
143
|
+
per-user flag), so venue favorites are derived + manual only. `brewery/info`'s
|
|
144
|
+
`claimed_status.follow_status` describes the **signed-in** account, like `have_had` below, so it is
|
|
145
|
+
never used for anyone else — the synced likes list is how to see another user's.
|
|
146
|
+
|
|
147
|
+
Cached reads (to stretch the ~100 calls/hour limit): `untappd_brewery_beers` keeps each brewery's
|
|
148
|
+
list for 7 days, and `untappd_venue_menu` / `untappd_venue_activity` keep results for 6 hours; each
|
|
149
|
+
reports where the answer came from and takes `refresh: true` to re-fetch. Untappd's `have_had` on a
|
|
150
|
+
brewery's beer list describes the **signed-in** account, never the user you're asking about, so it
|
|
151
|
+
isn't cached or returned — use `untappd_cache_has_had_many` for any user.
|
|
152
|
+
|
|
153
|
+
Menu picks: `untappd_menu_pick` (tap-list lines in, ranked not-had beers out),
|
|
154
|
+
`untappd_venue_pick` (same, from a venue's Untappd menu).
|
|
155
|
+
|
|
156
|
+
## Picking a beer off a menu
|
|
157
|
+
|
|
158
|
+
`untappd_menu_pick` turns "find Mer a beer" from a menu photo into one call.
|
|
159
|
+
Give it the lines as read off the board:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"username": "mer",
|
|
164
|
+
"items": [
|
|
165
|
+
"HAP & HARRY'S TENNESSEE LAGER $6",
|
|
166
|
+
"I BELIEVE IN NASHVILLE BLONDE",
|
|
167
|
+
"JACKALOPE FENNARIO IPA",
|
|
168
|
+
"BEARDED IRIS HOMESTYLE",
|
|
169
|
+
"BLACK ABBEY SEASONAL",
|
|
170
|
+
"ROTATING CIDER"
|
|
171
|
+
],
|
|
172
|
+
"venue_city": "Nashville",
|
|
173
|
+
"top_n": 2
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
In one call it:
|
|
178
|
+
|
|
179
|
+
1. **Freshens the cache.** If the user's distinct-beers list is already complete
|
|
180
|
+
it does an incremental refresh, stopping at the first beer that's cached and
|
|
181
|
+
unchanged (usually one page). If the list is empty or partial it backfills
|
|
182
|
+
in the same call until done or `api_budget` (default 60 calls, shared by
|
|
183
|
+
every step) runs out. You never need to call a sync tool first.
|
|
184
|
+
2. **Resolves each line to a beer.** It strips prices, sizes, ABV and
|
|
185
|
+
punctuation, searches Untappd (falling back to a query without the style
|
|
186
|
+
word, because Untappd search matches every term), and scores the candidates
|
|
187
|
+
on brewery match, beer-name match (so "Homestyle" isn't read as "Double
|
|
188
|
+
Homestyle"), check-in volume (the canonical entry wins over a 3-check-in
|
|
189
|
+
duplicate) and `venue_city`. Up to 4 searches run at once, and a 429 is
|
|
190
|
+
retried with backoff. Resolutions are cached on disk, so the next visit to
|
|
191
|
+
the same venue costs no searches.
|
|
192
|
+
3. **Handles generic lines.** `<Brewery> SEASONAL` / `ROTATING` becomes a
|
|
193
|
+
`possible` list of that brewery's seasonal-looking beers, marked
|
|
194
|
+
`needs_confirmation`. A line with no brewery (`ROTATING CIDER`) comes back
|
|
195
|
+
`unresolved: "ask bartender"`.
|
|
196
|
+
4. **Checks every beer against the cache** (both sources, as `has_had_many` does).
|
|
197
|
+
5. **Ranks the not-had beers** by global rating, applies `style_filter`, and
|
|
198
|
+
returns the top `top_n` as `picks`.
|
|
199
|
+
|
|
200
|
+
Output for the call above, with Mer having had Fennario (abridged: `not_had`
|
|
201
|
+
also lists the Tennessee Lager at 3.33, `possible` lists six candidates, and a
|
|
202
|
+
`freshness` block is appended):
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"picks": [
|
|
207
|
+
{ "item": "BEARDED IRIS HOMESTYLE", "bid": 1502190, "name": "Homestyle", "brewery": "Bearded Iris Brewing",
|
|
208
|
+
"style": "IPA - New England / Hazy", "abv": 6, "global_rating": 3.98,
|
|
209
|
+
"reason": "not had · 3.98 global rating · #1 of 3 not-had beers on this list" },
|
|
210
|
+
{ "item": "I BELIEVE IN NASHVILLE BLONDE", "bid": 3780741, "name": "I Believe In Nashville",
|
|
211
|
+
"brewery": "I Believe Brewing Company", "style": "Blonde / Golden Ale - American", "abv": 4.5,
|
|
212
|
+
"global_rating": 3.54, "reason": "not had · 3.54 global rating · #2 of 3 not-had beers on this list" }
|
|
213
|
+
],
|
|
214
|
+
"had": [
|
|
215
|
+
{ "item": "JACKALOPE FENNARIO IPA", "bid": 1893601, "name": "Fennario", "brewery": "Jackalope Brewing Company",
|
|
216
|
+
"count": 3, "last_date": "2025-08-01T20:00:00.000Z", "user_rating": 3.75 }
|
|
217
|
+
],
|
|
218
|
+
"possible": [
|
|
219
|
+
{ "item": "BLACK ABBEY SEASONAL", "brewery": "The Black Abbey Brewing Company",
|
|
220
|
+
"reason": "seasonal-looking beers from this brewery",
|
|
221
|
+
"candidates": [
|
|
222
|
+
{ "bid": 1223176, "name": "Chronicles Festbier", "style": "Festbier", "abv": 4.9, "global_rating": 3.672, "had": false }
|
|
223
|
+
],
|
|
224
|
+
"needs_confirmation": true }
|
|
225
|
+
],
|
|
226
|
+
"unresolved": [{ "item": "ROTATING CIDER", "reason": "ask bartender" }],
|
|
227
|
+
"sync": { "mode": "backfill", "api_calls": 1 },
|
|
228
|
+
"api_calls": 11,
|
|
229
|
+
"api_budget": 60,
|
|
230
|
+
"another_run_needed": false
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`sync` says what the refresh did; coverage lives in `freshness`. If the budget
|
|
235
|
+
runs out before the backfill finishes, `freshness.coverage_complete` is `false`,
|
|
236
|
+
`another_run_needed` is `true`, and the `freshness` caveat warns that a "not
|
|
237
|
+
had" may be a false negative. Calling again continues from where it stopped.
|
|
238
|
+
|
|
239
|
+
**Quota.** When an Untappd response carries `X-Ratelimit-Remaining`, the server
|
|
240
|
+
remembers it for the hour, and every budgeted tool (`menu_pick`, `venue_pick`,
|
|
241
|
+
`top_not_had`, both syncs) caps its budget to what's left. The result then
|
|
242
|
+
includes `quota_remaining`, plus `budget_clamped_to_quota: true` when the cap
|
|
243
|
+
applied. Untappd's public API documents the header; if the private API this
|
|
244
|
+
server uses doesn't send it, budgets work exactly as before.
|
|
245
|
+
|
|
246
|
+
`untappd_venue_pick` takes an Untappd venue URL (`https://untappd.com/v/<slug>/<id>`)
|
|
247
|
+
or id instead of `items`. It pages through the venue's whole verified menu, takes
|
|
248
|
+
the venue's city from it, and runs the same check and ranking. Menu items already
|
|
249
|
+
carry beer ids, so there is no search step.
|
|
92
250
|
|
|
93
251
|
## Check-in cache
|
|
94
252
|
|
|
@@ -96,10 +254,19 @@ The Untappd API only exposes paged lists (50 per page) and has **no** "has this
|
|
|
96
254
|
user ever had beer X?" lookup — answering that from the API alone means paging an
|
|
97
255
|
entire history (often 11k+ check-ins) against a tight ~100-calls/hour rate limit.
|
|
98
256
|
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`,
|
|
257
|
+
offline, with zero API calls. The mirror is a local SQLite file
|
|
258
|
+
(`node:sqlite`, default `~/.untappd-mcp/checkins.db`, overridable with
|
|
259
|
+
`UNTAPPD_CACHE_DB` / `UNTAPPD_CACHE_DIR`). The store is injectable, so another
|
|
101
260
|
deployment can back it differently without the tools changing.
|
|
102
261
|
|
|
262
|
+
**Persistence.** The file survives restarts, and a backfill that was cut off
|
|
263
|
+
(rate limit, crash, host quit) resumes from its saved offset on the next sync.
|
|
264
|
+
The file runs in WAL mode with a busy timeout and does every write in a
|
|
265
|
+
transaction, so two hosts open at the same time can share it. The schema is
|
|
266
|
+
versioned (`PRAGMA user_version`) and only ever migrated forward, adding
|
|
267
|
+
columns and tables, so an upgrade never wipes it. `untappd_cache_status` shows
|
|
268
|
+
the resolved path, size, schema version and per-user coverage.
|
|
269
|
+
|
|
103
270
|
Two sync sources fill the cache:
|
|
104
271
|
|
|
105
272
|
- **`untappd_sync_user_beers`** pages `user/beers` — the user's **complete
|
|
@@ -119,6 +286,18 @@ Both are resumable: they fetch `max_pages` per call (default 10), persist
|
|
|
119
286
|
progress after every page, and set `another_run_needed: true` until done — just
|
|
120
287
|
call again until it's `false`.
|
|
121
288
|
|
|
289
|
+
**Rate limit.** Every API response is recorded (time, status and any
|
|
290
|
+
`X-Ratelimit-*` / `Retry-After` headers) and, once the cache is open, logged in
|
|
291
|
+
the cache file so every process sharing it — and the next one — counts the same
|
|
292
|
+
hour. The syncs check that count **before every page** and stop cleanly with
|
|
293
|
+
`stopped_for_rate_limit: true` and `rate_limit.resume_at` when the next call
|
|
294
|
+
would leave fewer than 2 of the ~100 hourly calls; a 429 that slips through also
|
|
295
|
+
stops the run cleanly instead of failing it. Progress is already saved, so
|
|
296
|
+
re-run after `resume_at`. `untappd_cache_status` shows the current picture under
|
|
297
|
+
`rate_limit`. Note: the v4 API sends `X-Ratelimit-Remaining: 0` on every
|
|
298
|
+
successful response, so that header is ignored until it reports a live
|
|
299
|
+
(non-zero) value; the estimate rests on the local call count.
|
|
300
|
+
|
|
122
301
|
**Query** the cache with no further API calls. The has-had tools consult **both**
|
|
123
302
|
sources (a hit in either counts as had):
|
|
124
303
|
|
|
@@ -155,7 +334,8 @@ telling you to add them as a friend first.
|
|
|
155
334
|
user's dated check-ins, comments and venues — stay until you remove them. The
|
|
156
335
|
file is created owner-only (directory `0700`, database `0600`, and older
|
|
157
336
|
installs are tightened on open). `untappd_cache_forget` deletes one user's
|
|
158
|
-
cached check-ins, distinct-beers list
|
|
337
|
+
cached check-ins, distinct-beers list, venue history, wishlist copy, liked
|
|
338
|
+
breweries, local favorites, venue drinks and sync state after a confirmation
|
|
159
339
|
(preview shows the username and row counts); it touches only the local cache,
|
|
160
340
|
never Untappd, and a later sync can re-fetch. Deleting the file removes
|
|
161
341
|
everything.
|