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.
- 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 +23 -12
|
@@ -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
|
},
|
|
@@ -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
|
-
|
|
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 —
|
|
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
|
|
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
|
|
123
|
-
`
|
|
124
|
-
`
|
|
125
|
-
`
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|