untappd-mcp 2.5.1 → 2.6.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.
@@ -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)'),
@@ -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)'),
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { foodMeta } from './hints.js';
2
3
  import { confirmTokenParam, minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
3
4
  import { CONFIRM_FLOW, confirmWrite } from './confirm.js';
4
5
  const BidSchema = z.number().int().positive().describe('Untappd beer id (bid) — from untappd_search_beer');
@@ -7,6 +8,7 @@ export function registerWishlistTools(server, client) {
7
8
  title: 'Add a beer to your wishlist',
8
9
  description: `Add a beer to YOUR Untappd wishlist by its bid. Writes to your account. ${CONFIRM_FLOW}`,
9
10
  annotations: toolAnnotations({ title: 'Add a beer to your wishlist', readOnly: false, idempotent: true, openWorld: true, destructive: false }),
11
+ _meta: foodMeta('change', { aliases: ['want to try', 'save a beer'] }),
10
12
  inputSchema: z.object({
11
13
  bid: BidSchema,
12
14
  confirmToken: confirmTokenParam,
@@ -33,6 +35,7 @@ export function registerWishlistTools(server, client) {
33
35
  title: 'Remove a beer from your wishlist',
34
36
  description: `Remove a beer from YOUR Untappd wishlist by its bid. Writes to your account. ${CONFIRM_FLOW}`,
35
37
  annotations: toolAnnotations({ title: 'Remove a beer from your wishlist', readOnly: false, idempotent: true, openWorld: true, destructive: false }),
38
+ _meta: foodMeta('change'),
36
39
  inputSchema: z.object({
37
40
  bid: BidSchema,
38
41
  confirmToken: confirmTokenParam,
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.5.1'; // x-release-please-version
6
+ export const VERSION = '2.6.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "untappd-mcp",
3
- "version": "2.5.1",
3
+ "version": "2.6.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>",
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.5.1",
9
+ "version": "2.6.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "untappd-mcp",
14
- "version": "2.5.1",
14
+ "version": "2.6.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -73,6 +73,12 @@ answer, wrong reason. **The same silence bites the other way**: `compact:
73
73
  false` no longer buys you the full record either. It is dropped identically
74
74
  and you still get compact. `view: "full"` is the only way back.
75
75
 
76
+ `untappd_activity_feed` also takes a third rung, `view: "brief"`: one line per
77
+ check-in, `<user>: <beer> (<brewery>) at <venue> — <ISO date | no date> —
78
+ <rated N | not rated>`, each at most 120 characters. It is the shape a
79
+ context-group host reads aloud (below); use it when you only need to say what
80
+ friends have been drinking.
81
+
76
82
  Compact comes in two tiers here, and they are not the same thing:
77
83
 
78
84
  **A hand-written field projection — 9 tools.**
@@ -245,3 +251,25 @@ owner-only (dir `0700`, file `0600`).
245
251
  Every cache read returns a `freshness` block reporting each source's completeness
246
252
  separately (plus `coverage_complete` and a `caveat` when incomplete), so you can
247
253
  flag a "not found" as possibly a false negative until the relevant sync finishes.
254
+
255
+ ## In context groups
256
+
257
+ On an mcp-host connector this server is a member of the **food** group (every
258
+ tool but `untappd_healthcheck` carries a `_meta.mint` hint with
259
+ `domain: "food"`). Its one canonical read is **`untappd_activity_feed`** for
260
+ **what's new** — the friend feed, answered with `view: "brief"`. It takes no
261
+ subject and no date range: Untappd has no listable set of the person's own
262
+ subjects (no children, properties or accounts to fan out over), so the group
263
+ asks it once, not once per person. Nothing here answers what's due or what's
264
+ on; every other read is a `lookup` (`untappd_notifications` is a non-canonical
265
+ what's-new), and the writes are `send` (toast, comment, friend request),
266
+ `change` (check-in, deletes, wishlist, favourites, friend accept/reject/remove)
267
+ or `other` (the cache syncs and the pickers, which refresh the local cache as
268
+ they read).
269
+
270
+ A compound ask looks like *"what's new with food — any new reservations, and
271
+ what have my friends been drinking?"*: the host answers the Untappd half from
272
+ the feed's brief lines beside the group's other members. This server calls the
273
+ Untappd API directly, so it never answers "needs your browser open"; if it is
274
+ not configured, the group reports it as unable to answer and
275
+ `untappd_healthcheck` says which credential is missing.