untappd-mcp 2.5.2 → 2.6.1
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/dist/bundle.js +273 -146
- package/dist/tools/beer.js +4 -0
- package/dist/tools/brewery.js +4 -0
- package/dist/tools/cache.js +15 -0
- package/dist/tools/checkin.js +6 -0
- package/dist/tools/discover.js +4 -0
- package/dist/tools/favorites.js +4 -0
- package/dist/tools/feed.js +18 -2
- package/dist/tools/friends.js +6 -0
- package/dist/tools/hints.js +70 -0
- package/dist/tools/menu.js +3 -0
- package/dist/tools/resolve.js +3 -0
- package/dist/tools/trip.js +2 -0
- package/dist/tools/user.js +9 -0
- package/dist/tools/venue.js +6 -0
- package/dist/tools/wishlist.js +3 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/untappd-mcp/SKILL.md +32 -0
package/dist/tools/beer.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { foodMeta } from './hints.js';
|
|
2
3
|
import { resolveView, toolAnnotations, viewParam, viewResult } from '@chrischall/mcp-utils';
|
|
3
4
|
import { compactBeerSearch, compactCheckins, UNTAPPD_VIEWS, upstreamCompact } from '../compact.js';
|
|
4
5
|
import { beerMetaFrom } from '../cache/store.js';
|
|
@@ -33,6 +34,7 @@ export function registerBeerTools(server, client, cache) {
|
|
|
33
34
|
'beer id (bid), brewery, style, ABV, IBU, and global rating. Feed a bid into untappd_beer_info for full ' +
|
|
34
35
|
'detail. Read-only.',
|
|
35
36
|
annotations: toolAnnotations({ title: 'Search Untappd beers', readOnly: true, idempotent: true, openWorld: true }),
|
|
37
|
+
_meta: foodMeta('lookup', { aliases: ['beer', 'ipa', 'stout', 'brew'] }),
|
|
36
38
|
inputSchema: z.object({
|
|
37
39
|
query: z.string().min(1).describe('Beer name to search for'),
|
|
38
40
|
limit: z.number().int().min(1).max(50).optional().describe('Max results (1–50, default 25)'),
|
|
@@ -57,6 +59,7 @@ export function registerBeerTools(server, client, cache) {
|
|
|
57
59
|
description: 'Get full detail for a beer by its Untappd beer id (bid): description, style, ABV, IBU, brewery, rating, ' +
|
|
58
60
|
'total check-in count, and — on view:"full" — recent activity. Get a bid from untappd_search_beer. Read-only.',
|
|
59
61
|
annotations: toolAnnotations({ title: 'Get Untappd beer detail', readOnly: true, idempotent: true, openWorld: true }),
|
|
62
|
+
_meta: foodMeta('lookup'),
|
|
60
63
|
inputSchema: z.object({
|
|
61
64
|
bid: BidSchema,
|
|
62
65
|
// No local projection here: compact is forwarded to Untappd as its OWN
|
|
@@ -75,6 +78,7 @@ export function registerBeerTools(server, client, cache) {
|
|
|
75
78
|
description: 'Get the recent public check-ins for a beer by its bid — who drank it, their rating, comment, and venue. ' +
|
|
76
79
|
'Page backwards with max_id (the pagination.max_id from a prior call). Read-only.',
|
|
77
80
|
annotations: toolAnnotations({ title: 'Get recent check-ins for a beer', readOnly: true, idempotent: false, openWorld: true }),
|
|
81
|
+
_meta: foodMeta('lookup'),
|
|
78
82
|
inputSchema: z.object({
|
|
79
83
|
bid: BidSchema,
|
|
80
84
|
limit: z.number().int().min(1).max(50).optional().describe('Max check-ins (1–50, default 25)'),
|
package/dist/tools/brewery.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { foodMeta } from './hints.js';
|
|
2
3
|
import { minifiedResult, resolveView, toolAnnotations, viewParam, viewResult } from '@chrischall/mcp-utils';
|
|
3
4
|
import { UNTAPPD_VIEWS, compactBreweryBeerList, upstreamCompact } from '../compact.js';
|
|
4
5
|
import { BREWERY_BEERS_DEFAULT_LIMIT, BREWERY_BEERS_TTL_MS, getBreweryBeers } from '../cache/brewery-beers.js';
|
|
@@ -11,6 +12,7 @@ export function registerBreweryTools(server, client, cacheProvider) {
|
|
|
11
12
|
description: 'Search Untappd for breweries by name. Returns matches with their brewery id, location, type, and beer ' +
|
|
12
13
|
'count. Feed a brewery id into untappd_brewery_info for full detail. Read-only.',
|
|
13
14
|
annotations: toolAnnotations({ title: 'Search Untappd breweries', readOnly: true, idempotent: true, openWorld: true }),
|
|
15
|
+
_meta: foodMeta('lookup', { aliases: ['brewery', 'brewer'] }),
|
|
14
16
|
inputSchema: z.object({
|
|
15
17
|
query: z.string().min(1).describe('Brewery name to search for'),
|
|
16
18
|
limit: z.number().int().min(1).max(50).optional().describe('Max results (1–50, default 25)'),
|
|
@@ -27,6 +29,7 @@ export function registerBreweryTools(server, client, cacheProvider) {
|
|
|
27
29
|
'claimed_status.follow_status is whether the SIGNED-IN account follows/likes the brewery — never another ' +
|
|
28
30
|
"user's; for theirs, run untappd_sync_brewery_likes and read untappd_favorites_list (source \"untappd\").",
|
|
29
31
|
annotations: toolAnnotations({ title: 'Get Untappd brewery detail', readOnly: true, idempotent: true, openWorld: true }),
|
|
32
|
+
_meta: foodMeta('lookup'),
|
|
30
33
|
inputSchema: z.object({
|
|
31
34
|
brewery_id: z.number().int().positive().describe('Untappd brewery id'),
|
|
32
35
|
view: viewParam(UNTAPPD_VIEWS, { note: 'compact also asks Untappd for its own slim record, dropping the embedded activity/list blocks; "full" returns everything.' }),
|
|
@@ -50,6 +53,7 @@ export function registerBreweryTools(server, client, cacheProvider) {
|
|
|
50
53
|
'reason} for the brewery and on each favorite beer (derived from username\'s history or added with ' +
|
|
51
54
|
'untappd_favorite_add; default your account). view:"full" always calls Untappd and returns its raw page. Read-only.',
|
|
52
55
|
annotations: toolAnnotations({ title: "Get a brewery's beer list", readOnly: true, idempotent: true, openWorld: true }),
|
|
56
|
+
_meta: foodMeta('lookup'),
|
|
53
57
|
inputSchema: z.object({
|
|
54
58
|
brewery_id: z.number().int().positive().describe('Untappd brewery id'),
|
|
55
59
|
limit: z.number().int().min(1).max(50).optional().describe('Max beers (1–50, default 25)'),
|
package/dist/tools/cache.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { foodMeta } from './hints.js';
|
|
2
3
|
import { RateLimitError, confirmTokenParam, createHelpfulError, minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
3
4
|
import { beerMetaFrom, DEFAULT_VISIT_RADIUS_M } from '../cache/store.js';
|
|
4
5
|
import { syncCheckins } from '../cache/sync.js';
|
|
@@ -190,6 +191,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
190
191
|
'is ignored until it reports a live value) and any 429 wait. Use it to confirm the cache persisted across ' +
|
|
191
192
|
'restarts, to see how far a backfill has got, or to check whether a sync can run now.',
|
|
192
193
|
annotations: toolAnnotations({ title: 'Show the local cache location, schema and per-user coverage', readOnly: true, idempotent: true, openWorld: false }),
|
|
194
|
+
_meta: foodMeta('lookup'),
|
|
193
195
|
inputSchema: z.object({}),
|
|
194
196
|
}, async () => {
|
|
195
197
|
const cache = cacheProvider();
|
|
@@ -219,6 +221,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
219
221
|
'with stopped_for_rate_limit and rate_limit.resume_at — progress is saved; re-run after that time. Omit ' +
|
|
220
222
|
'username for your own account.',
|
|
221
223
|
annotations: toolAnnotations({ title: "Sync a user's check-ins into the cache", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
|
|
224
|
+
_meta: foodMeta('other'),
|
|
222
225
|
inputSchema: z.object({
|
|
223
226
|
username: UsernameArg,
|
|
224
227
|
max_pages: z
|
|
@@ -255,6 +258,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
255
258
|
'cleanly on a 429) with stopped_for_rate_limit and rate_limit.resume_at — progress is saved; re-run after ' +
|
|
256
259
|
'that time. Omit username for your own account.',
|
|
257
260
|
annotations: toolAnnotations({ title: "Sync a user's complete distinct-beers list into the cache", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
|
|
261
|
+
_meta: foodMeta('other'),
|
|
258
262
|
inputSchema: z.object({
|
|
259
263
|
username: UsernameArg,
|
|
260
264
|
max_pages: z
|
|
@@ -283,6 +287,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
283
287
|
'stopped; another_run_needed stays true until it finishes). Stops BEFORE the ~100 calls/hour rate ' +
|
|
284
288
|
'limit (or cleanly on a 429) with stopped_for_rate_limit and rate_limit.resume_at. Omit username for your own account.',
|
|
285
289
|
annotations: toolAnnotations({ title: "Sync a user's complete venue history into the cache", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
|
|
290
|
+
_meta: foodMeta('other'),
|
|
286
291
|
inputSchema: z.object({
|
|
287
292
|
username: UsernameArg,
|
|
288
293
|
max_pages: z
|
|
@@ -309,6 +314,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
309
314
|
'Only reads Untappd — never changes the wishlist (that is untappd_wishlist_add / _remove). Omit username for ' +
|
|
310
315
|
'your own account; another user\'s wishlist needs a public account or friendship.',
|
|
311
316
|
annotations: toolAnnotations({ title: "Sync a user's wishlist into the cache", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
|
|
317
|
+
_meta: foodMeta('other'),
|
|
312
318
|
inputSchema: z.object({
|
|
313
319
|
username: UsernameArg,
|
|
314
320
|
max_pages: z
|
|
@@ -344,6 +350,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
344
350
|
openWorld: true,
|
|
345
351
|
destructive: false,
|
|
346
352
|
}),
|
|
353
|
+
_meta: foodMeta('other'),
|
|
347
354
|
inputSchema: z.object({
|
|
348
355
|
username: UsernameArg,
|
|
349
356
|
max_pages: z
|
|
@@ -378,6 +385,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
378
385
|
openWorld: true,
|
|
379
386
|
destructive: false,
|
|
380
387
|
}),
|
|
388
|
+
_meta: foodMeta('other'),
|
|
381
389
|
inputSchema: z.object({
|
|
382
390
|
username: UsernameArg,
|
|
383
391
|
max_calls: z
|
|
@@ -438,6 +446,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
438
446
|
'reason}: derived from the visit history or added with untappd_favorite_add), and total_visits; plus a freshness ' +
|
|
439
447
|
'block (last_synced_at, coverage_complete, caveat). Run untappd_sync_user_venues first.',
|
|
440
448
|
annotations: toolAnnotations({ title: 'Check if a user has been to a venue (from the cache)', readOnly: true, idempotent: true, openWorld: false }),
|
|
449
|
+
_meta: foodMeta('lookup'),
|
|
441
450
|
inputSchema: z.object({
|
|
442
451
|
username: UsernameArg,
|
|
443
452
|
queries: z.array(VenueQueryArg).min(1).max(50).describe('Places to check (1–50); each needs venue_ids, name, or lat+lng'),
|
|
@@ -516,6 +525,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
516
525
|
'≥4.25 / had ≥3 times, or added with untappd_favorite_add). Reports per-source freshness so you can ' +
|
|
517
526
|
'caveat incomplete data. Requires bid or beer_name. Run untappd_sync_user_beers first for full coverage.',
|
|
518
527
|
annotations: toolAnnotations({ title: 'Check if a user has had a beer (from the cache)', readOnly: true, idempotent: true, openWorld: false }),
|
|
528
|
+
_meta: foodMeta('lookup', { aliases: ['have i had', 'tried before'] }),
|
|
519
529
|
inputSchema: z.object({
|
|
520
530
|
username: UsernameArg,
|
|
521
531
|
bid: z.number().int().positive().optional().describe('Exact Untappd beer id to look for'),
|
|
@@ -545,6 +555,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
545
555
|
'"any_not_had" (at least one of them still needs it) — with qualifying_bids and a per-user freshness block. ' +
|
|
546
556
|
'Run untappd_sync_user_beers first (for every user); the freshness block flags if coverage is incomplete.',
|
|
547
557
|
annotations: toolAnnotations({ title: 'Batch-check many beers against the cache', readOnly: true, idempotent: true, openWorld: false }),
|
|
558
|
+
_meta: foodMeta('lookup'),
|
|
548
559
|
inputSchema: z.object({
|
|
549
560
|
username: UsernameArg,
|
|
550
561
|
usernames: UsernamesArg,
|
|
@@ -606,6 +617,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
606
617
|
'untappd_sync_user_beers first; if coverage is incomplete the freshness caveat flags that a "not had" may be ' +
|
|
607
618
|
'a false negative.',
|
|
608
619
|
annotations: toolAnnotations({ title: 'From a list of beers, return the ones a user has NOT had', readOnly: true, idempotent: true, openWorld: false }),
|
|
620
|
+
_meta: foodMeta('lookup'),
|
|
609
621
|
inputSchema: z.object({
|
|
610
622
|
username: UsernameArg,
|
|
611
623
|
bids: z.array(z.number().int().positive()).min(1).max(500).describe('Candidate beer ids to filter (1–500)'),
|
|
@@ -635,6 +647,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
635
647
|
'for non-self accounts is only the recent window; for full coverage of which beers a user has had, use ' +
|
|
636
648
|
'untappd_cache_has_had / not_had instead. Run untappd_sync_checkins first.',
|
|
637
649
|
annotations: toolAnnotations({ title: 'Query cached check-ins with filters', readOnly: true, idempotent: true, openWorld: false }),
|
|
650
|
+
_meta: foodMeta('lookup'),
|
|
638
651
|
inputSchema: z.object({
|
|
639
652
|
username: UsernameArg,
|
|
640
653
|
brewery_id: z.number().int().positive().optional().describe('Exact brewery id'),
|
|
@@ -673,6 +686,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
673
686
|
'untappd_sync_wishlist and Untappd likes untappd_sync_brewery_likes — ' +
|
|
674
687
|
'and says so per beer (score, boosted_by, wishlisted, brewery_favorite/favorite).',
|
|
675
688
|
annotations: toolAnnotations({ title: 'Top-rated beers a user has NOT had, from a candidate list', readOnly: false, idempotent: false, openWorld: true, destructive: false }),
|
|
689
|
+
_meta: foodMeta('other', { aliases: ['what should i order', 'beers i have not had'] }),
|
|
676
690
|
inputSchema: z.object({
|
|
677
691
|
username: UsernameArg,
|
|
678
692
|
usernames: UsernamesArg,
|
|
@@ -855,6 +869,7 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
855
869
|
'cannot be re-fetched). Shared beer, venue and brewery facts are kept. The preview shows the username and exactly how many rows will be removed. Omit username ' +
|
|
856
870
|
`for your own account. ${CONFIRM_FLOW}`,
|
|
857
871
|
annotations: toolAnnotations({ title: "Forget a user's cached check-ins", readOnly: false, idempotent: true, openWorld: false, destructive: true }),
|
|
872
|
+
_meta: foodMeta('change'),
|
|
858
873
|
inputSchema: z.object({
|
|
859
874
|
username: UsernameArg,
|
|
860
875
|
confirmToken: confirmTokenParam,
|
package/dist/tools/checkin.js
CHANGED
|
@@ -4,6 +4,7 @@ import { pipeline } from 'node:stream/promises';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { delimiter, extname, join } from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { foodMeta } from './hints.js';
|
|
7
8
|
import { confirmTokenParam, McpToolError, assertPathWithinRoots, createHelpfulError, fileBlob, messageOf, minifiedResult, readEnvVar, readFileHead, sniffMimeBytes, toolAnnotations, UnreachableError, } from '@chrischall/mcp-utils';
|
|
8
9
|
import { CONFIRM_FLOW, confirmWrite } from './confirm.js';
|
|
9
10
|
const CheckinIdSchema = z.number().int().positive().describe('Untappd check-in id');
|
|
@@ -192,6 +193,7 @@ export function registerCheckinTools(server, client) {
|
|
|
192
193
|
`already toasted removes the toast. Writes to your Untappd account and is visible to others. ${CONFIRM_FLOW}`,
|
|
193
194
|
// Destructive: a toast lands in the check-in owner's notifications, and toggling it back off cannot un-notify them.
|
|
194
195
|
annotations: toolAnnotations({ title: 'Toast an Untappd check-in', readOnly: false, idempotent: false, openWorld: true, destructive: true }),
|
|
196
|
+
_meta: foodMeta('send', { aliases: ['toast', 'cheers'] }),
|
|
195
197
|
inputSchema: z.object({
|
|
196
198
|
checkin_id: CheckinIdSchema,
|
|
197
199
|
confirmToken: confirmTokenParam,
|
|
@@ -227,6 +229,7 @@ export function registerCheckinTools(server, client) {
|
|
|
227
229
|
CONFIRM_FLOW,
|
|
228
230
|
// Destructive: the comment reaches the check-in owner (and other readers); untappd_delete_comment removes it but cannot un-notify anyone.
|
|
229
231
|
annotations: toolAnnotations({ title: 'Comment on an Untappd check-in', readOnly: false, idempotent: false, openWorld: true, destructive: true }),
|
|
232
|
+
_meta: foodMeta('send', { aliases: ['comment'] }),
|
|
230
233
|
inputSchema: z.object({
|
|
231
234
|
checkin_id: CheckinIdSchema,
|
|
232
235
|
comment: z.string().min(1).max(2000).describe('Comment text to post'),
|
|
@@ -261,6 +264,7 @@ export function registerCheckinTools(server, client) {
|
|
|
261
264
|
title: 'Delete a comment from an Untappd check-in',
|
|
262
265
|
description: `Delete one of YOUR comments by its comment id (the id from a check-in's comments list). ${CONFIRM_FLOW}`,
|
|
263
266
|
annotations: toolAnnotations({ title: 'Delete a comment from an Untappd check-in', readOnly: false, idempotent: true, openWorld: true, destructive: true }),
|
|
267
|
+
_meta: foodMeta('change'),
|
|
264
268
|
inputSchema: z.object({
|
|
265
269
|
comment_id: z.number().int().positive().describe('Untappd comment id (from a check-in\'s comments.items)'),
|
|
266
270
|
confirmToken: confirmTokenParam,
|
|
@@ -287,6 +291,7 @@ export function registerCheckinTools(server, client) {
|
|
|
287
291
|
title: 'Delete an Untappd check-in',
|
|
288
292
|
description: `Permanently delete one of YOUR check-ins by its id. This is destructive and cannot be undone. ${CONFIRM_FLOW}`,
|
|
289
293
|
annotations: toolAnnotations({ title: 'Delete an Untappd check-in', readOnly: false, idempotent: true, openWorld: true, destructive: true }),
|
|
294
|
+
_meta: foodMeta('change'),
|
|
290
295
|
inputSchema: z.object({
|
|
291
296
|
checkin_id: CheckinIdSchema,
|
|
292
297
|
confirmToken: confirmTokenParam,
|
|
@@ -322,6 +327,7 @@ export function registerCheckinTools(server, client) {
|
|
|
322
327
|
`that will be posted. ${CONFIRM_FLOW}`,
|
|
323
328
|
// Destructive: it publishes to your public feed, where friends see it; untappd_delete_checkin cannot unpublish what they saw.
|
|
324
329
|
annotations: toolAnnotations({ title: 'Check in a beer on Untappd', readOnly: false, idempotent: false, openWorld: true, destructive: true }),
|
|
330
|
+
_meta: foodMeta('change', { aliases: ['check in', 'log a beer', 'rate a beer', 'drinking'] }),
|
|
325
331
|
inputSchema: z.object({
|
|
326
332
|
bid: z.number().int().positive().describe('Untappd beer id to check in (from untappd_search_beer)'),
|
|
327
333
|
rating: RatingSchema.optional().describe('Rating 0–5 in 0.25 increments (omit for no rating)'),
|
package/dist/tools/discover.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { foodMeta } from './hints.js';
|
|
2
3
|
import { minifiedResult, resolveView, toolAnnotations, viewParam, viewResult } from '@chrischall/mcp-utils';
|
|
3
4
|
import { compactCheckins, UNTAPPD_VIEWS } from '../compact.js';
|
|
4
5
|
export function registerDiscoverTools(server, client) {
|
|
@@ -7,6 +8,7 @@ export function registerDiscoverTools(server, client) {
|
|
|
7
8
|
description: 'Get the beers trending on Untappd right now, split into macro (big/widely-available) and micro ' +
|
|
8
9
|
'(craft/independent) lists. Read-only.',
|
|
9
10
|
annotations: toolAnnotations({ title: 'Get trending beers', readOnly: true, idempotent: false, openWorld: true }),
|
|
11
|
+
_meta: foodMeta('lookup', { aliases: ['popular beers', 'trending beers'] }),
|
|
10
12
|
inputSchema: z.object({}),
|
|
11
13
|
}, async () => {
|
|
12
14
|
const data = await client.get('/beer/trending');
|
|
@@ -17,6 +19,7 @@ export function registerDiscoverTools(server, client) {
|
|
|
17
19
|
description: 'Get your Untappd notifications — toasts, comments, friend requests, and badges earned on YOUR account, ' +
|
|
18
20
|
'plus news items. Read-only.',
|
|
19
21
|
annotations: toolAnnotations({ title: 'Get your Untappd notifications', readOnly: true, idempotent: false, openWorld: true }),
|
|
22
|
+
_meta: foodMeta('whats-new', { aliases: ['toasts', 'badges', 'friend requests', 'beer notifications'] }),
|
|
20
23
|
inputSchema: z.object({
|
|
21
24
|
limit: z.number().int().min(1).max(50).optional().describe('Max notifications (1–50, default 25)'),
|
|
22
25
|
offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
|
|
@@ -30,6 +33,7 @@ export function registerDiscoverTools(server, client) {
|
|
|
30
33
|
description: 'Get recent check-ins near a location (lat/lng) — what people are drinking nearby right now. Optionally ' +
|
|
31
34
|
'widen the search radius. Read-only.',
|
|
32
35
|
annotations: toolAnnotations({ title: 'Get nearby Untappd check-ins', readOnly: true, idempotent: false, openWorld: true }),
|
|
36
|
+
_meta: foodMeta('lookup', { aliases: ['drinking nearby'] }),
|
|
33
37
|
inputSchema: z.object({
|
|
34
38
|
lat: z.number().min(-90).max(90).describe('Latitude of the location'),
|
|
35
39
|
lng: z.number().min(-180).max(180).describe('Longitude of the location'),
|
package/dist/tools/favorites.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { foodMeta } from './hints.js';
|
|
2
3
|
import { minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
3
4
|
import { FAVORITE_KINDS } from '../cache/store.js';
|
|
4
5
|
import { DEFAULT_FAVORITE_THRESHOLDS, FAVORITE_SOURCES, listFavorites } from '../cache/favorites.js';
|
|
@@ -30,6 +31,7 @@ export function registerFavoriteTools(server, client, cacheProvider) {
|
|
|
30
31
|
'has_had, has_had_many, has_visited, brewery_beers, venue_menu, top_not_had and trip_check results; a favorite brewery also boosts its ' +
|
|
31
32
|
'beers in untappd_top_not_had. Omit username for your own account.',
|
|
32
33
|
annotations: toolAnnotations({ title: 'Mark a beer, brewery or venue as a favorite (local)', readOnly: false, idempotent: true, openWorld: false, destructive: false }),
|
|
34
|
+
_meta: foodMeta('change'),
|
|
33
35
|
inputSchema: z.object({
|
|
34
36
|
username: UsernameArg,
|
|
35
37
|
kind: KindArg,
|
|
@@ -49,6 +51,7 @@ export function registerFavoriteTools(server, client, cacheProvider) {
|
|
|
49
51
|
'when there was none. A DERIVED favorite (from the user\'s history) cannot be removed — it is computed from ' +
|
|
50
52
|
'the thresholds; change them in untappd_favorites_list instead. Omit username for your own account.',
|
|
51
53
|
annotations: toolAnnotations({ title: 'Remove a manual favorite (local)', readOnly: false, idempotent: true, openWorld: false, destructive: false }),
|
|
54
|
+
_meta: foodMeta('change'),
|
|
52
55
|
inputSchema: z.object({ username: UsernameArg, kind: KindArg, id: IdArg }),
|
|
53
56
|
}, async ({ username, kind, id }) => {
|
|
54
57
|
const user = resolveUser(username, client.loginName);
|
|
@@ -70,6 +73,7 @@ export function registerFavoriteTools(server, client, cacheProvider) {
|
|
|
70
73
|
'(untappd_sync_user_venues, untappd_sync_user_beers); "untappd" ones as the last untappd_sync_brewery_likes ' +
|
|
71
74
|
'(freshness.brewery_likes). Filter by kind and source. Omit username for your own account.',
|
|
72
75
|
annotations: toolAnnotations({ title: "List a user's favorite beers, breweries and venues", readOnly: true, idempotent: true, openWorld: false }),
|
|
76
|
+
_meta: foodMeta('lookup'),
|
|
73
77
|
inputSchema: z.object({
|
|
74
78
|
username: UsernameArg,
|
|
75
79
|
kind: z.enum(FAVORITE_KINDS).optional().describe('Only this kind (default all three)'),
|
package/dist/tools/feed.js
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { minifiedResult, resolveView, toolAnnotations,
|
|
2
|
+
import { minifiedResult, resolveView, toolAnnotations, viewResult } from '@chrischall/mcp-utils';
|
|
3
3
|
import { compactCheckins, UNTAPPD_VIEWS } from '../compact.js';
|
|
4
|
+
import { briefCheckins, foodMeta } from './hints.js';
|
|
5
|
+
/**
|
|
6
|
+
* The feed's `view`: the fleet's compact/full rungs plus `brief`, the one-line-per-item
|
|
7
|
+
* answer a context-group host reads aloud (the feed is this server's canonical whats-new).
|
|
8
|
+
*/
|
|
9
|
+
const FEED_VIEW = z
|
|
10
|
+
.enum(['compact', 'full', 'brief'])
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('Response shape: "compact" (default) projects each check-in to {id, user, beer, brewery, venue, rating, comment, ' +
|
|
13
|
+
'toast/comment counts}; "full" returns Untappd\'s whole ~5 KB record; "brief" returns one line per check-in: ' +
|
|
14
|
+
'<user>: <beer> (<brewery>) at <venue> — <ISO date or "no date"> — <rated N|not rated>, inside the untrusted-data ' +
|
|
15
|
+
'envelope as data: string[], with "-- " note lines ("-- none", "-- more").');
|
|
4
16
|
export function registerFeedTools(server, client) {
|
|
5
17
|
server.registerTool('untappd_activity_feed', {
|
|
6
18
|
title: 'Get Untappd friend activity feed',
|
|
7
19
|
description: 'Get your Untappd friend activity feed — the recent check-ins from people you follow, newest first. Page ' +
|
|
8
20
|
'backwards with max_id (the pagination.max_id from a prior call). Read-only.',
|
|
9
21
|
annotations: toolAnnotations({ title: 'Get Untappd friend activity feed', readOnly: true, idempotent: false, openWorld: true }),
|
|
22
|
+
_meta: foodMeta('whats-new', { canonical: true, aliases: ['beer', 'check-ins', 'friends drinking', 'what are my friends drinking', 'craft beer'] }),
|
|
10
23
|
inputSchema: z.object({
|
|
11
24
|
limit: z.number().int().min(1).max(50).optional().describe('Max check-ins (1–50, default 25)'),
|
|
12
25
|
max_id: z.number().int().positive().optional().describe('Return check-ins older than this id (for paging)'),
|
|
13
|
-
view:
|
|
26
|
+
view: FEED_VIEW,
|
|
14
27
|
}),
|
|
15
28
|
}, async ({ limit, max_id, view }) => {
|
|
16
29
|
const data = await client.get('/checkin/recent', { limit, max_id });
|
|
30
|
+
if (view === 'brief')
|
|
31
|
+
return { content: [{ type: 'text', text: briefCheckins(data) }] };
|
|
17
32
|
const v = resolveView(view, UNTAPPD_VIEWS);
|
|
18
33
|
return viewResult(v, v === 'compact' ? compactCheckins(data) : data);
|
|
19
34
|
});
|
|
@@ -22,6 +37,7 @@ export function registerFeedTools(server, client) {
|
|
|
22
37
|
description: 'Get full detail for a single check-in by its id: the beer, rating, comment, photos, venue, badges earned, ' +
|
|
23
38
|
'toasts, and comments. Get a check-in id from a feed or user-checkins result. Read-only.',
|
|
24
39
|
annotations: toolAnnotations({ title: 'Get Untappd check-in detail', readOnly: true, idempotent: true, openWorld: true }),
|
|
40
|
+
_meta: foodMeta('lookup'),
|
|
25
41
|
inputSchema: z.object({
|
|
26
42
|
checkin_id: z.number().int().positive().describe('Untappd check-in id'),
|
|
27
43
|
}),
|
package/dist/tools/friends.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { confirmTokenParam, minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
3
3
|
import { CONFIRM_FLOW, confirmWrite } from './confirm.js';
|
|
4
|
+
import { foodMeta } from './hints.js';
|
|
4
5
|
// The endpoint PATHS below are confirmed from the Untappd app's own JS bundle
|
|
5
6
|
// (`friend/request`, `friend/accept`, `friend/reject`, `friend/remove`). The
|
|
6
7
|
// HTTP method (POST) mirrors the other captured action-writes (toast/comment),
|
|
@@ -20,6 +21,7 @@ const ACTIONS = [
|
|
|
20
21
|
title: 'Send an Untappd friend request',
|
|
21
22
|
verb: 'send a friend request to',
|
|
22
23
|
detail: 'Sends a friend request to another user, who will be notified.',
|
|
24
|
+
intent: 'send',
|
|
23
25
|
},
|
|
24
26
|
{
|
|
25
27
|
tool: 'untappd_accept_friend',
|
|
@@ -27,6 +29,7 @@ const ACTIONS = [
|
|
|
27
29
|
title: 'Accept an Untappd friend request',
|
|
28
30
|
verb: 'accept the incoming friend request from',
|
|
29
31
|
detail: 'Accepts an incoming friend request (see untappd_pending_friends for pending uids).',
|
|
32
|
+
intent: 'change',
|
|
30
33
|
},
|
|
31
34
|
{
|
|
32
35
|
tool: 'untappd_reject_friend',
|
|
@@ -34,6 +37,7 @@ const ACTIONS = [
|
|
|
34
37
|
title: 'Reject an Untappd friend request',
|
|
35
38
|
verb: 'reject the incoming friend request from',
|
|
36
39
|
detail: 'Rejects/ignores an incoming friend request (see untappd_pending_friends for pending uids).',
|
|
40
|
+
intent: 'change',
|
|
37
41
|
},
|
|
38
42
|
{
|
|
39
43
|
tool: 'untappd_remove_friend',
|
|
@@ -41,6 +45,7 @@ const ACTIONS = [
|
|
|
41
45
|
title: 'Remove an Untappd friend',
|
|
42
46
|
verb: 'remove your friendship with',
|
|
43
47
|
detail: 'Removes an existing friend, or cancels a friend request you sent.',
|
|
48
|
+
intent: 'change',
|
|
44
49
|
},
|
|
45
50
|
];
|
|
46
51
|
export function registerFriendActionTools(server, client) {
|
|
@@ -55,6 +60,7 @@ export function registerFriendActionTools(server, client) {
|
|
|
55
60
|
// un-sent, and a rejection cannot be un-rejected, so none of them has an
|
|
56
61
|
// inverse in this tool set.
|
|
57
62
|
annotations: toolAnnotations({ title: action.title, readOnly: false, idempotent: true, openWorld: true, destructive: true }),
|
|
63
|
+
_meta: foodMeta(action.intent, action.intent === 'send' ? { aliases: ['friend request', 'add a friend'] } : {}),
|
|
58
64
|
inputSchema: z.object({
|
|
59
65
|
target_uid: TargetUidSchema,
|
|
60
66
|
confirmToken: confirmTokenParam,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { UNTRUSTED_CONTENT_RULE, untrustedEnvelope } from '@chrischall/mcp-utils';
|
|
2
|
+
/** `_meta` for an Untappd tool: every tool here is in the `food` domain. */
|
|
3
|
+
export function foodMeta(intent, extra = {}) {
|
|
4
|
+
return { mint: { domain: 'food', intent, ...extra } };
|
|
5
|
+
}
|
|
6
|
+
const BRIEF_MAX = 120;
|
|
7
|
+
function str(v) {
|
|
8
|
+
return typeof v === 'string' && v.trim() !== '' ? v.trim() : undefined;
|
|
9
|
+
}
|
|
10
|
+
function dict(v) {
|
|
11
|
+
return v && typeof v === 'object' ? v : {};
|
|
12
|
+
}
|
|
13
|
+
/** Untappd's RFC 2822 `created_at` as an ISO instant (to the second), or undefined when unparseable. */
|
|
14
|
+
function isoInstant(v) {
|
|
15
|
+
const s = str(v);
|
|
16
|
+
if (!s)
|
|
17
|
+
return undefined;
|
|
18
|
+
const t = Date.parse(s);
|
|
19
|
+
return Number.isNaN(t) ? undefined : new Date(t).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One check-in as one line, `<user>: <beer> (<brewery>) at <venue> — <ISO date | no date> — <rated N | not rated>`,
|
|
23
|
+
* cut to at most 120 characters as a whole, title first. A title starting with
|
|
24
|
+
* `-- ` or `\\` is escaped with one leading `\\`, so an item never reads as a
|
|
25
|
+
* note. Read only from the fields a check-in record carries; a rating of 0 is
|
|
26
|
+
* Untappd's "no rating".
|
|
27
|
+
*/
|
|
28
|
+
export function briefCheckinLine(item) {
|
|
29
|
+
const c = dict(item);
|
|
30
|
+
const who = str(dict(c.user).user_name) ?? 'someone';
|
|
31
|
+
const beer = str(dict(c.beer).beer_name) ?? 'a beer';
|
|
32
|
+
const brewery = str(dict(c.brewery).brewery_name);
|
|
33
|
+
const venue = str(dict(c.venue).venue_name);
|
|
34
|
+
const raw = `${who}: ${beer}${brewery ? ` (${brewery})` : ''}${venue ? ` at ${venue}` : ''}`;
|
|
35
|
+
// A title that could read as a note (`-- `) or lose a character to the host's one-backslash strip is escaped.
|
|
36
|
+
const title = raw.startsWith('-- ') || raw.startsWith('\\') ? `\\${raw}` : raw;
|
|
37
|
+
const rating = typeof c.rating_score === 'number' && c.rating_score > 0 ? `rated ${c.rating_score}` : 'not rated';
|
|
38
|
+
const tail = ` — ${isoInstant(c.created_at) ?? 'no date'} — ${rating}`;
|
|
39
|
+
const room = BRIEF_MAX - tail.length;
|
|
40
|
+
const shown = title.length > room ? `${title.slice(0, Math.max(room - 1, 0))}…` : title;
|
|
41
|
+
return `${shown}${tail}`;
|
|
42
|
+
}
|
|
43
|
+
/** Who writes the brief lines' text — the head of the untrusted-data envelope's note. */
|
|
44
|
+
const BRIEF_WHO_WROTE = 'Check-in user names and beer, brewery and venue names are written by other Untappd users and breweries.';
|
|
45
|
+
/**
|
|
46
|
+
* A `{ checkins: { items, pagination } }` response as brief lines, one per
|
|
47
|
+
* check-in, plus `-- ` notes: `-- none` for an empty page, `-- more` when
|
|
48
|
+
* Untappd returned an older-page cursor (`pagination.max_id`), and
|
|
49
|
+
* `-- error: …` for a shape this view does not read.
|
|
50
|
+
*/
|
|
51
|
+
export function briefCheckinLines(resp) {
|
|
52
|
+
const box = dict(dict(resp).checkins);
|
|
53
|
+
if (!Array.isArray(box.items)) {
|
|
54
|
+
return ['-- error: Untappd answered in a shape this brief view does not read; ask with view "full"'];
|
|
55
|
+
}
|
|
56
|
+
if (box.items.length === 0)
|
|
57
|
+
return ['-- none'];
|
|
58
|
+
const lines = box.items.map(briefCheckinLine);
|
|
59
|
+
const next = dict(box.pagination).max_id;
|
|
60
|
+
if (typeof next === 'number' && next > 0)
|
|
61
|
+
lines.push('-- more');
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The brief answer: the lines inside the fleet's untrusted-data envelope as
|
|
66
|
+
* `data: string[]`, minified, note first — the text in them is other people's.
|
|
67
|
+
*/
|
|
68
|
+
export function briefCheckins(resp) {
|
|
69
|
+
return JSON.stringify(untrustedEnvelope(briefCheckinLines(resp), { note: `${BRIEF_WHO_WROTE} ${UNTRUSTED_CONTENT_RULE}` }));
|
|
70
|
+
}
|
package/dist/tools/menu.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { foodMeta } from './hints.js';
|
|
2
3
|
import { createHelpfulError, minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
3
4
|
import { menuPick, venuePick } from '../menu/pick.js';
|
|
4
5
|
import { freshness, quotaFields, resolveUser, UsernameArg } from './cache.js';
|
|
@@ -47,6 +48,7 @@ export function registerMenuTools(server, client, cacheProvider, deps = {}) {
|
|
|
47
48
|
'a false negative and another_run_needed is true. api_budget is also capped to the quota Untappd reports, ' +
|
|
48
49
|
'when it reports one. Omit username for your own account.',
|
|
49
50
|
annotations: toolAnnotations({ title: "Pick beers off a tap list that a user hasn't had", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
|
|
51
|
+
_meta: foodMeta('other', { aliases: ['what should i order', 'tap list', 'beer menu'] }),
|
|
50
52
|
inputSchema: z.object({
|
|
51
53
|
username: UsernameArg,
|
|
52
54
|
items: z
|
|
@@ -84,6 +86,7 @@ export function registerMenuTools(server, client, cacheProvider, deps = {}) {
|
|
|
84
86
|
'(menu_complete is false only if the budget ran out mid-menu). A venue with no verified menu comes back ' +
|
|
85
87
|
'unresolved — use untappd_menu_pick with the tap list instead. Omit username for your own account.',
|
|
86
88
|
annotations: toolAnnotations({ title: "Pick beers off a venue's Untappd menu that a user hasn't had", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
|
|
89
|
+
_meta: foodMeta('other', { aliases: ['what should i order', 'on tap'] }),
|
|
87
90
|
inputSchema: z.object({
|
|
88
91
|
venue: z
|
|
89
92
|
.union([z.string().min(1), z.number().int().positive()])
|
package/dist/tools/resolve.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { foodMeta } from './hints.js';
|
|
2
3
|
import { createHelpfulError, minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
3
4
|
/**
|
|
4
5
|
* Parse an untappd.com URL into its entity type and id. Path shapes (confirmed
|
|
@@ -49,6 +50,7 @@ export function registerResolveTools(server, client) {
|
|
|
49
50
|
description: 'Parse an untappd.com URL (a beer /b/, brewery /w/, venue /v/, user /user/, or check-in link) into its ' +
|
|
50
51
|
'entity type and id, and name the tool to call next. Pure local parsing — no network. Read-only.',
|
|
51
52
|
annotations: toolAnnotations({ title: 'Resolve an Untappd URL', readOnly: true, idempotent: true, openWorld: false }),
|
|
53
|
+
_meta: foodMeta('lookup'),
|
|
52
54
|
inputSchema: z.object({
|
|
53
55
|
url: z.string().min(1).describe('An untappd.com URL to resolve'),
|
|
54
56
|
}),
|
|
@@ -66,6 +68,7 @@ export function registerResolveTools(server, client) {
|
|
|
66
68
|
description: 'Resolve an untappd.com URL AND fetch the entity detail in one call — the convenience combination of ' +
|
|
67
69
|
'untappd_resolve + the matching info tool. Returns { resolved, detail }. Read-only.',
|
|
68
70
|
annotations: toolAnnotations({ title: 'Open an Untappd URL', readOnly: true, idempotent: true, openWorld: true }),
|
|
71
|
+
_meta: foodMeta('lookup'),
|
|
69
72
|
inputSchema: z.object({
|
|
70
73
|
url: z.string().min(1).describe('An untappd.com URL to resolve and fetch'),
|
|
71
74
|
}),
|
package/dist/tools/trip.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { foodMeta } from './hints.js';
|
|
2
3
|
import { RateLimitError, createHelpfulError, messageOf, minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
3
4
|
import { DEFAULT_VISIT_RADIUS_M, haversineM } from '../cache/store.js';
|
|
4
5
|
import { BREWERY_BEERS_TTL_MS, getBreweryBeers } from '../cache/brewery-beers.js';
|
|
@@ -64,6 +65,7 @@ export function registerTripTools(server, client, cacheProvider) {
|
|
|
64
65
|
idempotent: true,
|
|
65
66
|
openWorld: true,
|
|
66
67
|
}),
|
|
68
|
+
_meta: foodMeta('lookup', { aliases: ['brewery crawl', 'bar crawl', 'beer trip'] }),
|
|
67
69
|
inputSchema: z.object({
|
|
68
70
|
username: UsernameArg,
|
|
69
71
|
usernames: z
|
package/dist/tools/user.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { foodMeta } from './hints.js';
|
|
2
3
|
import { createHelpfulError, minifiedResult, resolveView, toolAnnotations, viewParam, viewResult } from '@chrischall/mcp-utils';
|
|
3
4
|
import { compactCheckins, compactWishlist, compactUserBeers, UNTAPPD_VIEWS, upstreamCompact } from '../compact.js';
|
|
4
5
|
const UsernameArg = z
|
|
@@ -43,6 +44,7 @@ export function registerUserTools(server, client) {
|
|
|
43
44
|
description: "Get an Untappd user's profile: bio, location, total check-ins, distinct beers, badges, and stats. " +
|
|
44
45
|
'Omit username for your own account. Read-only.',
|
|
45
46
|
annotations: toolAnnotations({ title: 'Get Untappd user profile', readOnly: true, idempotent: true, openWorld: true }),
|
|
47
|
+
_meta: foodMeta('lookup'),
|
|
46
48
|
inputSchema: z.object({
|
|
47
49
|
username: UsernameArg,
|
|
48
50
|
view: viewParam(UNTAPPD_VIEWS, { note: 'compact also asks Untappd for its own slim record, dropping the embedded activity/list blocks; \'full\' returns everything.' }),
|
|
@@ -59,6 +61,7 @@ export function registerUserTools(server, client) {
|
|
|
59
61
|
'any other user Untappd returns just the ~50 most recent check-ins and ignores max_id — use untappd_user_beers ' +
|
|
60
62
|
'(has had) or untappd_user_venues (has been) for their full history. Omit username for your own. Read-only.',
|
|
61
63
|
annotations: toolAnnotations({ title: 'Get Untappd user check-ins', readOnly: true, idempotent: true, openWorld: true }),
|
|
64
|
+
_meta: foodMeta('lookup'),
|
|
62
65
|
inputSchema: z.object({
|
|
63
66
|
username: UsernameArg,
|
|
64
67
|
limit: z.number().int().min(1).max(50).optional().describe('Max check-ins (1–50, default 25)'),
|
|
@@ -74,6 +77,7 @@ export function registerUserTools(server, client) {
|
|
|
74
77
|
title: 'Get Untappd user wishlist',
|
|
75
78
|
description: "Get the beers on a user's wishlist. Supports sorting and paging. Omit username for your own account. Read-only.",
|
|
76
79
|
annotations: toolAnnotations({ title: 'Get Untappd user wishlist', readOnly: true, idempotent: true, openWorld: true }),
|
|
80
|
+
_meta: foodMeta('lookup', { aliases: ['wishlist', 'beers to try'] }),
|
|
77
81
|
inputSchema: z.object({
|
|
78
82
|
username: UsernameArg,
|
|
79
83
|
limit: z.number().int().min(1).max(50).optional().describe('Max beers (1–50, default 25)'),
|
|
@@ -94,6 +98,7 @@ export function registerUserTools(server, client) {
|
|
|
94
98
|
description: "Get the distinct (unique) beers a user has ever checked in, with their rating and check-in count per beer. " +
|
|
95
99
|
'Supports sorting and paging. Omit username for your own account. Read-only.',
|
|
96
100
|
annotations: toolAnnotations({ title: 'Get Untappd distinct beers', readOnly: true, idempotent: true, openWorld: true }),
|
|
101
|
+
_meta: foodMeta('lookup'),
|
|
97
102
|
inputSchema: z.object({
|
|
98
103
|
username: UsernameArg,
|
|
99
104
|
limit: z.number().int().min(1).max(50).optional().describe('Max beers (1–50, default 25)'),
|
|
@@ -113,6 +118,7 @@ export function registerUserTools(server, client) {
|
|
|
113
118
|
title: 'Get Untappd user badges',
|
|
114
119
|
description: "Get the badges a user has earned, most recent first. Omit username for your own account. Read-only.",
|
|
115
120
|
annotations: toolAnnotations({ title: 'Get Untappd user badges', readOnly: true, idempotent: true, openWorld: true }),
|
|
121
|
+
_meta: foodMeta('lookup'),
|
|
116
122
|
inputSchema: z.object({
|
|
117
123
|
username: UsernameArg,
|
|
118
124
|
limit: z.number().int().min(1).max(50).optional().describe('Max badges (1–50, default 25)'),
|
|
@@ -126,6 +132,7 @@ export function registerUserTools(server, client) {
|
|
|
126
132
|
title: 'Get Untappd user friends',
|
|
127
133
|
description: "Get a user's friend list. Omit username for your own account. Read-only.",
|
|
128
134
|
annotations: toolAnnotations({ title: 'Get Untappd user friends', readOnly: true, idempotent: true, openWorld: true }),
|
|
135
|
+
_meta: foodMeta('lookup'),
|
|
129
136
|
inputSchema: z.object({
|
|
130
137
|
username: UsernameArg,
|
|
131
138
|
limit: z.number().int().min(1).max(50).optional().describe('Max friends (1–50, default 25)'),
|
|
@@ -142,6 +149,7 @@ export function registerUserTools(server, client) {
|
|
|
142
149
|
'other users). Untappd returns at most 25 venues per page whatever limit you ask for — advance by the ' +
|
|
143
150
|
"returned pagination offset. Omit username for your own account. Read-only.",
|
|
144
151
|
annotations: toolAnnotations({ title: 'Get venues a user has checked in at', readOnly: true, idempotent: true, openWorld: true }),
|
|
152
|
+
_meta: foodMeta('lookup'),
|
|
145
153
|
inputSchema: z.object({
|
|
146
154
|
username: UsernameArg,
|
|
147
155
|
limit: z.number().int().min(1).max(50).optional().describe('Max venues (1–50; Untappd caps a page at 25)'),
|
|
@@ -166,6 +174,7 @@ export function registerUserTools(server, client) {
|
|
|
166
174
|
description: 'Get the incoming friend requests waiting on YOUR account — the users who have requested to be your ' +
|
|
167
175
|
'friend. Read-only.',
|
|
168
176
|
annotations: toolAnnotations({ title: 'Get your pending friend requests', readOnly: true, idempotent: true, openWorld: true }),
|
|
177
|
+
_meta: foodMeta('lookup'),
|
|
169
178
|
inputSchema: z.object({
|
|
170
179
|
limit: z.number().int().min(1).max(50).optional().describe('Max requests (1–50, default 25)'),
|
|
171
180
|
offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
|
package/dist/tools/venue.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { foodMeta } from './hints.js';
|
|
2
3
|
import { minifiedResult, resolveView, toolAnnotations, viewParam, viewResult } from '@chrischall/mcp-utils';
|
|
3
4
|
import { compactCheckins, UNTAPPD_VIEWS, upstreamCompact } from '../compact.js';
|
|
4
5
|
import { walkVenueMenu } from '../menu/venue-menu.js';
|
|
@@ -19,6 +20,7 @@ export function registerVenueTools(server, client, cacheProvider) {
|
|
|
19
20
|
description: 'Search Untappd for venues (bars, breweries, restaurants) by name. Returns matches with their venue id, ' +
|
|
20
21
|
'category, and location. Feed a venue id into untappd_venue_info for full detail. Read-only.',
|
|
21
22
|
annotations: toolAnnotations({ title: 'Search Untappd venues', readOnly: true, idempotent: true, openWorld: true }),
|
|
23
|
+
_meta: foodMeta('lookup', { aliases: ['bar', 'pub', 'taproom', 'brewpub'] }),
|
|
22
24
|
inputSchema: z.object({
|
|
23
25
|
query: z.string().min(1).describe('Venue name to search for'),
|
|
24
26
|
limit: z.number().int().min(1).max(50).optional().describe('Max results (1–50, default 25)'),
|
|
@@ -32,6 +34,7 @@ export function registerVenueTools(server, client, cacheProvider) {
|
|
|
32
34
|
description: 'Get full detail for a venue by its Untappd venue id: category, address, contact, rating, total check-ins, ' +
|
|
33
35
|
'and — on view:"full" — top beers and recent activity. Get an id from untappd_search_venue. Read-only.',
|
|
34
36
|
annotations: toolAnnotations({ title: 'Get Untappd venue detail', readOnly: true, idempotent: true, openWorld: true }),
|
|
37
|
+
_meta: foodMeta('lookup'),
|
|
35
38
|
inputSchema: z.object({
|
|
36
39
|
venue_id: z.number().int().positive().describe('Untappd venue id'),
|
|
37
40
|
view: viewParam(UNTAPPD_VIEWS, { note: 'compact also asks Untappd for its own slim record, dropping the embedded activity/list blocks; "full" returns everything.' }),
|
|
@@ -58,6 +61,7 @@ export function registerVenueTools(server, client, cacheProvider) {
|
|
|
58
61
|
'with untappd_favorite_add; default your account) — applied on read, so never stale. Get an id from ' +
|
|
59
62
|
'untappd_search_venue. Read-only.',
|
|
60
63
|
annotations: toolAnnotations({ title: "Get a venue's verified beer menu (section-paged)", readOnly: true, idempotent: true, openWorld: true }),
|
|
64
|
+
_meta: foodMeta('lookup', { aliases: ['on tap', 'tap list', 'beer menu'] }),
|
|
61
65
|
inputSchema: z.object({
|
|
62
66
|
venue_id: z.number().int().positive().describe('Untappd venue id'),
|
|
63
67
|
menu_id: z.number().int().positive().optional().describe('Restrict to a single menu id (from a prior result). Optional.'),
|
|
@@ -137,6 +141,7 @@ export function registerVenueTools(server, client, cacheProvider) {
|
|
|
137
141
|
description: 'Resolve a Foursquare venue id to its Untappd venue. Useful to turn a foursquare_id (e.g. from a check-in) ' +
|
|
138
142
|
'into an Untappd venue you can pass to untappd_venue_info / untappd_venue_activity. Read-only.',
|
|
139
143
|
annotations: toolAnnotations({ title: 'Look up an Untappd venue by Foursquare id', readOnly: true, idempotent: true, openWorld: true }),
|
|
144
|
+
_meta: foodMeta('lookup'),
|
|
140
145
|
inputSchema: z.object({
|
|
141
146
|
foursquare_id: z.string().min(1).describe('Foursquare venue id'),
|
|
142
147
|
}),
|
|
@@ -150,6 +155,7 @@ export function registerVenueTools(server, client, cacheProvider) {
|
|
|
150
155
|
'Page backwards with max_id (the pagination.max_id from a prior call). Cached locally for ' + TTL_HOURS + ' hours ' +
|
|
151
156
|
'per venue + limit + max_id (cache.hit, fetched_at, expires_at); refresh:true re-fetches. Read-only.',
|
|
152
157
|
annotations: toolAnnotations({ title: 'Get recent check-ins at a venue', readOnly: true, idempotent: false, openWorld: true }),
|
|
158
|
+
_meta: foodMeta('lookup'),
|
|
153
159
|
inputSchema: z.object({
|
|
154
160
|
venue_id: z.number().int().positive().describe('Untappd venue id'),
|
|
155
161
|
limit: z.number().int().min(1).max(50).optional().describe('Max check-ins (1–50, default 25)'),
|