untappd-mcp 1.10.1 → 1.11.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for Untappd — beers, breweries, venues, check-ins, wishlists, and your friend feed",
10
- "version": "1.10.1"
10
+ "version": "1.11.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Untappd",
16
16
  "source": "./",
17
17
  "description": "MCP server for Untappd — search beers/breweries/venues, read profiles/check-ins/wishlists, and post check-ins, toasts, and comments",
18
- "version": "1.10.1",
18
+ "version": "1.11.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "untappd-mcp",
3
3
  "displayName": "Untappd",
4
- "version": "1.10.1",
4
+ "version": "1.11.0",
5
5
  "description": "MCP server for Untappd — search beers/breweries/venues, read check-ins and wishlists, and post check-ins, toasts, and comments",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/bundle.js CHANGED
@@ -35675,7 +35675,7 @@ function toolAnnotations(opts = {}) {
35675
35675
  }
35676
35676
 
35677
35677
  // src/version.ts
35678
- var VERSION = "1.10.1";
35678
+ var VERSION = "1.11.0";
35679
35679
 
35680
35680
  // src/client.ts
35681
35681
  import { dirname, join } from "path";
@@ -36005,6 +36005,18 @@ function compactUserBeer(item) {
36005
36005
  last_had: i.recent_created_at
36006
36006
  };
36007
36007
  }
