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
package/dist/tools/user.js
CHANGED
|
@@ -16,6 +16,27 @@ function resolveUser(username, loginName) {
|
|
|
16
16
|
}
|
|
17
17
|
return encodeURIComponent(u);
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* user/venues sort values. Untappd silently IGNORES a value it doesn't know
|
|
21
|
+
* (the old 'name' / 'checkin' / 'highest_rated' never sorted anything), so the
|
|
22
|
+
* friendly aliases map onto the real keys. There is no rating sort for venues:
|
|
23
|
+
* 'highest_rated' is kept only so pre-existing callers don't start failing
|
|
24
|
+
* validation, as a DEPRECATED alias of highest_checkin.
|
|
25
|
+
*/
|
|
26
|
+
export const USER_VENUE_SORT_KEYS = {
|
|
27
|
+
date: 'date',
|
|
28
|
+
recent: 'recent',
|
|
29
|
+
date_asc: 'date_asc',
|
|
30
|
+
venue_name_asc: 'venue_name_asc',
|
|
31
|
+
venue_name_desc: 'venue_name_desc',
|
|
32
|
+
highest_checkin: 'highest_checkin',
|
|
33
|
+
lowest_checkin: 'lowest_checkin',
|
|
34
|
+
name: 'venue_name_asc',
|
|
35
|
+
checkin: 'highest_checkin',
|
|
36
|
+
/** @deprecated Untappd has no venue rating sort; kept for old callers. */
|
|
37
|
+
highest_rated: 'highest_checkin',
|
|
38
|
+
};
|
|
39
|
+
const USER_VENUE_SORTS = Object.keys(USER_VENUE_SORT_KEYS);
|
|
19
40
|
export function registerUserTools(server, client) {
|
|
20
41
|
server.registerTool('untappd_user_info', {
|
|
21
42
|
title: 'Get Untappd user profile',
|
|
@@ -34,7 +55,9 @@ export function registerUserTools(server, client) {
|
|
|
34
55
|
server.registerTool('untappd_user_checkins', {
|
|
35
56
|
title: 'Get Untappd user check-ins',
|
|
36
57
|
description: "Get a user's recent check-ins (most recent first): the beer, rating, comment, venue, and toasts/comments. " +
|
|
37
|
-
'Page backwards with max_id (the pagination.max_id from a prior call).
|
|
58
|
+
'Page backwards with max_id (the pagination.max_id from a prior call). Only your OWN history pages fully: for ' +
|
|
59
|
+
'any other user Untappd returns just the ~50 most recent check-ins and ignores max_id — use untappd_user_beers ' +
|
|
60
|
+
'(has had) or untappd_user_venues (has been) for their full history. Omit username for your own. Read-only.',
|
|
38
61
|
annotations: toolAnnotations({ title: 'Get Untappd user check-ins', readOnly: true, idempotent: true, openWorld: true }),
|
|
39
62
|
inputSchema: z.object({
|
|
40
63
|
username: UsernameArg,
|
|
@@ -114,20 +137,28 @@ export function registerUserTools(server, client) {
|
|
|
114
137
|
});
|
|
115
138
|
server.registerTool('untappd_user_venues', {
|
|
116
139
|
title: 'Get venues a user has checked in at',
|
|
117
|
-
description: "Get the venues a user has checked in at, most recent first, with per-venue
|
|
118
|
-
'
|
|
140
|
+
description: "Get the venues a user has checked in at, most recent first, with per-venue first/recent visit and " +
|
|
141
|
+
'check-in counts. Pages fully for any public/friend account (unlike user check-ins, which stop at ~50 for ' +
|
|
142
|
+
'other users). Untappd returns at most 25 venues per page whatever limit you ask for — advance by the ' +
|
|
143
|
+
"returned pagination offset. Omit username for your own account. Read-only.",
|
|
119
144
|
annotations: toolAnnotations({ title: 'Get venues a user has checked in at', readOnly: true, idempotent: true, openWorld: true }),
|
|
120
145
|
inputSchema: z.object({
|
|
121
146
|
username: UsernameArg,
|
|
122
|
-
limit: z.number().int().min(1).max(50).optional().describe('Max venues (1–50
|
|
147
|
+
limit: z.number().int().min(1).max(50).optional().describe('Max venues (1–50; Untappd caps a page at 25)'),
|
|
123
148
|
offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
|
|
124
149
|
sort: z
|
|
125
|
-
.enum(
|
|
150
|
+
.enum(USER_VENUE_SORTS)
|
|
126
151
|
.optional()
|
|
127
|
-
.describe('Sort order (default most recent)'
|
|
152
|
+
.describe('Sort order (default most recent). Untappd keys: date, recent, date_asc, venue_name_asc, venue_name_desc, ' +
|
|
153
|
+
'highest_checkin, lowest_checkin. Aliases: name = venue_name_asc, checkin = highest_checkin. ' +
|
|
154
|
+
'highest_rated is deprecated (Untappd has no venue rating sort) and is sent as highest_checkin.'),
|
|
128
155
|
}),
|
|
129
156
|
}, async ({ username, limit, offset, sort }) => {
|
|
130
|
-
const data = await client.get(`/user/venues/${resolveUser(username, client.loginName)}`, {
|
|
157
|
+
const data = await client.get(`/user/venues/${resolveUser(username, client.loginName)}`, {
|
|
158
|
+
limit,
|
|
159
|
+
offset,
|
|
160
|
+
sort: sort === undefined ? undefined : USER_VENUE_SORT_KEYS[sort],
|
|
161
|
+
});
|
|
131
162
|
return minifiedResult(data);
|
|
132
163
|
});
|
|
133
164
|
server.registerTool('untappd_pending_friends', {
|
package/dist/tools/venue.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { minifiedResult, resolveView, toolAnnotations, viewParam, viewResult } from '@chrischall/mcp-utils';
|
|
3
3
|
import { compactCheckins, UNTAPPD_VIEWS, upstreamCompact } from '../compact.js';
|
|
4
|
-
|
|
4
|
+
import { walkVenueMenu } from '../menu/venue-menu.js';
|
|
5
|
+
import { cachedResponse, VENUE_RESPONSE_TTL_MS } from '../cache/response-cache.js';
|
|
6
|
+
import { optionalCache } from './cache-optional.js';
|
|
7
|
+
import { favoriteLookup } from '../cache/favorites.js';
|
|
8
|
+
const TTL_HOURS = VENUE_RESPONSE_TTL_MS / 3_600_000;
|
|
9
|
+
const RefreshArg = z
|
|
10
|
+
.boolean()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe(`Bypass the ${TTL_HOURS}-hour local cache and call Untappd (default false)`);
|
|
13
|
+
export function registerVenueTools(server, client, cacheProvider) {
|
|
14
|
+
const cacheOrNone = optionalCache(cacheProvider);
|
|
15
|
+
/** The `cache` block a cached result carries (only when a cache is in use). */
|
|
16
|
+
const cacheBlock = (r) => r.expires_at === null ? {} : { cache: { hit: r.hit, fetched_at: r.fetched_at, expires_at: r.expires_at } };
|
|
5
17
|
server.registerTool('untappd_search_venue', {
|
|
6
18
|
title: 'Search Untappd venues',
|
|
7
19
|
description: 'Search Untappd for venues (bars, breweries, restaurants) by name. Returns matches with their venue id, ' +
|
|
@@ -36,10 +48,15 @@ export function registerVenueTools(server, client) {
|
|
|
36
48
|
'spans multiple sections — e.g. a 23-beer wall that comes back with 2 items. This tool forwards the ' +
|
|
37
49
|
'section_limit / section_offset paging params venue/info echoes back but never receives, walks sections up to a ' +
|
|
38
50
|
'per-call max_pages budget (respecting the ~100 calls/hour limit — it does NOT loop to completion in one call), and ' +
|
|
39
|
-
'flattens to [{bid, name, brewery, style, abv, price, serving_type, menu, section}]. Like the sync tools it is ' +
|
|
51
|
+
'flattens to [{bid, name, brewery, brewery_id, style, abv, price, serving_type, menu, section}]. Like the sync tools it is ' +
|
|
40
52
|
'resumable: when the budget runs out before full coverage it returns another_run_needed:true plus next_section_offset ' +
|
|
41
53
|
'to pass back on the next call. truncated:true means the upstream returned no more sections short of total_count ' +
|
|
42
|
-
'(e.g. it ignored the paging params) — not resumable.
|
|
54
|
+
'(e.g. it ignored the paging params) — not resumable. Results are cached locally for ' + TTL_HOURS + ' hours per ' +
|
|
55
|
+
'venue + paging params (cache.hit, fetched_at, expires_at) so repeated trip planning does not re-spend calls; ' +
|
|
56
|
+
'refresh:true re-fetches. With the cache, the result also carries favorite {is_favorite, sources, reason} for the ' +
|
|
57
|
+
'venue, and favorite / brewery_favorite on each beer that is one (derived from username\'s history or added ' +
|
|
58
|
+
'with untappd_favorite_add; default your account) — applied on read, so never stale. Get an id from ' +
|
|
59
|
+
'untappd_search_venue. Read-only.',
|
|
43
60
|
annotations: toolAnnotations({ title: "Get a venue's verified beer menu (section-paged)", readOnly: true, idempotent: true, openWorld: true }),
|
|
44
61
|
inputSchema: z.object({
|
|
45
62
|
venue_id: z.number().int().positive().describe('Untappd venue id'),
|
|
@@ -48,119 +65,72 @@ export function registerVenueTools(server, client) {
|
|
|
48
65
|
section_offset: z.number().int().min(0).optional().describe('Section offset to start from; pass a prior next_section_offset to resume (default 0).'),
|
|
49
66
|
max_pages: z.number().int().min(1).max(10).optional().describe('API calls to spend THIS run — page budget, not page size (default 3). Resume with next_section_offset if another_run_needed.'),
|
|
50
67
|
sort: z.string().optional().describe("Menu sort key (e.g. 'publish_order', 'highest_rated'). Optional."),
|
|
68
|
+
refresh: RefreshArg,
|
|
69
|
+
username: z
|
|
70
|
+
.string()
|
|
71
|
+
.min(1)
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Whose favorites to mark (cache only; default your configured account)'),
|
|
51
74
|
}),
|
|
52
|
-
}, async ({ venue_id, menu_id, section_limit, section_offset, max_pages, sort }) => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
}, async ({ venue_id, menu_id, section_limit, section_offset, max_pages, sort, refresh, username }) => {
|
|
76
|
+
const key = `venue_menu:${venue_id}:${JSON.stringify([menu_id ?? null, section_limit ?? null, section_offset ?? 0, max_pages ?? 3, sort ?? null])}`;
|
|
77
|
+
const cache = cacheOrNone();
|
|
78
|
+
const r = await cachedResponse(cache, key, 'venue_menu', () => fetchMenu(), { refresh });
|
|
79
|
+
// Favorites are applied on read (not cached), so a change shows even on a cache hit.
|
|
80
|
+
const user = username ?? client.loginName;
|
|
81
|
+
if (!cache || !user)
|
|
82
|
+
return minifiedResult({ ...r.data, ...cacheBlock(r) });
|
|
83
|
+
const beers = (Array.isArray(r.data.beers) ? r.data.beers : []);
|
|
84
|
+
const ids = (k) => beers.map((b) => b[k]).filter((x) => typeof x === 'number');
|
|
85
|
+
const favs = await favoriteLookup(cache, [user], { venue: [venue_id], beer: ids('bid'), brewery: ids('brewery_id') });
|
|
86
|
+
return minifiedResult({
|
|
87
|
+
...r.data,
|
|
88
|
+
favorite: favs.get('venue', venue_id),
|
|
89
|
+
beers: beers.map((b) => {
|
|
90
|
+
const beerFav = favs.get('beer', b.bid);
|
|
91
|
+
const breweryFav = favs.get('brewery', b.brewery_id);
|
|
92
|
+
return {
|
|
93
|
+
...b,
|
|
94
|
+
...(beerFav.is_favorite ? { favorite: beerFav } : {}),
|
|
95
|
+
...(breweryFav.is_favorite ? { brewery_favorite: breweryFav } : {}),
|
|
96
|
+
};
|
|
97
|
+
}),
|
|
98
|
+
...cacheBlock(r),
|
|
99
|
+
});
|
|
100
|
+
async function fetchMenu() {
|
|
101
|
+
// Per CLAUDE.md's rate-limit design this spends at most `max_pages` API
|
|
102
|
+
// calls per run and hands the caller next_section_offset to resume, rather
|
|
103
|
+
// than looping to completion. The walk itself lives in walkVenueMenu.
|
|
104
|
+
const startOffset = section_offset ?? 0;
|
|
105
|
+
const walk = await walkVenueMenu(client, venue_id, {
|
|
106
|
+
menuId: menu_id,
|
|
107
|
+
sectionLimit: section_limit,
|
|
108
|
+
sectionOffset: startOffset,
|
|
109
|
+
maxPages: max_pages ?? 3,
|
|
83
110
|
sort,
|
|
84
111
|
});
|
|
85
|
-
pagesFetched
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
88
|
-
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
sawMenu = true;
|
|
92
|
-
let added = 0;
|
|
93
|
-
let matchedItemCount = 0;
|
|
94
|
-
let sectionsReturned = 0;
|
|
95
|
-
for (const wrap of vb.items ?? []) {
|
|
96
|
-
const menu = wrap?.menu;
|
|
97
|
-
if (!menu)
|
|
98
|
-
continue;
|
|
99
|
-
if (menu_id && menu.menu_id !== menu_id)
|
|
100
|
-
continue;
|
|
101
|
-
if (typeof menu.total_item_count === 'number')
|
|
102
|
-
matchedItemCount += menu.total_item_count;
|
|
103
|
-
const sections = menu.sections?.items ?? [];
|
|
104
|
-
for (const section of sections) {
|
|
105
|
-
sectionsReturned++;
|
|
106
|
-
for (const it of section.items ?? []) {
|
|
107
|
-
const beer = it?.beer;
|
|
108
|
-
if (!beer || typeof beer.bid !== 'number')
|
|
109
|
-
continue;
|
|
110
|
-
const key = `${String(menu.menu_id)}:${String(section.section_id)}:${beer.bid}`;
|
|
111
|
-
if (seen.has(key))
|
|
112
|
-
continue;
|
|
113
|
-
seen.add(key);
|
|
114
|
-
const sectionName = section.section_name;
|
|
115
|
-
beers.push({
|
|
116
|
-
bid: beer.bid,
|
|
117
|
-
name: beer.beer_name,
|
|
118
|
-
brewery: it.brewery?.brewery_name,
|
|
119
|
-
style: beer.beer_style,
|
|
120
|
-
abv: beer.beer_abv,
|
|
121
|
-
price: it.price?.value,
|
|
122
|
-
serving_type: it.serving_type,
|
|
123
|
-
menu: menu.menu_name,
|
|
124
|
-
section: typeof sectionName === 'string' ? sectionName.trim() : sectionName,
|
|
125
|
-
});
|
|
126
|
-
added++;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// Coverage target: with a menu_id filter, aim for THAT menu's own item
|
|
131
|
-
// count — verfied_beers.total_count spans every menu, so a single-menu
|
|
132
|
-
// slice could never reach it (it would burn the whole page budget and
|
|
133
|
-
// wrongly report a shortfall). Without a filter, the venue-wide total.
|
|
134
|
-
totalCount = menu_id ? matchedItemCount : typeof vb.total_count === 'number' ? vb.total_count : totalCount;
|
|
135
|
-
offset += pageSize;
|
|
136
|
-
if (totalCount > 0 && beers.length >= totalCount) {
|
|
137
|
-
reachedEnd = true; // full coverage from this walk
|
|
138
|
-
break;
|
|
112
|
+
const { totalCount, sawMenu, pagesFetched, reachedEnd } = walk;
|
|
113
|
+
const beers = walk.beers.map(({ rawBeer: _b, rawBrewery: w, ...b }) => typeof w?.brewery_id === 'number' ? { ...b, brewery_id: w.brewery_id } : b);
|
|
114
|
+
if (!sawMenu) {
|
|
115
|
+
return { venue_id, total_count: 0, returned: 0, pages_fetched: pagesFetched, another_run_needed: false, truncated: false, beers: [], note: 'No verified menu on this venue.' };
|
|
139
116
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return
|
|
117
|
+
const covered = totalCount > 0 && beers.length >= totalCount;
|
|
118
|
+
const another_run_needed = !reachedEnd; // stopped only because the page budget ran out mid-list
|
|
119
|
+
// Only a walk that started at the top of the list can judge a genuine
|
|
120
|
+
// shortfall as truncated; a resumed tail legitimately returns fewer than
|
|
121
|
+
// the whole-menu total_count and is not truncated.
|
|
122
|
+
const truncated = reachedEnd && !covered && startOffset === 0 && totalCount > 0;
|
|
123
|
+
return {
|
|
124
|
+
venue_id,
|
|
125
|
+
total_count: totalCount,
|
|
126
|
+
returned: beers.length,
|
|
127
|
+
pages_fetched: pagesFetched,
|
|
128
|
+
another_run_needed,
|
|
129
|
+
...(another_run_needed ? { next_section_offset: walk.nextOffset } : {}),
|
|
130
|
+
truncated,
|
|
131
|
+
beers,
|
|
132
|
+
};
|
|
147
133
|
}
|
|
148
|
-
const covered = totalCount > 0 && beers.length >= totalCount;
|
|
149
|
-
const another_run_needed = !reachedEnd; // stopped only because the page budget ran out mid-list
|
|
150
|
-
// Only a walk that started at the top of the list can judge a genuine
|
|
151
|
-
// shortfall as truncated; a resumed tail legitimately returns fewer than
|
|
152
|
-
// the whole-menu total_count and is not truncated.
|
|
153
|
-
const truncated = reachedEnd && !covered && startOffset === 0 && totalCount > 0;
|
|
154
|
-
return minifiedResult({
|
|
155
|
-
venue_id,
|
|
156
|
-
total_count: totalCount,
|
|
157
|
-
returned: beers.length,
|
|
158
|
-
pages_fetched: pagesFetched,
|
|
159
|
-
another_run_needed,
|
|
160
|
-
...(another_run_needed ? { next_section_offset: offset } : {}),
|
|
161
|
-
truncated,
|
|
162
|
-
beers,
|
|
163
|
-
});
|
|
164
134
|
});
|
|
165
135
|
server.registerTool('untappd_venue_by_foursquare', {
|
|
166
136
|
title: 'Look up an Untappd venue by Foursquare id',
|
|
@@ -177,17 +147,23 @@ export function registerVenueTools(server, client) {
|
|
|
177
147
|
server.registerTool('untappd_venue_activity', {
|
|
178
148
|
title: 'Get recent check-ins at a venue',
|
|
179
149
|
description: 'Get the recent public check-ins at a venue by its id — who was there, what they drank, and their ratings. ' +
|
|
180
|
-
'Page backwards with max_id (the pagination.max_id from a prior call).
|
|
150
|
+
'Page backwards with max_id (the pagination.max_id from a prior call). Cached locally for ' + TTL_HOURS + ' hours ' +
|
|
151
|
+
'per venue + limit + max_id (cache.hit, fetched_at, expires_at); refresh:true re-fetches. Read-only.',
|
|
181
152
|
annotations: toolAnnotations({ title: 'Get recent check-ins at a venue', readOnly: true, idempotent: false, openWorld: true }),
|
|
182
153
|
inputSchema: z.object({
|
|
183
154
|
venue_id: z.number().int().positive().describe('Untappd venue id'),
|
|
184
155
|
limit: z.number().int().min(1).max(50).optional().describe('Max check-ins (1–50, default 25)'),
|
|
185
156
|
max_id: z.number().int().positive().optional().describe('Return check-ins older than this id (for paging)'),
|
|
157
|
+
refresh: RefreshArg,
|
|
186
158
|
view: viewParam(UNTAPPD_VIEWS, { note: 'compact projects each check-in to {id, user, beer, brewery, venue, rating, comment, toast/comment counts}; "full" returns Untappd\'s whole ~5 KB record.' }),
|
|
187
159
|
}),
|
|
188
|
-
}, async ({ venue_id, limit, max_id, view }) => {
|
|
189
|
-
|
|
160
|
+
}, async ({ venue_id, limit, max_id, refresh, view }) => {
|
|
161
|
+
// The RAW response is cached, so either view can be served from one entry.
|
|
162
|
+
const key = `venue_activity:${venue_id}:${limit ?? ''}:${max_id ?? ''}`;
|
|
163
|
+
const r = await cachedResponse(cacheOrNone(), key, 'venue_activity', () => client.get(`/venue/checkins/${venue_id}`, { limit, max_id }), { refresh });
|
|
190
164
|
const v = resolveView(view, UNTAPPD_VIEWS);
|
|
191
|
-
|
|
165
|
+
const shaped = v === 'compact' ? compactCheckins(r.data) : r.data;
|
|
166
|
+
const extra = cacheBlock(r);
|
|
167
|
+
return viewResult(v, Object.keys(extra).length && shaped && typeof shaped === 'object' ? { ...shaped, ...extra } : shaped);
|
|
192
168
|
});
|
|
193
169
|
}
|
package/dist/version.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
// json's `extra-files`), and `versionSyncTest` guards that it stays equal to
|
|
4
4
|
// package.json. Import VERSION wherever the version is needed rather than
|
|
5
5
|
// re-declaring it.
|
|
6
|
-
export const VERSION = '2.
|
|
6
|
+
export const VERSION = '2.5.0'; // x-release-please-version
|
package/mint.yaml
CHANGED
|
@@ -5,6 +5,18 @@ summary: >-
|
|
|
5
5
|
Search Untappd beers, breweries, and venues; read profiles, check-ins,
|
|
6
6
|
wishlists, and your friend feed; and post check-ins, toasts, and comments
|
|
7
7
|
to your account
|
|
8
|
+
examples:
|
|
9
|
+
- prompt: >-
|
|
10
|
+
What's trending on Untappd right now? Split it into the big macro beers
|
|
11
|
+
and the smaller craft ones.
|
|
12
|
+
note: Read-only; uses the trending beers list.
|
|
13
|
+
- prompt: >-
|
|
14
|
+
Look up Tree House Brewing on Untappd and list their highest-rated beers.
|
|
15
|
+
note: Read-only; brewery search, then its beer list sorted by rating.
|
|
16
|
+
- prompt: >-
|
|
17
|
+
Here's the tap list at the bar I'm at. Which two of these have I not had
|
|
18
|
+
yet, ranked by rating?
|
|
19
|
+
note: Checks your synced check-in history; nothing is posted.
|
|
8
20
|
env:
|
|
9
21
|
- name: UNTAPPD_USERNAME
|
|
10
22
|
required: true
|
|
@@ -31,6 +43,11 @@ env:
|
|
|
31
43
|
help: >-
|
|
32
44
|
IANA timezone (e.g. America/New_York) that check-ins are stamped with
|
|
33
45
|
when a call passes no timezone. Defaults to the server's own zone.
|
|
46
|
+
- name: UNTAPPD_RATE_LIMIT_PER_HOUR
|
|
47
|
+
required: false
|
|
48
|
+
help: >-
|
|
49
|
+
Hourly Untappd API call limit the syncs' stop-before-429 guard enforces
|
|
50
|
+
(default 100, Untappd's documented per-token limit).
|
|
34
51
|
- name: UNTAPPD_PHOTO_DIR
|
|
35
52
|
required: false
|
|
36
53
|
help: >-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "untappd-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/untappd-mcp",
|
|
5
5
|
"description": "Untappd MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -58,5 +58,9 @@
|
|
|
58
58
|
"typescript": "^7.0.2",
|
|
59
59
|
"vite": "^8.2.2",
|
|
60
60
|
"vitest": "^5.0.0"
|
|
61
|
+
},
|
|
62
|
+
"allowScripts": {
|
|
63
|
+
"esbuild": false,
|
|
64
|
+
"fsevents": false
|
|
61
65
|
}
|
|
62
66
|
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/untappd-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.5.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "untappd-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.5.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -79,6 +79,13 @@
|
|
|
79
79
|
"format": "string",
|
|
80
80
|
"isSecret": false
|
|
81
81
|
},
|
|
82
|
+
{
|
|
83
|
+
"name": "UNTAPPD_RATE_LIMIT_PER_HOUR",
|
|
84
|
+
"description": "Hourly Untappd API 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.",
|
|
85
|
+
"isRequired": false,
|
|
86
|
+
"format": "string",
|
|
87
|
+
"isSecret": false
|
|
88
|
+
},
|
|
82
89
|
{
|
|
83
90
|
"name": "UNTAPPD_PHOTO_DIR",
|
|
84
91
|
"description": "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.",
|
|
@@ -92,6 +99,13 @@
|
|
|
92
99
|
"isRequired": false,
|
|
93
100
|
"format": "string",
|
|
94
101
|
"isSecret": false
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "UNTAPPD_CACHE_DIR",
|
|
105
|
+
"description": "Directory for the cache instead of ~/.untappd-mcp (the file is checkins.db inside it). UNTAPPD_CACHE_DB wins if both are set. Local/stdio only.",
|
|
106
|
+
"isRequired": false,
|
|
107
|
+
"format": "string",
|
|
108
|
+
"isSecret": false
|
|
95
109
|
}
|
|
96
110
|
]
|
|
97
111
|
}
|
|
@@ -205,6 +205,36 @@ Query tools (has-had ones consult BOTH sources — a hit in either = had):
|
|
|
205
205
|
capped at `api_budget` per run (partial/another_run_needed when exceeded).
|
|
206
206
|
- `untappd_cache_query` — filter cached check-ins by brewery, style, `min_rating`,
|
|
207
207
|
venue, and date range.
|
|
208
|
+
- **Groups:** `untappd_cache_has_had_many` and `untappd_top_not_had` take
|
|
209
|
+
`usernames` (instead of `username`) plus `mode`: `none_had` (default — new to
|
|
210
|
+
everyone) or `any_not_had` (at least one of them still needs it). Each beer
|
|
211
|
+
lists `had_by` / `not_had_by`; freshness is reported per user.
|
|
212
|
+
- `untappd_sync_venue_drinks` — opt-in, costs one call per check-in: fetches the
|
|
213
|
+
beer from each cached venue's first/recent visit (`checkin/view`), budgeted by
|
|
214
|
+
`max_calls`; `untappd_cache_has_visited` then shows them as `drinks`. Partial
|
|
215
|
+
by design (two check-ins per venue at most).
|
|
216
|
+
- **Favorites** (local, no API): `untappd_favorites_list` (filter `kind`
|
|
217
|
+
beer|brewery|venue, `source` derived|manual|untappd, override `thresholds`),
|
|
218
|
+
`untappd_favorite_add` / `untappd_favorite_remove` (manual, with a note —
|
|
219
|
+
never written to Untappd). Derived defaults: venue ≥5 check-ins or ≥3 distinct
|
|
220
|
+
months; brewery ≥5 beers with avg ≥3.75, or ≥15 beers; beer rated ≥4.25 or
|
|
221
|
+
had ≥3 times. has_had / has_had_many / has_visited / brewery_beers / venue_menu /
|
|
222
|
+
top_not_had results carry `favorite` {is_favorite, sources, reason};
|
|
223
|
+
`untappd_top_not_had` boosts wishlisted (+0.5) and favorite-brewery (+0.25)
|
|
224
|
+
beers unless `prefer_favorites: false`. `untappd_sync_brewery_likes` mirrors
|
|
225
|
+
the breweries a user likes/follows on Untappd (resumable, rate-guarded); they
|
|
226
|
+
carry the source `untappd` (merged with derived/manual) and count for the boost.
|
|
227
|
+
Untappd has no followed-venue list, so venue favorites are derived + manual.
|
|
228
|
+
`brewery_info`'s `follow_status` is the signed-in account's, never another user's.
|
|
229
|
+
- `untappd_trip_check` — a whole trip in one call: per stop (`venue_id`, `name`
|
|
230
|
+
+ `city`/`state`, `lat`/`lng`, and/or `brewery_id`) → visited with dates,
|
|
231
|
+
beers had from that brewery, and the top N (default 4) not-had beers by global
|
|
232
|
+
rating (`style` filter). Cache only, except `brewery/beer_list` for a missing or
|
|
233
|
+
>7-day-old list (`api_budget`, default 5; 0 = cache only). Flags favorite
|
|
234
|
+
venues/breweries, boosts wishlist/favorite-brewery beers (`prefer_favorites`),
|
|
235
|
+
and hints when a favorite venue is within `near_favorite_radius_m` (2 km).
|
|
236
|
+
Sync first: `untappd_sync_user_venues`, `untappd_sync_user_beers`,
|
|
237
|
+
`untappd_sync_wishlist`, `untappd_sync_brewery_likes`.
|
|
208
238
|
|
|
209
239
|
**Retention.** The cache keeps what you sync until you remove it — including
|
|
210
240
|
friends' dated check-ins, comments and venues. `untappd_cache_forget` (confirmation-gated)
|