untappd-mcp 0.0.0 → 1.0.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 +9 -3
- package/.claude-plugin/plugin.json +8 -2
- package/README.md +7 -4
- package/dist/bundle.js +290 -5
- package/dist/client.js +12 -0
- package/dist/index.js +4 -0
- package/dist/tools/beer.js +14 -0
- package/dist/tools/brewery.js +18 -0
- package/dist/tools/checkin.js +107 -7
- package/dist/tools/discover.js +43 -0
- package/dist/tools/venue.js +14 -0
- package/dist/tools/wishlist.js +48 -0
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
|
@@ -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": "
|
|
10
|
+
"version": "1.0.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,14 +15,20 @@
|
|
|
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": "
|
|
18
|
+
"version": "1.0.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
22
22
|
"homepage": "https://github.com/chrischall/untappd-mcp",
|
|
23
23
|
"repository": "https://github.com/chrischall/untappd-mcp",
|
|
24
24
|
"license": "MIT",
|
|
25
|
-
"keywords": [
|
|
25
|
+
"keywords": [
|
|
26
|
+
"untappd",
|
|
27
|
+
"beer",
|
|
28
|
+
"breweries",
|
|
29
|
+
"checkins",
|
|
30
|
+
"mcp"
|
|
31
|
+
],
|
|
26
32
|
"category": "lifestyle"
|
|
27
33
|
}
|
|
28
34
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "untappd-mcp",
|
|
3
3
|
"displayName": "Untappd",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "1.0.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",
|
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
"homepage": "https://github.com/chrischall/untappd-mcp",
|
|
11
11
|
"repository": "https://github.com/chrischall/untappd-mcp",
|
|
12
12
|
"license": "MIT",
|
|
13
|
-
"keywords": [
|
|
13
|
+
"keywords": [
|
|
14
|
+
"untappd",
|
|
15
|
+
"beer",
|
|
16
|
+
"breweries",
|
|
17
|
+
"checkins",
|
|
18
|
+
"mcp"
|
|
19
|
+
],
|
|
14
20
|
"skills": "./SKILL.md",
|
|
15
21
|
"mcp": "./.mcp.json"
|
|
16
22
|
}
|
package/README.md
CHANGED
|
@@ -53,14 +53,17 @@ Keep these values private; do not commit them.
|
|
|
53
53
|
|
|
54
54
|
## Tools
|
|
55
55
|
|
|
56
|
-
Reads: `untappd_search_beer`, `untappd_beer_info`, `
|
|
57
|
-
`
|
|
56
|
+
Reads: `untappd_search_beer`, `untappd_beer_info`, `untappd_beer_activity`,
|
|
57
|
+
`untappd_search_brewery`, `untappd_brewery_info`, `untappd_brewery_beers`,
|
|
58
|
+
`untappd_search_venue`, `untappd_venue_info`, `untappd_venue_activity`,
|
|
58
59
|
`untappd_user_info`, `untappd_user_checkins`, `untappd_user_wishlist`,
|
|
59
60
|
`untappd_user_beers`, `untappd_user_badges`, `untappd_user_friends`,
|
|
60
|
-
`untappd_activity_feed`, `untappd_checkin_info`, `
|
|
61
|
+
`untappd_activity_feed`, `untappd_checkin_info`, `untappd_trending`,
|
|
62
|
+
`untappd_notifications`, `untappd_local_checkins`, `untappd_healthcheck`.
|
|
61
63
|
|
|
62
64
|
Writes (confirm-gated — return a dry-run preview unless called with
|
|
63
|
-
`confirm: true`): `untappd_toast`, `untappd_add_comment`, `untappd_checkin
|
|
65
|
+
`confirm: true`): `untappd_toast`, `untappd_add_comment`, `untappd_delete_comment`, `untappd_checkin`,
|
|
66
|
+
`untappd_wishlist_add`, `untappd_wishlist_remove`, `untappd_delete_checkin`.
|
|
64
67
|
|
|
65
68
|
## Development
|
|
66
69
|
|
package/dist/bundle.js
CHANGED
|
@@ -31073,6 +31073,11 @@ function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
|
31073
31073
|
return redacted;
|
|
31074
31074
|
return `${redacted.slice(0, max)}\u2026 [truncated]`;
|
|
31075
31075
|
}
|
|
31076
|
+
function messageOf(err) {
|
|
31077
|
+
if (err instanceof Error)
|
|
31078
|
+
return err.message;
|
|
31079
|
+
return String(err);
|
|
31080
|
+
}
|
|
31076
31081
|
|
|
31077
31082
|
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
31078
31083
|
function textResult(data) {
|
|
@@ -31111,6 +31116,21 @@ async function loadDotenvSafely(opts = {}) {
|
|
|
31111
31116
|
}
|
|
31112
31117
|
}
|
|
31113
31118
|
|
|
31119
|
+
// node_modules/@chrischall/mcp-utils/dist/fs/index.js
|
|
31120
|
+
import { openAsBlob } from "node:fs";
|
|
31121
|
+
async function fileBlob(path, opts = {}) {
|
|
31122
|
+
let blob;
|
|
31123
|
+
try {
|
|
31124
|
+
blob = await openAsBlob(path, opts.type !== void 0 ? { type: opts.type } : void 0);
|
|
31125
|
+
} catch {
|
|
31126
|
+
throw new Error(`Cannot read file for upload: ${path}`);
|
|
31127
|
+
}
|
|
31128
|
+
if (opts.maxBytes !== void 0 && blob.size > opts.maxBytes) {
|
|
31129
|
+
throw new Error(`${opts.label ?? "File"} is ${blob.size} bytes, over the ${opts.maxBytes}-byte limit: ${path}`);
|
|
31130
|
+
}
|
|
31131
|
+
return blob;
|
|
31132
|
+
}
|
|
31133
|
+
|
|
31114
31134
|
// node_modules/@chrischall/mcp-utils/dist/http/index.js
|
|
31115
31135
|
function buildQueryString(params) {
|
|
31116
31136
|
const parts = [];
|
|
@@ -31164,7 +31184,7 @@ function toolAnnotations(opts = {}) {
|
|
|
31164
31184
|
}
|
|
31165
31185
|
|
|
31166
31186
|
// src/version.ts
|
|
31167
|
-
var VERSION = "
|
|
31187
|
+
var VERSION = "1.0.0";
|
|
31168
31188
|
|
|
31169
31189
|
// src/client.ts
|
|
31170
31190
|
import { dirname, join } from "path";
|
|
@@ -31387,6 +31407,18 @@ var UntappdClient = class {
|
|
|
31387
31407
|
async write(method, path, opts = {}) {
|
|
31388
31408
|
return this.request(method, path, { ...opts, auth: "bearer" });
|
|
31389
31409
|
}
|
|
31410
|
+
/**
|
|
31411
|
+
* Raw binary PUT to an arbitrary URL — used for the presigned-S3 photo upload,
|
|
31412
|
+
* whose URL already carries its own auth query params, so NO Untappd token is
|
|
31413
|
+
* attached. `body` should be a file-backed Blob (streamed, not heap-buffered).
|
|
31414
|
+
*/
|
|
31415
|
+
async putBinary(url2, body, contentType) {
|
|
31416
|
+
const res = await this.send("PUT", url2, { headers: { "Content-Type": contentType }, body });
|
|
31417
|
+
if (!res.ok) {
|
|
31418
|
+
const text = await res.text().catch(() => "");
|
|
31419
|
+
throw new McpToolError(formatApiError(res.status, "PUT", url2.split("?")[0], text, { service: `${SERVICE} photo upload` }));
|
|
31420
|
+
}
|
|
31421
|
+
}
|
|
31390
31422
|
};
|
|
31391
31423
|
var client = new UntappdClient();
|
|
31392
31424
|
|
|
@@ -31427,6 +31459,23 @@ function registerBeerTools(server) {
|
|
|
31427
31459
|
return textResult(data);
|
|
31428
31460
|
}
|
|
31429
31461
|
);
|
|
31462
|
+
server.registerTool(
|
|
31463
|
+
"untappd_beer_activity",
|
|
31464
|
+
{
|
|
31465
|
+
title: "Get recent check-ins for a beer",
|
|
31466
|
+
description: "Get the recent public check-ins for a beer by its bid \u2014 who drank it, their rating, comment, and venue. Page backwards with max_id (the pagination.max_id from a prior call). Read-only.",
|
|
31467
|
+
annotations: toolAnnotations({ title: "Get recent check-ins for a beer", readOnly: true, idempotent: false, openWorld: true }),
|
|
31468
|
+
inputSchema: {
|
|
31469
|
+
bid: BidSchema,
|
|
31470
|
+
limit: external_exports.number().int().min(1).max(50).optional().describe("Max check-ins (1\u201350, default 25)"),
|
|
31471
|
+
max_id: external_exports.number().int().positive().optional().describe("Return check-ins older than this id (for paging)")
|
|
31472
|
+
}
|
|
31473
|
+
},
|
|
31474
|
+
async ({ bid, limit, max_id }) => {
|
|
31475
|
+
const data = await client.get(`/beer/checkins/${bid}`, { limit, max_id });
|
|
31476
|
+
return textResult(data);
|
|
31477
|
+
}
|
|
31478
|
+
);
|
|
31430
31479
|
}
|
|
31431
31480
|
|
|
31432
31481
|
// src/tools/brewery.ts
|
|
@@ -31464,6 +31513,24 @@ function registerBreweryTools(server) {
|
|
|
31464
31513
|
return textResult(data);
|
|
31465
31514
|
}
|
|
31466
31515
|
);
|
|
31516
|
+
server.registerTool(
|
|
31517
|
+
"untappd_brewery_beers",
|
|
31518
|
+
{
|
|
31519
|
+
title: "Get a brewery's beer list",
|
|
31520
|
+
description: "Get the beers a brewery makes, by brewery id, with per-beer rating and check-in counts. Supports sorting and paging. Get an id from untappd_search_brewery. Read-only.",
|
|
31521
|
+
annotations: toolAnnotations({ title: "Get a brewery's beer list", readOnly: true, idempotent: true, openWorld: true }),
|
|
31522
|
+
inputSchema: {
|
|
31523
|
+
brewery_id: external_exports.number().int().positive().describe("Untappd brewery id"),
|
|
31524
|
+
limit: external_exports.number().int().min(1).max(50).optional().describe("Max beers (1\u201350, default 25)"),
|
|
31525
|
+
offset: external_exports.number().int().min(0).optional().describe("Result offset for paging (default 0)"),
|
|
31526
|
+
sort: external_exports.enum(["name", "style", "abv", "rating", "count"]).optional().describe("Sort order (default by popularity)")
|
|
31527
|
+
}
|
|
31528
|
+
},
|
|
31529
|
+
async ({ brewery_id, limit, offset, sort }) => {
|
|
31530
|
+
const data = await client.get(`/brewery/beer_list/${brewery_id}`, { limit, offset, sort });
|
|
31531
|
+
return textResult(data);
|
|
31532
|
+
}
|
|
31533
|
+
);
|
|
31467
31534
|
}
|
|
31468
31535
|
|
|
31469
31536
|
// src/tools/venue.ts
|
|
@@ -31500,6 +31567,23 @@ function registerVenueTools(server) {
|
|
|
31500
31567
|
return textResult(data);
|
|
31501
31568
|
}
|
|
31502
31569
|
);
|
|
31570
|
+
server.registerTool(
|
|
31571
|
+
"untappd_venue_activity",
|
|
31572
|
+
{
|
|
31573
|
+
title: "Get recent check-ins at a venue",
|
|
31574
|
+
description: "Get the recent public check-ins at a venue by its id \u2014 who was there, what they drank, and their ratings. Page backwards with max_id (the pagination.max_id from a prior call). Read-only.",
|
|
31575
|
+
annotations: toolAnnotations({ title: "Get recent check-ins at a venue", readOnly: true, idempotent: false, openWorld: true }),
|
|
31576
|
+
inputSchema: {
|
|
31577
|
+
venue_id: external_exports.number().int().positive().describe("Untappd venue id"),
|
|
31578
|
+
limit: external_exports.number().int().min(1).max(50).optional().describe("Max check-ins (1\u201350, default 25)"),
|
|
31579
|
+
max_id: external_exports.number().int().positive().optional().describe("Return check-ins older than this id (for paging)")
|
|
31580
|
+
}
|
|
31581
|
+
},
|
|
31582
|
+
async ({ venue_id, limit, max_id }) => {
|
|
31583
|
+
const data = await client.get(`/venue/checkins/${venue_id}`, { limit, max_id });
|
|
31584
|
+
return textResult(data);
|
|
31585
|
+
}
|
|
31586
|
+
);
|
|
31503
31587
|
}
|
|
31504
31588
|
|
|
31505
31589
|
// src/tools/user.ts
|
|
@@ -31654,8 +31738,119 @@ function registerFeedTools(server) {
|
|
|
31654
31738
|
);
|
|
31655
31739
|
}
|
|
31656
31740
|
|
|
31741
|
+
// src/tools/discover.ts
|
|
31742
|
+
function registerDiscoverTools(server) {
|
|
31743
|
+
server.registerTool(
|
|
31744
|
+
"untappd_trending",
|
|
31745
|
+
{
|
|
31746
|
+
title: "Get trending beers",
|
|
31747
|
+
description: "Get the beers trending on Untappd right now, split into macro (big/widely-available) and micro (craft/independent) lists. Read-only.",
|
|
31748
|
+
annotations: toolAnnotations({ title: "Get trending beers", readOnly: true, idempotent: false, openWorld: true }),
|
|
31749
|
+
inputSchema: {}
|
|
31750
|
+
},
|
|
31751
|
+
async () => {
|
|
31752
|
+
const data = await client.get("/beer/trending");
|
|
31753
|
+
return textResult(data);
|
|
31754
|
+
}
|
|
31755
|
+
);
|
|
31756
|
+
server.registerTool(
|
|
31757
|
+
"untappd_notifications",
|
|
31758
|
+
{
|
|
31759
|
+
title: "Get your Untappd notifications",
|
|
31760
|
+
description: "Get your Untappd notifications \u2014 toasts, comments, friend requests, and badges earned on YOUR account, plus news items. Read-only.",
|
|
31761
|
+
annotations: toolAnnotations({ title: "Get your Untappd notifications", readOnly: true, idempotent: false, openWorld: true }),
|
|
31762
|
+
inputSchema: {
|
|
31763
|
+
limit: external_exports.number().int().min(1).max(50).optional().describe("Max notifications (1\u201350, default 25)"),
|
|
31764
|
+
offset: external_exports.number().int().min(0).optional().describe("Result offset for paging (default 0)")
|
|
31765
|
+
}
|
|
31766
|
+
},
|
|
31767
|
+
async ({ limit, offset }) => {
|
|
31768
|
+
const data = await client.get("/notifications", { limit, offset });
|
|
31769
|
+
return textResult(data);
|
|
31770
|
+
}
|
|
31771
|
+
);
|
|
31772
|
+
server.registerTool(
|
|
31773
|
+
"untappd_local_checkins",
|
|
31774
|
+
{
|
|
31775
|
+
title: "Get nearby Untappd check-ins",
|
|
31776
|
+
description: "Get recent check-ins near a location (lat/lng) \u2014 what people are drinking nearby right now. Optionally widen the search radius. Read-only.",
|
|
31777
|
+
annotations: toolAnnotations({ title: "Get nearby Untappd check-ins", readOnly: true, idempotent: false, openWorld: true }),
|
|
31778
|
+
inputSchema: {
|
|
31779
|
+
lat: external_exports.number().min(-90).max(90).describe("Latitude of the location"),
|
|
31780
|
+
lng: external_exports.number().min(-180).max(180).describe("Longitude of the location"),
|
|
31781
|
+
limit: external_exports.number().int().min(1).max(50).optional().describe("Max check-ins (1\u201350, default 25)"),
|
|
31782
|
+
radius: external_exports.number().int().min(1).max(25).optional().describe("Search radius (default per Untappd)")
|
|
31783
|
+
}
|
|
31784
|
+
},
|
|
31785
|
+
async ({ lat, lng, limit, radius }) => {
|
|
31786
|
+
const data = await client.get("/thepub/local", { lat, lng, limit, radius });
|
|
31787
|
+
return textResult(data);
|
|
31788
|
+
}
|
|
31789
|
+
);
|
|
31790
|
+
}
|
|
31791
|
+
|
|
31792
|
+
// src/tools/wishlist.ts
|
|
31793
|
+
var BidSchema2 = external_exports.number().int().positive().describe("Untappd beer id (bid) \u2014 from untappd_search_beer");
|
|
31794
|
+
function registerWishlistTools(server) {
|
|
31795
|
+
server.registerTool(
|
|
31796
|
+
"untappd_wishlist_add",
|
|
31797
|
+
{
|
|
31798
|
+
title: "Add a beer to your wishlist",
|
|
31799
|
+
description: "Add a beer to YOUR Untappd wishlist by its bid. Without confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it adds. Writes to your account.",
|
|
31800
|
+
annotations: toolAnnotations({ title: "Add a beer to your wishlist", readOnly: false, idempotent: true, openWorld: true }),
|
|
31801
|
+
inputSchema: {
|
|
31802
|
+
bid: BidSchema2,
|
|
31803
|
+
confirm: schemaConfirm
|
|
31804
|
+
}
|
|
31805
|
+
},
|
|
31806
|
+
async ({ bid, confirm }) => {
|
|
31807
|
+
if (confirm !== true) {
|
|
31808
|
+
return textResult({
|
|
31809
|
+
dryRun: true,
|
|
31810
|
+
action: "wishlist_add",
|
|
31811
|
+
bid,
|
|
31812
|
+
note: "Dry run \u2014 re-run with confirm: true to add this beer to your wishlist."
|
|
31813
|
+
});
|
|
31814
|
+
}
|
|
31815
|
+
const data = await client.write("GET", "/user/wishlist/add", { query: { bid } });
|
|
31816
|
+
return textResult({ added: true, bid, result: data?.result });
|
|
31817
|
+
}
|
|
31818
|
+
);
|
|
31819
|
+
server.registerTool(
|
|
31820
|
+
"untappd_wishlist_remove",
|
|
31821
|
+
{
|
|
31822
|
+
title: "Remove a beer from your wishlist",
|
|
31823
|
+
description: "Remove a beer from YOUR Untappd wishlist by its bid. Without confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it removes. Writes to your account.",
|
|
31824
|
+
annotations: toolAnnotations({ title: "Remove a beer from your wishlist", readOnly: false, idempotent: true, openWorld: true }),
|
|
31825
|
+
inputSchema: {
|
|
31826
|
+
bid: BidSchema2,
|
|
31827
|
+
confirm: schemaConfirm
|
|
31828
|
+
}
|
|
31829
|
+
},
|
|
31830
|
+
async ({ bid, confirm }) => {
|
|
31831
|
+
if (confirm !== true) {
|
|
31832
|
+
return textResult({
|
|
31833
|
+
dryRun: true,
|
|
31834
|
+
action: "wishlist_remove",
|
|
31835
|
+
bid,
|
|
31836
|
+
note: "Dry run \u2014 re-run with confirm: true to remove this beer from your wishlist."
|
|
31837
|
+
});
|
|
31838
|
+
}
|
|
31839
|
+
const data = await client.write("GET", "/user/wishlist/delete", { query: { bid } });
|
|
31840
|
+
return textResult({ removed: true, bid, result: data?.result });
|
|
31841
|
+
}
|
|
31842
|
+
);
|
|
31843
|
+
}
|
|
31844
|
+
|
|
31657
31845
|
// src/tools/checkin.ts
|
|
31846
|
+
import { existsSync } from "node:fs";
|
|
31847
|
+
import { extname } from "node:path";
|
|
31658
31848
|
var CheckinIdSchema = external_exports.number().int().positive().describe("Untappd check-in id");
|
|
31849
|
+
var PHOTO_CONTENT_TYPES = { jpg: "image/jpeg", png: "image/png" };
|
|
31850
|
+
function photoExt(path) {
|
|
31851
|
+
const ext = extname(path).slice(1).toLowerCase();
|
|
31852
|
+
return ext === "jpeg" ? "jpg" : ext;
|
|
31853
|
+
}
|
|
31659
31854
|
var RatingSchema = external_exports.number().min(0).max(5).refine((r) => Math.round(r * 4) === r * 4, { message: "rating must be a multiple of 0.25" });
|
|
31660
31855
|
function localTimezone() {
|
|
31661
31856
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
@@ -31713,25 +31908,83 @@ function registerCheckinTools(server) {
|
|
|
31713
31908
|
return textResult({ posted: true, checkin_id, response: data });
|
|
31714
31909
|
}
|
|
31715
31910
|
);
|
|
31911
|
+
server.registerTool(
|
|
31912
|
+
"untappd_delete_comment",
|
|
31913
|
+
{
|
|
31914
|
+
title: "Delete a comment from an Untappd check-in",
|
|
31915
|
+
description: "Delete one of YOUR comments by its comment id (the id from a check-in's comments list). Without confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it deletes.",
|
|
31916
|
+
annotations: toolAnnotations({ title: "Delete a comment from an Untappd check-in", readOnly: false, idempotent: true, openWorld: true }),
|
|
31917
|
+
inputSchema: {
|
|
31918
|
+
comment_id: external_exports.number().int().positive().describe("Untappd comment id (from a check-in's comments.items)"),
|
|
31919
|
+
confirm: schemaConfirm
|
|
31920
|
+
}
|
|
31921
|
+
},
|
|
31922
|
+
async ({ comment_id, confirm }) => {
|
|
31923
|
+
if (confirm !== true) {
|
|
31924
|
+
return textResult({
|
|
31925
|
+
dryRun: true,
|
|
31926
|
+
action: "delete_comment",
|
|
31927
|
+
comment_id,
|
|
31928
|
+
note: "Dry run \u2014 re-run with confirm: true to delete this comment from your Untappd account."
|
|
31929
|
+
});
|
|
31930
|
+
}
|
|
31931
|
+
const data = await client.write("POST", `/checkin/deletecomment/${comment_id}`);
|
|
31932
|
+
return textResult({ deleted: true, comment_id, result: data?.result });
|
|
31933
|
+
}
|
|
31934
|
+
);
|
|
31935
|
+
server.registerTool(
|
|
31936
|
+
"untappd_delete_checkin",
|
|
31937
|
+
{
|
|
31938
|
+
title: "Delete an Untappd check-in",
|
|
31939
|
+
description: "Permanently delete one of YOUR check-ins by its id. This is destructive and cannot be undone. Without confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it deletes.",
|
|
31940
|
+
annotations: toolAnnotations({ title: "Delete an Untappd check-in", readOnly: false, idempotent: true, openWorld: true }),
|
|
31941
|
+
inputSchema: {
|
|
31942
|
+
checkin_id: CheckinIdSchema,
|
|
31943
|
+
confirm: schemaConfirm
|
|
31944
|
+
}
|
|
31945
|
+
},
|
|
31946
|
+
async ({ checkin_id, confirm }) => {
|
|
31947
|
+
if (confirm !== true) {
|
|
31948
|
+
return textResult({
|
|
31949
|
+
dryRun: true,
|
|
31950
|
+
action: "delete_checkin",
|
|
31951
|
+
checkin_id,
|
|
31952
|
+
note: "Dry run \u2014 re-run with confirm: true to PERMANENTLY delete this check-in. This cannot be undone."
|
|
31953
|
+
});
|
|
31954
|
+
}
|
|
31955
|
+
const data = await client.write("POST", `/checkin/delete/${checkin_id}`);
|
|
31956
|
+
return textResult({ deleted: true, checkin_id, result: data?.result });
|
|
31957
|
+
}
|
|
31958
|
+
);
|
|
31716
31959
|
server.registerTool(
|
|
31717
31960
|
"untappd_checkin",
|
|
31718
31961
|
{
|
|
31719
31962
|
title: "Check in a beer on Untappd",
|
|
31720
|
-
description: "Post a NEW beer check-in to YOUR Untappd account \u2014 this publishes to your public feed. Provide the beer id (bid) from untappd_search_beer; optionally a rating (0\u20135 in 0.25 steps), a shout (comment),
|
|
31963
|
+
description: "Post a NEW beer check-in to YOUR Untappd account \u2014 this publishes to your public feed. Provide the beer id (bid) from untappd_search_beer; optionally a rating (0\u20135 in 0.25 steps), a shout (comment), a venue via foursquare_id, and a local photo via photo_path (JPEG/PNG). Without confirm: true it returns a dry-run preview of the exact fields and makes NO network call; with confirm: true it posts.",
|
|
31721
31964
|
annotations: toolAnnotations({ title: "Check in a beer on Untappd", readOnly: false, idempotent: false, openWorld: true }),
|
|
31722
31965
|
inputSchema: {
|
|
31723
31966
|
bid: external_exports.number().int().positive().describe("Untappd beer id to check in (from untappd_search_beer)"),
|
|
31724
31967
|
rating: RatingSchema.optional().describe("Rating 0\u20135 in 0.25 increments (omit for no rating)"),
|
|
31725
31968
|
shout: external_exports.string().max(2e3).optional().describe("Optional shout / comment text for the check-in"),
|
|
31726
31969
|
foursquare_id: external_exports.string().optional().describe("Optional Foursquare venue id to tag the check-in location"),
|
|
31970
|
+
photo_path: external_exports.string().optional().describe("Optional path to a local JPEG/PNG photo to attach to the check-in"),
|
|
31727
31971
|
geolat: external_exports.number().optional().describe("Optional latitude of the check-in"),
|
|
31728
31972
|
geolng: external_exports.number().optional().describe("Optional longitude of the check-in"),
|
|
31729
31973
|
container_id: external_exports.number().int().optional().describe("Optional serving container id (e.g. 1 = draft, 2 = bottle, 3 = can)"),
|
|
31730
31974
|
confirm: schemaConfirm
|
|
31731
31975
|
}
|
|
31732
31976
|
},
|
|
31733
|
-
async ({ bid, rating, shout, foursquare_id, geolat, geolng, container_id, confirm }) => {
|
|
31977
|
+
async ({ bid, rating, shout, foursquare_id, photo_path, geolat, geolng, container_id, confirm }) => {
|
|
31734
31978
|
const { timezone, gmt_offset } = localTimezone();
|
|
31979
|
+
let ext;
|
|
31980
|
+
if (photo_path !== void 0) {
|
|
31981
|
+
ext = photoExt(photo_path);
|
|
31982
|
+
if (!(ext in PHOTO_CONTENT_TYPES)) {
|
|
31983
|
+
throw createHelpfulError(`Unsupported photo type "${ext || "(none)"}".`, {
|
|
31984
|
+
hint: "Attach a .jpg, .jpeg, or .png file."
|
|
31985
|
+
});
|
|
31986
|
+
}
|
|
31987
|
+
}
|
|
31735
31988
|
const form = {
|
|
31736
31989
|
bid,
|
|
31737
31990
|
rating: rating !== void 0 ? rating.toFixed(2) : void 0,
|
|
@@ -31742,7 +31995,8 @@ function registerCheckinTools(server) {
|
|
|
31742
31995
|
container_id,
|
|
31743
31996
|
timezone,
|
|
31744
31997
|
gmt_offset,
|
|
31745
|
-
is_photo: "false",
|
|
31998
|
+
is_photo: photo_path !== void 0 ? "true" : "false",
|
|
31999
|
+
photo_file_ext: photo_path !== void 0 ? ext : void 0,
|
|
31746
32000
|
platform: "ios"
|
|
31747
32001
|
};
|
|
31748
32002
|
if (confirm !== true) {
|
|
@@ -31750,11 +32004,40 @@ function registerCheckinTools(server) {
|
|
|
31750
32004
|
dryRun: true,
|
|
31751
32005
|
action: "checkin",
|
|
31752
32006
|
form,
|
|
32007
|
+
photo: photo_path !== void 0 ? { path: photo_path, note: "will be uploaded after the check-in is created" } : void 0,
|
|
31753
32008
|
note: "Dry run \u2014 re-run with confirm: true to POST this check-in to your public Untappd feed."
|
|
31754
32009
|
});
|
|
31755
32010
|
}
|
|
32011
|
+
let blob;
|
|
32012
|
+
if (photo_path !== void 0) {
|
|
32013
|
+
if (!existsSync(photo_path)) throw new McpToolError(`Photo file not found: ${photo_path}`);
|
|
32014
|
+
blob = await fileBlob(photo_path);
|
|
32015
|
+
}
|
|
31756
32016
|
const data = await client.write("POST", "/checkin/add", { form });
|
|
31757
|
-
|
|
32017
|
+
let photo_attached = false;
|
|
32018
|
+
let photo_error;
|
|
32019
|
+
if (photo_path !== void 0) {
|
|
32020
|
+
if (data?.photo_upload?.url && data.checkin_id) {
|
|
32021
|
+
try {
|
|
32022
|
+
await client.putBinary(data.photo_upload.url, blob, PHOTO_CONTENT_TYPES[ext]);
|
|
32023
|
+
await client.write("POST", "/photo/uploadComplete", {
|
|
32024
|
+
form: { checkin_id: data.checkin_id, destination_url: data.photo_upload.destination_url }
|
|
32025
|
+
});
|
|
32026
|
+
photo_attached = true;
|
|
32027
|
+
} catch (e) {
|
|
32028
|
+
photo_error = `Check-in ${data.checkin_id} was created, but attaching the photo failed: ${messageOf(e)}. Retry attaching, or remove it with untappd_delete_checkin.`;
|
|
32029
|
+
}
|
|
32030
|
+
} else {
|
|
32031
|
+
photo_error = `Check-in ${data?.checkin_id} was created, but Untappd returned no photo upload URL, so no photo was attached.`;
|
|
32032
|
+
}
|
|
32033
|
+
}
|
|
32034
|
+
return textResult({
|
|
32035
|
+
checked_in: true,
|
|
32036
|
+
checkin_id: data?.checkin_id,
|
|
32037
|
+
photo_attached,
|
|
32038
|
+
...photo_error ? { photo_error } : {},
|
|
32039
|
+
response: data
|
|
32040
|
+
});
|
|
31758
32041
|
}
|
|
31759
32042
|
);
|
|
31760
32043
|
}
|
|
@@ -31800,6 +32083,8 @@ await runMcp({
|
|
|
31800
32083
|
registerVenueTools,
|
|
31801
32084
|
registerUserTools,
|
|
31802
32085
|
registerFeedTools,
|
|
32086
|
+
registerDiscoverTools,
|
|
32087
|
+
registerWishlistTools,
|
|
31803
32088
|
registerCheckinTools,
|
|
31804
32089
|
registerUtilityTools
|
|
31805
32090
|
]
|
package/dist/client.js
CHANGED
|
@@ -242,6 +242,18 @@ export class UntappdClient {
|
|
|
242
242
|
async write(method, path, opts = {}) {
|
|
243
243
|
return this.request(method, path, { ...opts, auth: 'bearer' });
|
|
244
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Raw binary PUT to an arbitrary URL — used for the presigned-S3 photo upload,
|
|
247
|
+
* whose URL already carries its own auth query params, so NO Untappd token is
|
|
248
|
+
* attached. `body` should be a file-backed Blob (streamed, not heap-buffered).
|
|
249
|
+
*/
|
|
250
|
+
async putBinary(url, body, contentType) {
|
|
251
|
+
const res = await this.send('PUT', url, { headers: { 'Content-Type': contentType }, body });
|
|
252
|
+
if (!res.ok) {
|
|
253
|
+
const text = await res.text().catch(() => '');
|
|
254
|
+
throw new McpToolError(formatApiError(res.status, 'PUT', url.split('?')[0], text, { service: `${SERVICE} photo upload` }));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
245
257
|
}
|
|
246
258
|
/**
|
|
247
259
|
* Module-level singleton shared by every tool module. Constructing it here (not
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { registerBreweryTools } from './tools/brewery.js';
|
|
|
6
6
|
import { registerVenueTools } from './tools/venue.js';
|
|
7
7
|
import { registerUserTools } from './tools/user.js';
|
|
8
8
|
import { registerFeedTools } from './tools/feed.js';
|
|
9
|
+
import { registerDiscoverTools } from './tools/discover.js';
|
|
10
|
+
import { registerWishlistTools } from './tools/wishlist.js';
|
|
9
11
|
import { registerCheckinTools } from './tools/checkin.js';
|
|
10
12
|
import { registerUtilityTools } from './tools/utilities.js';
|
|
11
13
|
// The Untappd client is a module-level singleton (imported by each tool module)
|
|
@@ -23,6 +25,8 @@ await runMcp({
|
|
|
23
25
|
registerVenueTools,
|
|
24
26
|
registerUserTools,
|
|
25
27
|
registerFeedTools,
|
|
28
|
+
registerDiscoverTools,
|
|
29
|
+
registerWishlistTools,
|
|
26
30
|
registerCheckinTools,
|
|
27
31
|
registerUtilityTools,
|
|
28
32
|
],
|
package/dist/tools/beer.js
CHANGED
|
@@ -38,4 +38,18 @@ export function registerBeerTools(server) {
|
|
|
38
38
|
const data = await client.get(`/beer/info/${bid}`, { compact: compact ? 'true' : undefined });
|
|
39
39
|
return textResult(data);
|
|
40
40
|
});
|
|
41
|
+
server.registerTool('untappd_beer_activity', {
|
|
42
|
+
title: 'Get recent check-ins for a beer',
|
|
43
|
+
description: 'Get the recent public check-ins for a beer by its bid — who drank it, their rating, comment, and venue. ' +
|
|
44
|
+
'Page backwards with max_id (the pagination.max_id from a prior call). Read-only.',
|
|
45
|
+
annotations: toolAnnotations({ title: 'Get recent check-ins for a beer', readOnly: true, idempotent: false, openWorld: true }),
|
|
46
|
+
inputSchema: {
|
|
47
|
+
bid: BidSchema,
|
|
48
|
+
limit: z.number().int().min(1).max(50).optional().describe('Max check-ins (1–50, default 25)'),
|
|
49
|
+
max_id: z.number().int().positive().optional().describe('Return check-ins older than this id (for paging)'),
|
|
50
|
+
},
|
|
51
|
+
}, async ({ bid, limit, max_id }) => {
|
|
52
|
+
const data = await client.get(`/beer/checkins/${bid}`, { limit, max_id });
|
|
53
|
+
return textResult(data);
|
|
54
|
+
});
|
|
41
55
|
}
|
package/dist/tools/brewery.js
CHANGED
|
@@ -29,4 +29,22 @@ export function registerBreweryTools(server) {
|
|
|
29
29
|
const data = await client.get(`/brewery/info/${brewery_id}`, { compact: compact ? 'true' : undefined });
|
|
30
30
|
return textResult(data);
|
|
31
31
|
});
|
|
32
|
+
server.registerTool('untappd_brewery_beers', {
|
|
33
|
+
title: "Get a brewery's beer list",
|
|
34
|
+
description: 'Get the beers a brewery makes, by brewery id, with per-beer rating and check-in counts. Supports sorting ' +
|
|
35
|
+
'and paging. Get an id from untappd_search_brewery. Read-only.',
|
|
36
|
+
annotations: toolAnnotations({ title: "Get a brewery's beer list", readOnly: true, idempotent: true, openWorld: true }),
|
|
37
|
+
inputSchema: {
|
|
38
|
+
brewery_id: z.number().int().positive().describe('Untappd brewery id'),
|
|
39
|
+
limit: z.number().int().min(1).max(50).optional().describe('Max beers (1–50, default 25)'),
|
|
40
|
+
offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
|
|
41
|
+
sort: z
|
|
42
|
+
.enum(['name', 'style', 'abv', 'rating', 'count'])
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('Sort order (default by popularity)'),
|
|
45
|
+
},
|
|
46
|
+
}, async ({ brewery_id, limit, offset, sort }) => {
|
|
47
|
+
const data = await client.get(`/brewery/beer_list/${brewery_id}`, { limit, offset, sort });
|
|
48
|
+
return textResult(data);
|
|
49
|
+
});
|
|
32
50
|
}
|
package/dist/tools/checkin.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { extname } from 'node:path';
|
|
1
3
|
import { z } from 'zod';
|
|
2
|
-
import { textResult, toolAnnotations, schemaConfirm } from '@chrischall/mcp-utils';
|
|
4
|
+
import { textResult, toolAnnotations, schemaConfirm, fileBlob, McpToolError, createHelpfulError, messageOf } from '@chrischall/mcp-utils';
|
|
3
5
|
import { client } from '../client.js';
|
|
4
6
|
const CheckinIdSchema = z.number().int().positive().describe('Untappd check-in id');
|
|
7
|
+
// Keyed by the NORMALISED extension that photoExt() returns (jpeg → jpg), so
|
|
8
|
+
// there is no dead `jpeg` entry.
|
|
9
|
+
const PHOTO_CONTENT_TYPES = { jpg: 'image/jpeg', png: 'image/png' };
|
|
10
|
+
function photoExt(path) {
|
|
11
|
+
const ext = extname(path).slice(1).toLowerCase();
|
|
12
|
+
return ext === 'jpeg' ? 'jpg' : ext;
|
|
13
|
+
}
|
|
5
14
|
// Untappd ratings are 0–5 in 0.25 increments; 0 (or omitted) means no rating.
|
|
6
15
|
const RatingSchema = z
|
|
7
16
|
.number()
|
|
@@ -60,18 +69,61 @@ export function registerCheckinTools(server) {
|
|
|
60
69
|
const data = await client.write('POST', `/checkin/addcomment/${checkin_id}`, { form: { comment } });
|
|
61
70
|
return textResult({ posted: true, checkin_id, response: data });
|
|
62
71
|
});
|
|
72
|
+
server.registerTool('untappd_delete_comment', {
|
|
73
|
+
title: 'Delete a comment from an Untappd check-in',
|
|
74
|
+
description: 'Delete one of YOUR comments by its comment id (the id from a check-in\'s comments list). Without ' +
|
|
75
|
+
'confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it deletes.',
|
|
76
|
+
annotations: toolAnnotations({ title: 'Delete a comment from an Untappd check-in', readOnly: false, idempotent: true, openWorld: true }),
|
|
77
|
+
inputSchema: {
|
|
78
|
+
comment_id: z.number().int().positive().describe('Untappd comment id (from a check-in\'s comments.items)'),
|
|
79
|
+
confirm: schemaConfirm,
|
|
80
|
+
},
|
|
81
|
+
}, async ({ comment_id, confirm }) => {
|
|
82
|
+
if (confirm !== true) {
|
|
83
|
+
return textResult({
|
|
84
|
+
dryRun: true,
|
|
85
|
+
action: 'delete_comment',
|
|
86
|
+
comment_id,
|
|
87
|
+
note: 'Dry run — re-run with confirm: true to delete this comment from your Untappd account.',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const data = await client.write('POST', `/checkin/deletecomment/${comment_id}`);
|
|
91
|
+
return textResult({ deleted: true, comment_id, result: data?.result });
|
|
92
|
+
});
|
|
93
|
+
server.registerTool('untappd_delete_checkin', {
|
|
94
|
+
title: 'Delete an Untappd check-in',
|
|
95
|
+
description: 'Permanently delete one of YOUR check-ins by its id. This is destructive and cannot be undone. Without ' +
|
|
96
|
+
'confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it deletes.',
|
|
97
|
+
annotations: toolAnnotations({ title: 'Delete an Untappd check-in', readOnly: false, idempotent: true, openWorld: true }),
|
|
98
|
+
inputSchema: {
|
|
99
|
+
checkin_id: CheckinIdSchema,
|
|
100
|
+
confirm: schemaConfirm,
|
|
101
|
+
},
|
|
102
|
+
}, async ({ checkin_id, confirm }) => {
|
|
103
|
+
if (confirm !== true) {
|
|
104
|
+
return textResult({
|
|
105
|
+
dryRun: true,
|
|
106
|
+
action: 'delete_checkin',
|
|
107
|
+
checkin_id,
|
|
108
|
+
note: 'Dry run — re-run with confirm: true to PERMANENTLY delete this check-in. This cannot be undone.',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const data = await client.write('POST', `/checkin/delete/${checkin_id}`);
|
|
112
|
+
return textResult({ deleted: true, checkin_id, result: data?.result });
|
|
113
|
+
});
|
|
63
114
|
server.registerTool('untappd_checkin', {
|
|
64
115
|
title: 'Check in a beer on Untappd',
|
|
65
116
|
description: 'Post a NEW beer check-in to YOUR Untappd account — this publishes to your public feed. Provide the beer id ' +
|
|
66
|
-
'(bid) from untappd_search_beer; optionally a rating (0–5 in 0.25 steps), a shout (comment),
|
|
67
|
-
'foursquare_id
|
|
68
|
-
'and makes NO network call; with confirm: true it posts.
|
|
117
|
+
'(bid) from untappd_search_beer; optionally a rating (0–5 in 0.25 steps), a shout (comment), a venue via ' +
|
|
118
|
+
'foursquare_id, and a local photo via photo_path (JPEG/PNG). Without confirm: true it returns a dry-run ' +
|
|
119
|
+
'preview of the exact fields and makes NO network call; with confirm: true it posts.',
|
|
69
120
|
annotations: toolAnnotations({ title: 'Check in a beer on Untappd', readOnly: false, idempotent: false, openWorld: true }),
|
|
70
121
|
inputSchema: {
|
|
71
122
|
bid: z.number().int().positive().describe('Untappd beer id to check in (from untappd_search_beer)'),
|
|
72
123
|
rating: RatingSchema.optional().describe('Rating 0–5 in 0.25 increments (omit for no rating)'),
|
|
73
124
|
shout: z.string().max(2000).optional().describe('Optional shout / comment text for the check-in'),
|
|
74
125
|
foursquare_id: z.string().optional().describe('Optional Foursquare venue id to tag the check-in location'),
|
|
126
|
+
photo_path: z.string().optional().describe('Optional path to a local JPEG/PNG photo to attach to the check-in'),
|
|
75
127
|
geolat: z.number().optional().describe('Optional latitude of the check-in'),
|
|
76
128
|
geolng: z.number().optional().describe('Optional longitude of the check-in'),
|
|
77
129
|
container_id: z
|
|
@@ -81,8 +133,17 @@ export function registerCheckinTools(server) {
|
|
|
81
133
|
.describe('Optional serving container id (e.g. 1 = draft, 2 = bottle, 3 = can)'),
|
|
82
134
|
confirm: schemaConfirm,
|
|
83
135
|
},
|
|
84
|
-
}, async ({ bid, rating, shout, foursquare_id, geolat, geolng, container_id, confirm }) => {
|
|
136
|
+
}, async ({ bid, rating, shout, foursquare_id, photo_path, geolat, geolng, container_id, confirm }) => {
|
|
85
137
|
const { timezone, gmt_offset } = localTimezone();
|
|
138
|
+
let ext;
|
|
139
|
+
if (photo_path !== undefined) {
|
|
140
|
+
ext = photoExt(photo_path);
|
|
141
|
+
if (!(ext in PHOTO_CONTENT_TYPES)) {
|
|
142
|
+
throw createHelpfulError(`Unsupported photo type "${ext || '(none)'}".`, {
|
|
143
|
+
hint: 'Attach a .jpg, .jpeg, or .png file.',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
86
147
|
const form = {
|
|
87
148
|
bid,
|
|
88
149
|
rating: rating !== undefined ? rating.toFixed(2) : undefined,
|
|
@@ -93,7 +154,8 @@ export function registerCheckinTools(server) {
|
|
|
93
154
|
container_id,
|
|
94
155
|
timezone,
|
|
95
156
|
gmt_offset,
|
|
96
|
-
is_photo: 'false',
|
|
157
|
+
is_photo: photo_path !== undefined ? 'true' : 'false',
|
|
158
|
+
photo_file_ext: photo_path !== undefined ? ext : undefined,
|
|
97
159
|
platform: 'ios',
|
|
98
160
|
};
|
|
99
161
|
if (confirm !== true) {
|
|
@@ -101,10 +163,48 @@ export function registerCheckinTools(server) {
|
|
|
101
163
|
dryRun: true,
|
|
102
164
|
action: 'checkin',
|
|
103
165
|
form,
|
|
166
|
+
photo: photo_path !== undefined ? { path: photo_path, note: 'will be uploaded after the check-in is created' } : undefined,
|
|
104
167
|
note: 'Dry run — re-run with confirm: true to POST this check-in to your public Untappd feed.',
|
|
105
168
|
});
|
|
106
169
|
}
|
|
170
|
+
// Open the photo BEFORE creating the check-in, so a missing/unreadable
|
|
171
|
+
// file fails fast without leaving an orphaned photo-less check-in behind.
|
|
172
|
+
let blob;
|
|
173
|
+
if (photo_path !== undefined) {
|
|
174
|
+
if (!existsSync(photo_path))
|
|
175
|
+
throw new McpToolError(`Photo file not found: ${photo_path}`);
|
|
176
|
+
blob = await fileBlob(photo_path); // file-backed, streamed — not heap-buffered
|
|
177
|
+
}
|
|
107
178
|
const data = await client.write('POST', '/checkin/add', { form });
|
|
108
|
-
|
|
179
|
+
// Photo is a follow-up S3 upload keyed to the returned checkin_id, then an
|
|
180
|
+
// uploadComplete call. The check-in already exists at this point, so a
|
|
181
|
+
// photo failure is surfaced explicitly (photo_error) rather than thrown —
|
|
182
|
+
// never silently dropped.
|
|
183
|
+
let photo_attached = false;
|
|
184
|
+
let photo_error;
|
|
185
|
+
if (photo_path !== undefined) {
|
|
186
|
+
if (data?.photo_upload?.url && data.checkin_id) {
|
|
187
|
+
try {
|
|
188
|
+
await client.putBinary(data.photo_upload.url, blob, PHOTO_CONTENT_TYPES[ext]);
|
|
189
|
+
await client.write('POST', '/photo/uploadComplete', {
|
|
190
|
+
form: { checkin_id: data.checkin_id, destination_url: data.photo_upload.destination_url },
|
|
191
|
+
});
|
|
192
|
+
photo_attached = true;
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
photo_error = `Check-in ${data.checkin_id} was created, but attaching the photo failed: ${messageOf(e)}. Retry attaching, or remove it with untappd_delete_checkin.`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
photo_error = `Check-in ${data?.checkin_id} was created, but Untappd returned no photo upload URL, so no photo was attached.`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return textResult({
|
|
203
|
+
checked_in: true,
|
|
204
|
+
checkin_id: data?.checkin_id,
|
|
205
|
+
photo_attached,
|
|
206
|
+
...(photo_error ? { photo_error } : {}),
|
|
207
|
+
response: data,
|
|
208
|
+
});
|
|
109
209
|
});
|
|
110
210
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { textResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
3
|
+
import { client } from '../client.js';
|
|
4
|
+
export function registerDiscoverTools(server) {
|
|
5
|
+
server.registerTool('untappd_trending', {
|
|
6
|
+
title: 'Get trending beers',
|
|
7
|
+
description: 'Get the beers trending on Untappd right now, split into macro (big/widely-available) and micro ' +
|
|
8
|
+
'(craft/independent) lists. Read-only.',
|
|
9
|
+
annotations: toolAnnotations({ title: 'Get trending beers', readOnly: true, idempotent: false, openWorld: true }),
|
|
10
|
+
inputSchema: {},
|
|
11
|
+
}, async () => {
|
|
12
|
+
const data = await client.get('/beer/trending');
|
|
13
|
+
return textResult(data);
|
|
14
|
+
});
|
|
15
|
+
server.registerTool('untappd_notifications', {
|
|
16
|
+
title: 'Get your Untappd notifications',
|
|
17
|
+
description: 'Get your Untappd notifications — toasts, comments, friend requests, and badges earned on YOUR account, ' +
|
|
18
|
+
'plus news items. Read-only.',
|
|
19
|
+
annotations: toolAnnotations({ title: 'Get your Untappd notifications', readOnly: true, idempotent: false, openWorld: true }),
|
|
20
|
+
inputSchema: {
|
|
21
|
+
limit: z.number().int().min(1).max(50).optional().describe('Max notifications (1–50, default 25)'),
|
|
22
|
+
offset: z.number().int().min(0).optional().describe('Result offset for paging (default 0)'),
|
|
23
|
+
},
|
|
24
|
+
}, async ({ limit, offset }) => {
|
|
25
|
+
const data = await client.get('/notifications', { limit, offset });
|
|
26
|
+
return textResult(data);
|
|
27
|
+
});
|
|
28
|
+
server.registerTool('untappd_local_checkins', {
|
|
29
|
+
title: 'Get nearby Untappd check-ins',
|
|
30
|
+
description: 'Get recent check-ins near a location (lat/lng) — what people are drinking nearby right now. Optionally ' +
|
|
31
|
+
'widen the search radius. Read-only.',
|
|
32
|
+
annotations: toolAnnotations({ title: 'Get nearby Untappd check-ins', readOnly: true, idempotent: false, openWorld: true }),
|
|
33
|
+
inputSchema: {
|
|
34
|
+
lat: z.number().min(-90).max(90).describe('Latitude of the location'),
|
|
35
|
+
lng: z.number().min(-180).max(180).describe('Longitude of the location'),
|
|
36
|
+
limit: z.number().int().min(1).max(50).optional().describe('Max check-ins (1–50, default 25)'),
|
|
37
|
+
radius: z.number().int().min(1).max(25).optional().describe('Search radius (default per Untappd)'),
|
|
38
|
+
},
|
|
39
|
+
}, async ({ lat, lng, limit, radius }) => {
|
|
40
|
+
const data = await client.get('/thepub/local', { lat, lng, limit, radius });
|
|
41
|
+
return textResult(data);
|
|
42
|
+
});
|
|
43
|
+
}
|
package/dist/tools/venue.js
CHANGED
|
@@ -28,4 +28,18 @@ export function registerVenueTools(server) {
|
|
|
28
28
|
const data = await client.get(`/venue/info/${venue_id}`, { compact: compact ? 'true' : undefined });
|
|
29
29
|
return textResult(data);
|
|
30
30
|
});
|
|
31
|
+
server.registerTool('untappd_venue_activity', {
|
|
32
|
+
title: 'Get recent check-ins at a venue',
|
|
33
|
+
description: 'Get the recent public check-ins at a venue by its id — who was there, what they drank, and their ratings. ' +
|
|
34
|
+
'Page backwards with max_id (the pagination.max_id from a prior call). Read-only.',
|
|
35
|
+
annotations: toolAnnotations({ title: 'Get recent check-ins at a venue', readOnly: true, idempotent: false, openWorld: true }),
|
|
36
|
+
inputSchema: {
|
|
37
|
+
venue_id: z.number().int().positive().describe('Untappd venue id'),
|
|
38
|
+
limit: z.number().int().min(1).max(50).optional().describe('Max check-ins (1–50, default 25)'),
|
|
39
|
+
max_id: z.number().int().positive().optional().describe('Return check-ins older than this id (for paging)'),
|
|
40
|
+
},
|
|
41
|
+
}, async ({ venue_id, limit, max_id }) => {
|
|
42
|
+
const data = await client.get(`/venue/checkins/${venue_id}`, { limit, max_id });
|
|
43
|
+
return textResult(data);
|
|
44
|
+
});
|
|
31
45
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { textResult, toolAnnotations, schemaConfirm } from '@chrischall/mcp-utils';
|
|
3
|
+
import { client } from '../client.js';
|
|
4
|
+
const BidSchema = z.number().int().positive().describe('Untappd beer id (bid) — from untappd_search_beer');
|
|
5
|
+
export function registerWishlistTools(server) {
|
|
6
|
+
server.registerTool('untappd_wishlist_add', {
|
|
7
|
+
title: 'Add a beer to your wishlist',
|
|
8
|
+
description: 'Add a beer to YOUR Untappd wishlist by its bid. Without confirm: true it returns a dry-run preview and ' +
|
|
9
|
+
'makes NO network call; with confirm: true it adds. Writes to your account.',
|
|
10
|
+
annotations: toolAnnotations({ title: 'Add a beer to your wishlist', readOnly: false, idempotent: true, openWorld: true }),
|
|
11
|
+
inputSchema: {
|
|
12
|
+
bid: BidSchema,
|
|
13
|
+
confirm: schemaConfirm,
|
|
14
|
+
},
|
|
15
|
+
}, async ({ bid, confirm }) => {
|
|
16
|
+
if (confirm !== true) {
|
|
17
|
+
return textResult({
|
|
18
|
+
dryRun: true,
|
|
19
|
+
action: 'wishlist_add',
|
|
20
|
+
bid,
|
|
21
|
+
note: 'Dry run — re-run with confirm: true to add this beer to your wishlist.',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const data = await client.write('GET', '/user/wishlist/add', { query: { bid } });
|
|
25
|
+
return textResult({ added: true, bid, result: data?.result });
|
|
26
|
+
});
|
|
27
|
+
server.registerTool('untappd_wishlist_remove', {
|
|
28
|
+
title: 'Remove a beer from your wishlist',
|
|
29
|
+
description: 'Remove a beer from YOUR Untappd wishlist by its bid. Without confirm: true it returns a dry-run preview ' +
|
|
30
|
+
'and makes NO network call; with confirm: true it removes. Writes to your account.',
|
|
31
|
+
annotations: toolAnnotations({ title: 'Remove a beer from your wishlist', readOnly: false, idempotent: true, openWorld: true }),
|
|
32
|
+
inputSchema: {
|
|
33
|
+
bid: BidSchema,
|
|
34
|
+
confirm: schemaConfirm,
|
|
35
|
+
},
|
|
36
|
+
}, async ({ bid, confirm }) => {
|
|
37
|
+
if (confirm !== true) {
|
|
38
|
+
return textResult({
|
|
39
|
+
dryRun: true,
|
|
40
|
+
action: 'wishlist_remove',
|
|
41
|
+
bid,
|
|
42
|
+
note: 'Dry run — re-run with confirm: true to remove this beer from your wishlist.',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const data = await client.write('GET', '/user/wishlist/delete', { query: { bid } });
|
|
46
|
+
return textResult({ removed: true, bid, result: data?.result });
|
|
47
|
+
});
|
|
48
|
+
}
|
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 = '
|
|
6
|
+
export const VERSION = '1.0.0'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "untappd-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.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>",
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
"zod": "^4.4.2"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^
|
|
49
|
+
"@types/node": "^26.1.1",
|
|
50
50
|
"@vitest/coverage-v8": "^4.1.2",
|
|
51
51
|
"esbuild": "^0.28.0",
|
|
52
|
-
"typescript": "^
|
|
52
|
+
"typescript": "^7.0.2",
|
|
53
53
|
"vitest": "^4.1.2"
|
|
54
54
|
}
|
|
55
55
|
}
|
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": "
|
|
9
|
+
"version": "1.0.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "untappd-mcp",
|
|
14
|
-
"version": "
|
|
14
|
+
"version": "1.0.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|