untappd-mcp 1.2.0 → 1.3.1
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/README.md +45 -0
- package/SKILL.md +29 -0
- package/dist/bundle.js +613 -47
- package/dist/cache/db.js +293 -0
- package/dist/cache/sync.js +202 -0
- package/dist/index.js +5 -0
- package/dist/tools/cache.js +174 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/bundle.js
CHANGED
|
@@ -101,7 +101,7 @@ var require_code = __commonJS({
|
|
|
101
101
|
}
|
|
102
102
|
exports._ = _;
|
|
103
103
|
var plus = new _Code("+");
|
|
104
|
-
function
|
|
104
|
+
function str2(strs, ...args) {
|
|
105
105
|
const expr = [safeStringify(strs[0])];
|
|
106
106
|
let i = 0;
|
|
107
107
|
while (i < args.length) {
|
|
@@ -112,7 +112,7 @@ var require_code = __commonJS({
|
|
|
112
112
|
optimize(expr);
|
|
113
113
|
return new _Code(expr);
|
|
114
114
|
}
|
|
115
|
-
exports.str =
|
|
115
|
+
exports.str = str2;
|
|
116
116
|
function addCodeArg(code, arg) {
|
|
117
117
|
if (arg instanceof _Code)
|
|
118
118
|
code.push(...arg._items);
|
|
@@ -155,7 +155,7 @@ var require_code = __commonJS({
|
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
157
|
function strConcat(c1, c2) {
|
|
158
|
-
return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 :
|
|
158
|
+
return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str2`${c1}${c2}`;
|
|
159
159
|
}
|
|
160
160
|
exports.strConcat = strConcat;
|
|
161
161
|
function interpolate(x) {
|
|
@@ -1117,22 +1117,22 @@ var require_util = __commonJS({
|
|
|
1117
1117
|
return (0, codegen_1._)`${topSchemaRef}${schemaPath}${(0, codegen_1.getProperty)(keyword)}`;
|
|
1118
1118
|
}
|
|
1119
1119
|
exports.schemaRefOrVal = schemaRefOrVal;
|
|
1120
|
-
function unescapeFragment(
|
|
1121
|
-
return unescapeJsonPointer(decodeURIComponent(
|
|
1120
|
+
function unescapeFragment(str2) {
|
|
1121
|
+
return unescapeJsonPointer(decodeURIComponent(str2));
|
|
1122
1122
|
}
|
|
1123
1123
|
exports.unescapeFragment = unescapeFragment;
|
|
1124
|
-
function escapeFragment(
|
|
1125
|
-
return encodeURIComponent(escapeJsonPointer(
|
|
1124
|
+
function escapeFragment(str2) {
|
|
1125
|
+
return encodeURIComponent(escapeJsonPointer(str2));
|
|
1126
1126
|
}
|
|
1127
1127
|
exports.escapeFragment = escapeFragment;
|
|
1128
|
-
function escapeJsonPointer(
|
|
1129
|
-
if (typeof
|
|
1130
|
-
return `${
|
|
1131
|
-
return
|
|
1128
|
+
function escapeJsonPointer(str2) {
|
|
1129
|
+
if (typeof str2 == "number")
|
|
1130
|
+
return `${str2}`;
|
|
1131
|
+
return str2.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
1132
1132
|
}
|
|
1133
1133
|
exports.escapeJsonPointer = escapeJsonPointer;
|
|
1134
|
-
function unescapeJsonPointer(
|
|
1135
|
-
return
|
|
1134
|
+
function unescapeJsonPointer(str2) {
|
|
1135
|
+
return str2.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
1136
1136
|
}
|
|
1137
1137
|
exports.unescapeJsonPointer = unescapeJsonPointer;
|
|
1138
1138
|
function eachItem(xs, f) {
|
|
@@ -2157,8 +2157,8 @@ var require_json_schema_traverse = __commonJS({
|
|
|
2157
2157
|
post(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex);
|
|
2158
2158
|
}
|
|
2159
2159
|
}
|
|
2160
|
-
function escapeJsonPtr(
|
|
2161
|
-
return
|
|
2160
|
+
function escapeJsonPtr(str2) {
|
|
2161
|
+
return str2.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2162
2162
|
}
|
|
2163
2163
|
}
|
|
2164
2164
|
});
|
|
@@ -3222,10 +3222,10 @@ var require_utils = __commonJS({
|
|
|
3222
3222
|
return { host, isIPV6: false };
|
|
3223
3223
|
}
|
|
3224
3224
|
}
|
|
3225
|
-
function findToken(
|
|
3225
|
+
function findToken(str2, token) {
|
|
3226
3226
|
let ind = 0;
|
|
3227
|
-
for (let i = 0; i <
|
|
3228
|
-
if (
|
|
3227
|
+
for (let i = 0; i < str2.length; i++) {
|
|
3228
|
+
if (str2[i] === token) ind++;
|
|
3229
3229
|
}
|
|
3230
3230
|
return ind;
|
|
3231
3231
|
}
|
|
@@ -3962,7 +3962,7 @@ var require_core = __commonJS({
|
|
|
3962
3962
|
var util_1 = require_util();
|
|
3963
3963
|
var $dataRefSchema = require_data();
|
|
3964
3964
|
var uri_1 = require_uri();
|
|
3965
|
-
var defaultRegExp = (
|
|
3965
|
+
var defaultRegExp = (str2, flags) => new RegExp(str2, flags);
|
|
3966
3966
|
defaultRegExp.code = "new RegExp";
|
|
3967
3967
|
var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"];
|
|
3968
3968
|
var EXT_SCOPE_NAMES = /* @__PURE__ */ new Set([
|
|
@@ -4757,16 +4757,16 @@ var require_ucs2length = __commonJS({
|
|
|
4757
4757
|
"node_modules/ajv/dist/runtime/ucs2length.js"(exports) {
|
|
4758
4758
|
"use strict";
|
|
4759
4759
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4760
|
-
function ucs2length(
|
|
4761
|
-
const len =
|
|
4760
|
+
function ucs2length(str2) {
|
|
4761
|
+
const len = str2.length;
|
|
4762
4762
|
let length = 0;
|
|
4763
4763
|
let pos = 0;
|
|
4764
4764
|
let value;
|
|
4765
4765
|
while (pos < len) {
|
|
4766
4766
|
length++;
|
|
4767
|
-
value =
|
|
4767
|
+
value = str2.charCodeAt(pos++);
|
|
4768
4768
|
if (value >= 55296 && value <= 56319 && pos < len) {
|
|
4769
|
-
value =
|
|
4769
|
+
value = str2.charCodeAt(pos);
|
|
4770
4770
|
if ((value & 64512) === 56320)
|
|
4771
4771
|
pos++;
|
|
4772
4772
|
}
|
|
@@ -6649,8 +6649,8 @@ var require_formats = __commonJS({
|
|
|
6649
6649
|
}
|
|
6650
6650
|
var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
|
|
6651
6651
|
var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
6652
|
-
function date5(
|
|
6653
|
-
const matches = DATE.exec(
|
|
6652
|
+
function date5(str2) {
|
|
6653
|
+
const matches = DATE.exec(str2);
|
|
6654
6654
|
if (!matches)
|
|
6655
6655
|
return false;
|
|
6656
6656
|
const year = +matches[1];
|
|
@@ -6669,8 +6669,8 @@ var require_formats = __commonJS({
|
|
|
6669
6669
|
}
|
|
6670
6670
|
var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
6671
6671
|
function getTime(strictTimeZone) {
|
|
6672
|
-
return function time3(
|
|
6673
|
-
const matches = TIME.exec(
|
|
6672
|
+
return function time3(str2) {
|
|
6673
|
+
const matches = TIME.exec(str2);
|
|
6674
6674
|
if (!matches)
|
|
6675
6675
|
return false;
|
|
6676
6676
|
const hr = +matches[1];
|
|
@@ -6716,8 +6716,8 @@ var require_formats = __commonJS({
|
|
|
6716
6716
|
var DATE_TIME_SEPARATOR = /t|\s/i;
|
|
6717
6717
|
function getDateTime(strictTimeZone) {
|
|
6718
6718
|
const time3 = getTime(strictTimeZone);
|
|
6719
|
-
return function date_time(
|
|
6720
|
-
const dateTime =
|
|
6719
|
+
return function date_time(str2) {
|
|
6720
|
+
const dateTime = str2.split(DATE_TIME_SEPARATOR);
|
|
6721
6721
|
return dateTime.length === 2 && date5(dateTime[0]) && time3(dateTime[1]);
|
|
6722
6722
|
};
|
|
6723
6723
|
}
|
|
@@ -6742,13 +6742,13 @@ var require_formats = __commonJS({
|
|
|
6742
6742
|
}
|
|
6743
6743
|
var NOT_URI_FRAGMENT = /\/|:/;
|
|
6744
6744
|
var URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
|
|
6745
|
-
function uri(
|
|
6746
|
-
return NOT_URI_FRAGMENT.test(
|
|
6745
|
+
function uri(str2) {
|
|
6746
|
+
return NOT_URI_FRAGMENT.test(str2) && URI.test(str2);
|
|
6747
6747
|
}
|
|
6748
6748
|
var BYTE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
|
|
6749
|
-
function byte(
|
|
6749
|
+
function byte(str2) {
|
|
6750
6750
|
BYTE.lastIndex = 0;
|
|
6751
|
-
return BYTE.test(
|
|
6751
|
+
return BYTE.test(str2);
|
|
6752
6752
|
}
|
|
6753
6753
|
var MIN_INT32 = -(2 ** 31);
|
|
6754
6754
|
var MAX_INT32 = 2 ** 31 - 1;
|
|
@@ -6762,11 +6762,11 @@ var require_formats = __commonJS({
|
|
|
6762
6762
|
return true;
|
|
6763
6763
|
}
|
|
6764
6764
|
var Z_ANCHOR = /[^\\]\\Z/;
|
|
6765
|
-
function regex(
|
|
6766
|
-
if (Z_ANCHOR.test(
|
|
6765
|
+
function regex(str2) {
|
|
6766
|
+
if (Z_ANCHOR.test(str2))
|
|
6767
6767
|
return false;
|
|
6768
6768
|
try {
|
|
6769
|
-
new RegExp(
|
|
6769
|
+
new RegExp(str2);
|
|
6770
6770
|
return true;
|
|
6771
6771
|
} catch (e) {
|
|
6772
6772
|
return false;
|
|
@@ -11306,14 +11306,14 @@ function promiseAllObject(promisesObj) {
|
|
|
11306
11306
|
}
|
|
11307
11307
|
function randomString(length = 10) {
|
|
11308
11308
|
const chars = "abcdefghijklmnopqrstuvwxyz";
|
|
11309
|
-
let
|
|
11309
|
+
let str2 = "";
|
|
11310
11310
|
for (let i = 0; i < length; i++) {
|
|
11311
|
-
|
|
11311
|
+
str2 += chars[Math.floor(Math.random() * chars.length)];
|
|
11312
11312
|
}
|
|
11313
|
-
return
|
|
11313
|
+
return str2;
|
|
11314
11314
|
}
|
|
11315
|
-
function esc(
|
|
11316
|
-
return JSON.stringify(
|
|
11315
|
+
function esc(str2) {
|
|
11316
|
+
return JSON.stringify(str2);
|
|
11317
11317
|
}
|
|
11318
11318
|
function slugify(input) {
|
|
11319
11319
|
return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -11427,8 +11427,8 @@ var primitiveTypes = /* @__PURE__ */ new Set([
|
|
|
11427
11427
|
"symbol",
|
|
11428
11428
|
"undefined"
|
|
11429
11429
|
]);
|
|
11430
|
-
function escapeRegex(
|
|
11431
|
-
return
|
|
11430
|
+
function escapeRegex(str2) {
|
|
11431
|
+
return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11432
11432
|
}
|
|
11433
11433
|
function clone(inst, def, params) {
|
|
11434
11434
|
const cl = new inst._zod.constr(def ?? inst._zod.def);
|
|
@@ -31067,8 +31067,8 @@ function redactSecrets(text) {
|
|
|
31067
31067
|
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
31068
31068
|
}
|
|
31069
31069
|
function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
31070
|
-
const
|
|
31071
|
-
const redacted = redactSecrets(
|
|
31070
|
+
const str2 = text === null || text === void 0 ? "" : String(text);
|
|
31071
|
+
const redacted = redactSecrets(str2);
|
|
31072
31072
|
if (redacted.length <= max)
|
|
31073
31073
|
return redacted;
|
|
31074
31074
|
return `${redacted.slice(0, max)}\u2026 [truncated]`;
|
|
@@ -31184,7 +31184,7 @@ function toolAnnotations(opts = {}) {
|
|
|
31184
31184
|
}
|
|
31185
31185
|
|
|
31186
31186
|
// src/version.ts
|
|
31187
|
-
var VERSION = "1.
|
|
31187
|
+
var VERSION = "1.3.1";
|
|
31188
31188
|
|
|
31189
31189
|
// src/client.ts
|
|
31190
31190
|
import { dirname, join } from "path";
|
|
@@ -32363,6 +32363,568 @@ function registerUtilityTools(server, client2) {
|
|
|
32363
32363
|
);
|
|
32364
32364
|
}
|
|
32365
32365
|
|
|
32366
|
+
// src/cache/db.ts
|
|
32367
|
+
import { DatabaseSync } from "node:sqlite";
|
|
32368
|
+
import { mkdirSync } from "node:fs";
|
|
32369
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
32370
|
+
import { homedir } from "node:os";
|
|
32371
|
+
function asDict2(v) {
|
|
32372
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
32373
|
+
}
|
|
32374
|
+
function num(v) {
|
|
32375
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
32376
|
+
}
|
|
32377
|
+
function str(v) {
|
|
32378
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
32379
|
+
}
|
|
32380
|
+
function mapCheckinRow(username, item) {
|
|
32381
|
+
const c = asDict2(item);
|
|
32382
|
+
const checkin_id = num(c.checkin_id);
|
|
32383
|
+
if (checkin_id === null) return null;
|
|
32384
|
+
const beer = asDict2(c.beer);
|
|
32385
|
+
const brewery = asDict2(c.brewery);
|
|
32386
|
+
const venue = asDict2(c.venue);
|
|
32387
|
+
return {
|
|
32388
|
+
checkin_id,
|
|
32389
|
+
username,
|
|
32390
|
+
bid: num(beer.bid),
|
|
32391
|
+
beer_name: str(beer.beer_name),
|
|
32392
|
+
brewery_id: num(brewery.brewery_id),
|
|
32393
|
+
brewery_name: str(brewery.brewery_name),
|
|
32394
|
+
beer_style: str(beer.beer_style),
|
|
32395
|
+
abv: num(beer.beer_abv),
|
|
32396
|
+
rating: num(c.rating_score),
|
|
32397
|
+
comment: str(c.checkin_comment),
|
|
32398
|
+
venue_id: num(venue.venue_id),
|
|
32399
|
+
venue_name: str(venue.venue_name),
|
|
32400
|
+
created_at: normaliseDate(c.created_at)
|
|
32401
|
+
};
|
|
32402
|
+
}
|
|
32403
|
+
function normaliseDate(v) {
|
|
32404
|
+
const s = str(v);
|
|
32405
|
+
if (!s) return null;
|
|
32406
|
+
const t = Date.parse(s);
|
|
32407
|
+
return Number.isNaN(t) ? s : new Date(t).toISOString();
|
|
32408
|
+
}
|
|
32409
|
+
var SCHEMA = `
|
|
32410
|
+
CREATE TABLE IF NOT EXISTS checkins (
|
|
32411
|
+
checkin_id INTEGER PRIMARY KEY,
|
|
32412
|
+
username TEXT NOT NULL,
|
|
32413
|
+
bid INTEGER,
|
|
32414
|
+
beer_name TEXT,
|
|
32415
|
+
brewery_id INTEGER,
|
|
32416
|
+
brewery_name TEXT,
|
|
32417
|
+
beer_style TEXT,
|
|
32418
|
+
abv REAL,
|
|
32419
|
+
rating REAL,
|
|
32420
|
+
comment TEXT,
|
|
32421
|
+
venue_id INTEGER,
|
|
32422
|
+
venue_name TEXT,
|
|
32423
|
+
created_at TEXT
|
|
32424
|
+
);
|
|
32425
|
+
CREATE INDEX IF NOT EXISTS idx_checkins_user_bid ON checkins(username, bid);
|
|
32426
|
+
CREATE INDEX IF NOT EXISTS idx_checkins_user_brewery ON checkins(username, brewery_id);
|
|
32427
|
+
|
|
32428
|
+
CREATE TABLE IF NOT EXISTS sync_state (
|
|
32429
|
+
username TEXT PRIMARY KEY,
|
|
32430
|
+
oldest_max_id INTEGER,
|
|
32431
|
+
newest_checkin_id INTEGER,
|
|
32432
|
+
catchup_max_id INTEGER,
|
|
32433
|
+
last_synced_at TEXT,
|
|
32434
|
+
backfill_complete INTEGER NOT NULL DEFAULT 0,
|
|
32435
|
+
total_checkins INTEGER
|
|
32436
|
+
);
|
|
32437
|
+
`;
|
|
32438
|
+
var CheckinCache = class _CheckinCache {
|
|
32439
|
+
db;
|
|
32440
|
+
constructor(db) {
|
|
32441
|
+
this.db = db;
|
|
32442
|
+
this.db.exec(SCHEMA);
|
|
32443
|
+
this.migrate();
|
|
32444
|
+
}
|
|
32445
|
+
// Forward-compat for a sync_state table created before a column existed. Each
|
|
32446
|
+
// ADD COLUMN throws harmlessly if the column is already present.
|
|
32447
|
+
migrate() {
|
|
32448
|
+
for (const col of ["catchup_max_id INTEGER"]) {
|
|
32449
|
+
try {
|
|
32450
|
+
this.db.exec(`ALTER TABLE sync_state ADD COLUMN ${col}`);
|
|
32451
|
+
} catch {
|
|
32452
|
+
}
|
|
32453
|
+
}
|
|
32454
|
+
}
|
|
32455
|
+
/** Open (creating parent dirs) a file-backed cache, or `:memory:` for tests. */
|
|
32456
|
+
static open(path) {
|
|
32457
|
+
if (path !== ":memory:") mkdirSync(dirname2(path), { recursive: true });
|
|
32458
|
+
return new _CheckinCache(new DatabaseSync(path));
|
|
32459
|
+
}
|
|
32460
|
+
close() {
|
|
32461
|
+
this.db.close();
|
|
32462
|
+
}
|
|
32463
|
+
/**
|
|
32464
|
+
* Upsert a batch of rows (dedupe on checkin_id) inside one transaction and
|
|
32465
|
+
* return the NET number of newly-inserted rows (updates to existing rows count
|
|
32466
|
+
* as 0), which is what "rows added" reports.
|
|
32467
|
+
*/
|
|
32468
|
+
upsertCheckins(username, rows) {
|
|
32469
|
+
if (rows.length === 0) return 0;
|
|
32470
|
+
const key = username.toLowerCase();
|
|
32471
|
+
const before = this.cachedCount(username);
|
|
32472
|
+
const stmt = this.db.prepare(`
|
|
32473
|
+
INSERT INTO checkins
|
|
32474
|
+
(checkin_id, username, bid, beer_name, brewery_id, brewery_name, beer_style, abv, rating, comment, venue_id, venue_name, created_at)
|
|
32475
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
32476
|
+
ON CONFLICT(checkin_id) DO UPDATE SET
|
|
32477
|
+
username=excluded.username, bid=excluded.bid, beer_name=excluded.beer_name,
|
|
32478
|
+
brewery_id=excluded.brewery_id, brewery_name=excluded.brewery_name, beer_style=excluded.beer_style,
|
|
32479
|
+
abv=excluded.abv, rating=excluded.rating, comment=excluded.comment,
|
|
32480
|
+
venue_id=excluded.venue_id, venue_name=excluded.venue_name, created_at=excluded.created_at
|
|
32481
|
+
`);
|
|
32482
|
+
this.db.exec("BEGIN");
|
|
32483
|
+
try {
|
|
32484
|
+
for (const r of rows) {
|
|
32485
|
+
stmt.run(
|
|
32486
|
+
r.checkin_id,
|
|
32487
|
+
key,
|
|
32488
|
+
r.bid,
|
|
32489
|
+
r.beer_name,
|
|
32490
|
+
r.brewery_id,
|
|
32491
|
+
r.brewery_name,
|
|
32492
|
+
r.beer_style,
|
|
32493
|
+
r.abv,
|
|
32494
|
+
r.rating,
|
|
32495
|
+
r.comment,
|
|
32496
|
+
r.venue_id,
|
|
32497
|
+
r.venue_name,
|
|
32498
|
+
r.created_at
|
|
32499
|
+
);
|
|
32500
|
+
}
|
|
32501
|
+
this.db.exec("COMMIT");
|
|
32502
|
+
} catch (e) {
|
|
32503
|
+
this.db.exec("ROLLBACK");
|
|
32504
|
+
throw e;
|
|
32505
|
+
}
|
|
32506
|
+
return this.cachedCount(username) - before;
|
|
32507
|
+
}
|
|
32508
|
+
/** Total cached check-ins for a user (= distinct check-ins, since PK is checkin_id). */
|
|
32509
|
+
cachedCount(username) {
|
|
32510
|
+
const row = this.db.prepare("SELECT COUNT(*) AS n FROM checkins WHERE username = ?").get(username.toLowerCase());
|
|
32511
|
+
return row.n;
|
|
32512
|
+
}
|
|
32513
|
+
/** Distinct beers (unique bids) cached for a user. */
|
|
32514
|
+
distinctBeerCount(username) {
|
|
32515
|
+
const row = this.db.prepare("SELECT COUNT(DISTINCT bid) AS n FROM checkins WHERE username = ? AND bid IS NOT NULL").get(username.toLowerCase());
|
|
32516
|
+
return row.n;
|
|
32517
|
+
}
|
|
32518
|
+
/** Highest cached checkin_id for a user (the true newest once contiguous), or null. */
|
|
32519
|
+
newestCachedId(username) {
|
|
32520
|
+
const row = this.db.prepare("SELECT MAX(checkin_id) AS m FROM checkins WHERE username = ?").get(username.toLowerCase());
|
|
32521
|
+
return row.m;
|
|
32522
|
+
}
|
|
32523
|
+
getState(username) {
|
|
32524
|
+
const row = this.db.prepare("SELECT * FROM sync_state WHERE username = ?").get(username.toLowerCase());
|
|
32525
|
+
if (!row) return void 0;
|
|
32526
|
+
return { ...row, username: row.username, backfill_complete: row.backfill_complete === 1 };
|
|
32527
|
+
}
|
|
32528
|
+
/**
|
|
32529
|
+
* Insert-or-merge sync state. Only keys PRESENT in `patch` are written, so an
|
|
32530
|
+
* explicit `null` (e.g. clearing `oldest_max_id` when backfill completes)
|
|
32531
|
+
* overrides the stored value — a `??` merge could not do that.
|
|
32532
|
+
*/
|
|
32533
|
+
setState(username, patch) {
|
|
32534
|
+
const key = username.toLowerCase();
|
|
32535
|
+
const cur = this.getState(key);
|
|
32536
|
+
const pick2 = (k, fallback) => k in patch ? patch[k] : cur?.[k] ?? fallback;
|
|
32537
|
+
const next = {
|
|
32538
|
+
username: key,
|
|
32539
|
+
oldest_max_id: pick2("oldest_max_id", null),
|
|
32540
|
+
newest_checkin_id: pick2("newest_checkin_id", null),
|
|
32541
|
+
catchup_max_id: pick2("catchup_max_id", null),
|
|
32542
|
+
last_synced_at: pick2("last_synced_at", null),
|
|
32543
|
+
backfill_complete: pick2("backfill_complete", false),
|
|
32544
|
+
total_checkins: pick2("total_checkins", null)
|
|
32545
|
+
};
|
|
32546
|
+
this.db.prepare(
|
|
32547
|
+
`INSERT INTO sync_state (username, oldest_max_id, newest_checkin_id, catchup_max_id, last_synced_at, backfill_complete, total_checkins)
|
|
32548
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
32549
|
+
ON CONFLICT(username) DO UPDATE SET
|
|
32550
|
+
oldest_max_id=excluded.oldest_max_id, newest_checkin_id=excluded.newest_checkin_id,
|
|
32551
|
+
catchup_max_id=excluded.catchup_max_id,
|
|
32552
|
+
last_synced_at=excluded.last_synced_at, backfill_complete=excluded.backfill_complete,
|
|
32553
|
+
total_checkins=excluded.total_checkins`
|
|
32554
|
+
).run(
|
|
32555
|
+
key,
|
|
32556
|
+
next.oldest_max_id,
|
|
32557
|
+
next.newest_checkin_id,
|
|
32558
|
+
next.catchup_max_id,
|
|
32559
|
+
next.last_synced_at,
|
|
32560
|
+
next.backfill_complete ? 1 : 0,
|
|
32561
|
+
next.total_checkins
|
|
32562
|
+
);
|
|
32563
|
+
}
|
|
32564
|
+
/** Exact bid match or case-insensitive substring match on beer name. */
|
|
32565
|
+
hasHad(username, opts) {
|
|
32566
|
+
const where = ["username = ?"];
|
|
32567
|
+
const params = [username.toLowerCase()];
|
|
32568
|
+
if (opts.bid !== void 0) {
|
|
32569
|
+
where.push("bid = ?");
|
|
32570
|
+
params.push(opts.bid);
|
|
32571
|
+
}
|
|
32572
|
+
if (opts.beerName !== void 0) {
|
|
32573
|
+
where.push("beer_name LIKE ? COLLATE NOCASE");
|
|
32574
|
+
params.push(`%${escapeLike(opts.beerName)}%`);
|
|
32575
|
+
}
|
|
32576
|
+
const clause = where.join(" AND ");
|
|
32577
|
+
const agg = this.db.prepare(`SELECT COUNT(*) AS n, MAX(rating) AS best FROM checkins WHERE ${clause}`).get(...params);
|
|
32578
|
+
const matches = this.db.prepare(
|
|
32579
|
+
`SELECT checkin_id, bid, beer_name, rating, venue_name, created_at
|
|
32580
|
+
FROM checkins WHERE ${clause} ORDER BY checkin_id DESC LIMIT 25`
|
|
32581
|
+
).all(...params);
|
|
32582
|
+
return {
|
|
32583
|
+
had: agg.n > 0,
|
|
32584
|
+
count: agg.n,
|
|
32585
|
+
last_date: matches[0]?.created_at ?? null,
|
|
32586
|
+
best_rating: agg.best,
|
|
32587
|
+
matches
|
|
32588
|
+
};
|
|
32589
|
+
}
|
|
32590
|
+
query(username, filters) {
|
|
32591
|
+
const where = ["username = ?"];
|
|
32592
|
+
const params = [username.toLowerCase()];
|
|
32593
|
+
if (filters.brewery_id !== void 0) {
|
|
32594
|
+
where.push("brewery_id = ?");
|
|
32595
|
+
params.push(filters.brewery_id);
|
|
32596
|
+
}
|
|
32597
|
+
if (filters.brewery !== void 0) {
|
|
32598
|
+
where.push("brewery_name LIKE ? COLLATE NOCASE");
|
|
32599
|
+
params.push(`%${escapeLike(filters.brewery)}%`);
|
|
32600
|
+
}
|
|
32601
|
+
if (filters.style !== void 0) {
|
|
32602
|
+
where.push("beer_style LIKE ? COLLATE NOCASE");
|
|
32603
|
+
params.push(`%${escapeLike(filters.style)}%`);
|
|
32604
|
+
}
|
|
32605
|
+
if (filters.min_rating !== void 0) {
|
|
32606
|
+
where.push("rating >= ?");
|
|
32607
|
+
params.push(filters.min_rating);
|
|
32608
|
+
}
|
|
32609
|
+
if (filters.venue !== void 0) {
|
|
32610
|
+
where.push("venue_name LIKE ? COLLATE NOCASE");
|
|
32611
|
+
params.push(`%${escapeLike(filters.venue)}%`);
|
|
32612
|
+
}
|
|
32613
|
+
if (filters.venue_id !== void 0) {
|
|
32614
|
+
where.push("venue_id = ?");
|
|
32615
|
+
params.push(filters.venue_id);
|
|
32616
|
+
}
|
|
32617
|
+
if (filters.date_from !== void 0) {
|
|
32618
|
+
where.push("substr(created_at, 1, 10) >= ?");
|
|
32619
|
+
params.push(filters.date_from);
|
|
32620
|
+
}
|
|
32621
|
+
if (filters.date_to !== void 0) {
|
|
32622
|
+
where.push("substr(created_at, 1, 10) <= ?");
|
|
32623
|
+
params.push(filters.date_to);
|
|
32624
|
+
}
|
|
32625
|
+
const order = filters.sort === "oldest" ? "checkin_id ASC" : filters.sort === "highest_rated" ? "rating DESC, checkin_id DESC" : filters.sort === "lowest_rated" ? "rating ASC, checkin_id DESC" : "checkin_id DESC";
|
|
32626
|
+
const limit = Math.min(Math.max(filters.limit ?? 25, 1), 200);
|
|
32627
|
+
const rows = this.db.prepare(`SELECT * FROM checkins WHERE ${where.join(" AND ")} ORDER BY ${order} LIMIT ?`).all(...params, limit);
|
|
32628
|
+
return rows;
|
|
32629
|
+
}
|
|
32630
|
+
};
|
|
32631
|
+
function escapeLike(s) {
|
|
32632
|
+
return s.replace(/[%_]/g, " ");
|
|
32633
|
+
}
|
|
32634
|
+
function defaultCachePath() {
|
|
32635
|
+
return readEnvVar("UNTAPPD_CACHE_DB") ?? join2(homedir(), ".untappd-mcp", "checkins.db");
|
|
32636
|
+
}
|
|
32637
|
+
|
|
32638
|
+
// src/cache/sync.ts
|
|
32639
|
+
var PAGE_LIMIT = 50;
|
|
32640
|
+
async function fetchPage(client2, encodedUser, maxId) {
|
|
32641
|
+
const data = await client2.get(
|
|
32642
|
+
`/user/checkins/${encodedUser}`,
|
|
32643
|
+
{ limit: PAGE_LIMIT, max_id: maxId }
|
|
32644
|
+
);
|
|
32645
|
+
const box = data?.checkins ?? {};
|
|
32646
|
+
const items = Array.isArray(box.items) ? box.items : [];
|
|
32647
|
+
const raw = box.pagination?.max_id;
|
|
32648
|
+
const nextMaxId = typeof raw === "number" && raw > 0 ? raw : null;
|
|
32649
|
+
return { items, nextMaxId };
|
|
32650
|
+
}
|
|
32651
|
+
function rowsOf(username, items) {
|
|
32652
|
+
const rows = [];
|
|
32653
|
+
for (const it of items) {
|
|
32654
|
+
const r = mapCheckinRow(username, it);
|
|
32655
|
+
if (r) rows.push(r);
|
|
32656
|
+
}
|
|
32657
|
+
return rows;
|
|
32658
|
+
}
|
|
32659
|
+
async function fetchTotalCheckins(client2, encodedUser) {
|
|
32660
|
+
try {
|
|
32661
|
+
const info = await client2.get(`/user/info/${encodedUser}`);
|
|
32662
|
+
const t = info?.user?.stats?.total_checkins;
|
|
32663
|
+
return typeof t === "number" ? t : null;
|
|
32664
|
+
} catch {
|
|
32665
|
+
return null;
|
|
32666
|
+
}
|
|
32667
|
+
}
|
|
32668
|
+
async function syncCheckins(client2, cache, rawUsername, maxPages = 10) {
|
|
32669
|
+
const encodedUser = encodeURIComponent(rawUsername);
|
|
32670
|
+
const prior = cache.getState(rawUsername);
|
|
32671
|
+
const priorNewest = prior?.newest_checkin_id ?? null;
|
|
32672
|
+
let pages = 0;
|
|
32673
|
+
let added = 0;
|
|
32674
|
+
let newest = priorNewest;
|
|
32675
|
+
let backfillComplete = prior?.backfill_complete ?? false;
|
|
32676
|
+
let oldestMaxId = prior?.oldest_max_id ?? null;
|
|
32677
|
+
let catchupMaxId = prior?.catchup_max_id ?? null;
|
|
32678
|
+
let catchupInProgress = catchupMaxId !== null;
|
|
32679
|
+
const now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
32680
|
+
const firstFetch = async (maxId) => {
|
|
32681
|
+
try {
|
|
32682
|
+
return await fetchPage(client2, encodedUser, maxId);
|
|
32683
|
+
} catch (e) {
|
|
32684
|
+
if (e instanceof RateLimitError) throw e;
|
|
32685
|
+
throw createHelpfulError(`Could not fetch check-ins for "${rawUsername}": ${messageOf(e)}`, {
|
|
32686
|
+
hint: "Untappd only returns another user's check-ins if their account is public or they're your friend. If the account is private, add them as a friend (untappd_add_friend) first, or sync only your own account."
|
|
32687
|
+
});
|
|
32688
|
+
}
|
|
32689
|
+
};
|
|
32690
|
+
if (priorNewest !== null) {
|
|
32691
|
+
let maxId = catchupMaxId ?? void 0;
|
|
32692
|
+
let caughtUp = false;
|
|
32693
|
+
for (; pages < maxPages; ) {
|
|
32694
|
+
const page = pages === 0 ? await firstFetch(maxId) : await fetchPage(client2, encodedUser, maxId);
|
|
32695
|
+
pages++;
|
|
32696
|
+
if (page.items.length === 0) {
|
|
32697
|
+
caughtUp = true;
|
|
32698
|
+
break;
|
|
32699
|
+
}
|
|
32700
|
+
const rows = rowsOf(rawUsername, page.items);
|
|
32701
|
+
added += cache.upsertCheckins(rawUsername, rows);
|
|
32702
|
+
const oldestOnPage = rows[rows.length - 1].checkin_id;
|
|
32703
|
+
if (oldestOnPage <= priorNewest) {
|
|
32704
|
+
caughtUp = true;
|
|
32705
|
+
break;
|
|
32706
|
+
}
|
|
32707
|
+
if (page.nextMaxId === null) {
|
|
32708
|
+
caughtUp = true;
|
|
32709
|
+
backfillComplete = true;
|
|
32710
|
+
oldestMaxId = null;
|
|
32711
|
+
break;
|
|
32712
|
+
}
|
|
32713
|
+
catchupMaxId = page.nextMaxId;
|
|
32714
|
+
cache.setState(rawUsername, { catchup_max_id: catchupMaxId, last_synced_at: now() });
|
|
32715
|
+
maxId = page.nextMaxId;
|
|
32716
|
+
}
|
|
32717
|
+
if (caughtUp) {
|
|
32718
|
+
newest = cache.newestCachedId(rawUsername);
|
|
32719
|
+
catchupMaxId = null;
|
|
32720
|
+
catchupInProgress = false;
|
|
32721
|
+
cache.setState(rawUsername, {
|
|
32722
|
+
newest_checkin_id: newest,
|
|
32723
|
+
catchup_max_id: null,
|
|
32724
|
+
backfill_complete: backfillComplete,
|
|
32725
|
+
oldest_max_id: oldestMaxId,
|
|
32726
|
+
last_synced_at: now()
|
|
32727
|
+
});
|
|
32728
|
+
} else {
|
|
32729
|
+
catchupInProgress = true;
|
|
32730
|
+
}
|
|
32731
|
+
}
|
|
32732
|
+
if (!backfillComplete && !catchupInProgress) {
|
|
32733
|
+
let maxId = oldestMaxId ?? void 0;
|
|
32734
|
+
for (; pages < maxPages; ) {
|
|
32735
|
+
const first = pages === 0;
|
|
32736
|
+
const page = first ? await firstFetch(maxId) : await fetchPage(client2, encodedUser, maxId);
|
|
32737
|
+
pages++;
|
|
32738
|
+
if (page.items.length === 0) {
|
|
32739
|
+
backfillComplete = true;
|
|
32740
|
+
break;
|
|
32741
|
+
}
|
|
32742
|
+
const rows = rowsOf(rawUsername, page.items);
|
|
32743
|
+
added += cache.upsertCheckins(rawUsername, rows);
|
|
32744
|
+
if (newest === null) newest = rows[0].checkin_id;
|
|
32745
|
+
oldestMaxId = page.nextMaxId;
|
|
32746
|
+
if (page.nextMaxId === null) backfillComplete = true;
|
|
32747
|
+
cache.setState(rawUsername, {
|
|
32748
|
+
newest_checkin_id: newest,
|
|
32749
|
+
oldest_max_id: oldestMaxId,
|
|
32750
|
+
backfill_complete: backfillComplete,
|
|
32751
|
+
last_synced_at: now()
|
|
32752
|
+
});
|
|
32753
|
+
if (backfillComplete) break;
|
|
32754
|
+
maxId = page.nextMaxId ?? void 0;
|
|
32755
|
+
}
|
|
32756
|
+
}
|
|
32757
|
+
const total = await fetchTotalCheckins(client2, encodedUser);
|
|
32758
|
+
const cached2 = cache.cachedCount(rawUsername);
|
|
32759
|
+
cache.setState(rawUsername, {
|
|
32760
|
+
newest_checkin_id: newest,
|
|
32761
|
+
oldest_max_id: oldestMaxId,
|
|
32762
|
+
catchup_max_id: catchupMaxId,
|
|
32763
|
+
backfill_complete: backfillComplete,
|
|
32764
|
+
total_checkins: total,
|
|
32765
|
+
last_synced_at: now()
|
|
32766
|
+
});
|
|
32767
|
+
const backfillPercent = total && total > 0 ? Math.min(100, Math.round(cached2 / total * 100)) : backfillComplete && !catchupInProgress ? 100 : null;
|
|
32768
|
+
const anotherRunNeeded = !backfillComplete || catchupInProgress;
|
|
32769
|
+
const note = catchupInProgress ? "More new check-ins remain than fit in this run \u2014 run untappd_sync_checkins again to finish catching up the newest check-ins." : backfillComplete ? "Full history is cached. Re-run occasionally to pick up new check-ins." : `Backfill incomplete \u2014 run untappd_sync_checkins again to fetch the next ${maxPages} pages of older check-ins.`;
|
|
32770
|
+
return {
|
|
32771
|
+
username: rawUsername,
|
|
32772
|
+
rows_added: added,
|
|
32773
|
+
pages_fetched: pages,
|
|
32774
|
+
cached_checkins: cached2,
|
|
32775
|
+
distinct_beers: cache.distinctBeerCount(rawUsername),
|
|
32776
|
+
total_checkins: total,
|
|
32777
|
+
backfill_percent: backfillPercent,
|
|
32778
|
+
backfill_complete: backfillComplete,
|
|
32779
|
+
catchup_in_progress: catchupInProgress,
|
|
32780
|
+
another_run_needed: anotherRunNeeded,
|
|
32781
|
+
last_synced_at: now(),
|
|
32782
|
+
note
|
|
32783
|
+
};
|
|
32784
|
+
}
|
|
32785
|
+
|
|
32786
|
+
// src/tools/cache.ts
|
|
32787
|
+
var UsernameArg2 = external_exports.string().min(1).optional().describe("Untappd username. Omit to use your own configured account (UNTAPPD_USERNAME).");
|
|
32788
|
+
function resolveUser2(username, loginName) {
|
|
32789
|
+
const u = username ?? loginName;
|
|
32790
|
+
if (!u) {
|
|
32791
|
+
throw createHelpfulError("No username given and no configured account to fall back to.", {
|
|
32792
|
+
hint: "Pass `username`, or set UNTAPPD_USERNAME so cache tools default to your own account."
|
|
32793
|
+
});
|
|
32794
|
+
}
|
|
32795
|
+
return u;
|
|
32796
|
+
}
|
|
32797
|
+
function freshness(cache, username) {
|
|
32798
|
+
const state = cache.getState(username);
|
|
32799
|
+
const cached2 = cache.cachedCount(username);
|
|
32800
|
+
const backfillComplete = state?.backfill_complete ?? false;
|
|
32801
|
+
const percent = state?.total_checkins && state.total_checkins > 0 ? Math.min(100, Math.round(cached2 / state.total_checkins * 100)) : backfillComplete ? 100 : null;
|
|
32802
|
+
const catchupInProgress = (state?.catchup_max_id ?? null) !== null;
|
|
32803
|
+
let caveat;
|
|
32804
|
+
if (!state || state.last_synced_at === null) {
|
|
32805
|
+
caveat = "This user has never been synced \u2014 the cache is EMPTY. Run untappd_sync_checkins first; results here are not authoritative.";
|
|
32806
|
+
} else if (catchupInProgress) {
|
|
32807
|
+
caveat = "A batch of the newest check-ins is still being caught up (last sync ran out of pages). Recent check-ins may be missing \u2014 run untappd_sync_checkins again to finish.";
|
|
32808
|
+
} else if (!backfillComplete) {
|
|
32809
|
+
caveat = `Backfill is incomplete${percent !== null ? ` (~${percent}% of history cached)` : ""}. A "not found" result may be a false negative for older check-ins \u2014 run untappd_sync_checkins again to keep backfilling.`;
|
|
32810
|
+
}
|
|
32811
|
+
return {
|
|
32812
|
+
last_synced_at: state?.last_synced_at ?? null,
|
|
32813
|
+
backfill_complete: backfillComplete,
|
|
32814
|
+
catchup_in_progress: catchupInProgress,
|
|
32815
|
+
backfill_percent: percent,
|
|
32816
|
+
cached_checkins: cached2,
|
|
32817
|
+
distinct_beers: cache.distinctBeerCount(username),
|
|
32818
|
+
...caveat ? { caveat } : {}
|
|
32819
|
+
};
|
|
32820
|
+
}
|
|
32821
|
+
function registerCacheTools(server, client2, cacheProvider = defaultCacheProvider()) {
|
|
32822
|
+
server.registerTool(
|
|
32823
|
+
"untappd_sync_checkins",
|
|
32824
|
+
{
|
|
32825
|
+
title: "Sync Untappd check-ins into the local cache",
|
|
32826
|
+
description: "Fetch a user's check-ins into a local SQLite cache so history can be queried without paging the API. Incremental (stops once it reaches already-cached check-ins) and resumable: if the backfill is not yet complete it pages backwards up to max_pages per call and persists progress after every page, so run it repeatedly until backfill_complete is true. Syncing another user requires their account be public or a friend. Omit username for your own account.",
|
|
32827
|
+
annotations: toolAnnotations({ title: "Sync Untappd check-ins into the local cache", readOnly: false, idempotent: false, openWorld: true }),
|
|
32828
|
+
inputSchema: {
|
|
32829
|
+
username: UsernameArg2,
|
|
32830
|
+
max_pages: external_exports.number().int().min(1).max(50).optional().describe("Total pages (50 check-ins each) to fetch this run, incremental + backfill (default 10). Keep modest to respect the ~100 calls/hour rate limit.")
|
|
32831
|
+
}
|
|
32832
|
+
},
|
|
32833
|
+
async ({ username, max_pages }) => {
|
|
32834
|
+
const user = resolveUser2(username, client2.loginName);
|
|
32835
|
+
const summary = await syncCheckins(client2, cacheProvider(), user, max_pages ?? 10);
|
|
32836
|
+
return textResult(summary);
|
|
32837
|
+
}
|
|
32838
|
+
);
|
|
32839
|
+
server.registerTool(
|
|
32840
|
+
"untappd_cache_has_had",
|
|
32841
|
+
{
|
|
32842
|
+
title: "Check if a user has had a beer (from local cache)",
|
|
32843
|
+
description: 'Answer "has this user ever checked in this beer?" from the LOCAL cache only \u2014 NO API call. Match by exact bid, or by a case-insensitive substring of the beer name. Returns whether they had it, how many times, the best rating, the last date, and the matching check-ins. Always reports cache freshness so you can caveat incomplete data. Requires bid or beer_name. Run untappd_sync_checkins first.',
|
|
32844
|
+
annotations: toolAnnotations({ title: "Check if a user has had a beer (from local cache)", readOnly: true, idempotent: true, openWorld: false }),
|
|
32845
|
+
inputSchema: {
|
|
32846
|
+
username: UsernameArg2,
|
|
32847
|
+
bid: external_exports.number().int().positive().optional().describe("Exact Untappd beer id to look for"),
|
|
32848
|
+
beer_name: external_exports.string().min(1).optional().describe("Case-insensitive substring match on the beer name")
|
|
32849
|
+
}
|
|
32850
|
+
},
|
|
32851
|
+
async ({ username, bid, beer_name }) => {
|
|
32852
|
+
if (bid === void 0 && beer_name === void 0) {
|
|
32853
|
+
throw createHelpfulError("Provide either bid or beer_name.", { hint: "bid is exact; beer_name is a case-insensitive substring match." });
|
|
32854
|
+
}
|
|
32855
|
+
const user = resolveUser2(username, client2.loginName);
|
|
32856
|
+
const cache = cacheProvider();
|
|
32857
|
+
const result = cache.hasHad(user, { bid, beerName: beer_name });
|
|
32858
|
+
return textResult({ username: user, query: { bid, beer_name }, ...result, freshness: freshness(cache, user) });
|
|
32859
|
+
}
|
|
32860
|
+
);
|
|
32861
|
+
server.registerTool(
|
|
32862
|
+
"untappd_cache_has_had_many",
|
|
32863
|
+
{
|
|
32864
|
+
title: "Batch-check many beers against the local cache",
|
|
32865
|
+
description: "Cross-check a list of beer ids against a user's cached history in ONE call \u2014 NO API call. Returns had/not-had per bid (with count and last date when had). Ideal for checking a whole venue menu at once. Run untappd_sync_checkins first; the freshness block flags if the cache is incomplete.",
|
|
32866
|
+
annotations: toolAnnotations({ title: "Batch-check many beers against the local cache", readOnly: true, idempotent: true, openWorld: false }),
|
|
32867
|
+
inputSchema: {
|
|
32868
|
+
username: UsernameArg2,
|
|
32869
|
+
bids: external_exports.array(external_exports.number().int().positive()).min(1).max(500).describe("Beer ids to check (1\u2013500)")
|
|
32870
|
+
}
|
|
32871
|
+
},
|
|
32872
|
+
async ({ username, bids }) => {
|
|
32873
|
+
const user = resolveUser2(username, client2.loginName);
|
|
32874
|
+
const cache = cacheProvider();
|
|
32875
|
+
const seen = /* @__PURE__ */ new Set();
|
|
32876
|
+
const results = [];
|
|
32877
|
+
let hadCount = 0;
|
|
32878
|
+
for (const bid of bids) {
|
|
32879
|
+
if (seen.has(bid)) continue;
|
|
32880
|
+
seen.add(bid);
|
|
32881
|
+
const r = cache.hasHad(user, { bid });
|
|
32882
|
+
if (r.had) hadCount++;
|
|
32883
|
+
results.push({ bid, had: r.had, count: r.count, last_date: r.last_date, best_rating: r.best_rating });
|
|
32884
|
+
}
|
|
32885
|
+
return textResult({
|
|
32886
|
+
username: user,
|
|
32887
|
+
checked: seen.size,
|
|
32888
|
+
had: hadCount,
|
|
32889
|
+
not_had: seen.size - hadCount,
|
|
32890
|
+
results,
|
|
32891
|
+
freshness: freshness(cache, user)
|
|
32892
|
+
});
|
|
32893
|
+
}
|
|
32894
|
+
);
|
|
32895
|
+
server.registerTool(
|
|
32896
|
+
"untappd_cache_query",
|
|
32897
|
+
{
|
|
32898
|
+
title: "Query cached check-ins with filters",
|
|
32899
|
+
description: "Query a user's cached check-ins by brewery, style, minimum rating, venue, and/or date range, with sorting and a limit \u2014 from the LOCAL cache only, NO API call. Returns the matching check-ins plus cache freshness. Run untappd_sync_checkins first.",
|
|
32900
|
+
annotations: toolAnnotations({ title: "Query cached check-ins with filters", readOnly: true, idempotent: true, openWorld: false }),
|
|
32901
|
+
inputSchema: {
|
|
32902
|
+
username: UsernameArg2,
|
|
32903
|
+
brewery_id: external_exports.number().int().positive().optional().describe("Exact brewery id"),
|
|
32904
|
+
brewery: external_exports.string().min(1).optional().describe("Case-insensitive substring match on brewery name"),
|
|
32905
|
+
style: external_exports.string().min(1).optional().describe('Case-insensitive substring match on beer style (e.g. "IPA")'),
|
|
32906
|
+
min_rating: external_exports.number().min(0).max(5).optional().describe("Only check-ins you rated at least this (0\u20135)"),
|
|
32907
|
+
venue: external_exports.string().min(1).optional().describe("Case-insensitive substring match on venue name"),
|
|
32908
|
+
venue_id: external_exports.number().int().positive().optional().describe("Exact venue id"),
|
|
32909
|
+
date_from: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Only check-ins on/after this date (YYYY-MM-DD, UTC)"),
|
|
32910
|
+
date_to: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Only check-ins on/before this date (YYYY-MM-DD, UTC)"),
|
|
32911
|
+
sort: external_exports.enum(["recent", "oldest", "highest_rated", "lowest_rated"]).optional().describe("Sort order (default recent first)"),
|
|
32912
|
+
limit: external_exports.number().int().min(1).max(200).optional().describe("Max rows (1\u2013200, default 25)")
|
|
32913
|
+
}
|
|
32914
|
+
},
|
|
32915
|
+
async ({ username, ...filters }) => {
|
|
32916
|
+
const user = resolveUser2(username, client2.loginName);
|
|
32917
|
+
const cache = cacheProvider();
|
|
32918
|
+
const rows = cache.query(user, filters);
|
|
32919
|
+
return textResult({ username: user, count: rows.length, filters, results: rows, freshness: freshness(cache, user) });
|
|
32920
|
+
}
|
|
32921
|
+
);
|
|
32922
|
+
}
|
|
32923
|
+
function defaultCacheProvider() {
|
|
32924
|
+
let cache;
|
|
32925
|
+
return () => cache ??= CheckinCache.open(defaultCachePath());
|
|
32926
|
+
}
|
|
32927
|
+
|
|
32366
32928
|
// src/index.ts
|
|
32367
32929
|
var client = new UntappdClient();
|
|
32368
32930
|
await runMcp({
|
|
@@ -32380,6 +32942,10 @@ await runMcp({
|
|
|
32380
32942
|
(s) => registerFriendActionTools(s, client),
|
|
32381
32943
|
(s) => registerWishlistTools(s, client),
|
|
32382
32944
|
(s) => registerCheckinTools(s, client),
|
|
32383
|
-
(s) => registerUtilityTools(s, client)
|
|
32945
|
+
(s) => registerUtilityTools(s, client),
|
|
32946
|
+
// Cache tools live only on the Node/stdio server: they need a local SQLite
|
|
32947
|
+
// file, which the filesystem-less Cloudflare Worker (src/worker.ts) can't
|
|
32948
|
+
// provide, so they are intentionally NOT registered there.
|
|
32949
|
+
(s) => registerCacheTools(s, client)
|
|
32384
32950
|
]
|
|
32385
32951
|
});
|