untappd-mcp 1.10.0 → 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.0"
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.0",
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.0",
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.0";
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.0'; // 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.0",
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.0",
9
+ "version": "1.11.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "untappd-mcp",
14
- "version": "1.10.0",
14
+ "version": "1.11.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -40,6 +40,106 @@ Run `untappd_healthcheck` to confirm login works.
40
40
 
41
41
  Most user tools default `username` to your configured account when omitted.
42
42
 
43
+ ## Response shape (`view`)
44
+
45
+ Thirteen of this server's 45 tools take `view: "compact" | "full"`, and
46
+ **`compact` is the DEFAULT** — the slim rung is what you get without asking
47
+ for it.
48
+
49
+ **If you were told to pass `compact: true`, that advice is stale.** It used to
50
+ be true: the projection shipped as an opt-in boolean defaulting to `false`,
51
+ and the tool descriptions asked the caller to please turn it on. An efficiency
52
+ you have to request is one that mostly is not requested — and the caller
53
+ paying for the fat record was the one least able to know a slim rung existed.
54
+ So it flipped. The old parameter is gone; it is not declared anywhere, so zod
55
+ drops the unknown key and the call succeeds. You get the compact rung — right
56
+ answer, wrong reason. **The same silence bites the other way**: `compact:
57
+ false` no longer buys you the full record either. It is dropped identically
58
+ and you still get compact. `view: "full"` is the only way back.
59
+
60
+ Compact comes in two tiers here, and they are not the same thing:
61
+
62
+ **A hand-written field projection — 9 tools.**
63
+
64
+ - Check-ins (`untappd_activity_feed`, `untappd_user_checkins`,
65
+ `untappd_beer_activity`, `untappd_venue_activity`,
66
+ `untappd_local_checkins`): each ~5 KB record becomes `{checkin_id,
67
+ created_at, user, rating, comment, beer{bid, name, style, abv}, brewery,
68
+ venue, toasts, comments, has_photo}`. A default page of 25 was upwards of
69
+ 100 KB to learn what someone drank.
70
+ - `untappd_search_beer`: each ~1.2 KB match becomes `{bid, name, style, abv,
71
+ ibu, brewery, checkin_count, have_had}` — the long `beer_description` and
72
+ the nested brewery record go.
73
+ - `untappd_user_wishlist`: the same beer core plus `added_at`.
74
+ - `untappd_user_beers`: the same beer core plus `your_count`, `your_rating`,
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.
86
+
87
+ **An upstream request for less — 4 tools.** `untappd_user_info`,
88
+ `untappd_beer_info`, `untappd_brewery_info` and `untappd_venue_info` forward
89
+ `compact=true` to Untappd's own endpoint, which drops the embedded
90
+ activity/list blocks server side. **There is no local projection on these**:
91
+ the entity's own fields are byte-identical on both rungs. What compact saves
92
+ is Untappd's embedded blocks — and the bandwidth, not just the context. Do not
93
+ expect a shorter beer record from `untappd_beer_info`; expect the recent-
94
+ activity block to be absent.
95
+
96
+ The projectors are **drift-safe**. Each keys off a documented container
97
+ (`checkins.items`, `beers.items`); if that array is not where it is expected —
98
+ this is a reverse-engineered API — the projector warns on stderr and returns
99
+ the RAW response rather than an empty or wrong one. A short answer is never
100
+ silently manufactured.
101
+
102
+ `view: "full"` returns Untappd's response untouched. There is deliberately
103
+ **no `raw` rung**: nothing here re-serialises or normalises a payload, so
104
+ `full` already IS the upstream response and a third value would silently alias
105
+ one that exists.
106
+
107
+ ### The 32 tools without `view`
108
+
109
+ Each for its own reason — and none of them will tell you it ignored the
110
+ parameter, because an undeclared key is dropped by zod without a warning:
111
+
112
+ - **The 11 confirm-gated writes** (`untappd_checkin`, `untappd_toast`,
113
+ `untappd_add_comment`, the two deletes, the wishlist pair, the four friend
114
+ actions) answer with a dry-run preview or a receipt. Nothing in a receipt is
115
+ decoration.
116
+ - **`untappd_sync_checkins` / `untappd_sync_user_beers`** answer with sync
117
+ PROGRESS — pages walked, `another_run_needed`, `backfill_complete`. Slimming
118
+ a progress report is how you lose the field that says it is not finished.
119
+ - **The five cache tools** (`untappd_cache_has_had`, `…_has_had_many`,
120
+ `…_not_had`, `untappd_cache_query`, `untappd_top_not_had`) answer from the
121
+ local SQLite mirror in a shape this repo already wrote — a verdict plus the
122
+ `freshness` block that says whether a "not found" might be a false negative.
123
+ There is no upstream payload behind them to project away from.
124
+ - **`untappd_checkin_info`** exists to return the FULL record for one check-in
125
+ — photos, badges earned, toasts, comments. The ~5 KB the check-in projection
126
+ removes is exactly what you called this tool to get.
127
+ - **`untappd_venue_menu`** is already its own hand-written projection, with a
128
+ per-call page budget and a resumable `next_section_offset` contract. A
129
+ second projection on top of it would fight the first.
130
+ - **`untappd_resolve`, `untappd_open_url`, `untappd_healthcheck`** return a
131
+ verdict or a diagnostic.
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.
142
+
43
143
  ## Write tools (confirm-gated — these post to your public account)
44
144
 
45
145
  Each returns a dry-run preview and makes NO network call unless called with