untappd-mcp 1.11.4 → 2.0.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.
@@ -30,7 +30,7 @@ export function registerBeerTools(server, client, cache) {
30
30
  'beer id (bid), brewery, style, ABV, IBU, and global rating. Feed a bid into untappd_beer_info for full ' +
31
31
  'detail. Read-only.',
32
32
  annotations: toolAnnotations({ title: 'Search Untappd beers', readOnly: true, idempotent: true, openWorld: true }),
33
- inputSchema: {
33
+ inputSchema: z.object({
34
34
  query: z.string().min(1).describe('Beer name to search for'),
35
35
  limit: z.number().int().min(1).max(50).optional().describe('Max results (1–50, default 25)'),
36
36
  offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
@@ -39,7 +39,7 @@ export function registerBeerTools(server, client, cache) {
39
39
  .optional()
40
40
  .describe('Sort order: checkin (relevance, default), name, or count'),
41
41
  view: viewParam(UNTAPPD_VIEWS, { note: 'compact projects each match to {bid, name, style, abv, ibu, brewery, checkin_count, have_had}; "full" returns Untappd\'s whole ~1.2 KB search item, including the long beer_description and the nested brewery record.' }),
42
- },
42
+ }),
43
43
  }, async ({ query, limit, offset, sort, view }) => {
44
44
  const data = await client.get('/search/beer', { q: query, limit, offset, sort });
45
45
  const items = data?.beers?.items;
@@ -54,13 +54,13 @@ export function registerBeerTools(server, client, cache) {
54
54
  description: 'Get full detail for a beer by its Untappd beer id (bid): description, style, ABV, IBU, brewery, rating, ' +
55
55
  'total check-in count, and — on view:"full" — recent activity. Get a bid from untappd_search_beer. Read-only.',
56
56
  annotations: toolAnnotations({ title: 'Get Untappd beer detail', readOnly: true, idempotent: true, openWorld: true }),
57
- inputSchema: {
57
+ inputSchema: z.object({
58
58
  bid: BidSchema,
59
59
  // No local projection here: compact is forwarded to Untappd as its OWN
60
60
  // `compact=true`, which drops the embedded recent-activity block server
61
61
  // side. What comes back is passed through on both rungs.
62
62
  view: viewParam(UNTAPPD_VIEWS, { note: 'compact asks Untappd for its own slim record, dropping the embedded recent-activity (media/check-in) block server side; "full" returns the whole record including that activity. No local projection — the beer fields themselves are identical on both rungs.' }),
63
- },
63
+ }),
64
64
  }, async ({ bid, view }) => {
65
65
  const v = resolveView(view, UNTAPPD_VIEWS);
66
66
  const data = await client.get(`/beer/info/${bid}`, { compact: upstreamCompact(v) });
@@ -72,12 +72,12 @@ export function registerBeerTools(server, client, cache) {
72
72
  description: 'Get the recent public check-ins for a beer by its bid — who drank it, their rating, comment, and venue. ' +
73
73
  'Page backwards with max_id (the pagination.max_id from a prior call). Read-only.',
74
74
  annotations: toolAnnotations({ title: 'Get recent check-ins for a beer', readOnly: true, idempotent: false, openWorld: true }),
75
- inputSchema: {
75
+ inputSchema: z.object({
76
76
  bid: BidSchema,
77
77
  limit: z.number().int().min(1).max(50).optional().describe('Max check-ins (1–50, default 25)'),
78
78
  max_id: z.number().int().positive().optional().describe('Return check-ins older than this id (for paging)'),
79
79
  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.' }),
80
- },
80
+ }),
81
81
  }, async ({ bid, limit, max_id, view }) => {
82
82
  const data = await client.get(`/beer/checkins/${bid}`, { limit, max_id });
83
83
  const v = resolveView(view, UNTAPPD_VIEWS);
@@ -7,11 +7,11 @@ export function registerBreweryTools(server, client) {
7
7
  description: 'Search Untappd for breweries by name. Returns matches with their brewery id, location, type, and beer ' +
8
8
  'count. Feed a brewery id into untappd_brewery_info for full detail. Read-only.',
9
9
  annotations: toolAnnotations({ title: 'Search Untappd breweries', readOnly: true, idempotent: true, openWorld: true }),
10
- inputSchema: {
10
+ inputSchema: z.object({
11
11
  query: z.string().min(1).describe('Brewery name to search for'),
12
12
  limit: z.number().int().min(1).max(50).optional().describe('Max results (1–50, default 25)'),
13
13
  offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
14
- },
14
+ }),
15
15
  }, async ({ query, limit, offset }) => {
16
16
  const data = await client.get('/search/brewery', { q: query, limit, offset });
17
17
  return minifiedResult(data);
@@ -21,10 +21,10 @@ export function registerBreweryTools(server, client) {
21
21
  description: 'Get full detail for a brewery by its Untappd brewery id: description, location, type, rating, total ' +
22
22
  'check-ins, and popular beers. Get an id from untappd_search_brewery. Read-only.',
23
23
  annotations: toolAnnotations({ title: 'Get Untappd brewery detail', readOnly: true, idempotent: true, openWorld: true }),
24
- inputSchema: {
24
+ inputSchema: z.object({
25
25
  brewery_id: z.number().int().positive().describe('Untappd brewery id'),
26
26
  view: viewParam(UNTAPPD_VIEWS, { note: 'compact also asks Untappd for its own slim record, dropping the embedded activity/list blocks; "full" returns everything.' }),
27
- },
27
+ }),
28
28
  }, async ({ brewery_id, view }) => {
29
29
  const v = resolveView(view, UNTAPPD_VIEWS);
30
30
  const data = await client.get(`/brewery/info/${brewery_id}`, { compact: upstreamCompact(v) });
@@ -35,7 +35,7 @@ export function registerBreweryTools(server, client) {
35
35
  description: 'Get the beers a brewery makes, by brewery id, with per-beer rating and check-in counts. Supports sorting ' +
36
36
  'and paging. Get an id from untappd_search_brewery. Read-only.',
37
37
  annotations: toolAnnotations({ title: "Get a brewery's beer list", readOnly: true, idempotent: true, openWorld: true }),
38
- inputSchema: {
38
+ inputSchema: z.object({
39
39
  brewery_id: z.number().int().positive().describe('Untappd brewery id'),
40
40
  limit: z.number().int().min(1).max(50).optional().describe('Max beers (1–50, default 25)'),
41
41
  offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
@@ -46,7 +46,7 @@ export function registerBreweryTools(server, client) {
46
46
  view: viewParam(UNTAPPD_VIEWS, {
47
47
  note: 'compact keeps each beer\'s identity, rating and counts and drops the description, label URLs and the copy of this brewery repeated on every row; "full" returns Untappd\'s whole page.',
48
48
  }),
49
- },
49
+ }),
50
50
  },
51
51
  // Unlike its `/brewery/info` sibling above, this rung is NOT forwarded to
52
52
  // Untappd: `/brewery/beer_list/` is not one of the endpoints documented to
@@ -93,7 +93,7 @@ export function registerCacheTools(server, client, cacheProvider) {
93
93
  'total_checkins is cached. Pass force_backfill: true to reset a cache wrongly marked complete and re-page ' +
94
94
  'the whole history (cached rows are kept). Omit username for your own account.',
95
95
  annotations: toolAnnotations({ title: "Sync a user's check-ins into the cache", readOnly: false, idempotent: false, openWorld: true }),
96
- inputSchema: {
96
+ inputSchema: z.object({
97
97
  username: UsernameArg,
98
98
  max_pages: z
99
99
  .number()
@@ -106,7 +106,7 @@ export function registerCacheTools(server, client, cacheProvider) {
106
106
  .boolean()
107
107
  .optional()
108
108
  .describe('Reset the sync state (clear backfill_complete + cursors) but KEEP cached rows, then re-page the whole history from newest. Use to recover a cache wrongly marked complete.'),
109
- },
109
+ }),
110
110
  }, async ({ username, max_pages, force_backfill }) => {
111
111
  const user = resolveUser(username, client.loginName);
112
112
  const summary = await syncCheckins(client, cacheProvider(), user, {
@@ -124,7 +124,7 @@ export function registerCacheTools(server, client, cacheProvider) {
124
124
  'per call and persists progress; run again until another_run_needed is false. Feeds the same ' +
125
125
  'untappd_cache_has_had / has_had_many / not_had tools. Omit username for your own account.',
126
126
  annotations: toolAnnotations({ title: "Sync a user's complete distinct-beers list into the cache", readOnly: false, idempotent: false, openWorld: true }),
127
- inputSchema: {
127
+ inputSchema: z.object({
128
128
  username: UsernameArg,
129
129
  max_pages: z
130
130
  .number()
@@ -133,7 +133,7 @@ export function registerCacheTools(server, client, cacheProvider) {
133
133
  .max(50)
134
134
  .optional()
135
135
  .describe('Pages (50 beers each) to fetch this run (default 10). Keep modest to respect the ~100 calls/hour rate limit.'),
136
- },
136
+ }),
137
137
  }, async ({ username, max_pages }) => {
138
138
  const user = resolveUser(username, client.loginName);
139
139
  const summary = await syncUserBeers(client, cacheProvider(), user, max_pages ?? 10);
@@ -147,11 +147,11 @@ export function registerCacheTools(server, client, cacheProvider) {
147
147
  'last date, which sources matched, and any detailed check-ins. Reports per-source freshness so you can ' +
148
148
  'caveat incomplete data. Requires bid or beer_name. Run untappd_sync_user_beers first for full coverage.',
149
149
  annotations: toolAnnotations({ title: 'Check if a user has had a beer (from the cache)', readOnly: true, idempotent: true, openWorld: false }),
150
- inputSchema: {
150
+ inputSchema: z.object({
151
151
  username: UsernameArg,
152
152
  bid: z.number().int().positive().optional().describe('Exact Untappd beer id to look for'),
153
153
  beer_name: z.string().min(1).optional().describe('Case-insensitive substring match on the beer name'),
154
- },
154
+ }),
155
155
  }, async ({ username, bid, beer_name }) => {
156
156
  if (bid === undefined && beer_name === undefined) {
157
157
  throw createHelpfulError('Provide either bid or beer_name.', { hint: 'bid is exact; beer_name is a case-insensitive substring match.' });
@@ -168,10 +168,10 @@ export function registerCacheTools(server, client, cacheProvider) {
168
168
  'for checking a whole venue menu at once. Run untappd_sync_user_beers first; the freshness block flags if ' +
169
169
  'coverage is incomplete.',
170
170
  annotations: toolAnnotations({ title: 'Batch-check many beers against the cache', readOnly: true, idempotent: true, openWorld: false }),
171
- inputSchema: {
171
+ inputSchema: z.object({
172
172
  username: UsernameArg,
173
173
  bids: z.array(z.number().int().positive()).min(1).max(500).describe('Beer ids to check (1–500)'),
174
- },
174
+ }),
175
175
  }, async ({ username, bids }) => {
176
176
  const user = resolveUser(username, client.loginName);
177
177
  const cache = cacheProvider();
@@ -204,10 +204,10 @@ export function registerCacheTools(server, client, cacheProvider) {
204
204
  'untappd_sync_user_beers first; if coverage is incomplete the freshness caveat flags that a "not had" may be ' +
205
205
  'a false negative.',
206
206
  annotations: toolAnnotations({ title: 'From a list of beers, return the ones a user has NOT had', readOnly: true, idempotent: true, openWorld: false }),
207
- inputSchema: {
207
+ inputSchema: z.object({
208
208
  username: UsernameArg,
209
209
  bids: z.array(z.number().int().positive()).min(1).max(500).describe('Candidate beer ids to filter (1–500)'),
210
- },
210
+ }),
211
211
  }, async ({ username, bids }) => {
212
212
  const user = resolveUser(username, client.loginName);
213
213
  const cache = cacheProvider();
@@ -240,7 +240,7 @@ export function registerCacheTools(server, client, cacheProvider) {
240
240
  'for non-self accounts is only the recent window; for full coverage of which beers a user has had, use ' +
241
241
  'untappd_cache_has_had / not_had instead. Run untappd_sync_checkins first.',
242
242
  annotations: toolAnnotations({ title: 'Query cached check-ins with filters', readOnly: true, idempotent: true, openWorld: false }),
243
- inputSchema: {
243
+ inputSchema: z.object({
244
244
  username: UsernameArg,
245
245
  brewery_id: z.number().int().positive().optional().describe('Exact brewery id'),
246
246
  brewery: z.string().min(1).optional().describe('Case-insensitive substring match on brewery name'),
@@ -255,7 +255,7 @@ export function registerCacheTools(server, client, cacheProvider) {
255
255
  .optional()
256
256
  .describe('Sort order (default recent first)'),
257
257
  limit: z.number().int().min(1).max(200).optional().describe('Max rows (1–200, default 25)'),
258
- },
258
+ }),
259
259
  }, async ({ username, ...filters }) => {
260
260
  const user = resolveUser(username, client.loginName);
261
261
  const cache = cacheProvider();
@@ -272,7 +272,7 @@ export function registerCacheTools(server, client, cacheProvider) {
272
272
  're-running fills the rest. Reports the same freshness/caveat block as untappd_cache_not_had. Omit username ' +
273
273
  'for your own account.',
274
274
  annotations: toolAnnotations({ title: 'Top-rated beers a user has NOT had, from a candidate list', readOnly: false, idempotent: false, openWorld: true }),
275
- inputSchema: {
275
+ inputSchema: z.object({
276
276
  username: UsernameArg,
277
277
  bids: z.array(z.number().int().positive()).min(1).max(100).describe('Candidate beer ids (1–100)'),
278
278
  top_n: z.number().int().min(1).max(10).optional().describe('How many top beers to return (default 2, max 10)'),
@@ -288,7 +288,7 @@ export function registerCacheTools(server, client, cacheProvider) {
288
288
  .max(100)
289
289
  .optional()
290
290
  .describe('Max beer/info API calls this run for uncached/stale metadata (default 25). Keep modest to respect the ~100 calls/hour rate limit.'),
291
- },
291
+ }),
292
292
  }, async ({ username, bids, top_n, style, api_budget }) => {
293
293
  const user = resolveUser(username, client.loginName);
294
294
  const cache = cacheProvider();
@@ -29,10 +29,10 @@ export function registerCheckinTools(server, client) {
29
29
  'already toasted removes the toast. Without confirm: true it returns a dry-run preview and makes NO network ' +
30
30
  'call; with confirm: true it posts. Writes to your Untappd account and is visible to others.',
31
31
  annotations: toolAnnotations({ title: 'Toast an Untappd check-in', readOnly: false, idempotent: false, openWorld: true }),
32
- inputSchema: {
32
+ inputSchema: z.object({
33
33
  checkin_id: CheckinIdSchema,
34
34
  confirm: schemaConfirm,
35
- },
35
+ }),
36
36
  }, async ({ checkin_id, confirm }) => {
37
37
  if (confirm !== true) {
38
38
  return minifiedResult({
@@ -50,11 +50,11 @@ export function registerCheckinTools(server, client) {
50
50
  description: 'Post a comment on a check-in from YOUR account. Without confirm: true it returns a dry-run preview and ' +
51
51
  'makes NO network call; with confirm: true it posts. Writes to your Untappd account and is visible to others.',
52
52
  annotations: toolAnnotations({ title: 'Comment on an Untappd check-in', readOnly: false, idempotent: false, openWorld: true }),
53
- inputSchema: {
53
+ inputSchema: z.object({
54
54
  checkin_id: CheckinIdSchema,
55
55
  comment: z.string().min(1).max(2000).describe('Comment text to post'),
56
56
  confirm: schemaConfirm,
57
- },
57
+ }),
58
58
  }, async ({ checkin_id, comment, confirm }) => {
59
59
  if (confirm !== true) {
60
60
  return minifiedResult({
@@ -73,10 +73,10 @@ export function registerCheckinTools(server, client) {
73
73
  description: 'Delete one of YOUR comments by its comment id (the id from a check-in\'s comments list). Without ' +
74
74
  'confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it deletes.',
75
75
  annotations: toolAnnotations({ title: 'Delete a comment from an Untappd check-in', readOnly: false, idempotent: true, openWorld: true }),
76
- inputSchema: {
76
+ inputSchema: z.object({
77
77
  comment_id: z.number().int().positive().describe('Untappd comment id (from a check-in\'s comments.items)'),
78
78
  confirm: schemaConfirm,
79
- },
79
+ }),
80
80
  }, async ({ comment_id, confirm }) => {
81
81
  if (confirm !== true) {
82
82
  return minifiedResult({
@@ -94,10 +94,10 @@ export function registerCheckinTools(server, client) {
94
94
  description: 'Permanently delete one of YOUR check-ins by its id. This is destructive and cannot be undone. Without ' +
95
95
  'confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it deletes.',
96
96
  annotations: toolAnnotations({ title: 'Delete an Untappd check-in', readOnly: false, idempotent: true, openWorld: true }),
97
- inputSchema: {
97
+ inputSchema: z.object({
98
98
  checkin_id: CheckinIdSchema,
99
99
  confirm: schemaConfirm,
100
- },
100
+ }),
101
101
  }, async ({ checkin_id, confirm }) => {
102
102
  if (confirm !== true) {
103
103
  return minifiedResult({
@@ -117,7 +117,7 @@ export function registerCheckinTools(server, client) {
117
117
  'foursquare_id, and a local photo via photo_path (JPEG/PNG). Without confirm: true it returns a dry-run ' +
118
118
  'preview of the exact fields and makes NO network call; with confirm: true it posts.',
119
119
  annotations: toolAnnotations({ title: 'Check in a beer on Untappd', readOnly: false, idempotent: false, openWorld: true }),
120
- inputSchema: {
120
+ inputSchema: z.object({
121
121
  bid: z.number().int().positive().describe('Untappd beer id to check in (from untappd_search_beer)'),
122
122
  rating: RatingSchema.optional().describe('Rating 0–5 in 0.25 increments (omit for no rating)'),
123
123
  shout: z.string().max(2000).optional().describe('Optional shout / comment text for the check-in'),
@@ -131,7 +131,7 @@ export function registerCheckinTools(server, client) {
131
131
  .optional()
132
132
  .describe('Optional serving container id (e.g. 1 = draft, 2 = bottle, 3 = can)'),
133
133
  confirm: schemaConfirm,
134
- },
134
+ }),
135
135
  }, async ({ bid, rating, shout, foursquare_id, photo_path, geolat, geolng, container_id, confirm }) => {
136
136
  const { timezone, gmt_offset } = localTimezone();
137
137
  let ext;
@@ -7,7 +7,7 @@ export function registerDiscoverTools(server, client) {
7
7
  description: 'Get the beers trending on Untappd right now, split into macro (big/widely-available) and micro ' +
8
8
  '(craft/independent) lists. Read-only.',
9
9
  annotations: toolAnnotations({ title: 'Get trending beers', readOnly: true, idempotent: false, openWorld: true }),
10
- inputSchema: {},
10
+ inputSchema: z.object({}),
11
11
  }, async () => {
12
12
  const data = await client.get('/beer/trending');
13
13
  return minifiedResult(data);
@@ -17,10 +17,10 @@ export function registerDiscoverTools(server, client) {
17
17
  description: 'Get your Untappd notifications — toasts, comments, friend requests, and badges earned on YOUR account, ' +
18
18
  'plus news items. Read-only.',
19
19
  annotations: toolAnnotations({ title: 'Get your Untappd notifications', readOnly: true, idempotent: false, openWorld: true }),
20
- inputSchema: {
20
+ inputSchema: z.object({
21
21
  limit: z.number().int().min(1).max(50).optional().describe('Max notifications (1–50, default 25)'),
22
22
  offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
23
- },
23
+ }),
24
24
  }, async ({ limit, offset }) => {
25
25
  const data = await client.get('/notifications', { limit, offset });
26
26
  return minifiedResult(data);
@@ -30,13 +30,13 @@ export function registerDiscoverTools(server, client) {
30
30
  description: 'Get recent check-ins near a location (lat/lng) — what people are drinking nearby right now. Optionally ' +
31
31
  'widen the search radius. Read-only.',
32
32
  annotations: toolAnnotations({ title: 'Get nearby Untappd check-ins', readOnly: true, idempotent: false, openWorld: true }),
33
- inputSchema: {
33
+ inputSchema: z.object({
34
34
  lat: z.number().min(-90).max(90).describe('Latitude of the location'),
35
35
  lng: z.number().min(-180).max(180).describe('Longitude of the location'),
36
36
  limit: z.number().int().min(1).max(50).optional().describe('Max check-ins (1–50, default 25)'),
37
37
  radius: z.number().int().min(1).max(25).optional().describe('Search radius (default per Untappd)'),
38
38
  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.' }),
39
- },
39
+ }),
40
40
  }, async ({ lat, lng, limit, radius, view }) => {
41
41
  const data = await client.get('/thepub/local', { lat, lng, limit, radius });
42
42
  const v = resolveView(view, UNTAPPD_VIEWS);
@@ -7,11 +7,11 @@ export function registerFeedTools(server, client) {
7
7
  description: 'Get your Untappd friend activity feed — the recent check-ins from people you follow, newest first. Page ' +
8
8
  'backwards with max_id (the pagination.max_id from a prior call). Read-only.',
9
9
  annotations: toolAnnotations({ title: 'Get Untappd friend activity feed', readOnly: true, idempotent: false, openWorld: true }),
10
- inputSchema: {
10
+ inputSchema: z.object({
11
11
  limit: z.number().int().min(1).max(50).optional().describe('Max check-ins (1–50, default 25)'),
12
12
  max_id: z.number().int().positive().optional().describe('Return check-ins older than this id (for paging)'),
13
13
  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.' }),
14
- },
14
+ }),
15
15
  }, async ({ limit, max_id, view }) => {
16
16
  const data = await client.get('/checkin/recent', { limit, max_id });
17
17
  const v = resolveView(view, UNTAPPD_VIEWS);
@@ -22,9 +22,9 @@ export function registerFeedTools(server, client) {
22
22
  description: 'Get full detail for a single check-in by its id: the beer, rating, comment, photos, venue, badges earned, ' +
23
23
  'toasts, and comments. Get a check-in id from a feed or user-checkins result. Read-only.',
24
24
  annotations: toolAnnotations({ title: 'Get Untappd check-in detail', readOnly: true, idempotent: true, openWorld: true }),
25
- inputSchema: {
25
+ inputSchema: z.object({
26
26
  checkin_id: z.number().int().positive().describe('Untappd check-in id'),
27
- },
27
+ }),
28
28
  }, async ({ checkin_id }) => {
29
29
  const data = await client.get(`/checkin/view/${checkin_id}`);
30
30
  return minifiedResult(data);
@@ -51,10 +51,10 @@ export function registerFriendActionTools(server, client) {
51
51
  'performs the action. Note: this endpoint path is taken from the Untappd app but is not otherwise ' +
52
52
  'independently verified.',
53
53
  annotations: toolAnnotations({ title: action.title, readOnly: false, idempotent: true, openWorld: true }),
54
- inputSchema: {
54
+ inputSchema: z.object({
55
55
  target_uid: TargetUidSchema,
56
56
  confirm: schemaConfirm,
57
- },
57
+ }),
58
58
  }, async ({ target_uid, confirm }) => {
59
59
  if (confirm !== true) {
60
60
  return minifiedResult({
@@ -49,9 +49,9 @@ export function registerResolveTools(server, client) {
49
49
  description: 'Parse an untappd.com URL (a beer /b/, brewery /w/, venue /v/, user /user/, or check-in link) into its ' +
50
50
  'entity type and id, and name the tool to call next. Pure local parsing — no network. Read-only.',
51
51
  annotations: toolAnnotations({ title: 'Resolve an Untappd URL', readOnly: true, idempotent: true, openWorld: false }),
52
- inputSchema: {
52
+ inputSchema: z.object({
53
53
  url: z.string().min(1).describe('An untappd.com URL to resolve'),
54
- },
54
+ }),
55
55
  }, async ({ url }) => {
56
56
  const resolved = parseUntappdUrl(url);
57
57
  if (!resolved) {
@@ -66,9 +66,9 @@ export function registerResolveTools(server, client) {
66
66
  description: 'Resolve an untappd.com URL AND fetch the entity detail in one call — the convenience combination of ' +
67
67
  'untappd_resolve + the matching info tool. Returns { resolved, detail }. Read-only.',
68
68
  annotations: toolAnnotations({ title: 'Open an Untappd URL', readOnly: true, idempotent: true, openWorld: true }),
69
- inputSchema: {
69
+ inputSchema: z.object({
70
70
  url: z.string().min(1).describe('An untappd.com URL to resolve and fetch'),
71
- },
71
+ }),
72
72
  }, async ({ url }) => {
73
73
  const resolved = parseUntappdUrl(url);
74
74
  if (!resolved) {
@@ -22,10 +22,10 @@ export function registerUserTools(server, client) {
22
22
  description: "Get an Untappd user's profile: bio, location, total check-ins, distinct beers, badges, and stats. " +
23
23
  'Omit username for your own account. Read-only.',
24
24
  annotations: toolAnnotations({ title: 'Get Untappd user profile', readOnly: true, idempotent: true, openWorld: true }),
25
- inputSchema: {
25
+ inputSchema: z.object({
26
26
  username: UsernameArg,
27
27
  view: viewParam(UNTAPPD_VIEWS, { note: 'compact also asks Untappd for its own slim record, dropping the embedded activity/list blocks; \'full\' returns everything.' }),
28
- },
28
+ }),
29
29
  }, async ({ username, view }) => {
30
30
  const v = resolveView(view, UNTAPPD_VIEWS);
31
31
  const data = await client.get(`/user/info/${resolveUser(username, client.loginName)}`, { compact: upstreamCompact(v) });
@@ -36,12 +36,12 @@ export function registerUserTools(server, client) {
36
36
  description: "Get a user's recent check-ins (most recent first): the beer, rating, comment, venue, and toasts/comments. " +
37
37
  'Page backwards with max_id (the pagination.max_id from a prior call). Omit username for your own. Read-only.',
38
38
  annotations: toolAnnotations({ title: 'Get Untappd user check-ins', readOnly: true, idempotent: true, openWorld: true }),
39
- inputSchema: {
39
+ inputSchema: z.object({
40
40
  username: UsernameArg,
41
41
  limit: z.number().int().min(1).max(50).optional().describe('Max check-ins (1–50, default 25)'),
42
42
  max_id: z.number().int().positive().optional().describe('Return check-ins older than this id (for paging)'),
43
43
  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.' }),
44
- },
44
+ }),
45
45
  }, async ({ username, limit, max_id, view }) => {
46
46
  const data = await client.get(`/user/checkins/${resolveUser(username, client.loginName)}`, { limit, max_id });
47
47
  const v = resolveView(view, UNTAPPD_VIEWS);
@@ -51,7 +51,7 @@ export function registerUserTools(server, client) {
51
51
  title: 'Get Untappd user wishlist',
52
52
  description: "Get the beers on a user's wishlist. Supports sorting and paging. Omit username for your own account. Read-only.",
53
53
  annotations: toolAnnotations({ title: 'Get Untappd user wishlist', readOnly: true, idempotent: true, openWorld: true }),
54
- inputSchema: {
54
+ inputSchema: z.object({
55
55
  username: UsernameArg,
56
56
  limit: z.number().int().min(1).max(50).optional().describe('Max beers (1–50, default 25)'),
57
57
  offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
@@ -60,7 +60,7 @@ export function registerUserTools(server, client) {
60
60
  .optional()
61
61
  .describe('Sort order (default date added, newest first)'),
62
62
  view: viewParam(UNTAPPD_VIEWS, { note: 'compact projects each wishlisted beer to {bid, name, style, abv, ibu, brewery, added_at}; "full" returns Untappd\'s whole ~1.2 KB beer record per entry, including the long beer_description and the nested brewery record.' }),
63
- },
63
+ }),
64
64
  }, async ({ username, limit, offset, sort, view }) => {
65
65
  const data = await client.get(`/user/wishlist/${resolveUser(username, client.loginName)}`, { limit, offset, sort });
66
66
  const v = resolveView(view, UNTAPPD_VIEWS);
@@ -71,7 +71,7 @@ export function registerUserTools(server, client) {
71
71
  description: "Get the distinct (unique) beers a user has ever checked in, with their rating and check-in count per beer. " +
72
72
  'Supports sorting and paging. Omit username for your own account. Read-only.',
73
73
  annotations: toolAnnotations({ title: 'Get Untappd distinct beers', readOnly: true, idempotent: true, openWorld: true }),
74
- inputSchema: {
74
+ inputSchema: z.object({
75
75
  username: UsernameArg,
76
76
  limit: z.number().int().min(1).max(50).optional().describe('Max beers (1–50, default 25)'),
77
77
  offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
@@ -80,7 +80,7 @@ export function registerUserTools(server, client) {
80
80
  .optional()
81
81
  .describe('Sort order (default date, most recent first)'),
82
82
  view: viewParam(UNTAPPD_VIEWS, { note: 'compact projects each distinct beer to {bid, name, style, abv, ibu, brewery, your_count, your_rating, global_rating, last_had}; "full" returns Untappd\'s whole ~1.2 KB beer record per entry, including the long beer_description and the nested brewery record.' }),
83
- },
83
+ }),
84
84
  }, async ({ username, limit, offset, sort, view }) => {
85
85
  const data = await client.get(`/user/beers/${resolveUser(username, client.loginName)}`, { limit, offset, sort });
86
86
  const v = resolveView(view, UNTAPPD_VIEWS);
@@ -90,11 +90,11 @@ export function registerUserTools(server, client) {
90
90
  title: 'Get Untappd user badges',
91
91
  description: "Get the badges a user has earned, most recent first. Omit username for your own account. Read-only.",
92
92
  annotations: toolAnnotations({ title: 'Get Untappd user badges', readOnly: true, idempotent: true, openWorld: true }),
93
- inputSchema: {
93
+ inputSchema: z.object({
94
94
  username: UsernameArg,
95
95
  limit: z.number().int().min(1).max(50).optional().describe('Max badges (1–50, default 25)'),
96
96
  offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
97
- },
97
+ }),
98
98
  }, async ({ username, limit, offset }) => {
99
99
  const data = await client.get(`/user/badges/${resolveUser(username, client.loginName)}`, { limit, offset });
100
100
  return minifiedResult(data);
@@ -103,11 +103,11 @@ export function registerUserTools(server, client) {
103
103
  title: 'Get Untappd user friends',
104
104
  description: "Get a user's friend list. Omit username for your own account. Read-only.",
105
105
  annotations: toolAnnotations({ title: 'Get Untappd user friends', readOnly: true, idempotent: true, openWorld: true }),
106
- inputSchema: {
106
+ inputSchema: z.object({
107
107
  username: UsernameArg,
108
108
  limit: z.number().int().min(1).max(50).optional().describe('Max friends (1–50, default 25)'),
109
109
  offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
110
- },
110
+ }),
111
111
  }, async ({ username, limit, offset }) => {
112
112
  const data = await client.get(`/user/friends/${resolveUser(username, client.loginName)}`, { limit, offset });
113
113
  return minifiedResult(data);
@@ -117,7 +117,7 @@ export function registerUserTools(server, client) {
117
117
  description: "Get the venues a user has checked in at, most recent first, with per-venue check-in counts. Supports " +
118
118
  'sorting and paging. Omit username for your own account. Read-only.',
119
119
  annotations: toolAnnotations({ title: 'Get venues a user has checked in at', readOnly: true, idempotent: true, openWorld: true }),
120
- inputSchema: {
120
+ inputSchema: z.object({
121
121
  username: UsernameArg,
122
122
  limit: z.number().int().min(1).max(50).optional().describe('Max venues (1–50, default 25)'),
123
123
  offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
@@ -125,7 +125,7 @@ export function registerUserTools(server, client) {
125
125
  .enum(['date', 'name', 'checkin', 'highest_rated'])
126
126
  .optional()
127
127
  .describe('Sort order (default most recent)'),
128
- },
128
+ }),
129
129
  }, async ({ username, limit, offset, sort }) => {
130
130
  const data = await client.get(`/user/venues/${resolveUser(username, client.loginName)}`, { limit, offset, sort });
131
131
  return minifiedResult(data);
@@ -135,10 +135,10 @@ export function registerUserTools(server, client) {
135
135
  description: 'Get the incoming friend requests waiting on YOUR account — the users who have requested to be your ' +
136
136
  'friend. Read-only.',
137
137
  annotations: toolAnnotations({ title: 'Get your pending friend requests', readOnly: true, idempotent: true, openWorld: true }),
138
- inputSchema: {
138
+ inputSchema: z.object({
139
139
  limit: z.number().int().min(1).max(50).optional().describe('Max requests (1–50, default 25)'),
140
140
  offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
141
- },
141
+ }),
142
142
  }, async ({ limit, offset }) => {
143
143
  const data = await client.get('/user/pending', { limit, offset });
144
144
  return minifiedResult(data);
@@ -1,3 +1,4 @@
1
+ import { z } from 'zod';
1
2
  import { minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
2
3
  import { VERSION } from '../version.js';
3
4
  import { toolInventory } from './diagnostics.js';
@@ -9,7 +10,7 @@ export function registerUtilityTools(server, client) {
9
10
  'and the exact set of tools this build exposes (count, a stable hash, and their names) so you can confirm ' +
10
11
  'which build is live. Read-only.',
11
12
  annotations: toolAnnotations({ title: 'Untappd healthcheck', readOnly: true, idempotent: true, openWorld: true }),
12
- inputSchema: {},
13
+ inputSchema: z.object({}),
13
14
  }, async () => {
14
15
  // Always report build/tool diagnostics, even when credentials are absent,
15
16
  // so the live build and its toolset can be confirmed from the client.
@@ -7,10 +7,10 @@ export function registerVenueTools(server, client) {
7
7
  description: 'Search Untappd for venues (bars, breweries, restaurants) by name. Returns matches with their venue id, ' +
8
8
  'category, and location. Feed a venue id into untappd_venue_info for full detail. Read-only.',
9
9
  annotations: toolAnnotations({ title: 'Search Untappd venues', readOnly: true, idempotent: true, openWorld: true }),
10
- inputSchema: {
10
+ inputSchema: z.object({
11
11
  query: z.string().min(1).describe('Venue name to search for'),
12
12
  limit: z.number().int().min(1).max(50).optional().describe('Max results (1–50, default 25)'),
13
- },
13
+ }),
14
14
  }, async ({ query, limit }) => {
15
15
  const data = await client.get('/search/venue', { q: query, limit });
16
16
  return minifiedResult(data);
@@ -20,10 +20,10 @@ export function registerVenueTools(server, client) {
20
20
  description: 'Get full detail for a venue by its Untappd venue id: category, address, contact, rating, total check-ins, ' +
21
21
  'and — on view:"full" — top beers and recent activity. Get an id from untappd_search_venue. Read-only.',
22
22
  annotations: toolAnnotations({ title: 'Get Untappd venue detail', readOnly: true, idempotent: true, openWorld: true }),
23
- inputSchema: {
23
+ inputSchema: z.object({
24
24
  venue_id: z.number().int().positive().describe('Untappd venue id'),
25
25
  view: viewParam(UNTAPPD_VIEWS, { note: 'compact also asks Untappd for its own slim record, dropping the embedded activity/list blocks; "full" returns everything.' }),
26
- },
26
+ }),
27
27
  }, async ({ venue_id, view }) => {
28
28
  const v = resolveView(view, UNTAPPD_VIEWS);
29
29
  const data = await client.get(`/venue/info/${venue_id}`, { compact: upstreamCompact(v) });
@@ -41,14 +41,14 @@ export function registerVenueTools(server, client) {
41
41
  'to pass back on the next call. truncated:true means the upstream returned no more sections short of total_count ' +
42
42
  '(e.g. it ignored the paging params) — not resumable. Get an id from untappd_search_venue. Read-only.',
43
43
  annotations: toolAnnotations({ title: "Get a venue's verified beer menu (section-paged)", readOnly: true, idempotent: true, openWorld: true }),
44
- inputSchema: {
44
+ inputSchema: z.object({
45
45
  venue_id: z.number().int().positive().describe('Untappd venue id'),
46
46
  menu_id: z.number().int().positive().optional().describe('Restrict to a single menu id (from a prior result). Optional.'),
47
47
  section_limit: z.number().int().min(1).max(50).optional().describe('Sections fetched per API call — page size (default 50).'),
48
48
  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
49
  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
50
  sort: z.string().optional().describe("Menu sort key (e.g. 'publish_order', 'highest_rated'). Optional."),
51
- },
51
+ }),
52
52
  }, async ({ venue_id, menu_id, section_limit, section_offset, max_pages, sort }) => {
53
53
  // venue/info pages its MENUS with limit/offset, but caps each menu's SECTION
54
54
  // list — echoing `section_limit`/`section_offset` back as accepted params
@@ -167,9 +167,9 @@ export function registerVenueTools(server, client) {
167
167
  description: 'Resolve a Foursquare venue id to its Untappd venue. Useful to turn a foursquare_id (e.g. from a check-in) ' +
168
168
  'into an Untappd venue you can pass to untappd_venue_info / untappd_venue_activity. Read-only.',
169
169
  annotations: toolAnnotations({ title: 'Look up an Untappd venue by Foursquare id', readOnly: true, idempotent: true, openWorld: true }),
170
- inputSchema: {
170
+ inputSchema: z.object({
171
171
  foursquare_id: z.string().min(1).describe('Foursquare venue id'),
172
- },
172
+ }),
173
173
  }, async ({ foursquare_id }) => {
174
174
  const data = await client.get(`/venue/foursquare_lookup/${encodeURIComponent(foursquare_id)}`);
175
175
  return minifiedResult(data);
@@ -179,12 +179,12 @@ export function registerVenueTools(server, client) {
179
179
  description: 'Get the recent public check-ins at a venue by its id — who was there, what they drank, and their ratings. ' +
180
180
  'Page backwards with max_id (the pagination.max_id from a prior call). Read-only.',
181
181
  annotations: toolAnnotations({ title: 'Get recent check-ins at a venue', readOnly: true, idempotent: false, openWorld: true }),
182
- inputSchema: {
182
+ inputSchema: z.object({
183
183
  venue_id: z.number().int().positive().describe('Untappd venue id'),
184
184
  limit: z.number().int().min(1).max(50).optional().describe('Max check-ins (1–50, default 25)'),
185
185
  max_id: z.number().int().positive().optional().describe('Return check-ins older than this id (for paging)'),
186
186
  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
- },
187
+ }),
188
188
  }, async ({ venue_id, limit, max_id, view }) => {
189
189
  const data = await client.get(`/venue/checkins/${venue_id}`, { limit, max_id });
190
190
  const v = resolveView(view, UNTAPPD_VIEWS);
@@ -7,10 +7,10 @@ export function registerWishlistTools(server, client) {
7
7
  description: 'Add a beer to YOUR Untappd wishlist by its bid. Without confirm: true it returns a dry-run preview and ' +
8
8
  'makes NO network call; with confirm: true it adds. Writes to your account.',
9
9
  annotations: toolAnnotations({ title: 'Add a beer to your wishlist', readOnly: false, idempotent: true, openWorld: true }),
10
- inputSchema: {
10
+ inputSchema: z.object({
11
11
  bid: BidSchema,
12
12
  confirm: schemaConfirm,
13
- },
13
+ }),
14
14
  }, async ({ bid, confirm }) => {
15
15
  if (confirm !== true) {
16
16
  return minifiedResult({
@@ -28,10 +28,10 @@ export function registerWishlistTools(server, client) {
28
28
  description: 'Remove a beer from YOUR Untappd wishlist by its bid. Without confirm: true it returns a dry-run preview ' +
29
29
  'and makes NO network call; with confirm: true it removes. Writes to your account.',
30
30
  annotations: toolAnnotations({ title: 'Remove a beer from your wishlist', readOnly: false, idempotent: true, openWorld: true }),
31
- inputSchema: {
31
+ inputSchema: z.object({
32
32
  bid: BidSchema,
33
33
  confirm: schemaConfirm,
34
- },
34
+ }),
35
35
  }, async ({ bid, confirm }) => {
36
36
  if (confirm !== true) {
37
37
  return minifiedResult({
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 = '1.11.4'; // x-release-please-version
6
+ export const VERSION = '2.0.0'; // x-release-please-version