36008
+ function compactBreweryBeer(item) {
36009
+ const i = asDict(item) ?? {};
36010
+ const beer = asDict(i.beer) ?? {};
36011
+ return {
36012
+ ...beerCore(i),
36013
+ rating: beer.rating_score,
36014
+ rating_count: beer.rating_count,
36015
+ checkin_count: i.total_count,
36016
+ have_had: i.has_had,
36017
+ your_count: i.total_user_count
36018
+ };
36019
+ }
36008
36020
  function projectItems(resp, container, mapFn) {
36009
36021
  const r = asDict(resp);
36010
36022
  const box = asDict(r?.[container]);
@@ -36020,6 +36032,12 @@ var compactCheckins = (resp) => projectItems(resp, "checkins", compactCheckin);
36020
36032
  var compactBeerSearch = (resp) => projectItems(resp, "beers", compactBeerResult);
36021
36033
  var compactWishlist = (resp) => projectItems(resp, "beers", compactWishlistBeer);
36022
36034
  var compactUserBeers = (resp) => projectItems(resp, "beers", compactUserBeer);
36035
+ function compactBreweryBeers(resp) {
36036
+ const projected = projectItems(resp, "beers", compactBreweryBeer);
36037
+ if (projected === resp) return resp;
36038
+ const { sorting_options: _dropped, ...rest } = projected;
36039
+ return rest;
36040
+ }
36023
36041
  var UNTAPPD_VIEWS = ["compact", "full"];
36024
36042
  function upstreamCompact(view) {
36025
36043
  return view === "compact" ? "true" : void 0;
@@ -36673,12 +36691,21 @@ function registerBreweryTools(server, client2) {
36673
36691
  brewery_id: external_exports.number().int().positive().describe("Untappd brewery id"),
36674
36692
  limit: external_exports.number().int().min(1).max(50).optional().describe("Max beers (1\u201350, default 25)"),
36675
36693
  offset: external_exports.number().int().min(0).optional().describe("Result offset for paging (default 0)"),
36676
- sort: external_exports.enum(["name", "style", "abv", "rating", "count"]).optional().describe("Sort order (default by popularity)")
36694
+ sort: external_exports.enum(["name", "style", "abv", "rating", "count"]).optional().describe("Sort order (default by popularity)"),
36695
+ view: viewParam(UNTAPPD_VIEWS, {
36696
+ 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.`
36697
+ })
36677
36698
  }
36678
36699
  },
36679
- async ({ brewery_id, limit, offset, sort }) => {
36700
+ // Unlike its `/brewery/info` sibling above, this rung is NOT forwarded to
36701
+ // Untappd: `/brewery/beer_list/` is not one of the endpoints documented to
36702
+ // honour an upstream `compact`, and sending an unverified parameter to see
36703
+ // what happens is not a rung. The projection is entirely ours and is
36704
+ // grounded on a live capture — see `compactBreweryBeer`.
36705
+ async ({ brewery_id, limit, offset, sort, view }) => {
36706
+ const v = resolveView(view, UNTAPPD_VIEWS);
36680
36707
  const data = await client2.get(`/brewery/beer_list/${brewery_id}`, { limit, offset, sort });
36681
- return minifiedResult(data);
36708
+ return viewResult(v, v === "compact" ? compactBreweryBeers(data) : data);
36682
36709
  }
36683
36710
  );
36684
36711
  }
package/dist/compact.js CHANGED
@@ -75,6 +75,36 @@ export function compactUserBeer(item) {
75
75
  last_had: i.recent_created_at,
76
76
  };
77
77
  }
78
+ /**
79
+ * Slim one `/brewery/beer_list/` item.
80
+ *
81
+ * This endpoint needs its OWN projector rather than reusing
82
+ * {@link compactBeerResult}, and the reason is easy to miss: the item shape IS
83
+ * `{beer:{…}, brewery:{…}}`-wrapped, so `beerCore` fits perfectly — but the two
84
+ * fields the search projector adds on top of it are named differently here.
85
+ * `/search/beer` says `checkin_count` / `have_had`; `/brewery/beer_list/` says
86
+ * `total_count` / `has_had`. Reusing the search projector would therefore have
87
+ * emitted a page of records carrying two silent `undefined`s — a beer with no
88
+ * check-ins that you have never had — and `projectItems`' drift guard cannot
89
+ * catch that, because the CONTAINER is exactly where it expects it to be.
90
+ * Established against a live capture of brewery 1142, not by inference.
91
+ *
92
+ * Rating rides along because the tool's own description promises "per-beer
93
+ * rating and check-in counts"; a compact rung that dropped them would
94
+ * contradict the thing the caller was told they were getting.
95
+ */
96
+ export function compactBreweryBeer(item) {
97
+ const i = asDict(item) ?? {};
98
+ const beer = asDict(i.beer) ?? {};
99
+ return {
100
+ ...beerCore(i),
101
+ rating: beer.rating_score,
102
+ rating_count: beer.rating_count,
103
+ checkin_count: i.total_count,
104
+ have_had: i.has_had,
105
+ your_count: i.total_user_count,
106
+ };
107
+ }
78
108
  /**
79
109
  * Project `<container>.items` in a response with `mapFn`, preserving the rest of
80
110
  * the container (pagination etc.). Drift-safe: returns the raw response
@@ -94,6 +124,22 @@ export const compactCheckins = (resp) => projectItems(resp, 'checkins', compactC
94
124
  export const compactBeerSearch = (resp) => projectItems(resp, 'beers', compactBeerResult);
95
125
  export const compactWishlist = (resp) => projectItems(resp, 'beers', compactWishlistBeer);
96
126
  export const compactUserBeers = (resp) => projectItems(resp, 'beers', compactUserBeer);
127
+ /**
128
+ * Project a brewery beer list, and drop `sorting_options` with it.
129
+ *
130
+ * `sorting_options` is 16 `{sort_key, sort_name}` pairs of UI chrome repeated on
131
+ * every page. The tool's `sort` parameter is already a closed enum on its input
132
+ * schema, so the list tells a caller nothing they were not handed with the
133
+ * schema — it is pure duplication of a contract they already hold.
134
+ */
135
+ export function compactBreweryBeers(resp) {
136
+ const projected = projectItems(resp, 'beers', compactBreweryBeer);
137
+ // projectItems returns the SAME object on drift; don't reshape that.
138
+ if (projected === resp)
139
+ return resp;
140
+ const { sorting_options: _dropped, ...rest } = projected;
141
+ return rest;
142
+ }
97
143
  /**
98
144
  * The rungs this server honours (`@chrischall/mcp-utils`' `view` vocabulary,
99
145
  * and `chrischall/workflows` `docs/fleet-conventions.md`, "Response shape").
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { minifiedResult, resolveView, toolAnnotations, viewParam, viewResult } from '@chrischall/mcp-utils';
3
- import { UNTAPPD_VIEWS, upstreamCompact } from '../compact.js';
3
+ import { UNTAPPD_VIEWS, compactBreweryBeers, upstreamCompact } from '../compact.js';
4
4
  export function registerBreweryTools(server, client) {
5
5
  server.registerTool('untappd_search_brewery', {
6
6
  title: 'Search Untappd breweries',
@@ -43,9 +43,19 @@ export function registerBreweryTools(server, client) {
43
43
  .enum(['name', 'style', 'abv', 'rating', 'count'])
44
44
  .optional()
45
45
  .describe('Sort order (default by popularity)'),
46
+ view: viewParam(UNTAPPD_VIEWS, {
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
+ }),
46
49
  },
47
- }, async ({ brewery_id, limit, offset, sort }) => {
50
+ },
51
+ // Unlike its `/brewery/info` sibling above, this rung is NOT forwarded to
52
+ // Untappd: `/brewery/beer_list/` is not one of the endpoints documented to
53
+ // honour an upstream `compact`, and sending an unverified parameter to see
54
+ // what happens is not a rung. The projection is entirely ours and is
55
+ // grounded on a live capture — see `compactBreweryBeer`.
56
+ async ({ brewery_id, limit, offset, sort, view }) => {
57
+ const v = resolveView(view, UNTAPPD_VIEWS);
48
58
  const data = await client.get(`/brewery/beer_list/${brewery_id}`, { limit, offset, sort });
49
- return minifiedResult(data);
59
+ return viewResult(v, v === 'compact' ? compactBreweryBeers(data) : data);
50
60
  });
51
61
  }
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.10.1'; // x-release-please-version
6
+ export const VERSION = '1.11.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "untappd-mcp",
3
- "version": "1.10.1",
3
+ "version": "1.11.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": "1.10.1",
9
+ "version": "1.11.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "untappd-mcp",
14
- "version": "1.10.1",
14
+ "version": "1.11.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -42,7 +42,7 @@ Most user tools default `username` to your configured account when omitted.
42
42
 
43
43
  ## Response shape (`view`)
44
44
 
45
- Twelve of this server's 45 tools take `view: "compact" | "full"`, and
45
+ Thirteen of this server's 45 tools take `view: "compact" | "full"`, and
46
46
  **`compact` is the DEFAULT** — the slim rung is what you get without asking
47
47
  for it.
48
48
 
@@ -59,7 +59,7 @@ and you still get compact. `view: "full"` is the only way back.
59
59
 
60
60
  Compact comes in two tiers here, and they are not the same thing:
61
61
 
62
- **A hand-written field projection — 8 tools.**
62
+ **A hand-written field projection — 9 tools.**
63
63
 
64
64
  - Check-ins (`untappd_activity_feed`, `untappd_user_checkins`,
65
65
  `untappd_beer_activity`, `untappd_venue_activity`,
@@ -73,6 +73,16 @@ Compact comes in two tiers here, and they are not the same thing:
73
73
  - `untappd_user_wishlist`: the same beer core plus `added_at`.
74
74
  - `untappd_user_beers`: the same beer core plus `your_count`, `your_rating`,
75
75
  `global_rating`, `last_had`.
76
+ - `untappd_brewery_beers`: the same beer core plus `rating`, `rating_count`,
77
+ `checkin_count`, `have_had`, `your_count`. It has its **own** projector
78
+ rather than sharing `untappd_search_beer`'s, and the reason is a trap worth
79
+ knowing: the item shape is identically `{beer:{…}}`-wrapped, but this
80
+ endpoint spells the two surrounding fields `total_count` / `has_had` where
81
+ `/search/beer` says `checkin_count` / `have_had`. Sharing the projector
82
+ would return beers with no check-ins that you had never had, and the
83
+ drift guard would not fire — the container was where it was expected. Per
84
+ page it also drops `sorting_options` and the copy of the same brewery
85
+ object repeated on every row.
76
86
 
77
87
  **An upstream request for less — 4 tools.** `untappd_user_info`,
78
88
  `untappd_beer_info`, `untappd_brewery_info` and `untappd_venue_info` forward
@@ -94,7 +104,7 @@ silently manufactured.
94
104
  `full` already IS the upstream response and a third value would silently alias
95
105
  one that exists.
96
106
 
97
- ### The 33 tools without `view`
107
+ ### The 32 tools without `view`
98
108
 
99
109
  Each for its own reason — and none of them will tell you it ignored the
100
110
  parameter, because an undeclared key is dropped by zod without a warning:
@@ -119,15 +129,16 @@ parameter, because an undeclared key is dropped by zod without a warning:
119
129
  second projection on top of it would fight the first.
120
130
  - **`untappd_resolve`, `untappd_open_url`, `untappd_healthcheck`** return a
121
131
  verdict or a diagnostic.
122
- - **The remaining ten reads** — `untappd_trending`, `untappd_notifications`,
123
- `untappd_brewery_beers`, `untappd_search_brewery`, `untappd_search_venue`,
124
- `untappd_user_badges`, `untappd_user_friends`, `untappd_user_venues`,
125
- `untappd_pending_friends`, `untappd_venue_by_foursquare` — hand back
126
- Untappd's payload as it arrived. No projector has been written for their
127
- shapes, so there is no rung to ask for and **no slim option exists**.
128
- `untappd_brewery_beers` is the one worth budgeting for: it returns a beer
129
- list, which is the shape the beer projectors handle, but it is not wired to
130
- one — a 50-beer page arrives as 50 full records.
132
+ - **The remaining nine reads** — `untappd_trending`, `untappd_notifications`,
133
+ `untappd_search_brewery`, `untappd_search_venue`, `untappd_user_badges`,
134
+ `untappd_user_friends`, `untappd_user_venues`, `untappd_pending_friends`,
135
+ `untappd_venue_by_foursquare` — hand back Untappd's payload as it arrived.
136
+ No projector has been written for their shapes, so there is no rung to ask
137
+ for and **no slim option exists**.
138
+
139
+ `untappd_brewery_beers` **used to be tenth on this list and no longer is**
140
+ (#161)it takes a `view` now, and its field shape is documented with the
141
+ other projections above.
131
142
 
132
143
  ## Write tools (confirm-gated — these post to your public account)
133
144