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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +31 -4
- package/dist/compact.js +46 -0
- package/dist/tools/brewery.js +13 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/untappd-mcp/SKILL.md +100 -0
|
@@ -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
|
+
"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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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").
|
package/dist/tools/brewery.js
CHANGED
|
@@ -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
|
-
},
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
9
|
+
"version": "1.11.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "untappd-mcp",
|
|
14
|
-
"version": "1.
|
|
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
|