untappd-mcp 1.3.1 → 1.4.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/README.md +15 -6
- package/SKILL.md +5 -5
- package/dist/bundle.js +236 -138
- package/dist/cache/db.js +36 -264
- package/dist/cache/store.js +333 -0
- package/dist/cache/sync.js +11 -11
- package/dist/index.js +12 -4
- package/dist/tools/cache.js +24 -30
- package/dist/tools/diagnostics.js +33 -0
- package/dist/tools/utilities.js +10 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/bundle.js
CHANGED
|
@@ -31184,7 +31184,7 @@ function toolAnnotations(opts = {}) {
|
|
|
31184
31184
|
}
|
|
31185
31185
|
|
|
31186
31186
|
// src/version.ts
|
|
31187
|
-
var VERSION = "1.
|
|
31187
|
+
var VERSION = "1.4.0";
|
|
31188
31188
|
|
|
31189
31189
|
// src/client.ts
|
|
31190
31190
|
import { dirname, join } from "path";
|
|
@@ -32333,21 +32333,46 @@ function registerCheckinTools(server, client2) {
|
|
|
32333
32333
|
);
|
|
32334
32334
|
}
|
|
32335
32335
|
|
|
32336
|
+
// src/tools/diagnostics.ts
|
|
32337
|
+
function registeredToolNames(server) {
|
|
32338
|
+
const reg = server._registeredTools;
|
|
32339
|
+
return reg ? Object.keys(reg).sort() : [];
|
|
32340
|
+
}
|
|
32341
|
+
function toolsHash(names) {
|
|
32342
|
+
let h = 2166136261;
|
|
32343
|
+
const s = names.join(",");
|
|
32344
|
+
for (let i = 0; i < s.length; i++) {
|
|
32345
|
+
h ^= s.charCodeAt(i);
|
|
32346
|
+
h = Math.imul(h, 16777619);
|
|
32347
|
+
}
|
|
32348
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
32349
|
+
}
|
|
32350
|
+
function toolInventory(server) {
|
|
32351
|
+
const names = registeredToolNames(server);
|
|
32352
|
+
return { tool_count: names.length, tools_hash: toolsHash(names), tools: names };
|
|
32353
|
+
}
|
|
32354
|
+
function logRegisteredTools(server, context) {
|
|
32355
|
+
const names = registeredToolNames(server);
|
|
32356
|
+
console.error(`[untappd-mcp] ${context}: ${names.length} tools registered [${toolsHash(names)}]: ${names.join(", ")}`);
|
|
32357
|
+
}
|
|
32358
|
+
|
|
32336
32359
|
// src/tools/utilities.ts
|
|
32337
32360
|
function registerUtilityTools(server, client2) {
|
|
32338
32361
|
server.registerTool(
|
|
32339
32362
|
"untappd_healthcheck",
|
|
32340
32363
|
{
|
|
32341
32364
|
title: "Untappd healthcheck",
|
|
32342
|
-
description: "Verify Untappd connectivity and that credentials are configured and can log in. Performs a lightweight authenticated request (your recent feed) and reports whether it succeeded. Read-only.",
|
|
32365
|
+
description: "Verify Untappd connectivity and that credentials are configured and can log in. Performs a lightweight authenticated request (your recent feed) and reports whether it succeeded, plus the running server version and the exact set of tools this build exposes (count, a stable hash, and their names) so you can confirm which build is live. Read-only.",
|
|
32343
32366
|
annotations: toolAnnotations({ title: "Untappd healthcheck", readOnly: true, idempotent: true, openWorld: true }),
|
|
32344
32367
|
inputSchema: {}
|
|
32345
32368
|
},
|
|
32346
32369
|
async () => {
|
|
32370
|
+
const build = { server_version: VERSION, ...toolInventory(server) };
|
|
32347
32371
|
if (!client2.configured) {
|
|
32348
32372
|
return textResult({
|
|
32349
32373
|
ok: false,
|
|
32350
32374
|
configured: false,
|
|
32375
|
+
...build,
|
|
32351
32376
|
note: "Untappd credentials are not set. Configure UNTAPPD_USERNAME, UNTAPPD_PASSWORD, UNTAPPD_CLIENT_ID, and UNTAPPD_CLIENT_SECRET."
|
|
32352
32377
|
});
|
|
32353
32378
|
}
|
|
@@ -32357,17 +32382,14 @@ function registerUtilityTools(server, client2) {
|
|
|
32357
32382
|
configured: true,
|
|
32358
32383
|
account: client2.loginName,
|
|
32359
32384
|
feed_reachable: feed?.checkins !== void 0,
|
|
32385
|
+
...build,
|
|
32360
32386
|
note: "Logged in to Untappd and fetched the friend feed successfully."
|
|
32361
32387
|
});
|
|
32362
32388
|
}
|
|
32363
32389
|
);
|
|
32364
32390
|
}
|
|
32365
32391
|
|
|
32366
|
-
// src/cache/
|
|
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";
|
|
32392
|
+
// src/cache/store.ts
|
|
32371
32393
|
function asDict2(v) {
|
|
32372
32394
|
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
32373
32395
|
}
|
|
@@ -32406,60 +32428,47 @@ function normaliseDate(v) {
|
|
|
32406
32428
|
const t = Date.parse(s);
|
|
32407
32429
|
return Number.isNaN(t) ? s : new Date(t).toISOString();
|
|
32408
32430
|
}
|
|
32409
|
-
var
|
|
32410
|
-
CREATE TABLE IF NOT EXISTS checkins (
|
|
32411
|
-
|
|
32412
|
-
|
|
32413
|
-
|
|
32414
|
-
|
|
32415
|
-
|
|
32416
|
-
|
|
32417
|
-
|
|
32418
|
-
|
|
32419
|
-
|
|
32420
|
-
|
|
32421
|
-
|
|
32422
|
-
|
|
32423
|
-
|
|
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
|
-
|
|
32429
|
-
|
|
32430
|
-
|
|
32431
|
-
|
|
32432
|
-
|
|
32433
|
-
|
|
32434
|
-
|
|
32435
|
-
|
|
32436
|
-
|
|
32437
|
-
|
|
32438
|
-
var
|
|
32439
|
-
db;
|
|
32431
|
+
var SCHEMA_STATEMENTS = [
|
|
32432
|
+
`CREATE TABLE IF NOT EXISTS checkins (
|
|
32433
|
+
checkin_id INTEGER PRIMARY KEY,
|
|
32434
|
+
username TEXT NOT NULL,
|
|
32435
|
+
bid INTEGER,
|
|
32436
|
+
beer_name TEXT,
|
|
32437
|
+
brewery_id INTEGER,
|
|
32438
|
+
brewery_name TEXT,
|
|
32439
|
+
beer_style TEXT,
|
|
32440
|
+
abv REAL,
|
|
32441
|
+
rating REAL,
|
|
32442
|
+
comment TEXT,
|
|
32443
|
+
venue_id INTEGER,
|
|
32444
|
+
venue_name TEXT,
|
|
32445
|
+
created_at TEXT
|
|
32446
|
+
)`,
|
|
32447
|
+
`CREATE INDEX IF NOT EXISTS idx_checkins_user_bid ON checkins(username, bid)`,
|
|
32448
|
+
`CREATE INDEX IF NOT EXISTS idx_checkins_user_brewery ON checkins(username, brewery_id)`,
|
|
32449
|
+
`CREATE TABLE IF NOT EXISTS sync_state (
|
|
32450
|
+
username TEXT PRIMARY KEY,
|
|
32451
|
+
oldest_max_id INTEGER,
|
|
32452
|
+
newest_checkin_id INTEGER,
|
|
32453
|
+
catchup_max_id INTEGER,
|
|
32454
|
+
last_synced_at TEXT,
|
|
32455
|
+
backfill_complete INTEGER NOT NULL DEFAULT 0,
|
|
32456
|
+
total_checkins INTEGER
|
|
32457
|
+
)`
|
|
32458
|
+
];
|
|
32459
|
+
var MIGRATIONS = ["ALTER TABLE sync_state ADD COLUMN catchup_max_id INTEGER"];
|
|
32460
|
+
var CheckinStoreCore = class {
|
|
32440
32461
|
constructor(db) {
|
|
32441
32462
|
this.db = db;
|
|
32442
|
-
this.db.
|
|
32443
|
-
|
|
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"]) {
|
|
32463
|
+
for (const stmt of SCHEMA_STATEMENTS) this.db.execScript(stmt);
|
|
32464
|
+
for (const mig of MIGRATIONS) {
|
|
32449
32465
|
try {
|
|
32450
|
-
this.db.
|
|
32466
|
+
this.db.execScript(mig);
|
|
32451
32467
|
} catch {
|
|
32452
32468
|
}
|
|
32453
32469
|
}
|
|
32454
32470
|
}
|
|
32455
|
-
|
|
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
|
-
}
|
|
32471
|
+
db;
|
|
32463
32472
|
/**
|
|
32464
32473
|
* Upsert a batch of rows (dedupe on checkin_id) inside one transaction and
|
|
32465
32474
|
* return the NET number of newly-inserted rows (updates to existing rows count
|
|
@@ -32469,7 +32478,7 @@ var CheckinCache = class _CheckinCache {
|
|
|
32469
32478
|
if (rows.length === 0) return 0;
|
|
32470
32479
|
const key = username.toLowerCase();
|
|
32471
32480
|
const before = this.cachedCount(username);
|
|
32472
|
-
const
|
|
32481
|
+
const sql = `
|
|
32473
32482
|
INSERT INTO checkins
|
|
32474
32483
|
(checkin_id, username, bid, beer_name, brewery_id, brewery_name, beer_style, abv, rating, comment, venue_id, venue_name, created_at)
|
|
32475
32484
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -32477,12 +32486,10 @@ var CheckinCache = class _CheckinCache {
|
|
|
32477
32486
|
username=excluded.username, bid=excluded.bid, beer_name=excluded.beer_name,
|
|
32478
32487
|
brewery_id=excluded.brewery_id, brewery_name=excluded.brewery_name, beer_style=excluded.beer_style,
|
|
32479
32488
|
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 {
|
|
32489
|
+
venue_id=excluded.venue_id, venue_name=excluded.venue_name, created_at=excluded.created_at`;
|
|
32490
|
+
this.db.transaction(() => {
|
|
32484
32491
|
for (const r of rows) {
|
|
32485
|
-
|
|
32492
|
+
this.db.run(sql, [
|
|
32486
32493
|
r.checkin_id,
|
|
32487
32494
|
key,
|
|
32488
32495
|
r.bid,
|
|
@@ -32496,34 +32503,41 @@ var CheckinCache = class _CheckinCache {
|
|
|
32496
32503
|
r.venue_id,
|
|
32497
32504
|
r.venue_name,
|
|
32498
32505
|
r.created_at
|
|
32499
|
-
);
|
|
32506
|
+
]);
|
|
32500
32507
|
}
|
|
32501
|
-
|
|
32502
|
-
} catch (e) {
|
|
32503
|
-
this.db.exec("ROLLBACK");
|
|
32504
|
-
throw e;
|
|
32505
|
-
}
|
|
32508
|
+
});
|
|
32506
32509
|
return this.cachedCount(username) - before;
|
|
32507
32510
|
}
|
|
32508
32511
|
/** Total cached check-ins for a user (= distinct check-ins, since PK is checkin_id). */
|
|
32509
32512
|
cachedCount(username) {
|
|
32510
|
-
const row = this.db.
|
|
32511
|
-
return row
|
|
32513
|
+
const row = this.db.get("SELECT COUNT(*) AS n FROM checkins WHERE username = ?", [username.toLowerCase()]);
|
|
32514
|
+
return Number(row?.n ?? 0);
|
|
32512
32515
|
}
|
|
32513
32516
|
/** Distinct beers (unique bids) cached for a user. */
|
|
32514
32517
|
distinctBeerCount(username) {
|
|
32515
|
-
const row = this.db.
|
|
32516
|
-
|
|
32518
|
+
const row = this.db.get(
|
|
32519
|
+
"SELECT COUNT(DISTINCT bid) AS n FROM checkins WHERE username = ? AND bid IS NOT NULL",
|
|
32520
|
+
[username.toLowerCase()]
|
|
32521
|
+
);
|
|
32522
|
+
return Number(row?.n ?? 0);
|
|
32517
32523
|
}
|
|
32518
32524
|
/** Highest cached checkin_id for a user (the true newest once contiguous), or null. */
|
|
32519
32525
|
newestCachedId(username) {
|
|
32520
|
-
const row = this.db.
|
|
32521
|
-
return row
|
|
32526
|
+
const row = this.db.get("SELECT MAX(checkin_id) AS m FROM checkins WHERE username = ?", [username.toLowerCase()]);
|
|
32527
|
+
return num(row?.m);
|
|
32522
32528
|
}
|
|
32523
32529
|
getState(username) {
|
|
32524
|
-
const row = this.db.
|
|
32530
|
+
const row = this.db.get("SELECT * FROM sync_state WHERE username = ?", [username.toLowerCase()]);
|
|
32525
32531
|
if (!row) return void 0;
|
|
32526
|
-
return {
|
|
32532
|
+
return {
|
|
32533
|
+
username: String(row.username),
|
|
32534
|
+
oldest_max_id: num(row.oldest_max_id),
|
|
32535
|
+
newest_checkin_id: num(row.newest_checkin_id),
|
|
32536
|
+
catchup_max_id: num(row.catchup_max_id),
|
|
32537
|
+
last_synced_at: str(row.last_synced_at),
|
|
32538
|
+
backfill_complete: Number(row.backfill_complete) === 1,
|
|
32539
|
+
total_checkins: num(row.total_checkins)
|
|
32540
|
+
};
|
|
32527
32541
|
}
|
|
32528
32542
|
/**
|
|
32529
32543
|
* Insert-or-merge sync state. Only keys PRESENT in `patch` are written, so an
|
|
@@ -32543,22 +32557,23 @@ var CheckinCache = class _CheckinCache {
|
|
|
32543
32557
|
backfill_complete: pick2("backfill_complete", false),
|
|
32544
32558
|
total_checkins: pick2("total_checkins", null)
|
|
32545
32559
|
};
|
|
32546
|
-
this.db.
|
|
32560
|
+
this.db.run(
|
|
32547
32561
|
`INSERT INTO sync_state (username, oldest_max_id, newest_checkin_id, catchup_max_id, last_synced_at, backfill_complete, total_checkins)
|
|
32548
|
-
|
|
32549
|
-
|
|
32550
|
-
|
|
32551
|
-
|
|
32552
|
-
|
|
32553
|
-
|
|
32554
|
-
|
|
32555
|
-
|
|
32556
|
-
|
|
32557
|
-
|
|
32558
|
-
|
|
32559
|
-
|
|
32560
|
-
|
|
32561
|
-
|
|
32562
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
32563
|
+
ON CONFLICT(username) DO UPDATE SET
|
|
32564
|
+
oldest_max_id=excluded.oldest_max_id, newest_checkin_id=excluded.newest_checkin_id,
|
|
32565
|
+
catchup_max_id=excluded.catchup_max_id,
|
|
32566
|
+
last_synced_at=excluded.last_synced_at, backfill_complete=excluded.backfill_complete,
|
|
32567
|
+
total_checkins=excluded.total_checkins`,
|
|
32568
|
+
[
|
|
32569
|
+
key,
|
|
32570
|
+
next.oldest_max_id,
|
|
32571
|
+
next.newest_checkin_id,
|
|
32572
|
+
next.catchup_max_id,
|
|
32573
|
+
next.last_synced_at,
|
|
32574
|
+
next.backfill_complete ? 1 : 0,
|
|
32575
|
+
next.total_checkins
|
|
32576
|
+
]
|
|
32562
32577
|
);
|
|
32563
32578
|
}
|
|
32564
32579
|
/** Exact bid match or case-insensitive substring match on beer name. */
|
|
@@ -32574,16 +32589,18 @@ var CheckinCache = class _CheckinCache {
|
|
|
32574
32589
|
params.push(`%${escapeLike(opts.beerName)}%`);
|
|
32575
32590
|
}
|
|
32576
32591
|
const clause = where.join(" AND ");
|
|
32577
|
-
const agg = this.db.
|
|
32578
|
-
const matches = this.db.
|
|
32592
|
+
const agg = this.db.get(`SELECT COUNT(*) AS n, MAX(rating) AS best FROM checkins WHERE ${clause}`, params);
|
|
32593
|
+
const matches = this.db.all(
|
|
32579
32594
|
`SELECT checkin_id, bid, beer_name, rating, venue_name, created_at
|
|
32580
|
-
|
|
32581
|
-
|
|
32595
|
+
FROM checkins WHERE ${clause} ORDER BY checkin_id DESC LIMIT 25`,
|
|
32596
|
+
params
|
|
32597
|
+
);
|
|
32598
|
+
const count2 = Number(agg?.n ?? 0);
|
|
32582
32599
|
return {
|
|
32583
|
-
had:
|
|
32584
|
-
count:
|
|
32600
|
+
had: count2 > 0,
|
|
32601
|
+
count: count2,
|
|
32585
32602
|
last_date: matches[0]?.created_at ?? null,
|
|
32586
|
-
best_rating: agg
|
|
32603
|
+
best_rating: num(agg?.best),
|
|
32587
32604
|
matches
|
|
32588
32605
|
};
|
|
32589
32606
|
}
|
|
@@ -32624,16 +32641,45 @@ var CheckinCache = class _CheckinCache {
|
|
|
32624
32641
|
}
|
|
32625
32642
|
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
32643
|
const limit = Math.min(Math.max(filters.limit ?? 25, 1), 200);
|
|
32627
|
-
|
|
32628
|
-
|
|
32644
|
+
return this.db.all(
|
|
32645
|
+
`SELECT * FROM checkins WHERE ${where.join(" AND ")} ORDER BY ${order} LIMIT ?`,
|
|
32646
|
+
[...params, limit]
|
|
32647
|
+
);
|
|
32648
|
+
}
|
|
32649
|
+
};
|
|
32650
|
+
var LocalCacheStore = class {
|
|
32651
|
+
constructor(core) {
|
|
32652
|
+
this.core = core;
|
|
32653
|
+
}
|
|
32654
|
+
core;
|
|
32655
|
+
async upsertCheckins(username, rows) {
|
|
32656
|
+
return this.core.upsertCheckins(username, rows);
|
|
32657
|
+
}
|
|
32658
|
+
async cachedCount(username) {
|
|
32659
|
+
return this.core.cachedCount(username);
|
|
32660
|
+
}
|
|
32661
|
+
async distinctBeerCount(username) {
|
|
32662
|
+
return this.core.distinctBeerCount(username);
|
|
32663
|
+
}
|
|
32664
|
+
async newestCachedId(username) {
|
|
32665
|
+
return this.core.newestCachedId(username);
|
|
32666
|
+
}
|
|
32667
|
+
async getState(username) {
|
|
32668
|
+
return this.core.getState(username);
|
|
32669
|
+
}
|
|
32670
|
+
async setState(username, patch) {
|
|
32671
|
+
this.core.setState(username, patch);
|
|
32672
|
+
}
|
|
32673
|
+
async hasHad(username, opts) {
|
|
32674
|
+
return this.core.hasHad(username, opts);
|
|
32675
|
+
}
|
|
32676
|
+
async query(username, filters) {
|
|
32677
|
+
return this.core.query(username, filters);
|
|
32629
32678
|
}
|
|
32630
32679
|
};
|
|
32631
32680
|
function escapeLike(s) {
|
|
32632
32681
|
return s.replace(/[%_]/g, " ");
|
|
32633
32682
|
}
|
|
32634
|
-
function defaultCachePath() {
|
|
32635
|
-
return readEnvVar("UNTAPPD_CACHE_DB") ?? join2(homedir(), ".untappd-mcp", "checkins.db");
|
|
32636
|
-
}
|
|
32637
32683
|
|
|
32638
32684
|
// src/cache/sync.ts
|
|
32639
32685
|
var PAGE_LIMIT = 50;
|
|
@@ -32667,7 +32713,7 @@ async function fetchTotalCheckins(client2, encodedUser) {
|
|
|
32667
32713
|
}
|
|
32668
32714
|
async function syncCheckins(client2, cache, rawUsername, maxPages = 10) {
|
|
32669
32715
|
const encodedUser = encodeURIComponent(rawUsername);
|
|
32670
|
-
const prior = cache.getState(rawUsername);
|
|
32716
|
+
const prior = await cache.getState(rawUsername);
|
|
32671
32717
|
const priorNewest = prior?.newest_checkin_id ?? null;
|
|
32672
32718
|
let pages = 0;
|
|
32673
32719
|
let added = 0;
|
|
@@ -32698,7 +32744,7 @@ async function syncCheckins(client2, cache, rawUsername, maxPages = 10) {
|
|
|
32698
32744
|
break;
|
|
32699
32745
|
}
|
|
32700
32746
|
const rows = rowsOf(rawUsername, page.items);
|
|
32701
|
-
added += cache.upsertCheckins(rawUsername, rows);
|
|
32747
|
+
added += await cache.upsertCheckins(rawUsername, rows);
|
|
32702
32748
|
const oldestOnPage = rows[rows.length - 1].checkin_id;
|
|
32703
32749
|
if (oldestOnPage <= priorNewest) {
|
|
32704
32750
|
caughtUp = true;
|
|
@@ -32711,14 +32757,14 @@ async function syncCheckins(client2, cache, rawUsername, maxPages = 10) {
|
|
|
32711
32757
|
break;
|
|
32712
32758
|
}
|
|
32713
32759
|
catchupMaxId = page.nextMaxId;
|
|
32714
|
-
cache.setState(rawUsername, { catchup_max_id: catchupMaxId, last_synced_at: now() });
|
|
32760
|
+
await cache.setState(rawUsername, { catchup_max_id: catchupMaxId, last_synced_at: now() });
|
|
32715
32761
|
maxId = page.nextMaxId;
|
|
32716
32762
|
}
|
|
32717
32763
|
if (caughtUp) {
|
|
32718
|
-
newest = cache.newestCachedId(rawUsername);
|
|
32764
|
+
newest = await cache.newestCachedId(rawUsername);
|
|
32719
32765
|
catchupMaxId = null;
|
|
32720
32766
|
catchupInProgress = false;
|
|
32721
|
-
cache.setState(rawUsername, {
|
|
32767
|
+
await cache.setState(rawUsername, {
|
|
32722
32768
|
newest_checkin_id: newest,
|
|
32723
32769
|
catchup_max_id: null,
|
|
32724
32770
|
backfill_complete: backfillComplete,
|
|
@@ -32740,11 +32786,11 @@ async function syncCheckins(client2, cache, rawUsername, maxPages = 10) {
|
|
|
32740
32786
|
break;
|
|
32741
32787
|
}
|
|
32742
32788
|
const rows = rowsOf(rawUsername, page.items);
|
|
32743
|
-
added += cache.upsertCheckins(rawUsername, rows);
|
|
32789
|
+
added += await cache.upsertCheckins(rawUsername, rows);
|
|
32744
32790
|
if (newest === null) newest = rows[0].checkin_id;
|
|
32745
32791
|
oldestMaxId = page.nextMaxId;
|
|
32746
32792
|
if (page.nextMaxId === null) backfillComplete = true;
|
|
32747
|
-
cache.setState(rawUsername, {
|
|
32793
|
+
await cache.setState(rawUsername, {
|
|
32748
32794
|
newest_checkin_id: newest,
|
|
32749
32795
|
oldest_max_id: oldestMaxId,
|
|
32750
32796
|
backfill_complete: backfillComplete,
|
|
@@ -32755,8 +32801,8 @@ async function syncCheckins(client2, cache, rawUsername, maxPages = 10) {
|
|
|
32755
32801
|
}
|
|
32756
32802
|
}
|
|
32757
32803
|
const total = await fetchTotalCheckins(client2, encodedUser);
|
|
32758
|
-
const cached2 = cache.cachedCount(rawUsername);
|
|
32759
|
-
cache.setState(rawUsername, {
|
|
32804
|
+
const cached2 = await cache.cachedCount(rawUsername);
|
|
32805
|
+
await cache.setState(rawUsername, {
|
|
32760
32806
|
newest_checkin_id: newest,
|
|
32761
32807
|
oldest_max_id: oldestMaxId,
|
|
32762
32808
|
catchup_max_id: catchupMaxId,
|
|
@@ -32772,7 +32818,7 @@ async function syncCheckins(client2, cache, rawUsername, maxPages = 10) {
|
|
|
32772
32818
|
rows_added: added,
|
|
32773
32819
|
pages_fetched: pages,
|
|
32774
32820
|
cached_checkins: cached2,
|
|
32775
|
-
distinct_beers: cache.distinctBeerCount(rawUsername),
|
|
32821
|
+
distinct_beers: await cache.distinctBeerCount(rawUsername),
|
|
32776
32822
|
total_checkins: total,
|
|
32777
32823
|
backfill_percent: backfillPercent,
|
|
32778
32824
|
backfill_complete: backfillComplete,
|
|
@@ -32794,9 +32840,9 @@ function resolveUser2(username, loginName) {
|
|
|
32794
32840
|
}
|
|
32795
32841
|
return u;
|
|
32796
32842
|
}
|
|
32797
|
-
function freshness(cache, username) {
|
|
32798
|
-
const state = cache.getState(username);
|
|
32799
|
-
const cached2 = cache.cachedCount(username);
|
|
32843
|
+
async function freshness(cache, username) {
|
|
32844
|
+
const state = await cache.getState(username);
|
|
32845
|
+
const cached2 = await cache.cachedCount(username);
|
|
32800
32846
|
const backfillComplete = state?.backfill_complete ?? false;
|
|
32801
32847
|
const percent = state?.total_checkins && state.total_checkins > 0 ? Math.min(100, Math.round(cached2 / state.total_checkins * 100)) : backfillComplete ? 100 : null;
|
|
32802
32848
|
const catchupInProgress = (state?.catchup_max_id ?? null) !== null;
|
|
@@ -32814,17 +32860,17 @@ function freshness(cache, username) {
|
|
|
32814
32860
|
catchup_in_progress: catchupInProgress,
|
|
32815
32861
|
backfill_percent: percent,
|
|
32816
32862
|
cached_checkins: cached2,
|
|
32817
|
-
distinct_beers: cache.distinctBeerCount(username),
|
|
32863
|
+
distinct_beers: await cache.distinctBeerCount(username),
|
|
32818
32864
|
...caveat ? { caveat } : {}
|
|
32819
32865
|
};
|
|
32820
32866
|
}
|
|
32821
|
-
function registerCacheTools(server, client2, cacheProvider
|
|
32867
|
+
function registerCacheTools(server, client2, cacheProvider) {
|
|
32822
32868
|
server.registerTool(
|
|
32823
32869
|
"untappd_sync_checkins",
|
|
32824
32870
|
{
|
|
32825
|
-
title: "Sync Untappd check-ins into the
|
|
32826
|
-
description: "Fetch a user's check-ins into a
|
|
32827
|
-
annotations: toolAnnotations({ title: "Sync Untappd check-ins into the
|
|
32871
|
+
title: "Sync Untappd check-ins into the cache",
|
|
32872
|
+
description: "Fetch a user's check-ins into a persistent 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 another_run_needed is false. Syncing another user requires their account be public or a friend. Omit username for your own account.",
|
|
32873
|
+
annotations: toolAnnotations({ title: "Sync Untappd check-ins into the cache", readOnly: false, idempotent: false, openWorld: true }),
|
|
32828
32874
|
inputSchema: {
|
|
32829
32875
|
username: UsernameArg2,
|
|
32830
32876
|
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.")
|
|
@@ -32839,9 +32885,9 @@ function registerCacheTools(server, client2, cacheProvider = defaultCacheProvide
|
|
|
32839
32885
|
server.registerTool(
|
|
32840
32886
|
"untappd_cache_has_had",
|
|
32841
32887
|
{
|
|
32842
|
-
title: "Check if a user has had a beer (from
|
|
32843
|
-
description: 'Answer "has this user ever checked in this beer?" from the
|
|
32844
|
-
annotations: toolAnnotations({ title: "Check if a user has had a beer (from
|
|
32888
|
+
title: "Check if a user has had a beer (from the cache)",
|
|
32889
|
+
description: 'Answer "has this user ever checked in this beer?" from the 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.',
|
|
32890
|
+
annotations: toolAnnotations({ title: "Check if a user has had a beer (from the cache)", readOnly: true, idempotent: true, openWorld: false }),
|
|
32845
32891
|
inputSchema: {
|
|
32846
32892
|
username: UsernameArg2,
|
|
32847
32893
|
bid: external_exports.number().int().positive().optional().describe("Exact Untappd beer id to look for"),
|
|
@@ -32854,16 +32900,16 @@ function registerCacheTools(server, client2, cacheProvider = defaultCacheProvide
|
|
|
32854
32900
|
}
|
|
32855
32901
|
const user = resolveUser2(username, client2.loginName);
|
|
32856
32902
|
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) });
|
|
32903
|
+
const result = await cache.hasHad(user, { bid, beerName: beer_name });
|
|
32904
|
+
return textResult({ username: user, query: { bid, beer_name }, ...result, freshness: await freshness(cache, user) });
|
|
32859
32905
|
}
|
|
32860
32906
|
);
|
|
32861
32907
|
server.registerTool(
|
|
32862
32908
|
"untappd_cache_has_had_many",
|
|
32863
32909
|
{
|
|
32864
|
-
title: "Batch-check many beers against the
|
|
32910
|
+
title: "Batch-check many beers against the cache",
|
|
32865
32911
|
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
|
|
32912
|
+
annotations: toolAnnotations({ title: "Batch-check many beers against the cache", readOnly: true, idempotent: true, openWorld: false }),
|
|
32867
32913
|
inputSchema: {
|
|
32868
32914
|
username: UsernameArg2,
|
|
32869
32915
|
bids: external_exports.array(external_exports.number().int().positive()).min(1).max(500).describe("Beer ids to check (1\u2013500)")
|
|
@@ -32878,7 +32924,7 @@ function registerCacheTools(server, client2, cacheProvider = defaultCacheProvide
|
|
|
32878
32924
|
for (const bid of bids) {
|
|
32879
32925
|
if (seen.has(bid)) continue;
|
|
32880
32926
|
seen.add(bid);
|
|
32881
|
-
const r = cache.hasHad(user, { bid });
|
|
32927
|
+
const r = await cache.hasHad(user, { bid });
|
|
32882
32928
|
if (r.had) hadCount++;
|
|
32883
32929
|
results.push({ bid, had: r.had, count: r.count, last_date: r.last_date, best_rating: r.best_rating });
|
|
32884
32930
|
}
|
|
@@ -32888,7 +32934,7 @@ function registerCacheTools(server, client2, cacheProvider = defaultCacheProvide
|
|
|
32888
32934
|
had: hadCount,
|
|
32889
32935
|
not_had: seen.size - hadCount,
|
|
32890
32936
|
results,
|
|
32891
|
-
freshness: freshness(cache, user)
|
|
32937
|
+
freshness: await freshness(cache, user)
|
|
32892
32938
|
});
|
|
32893
32939
|
}
|
|
32894
32940
|
);
|
|
@@ -32896,7 +32942,7 @@ function registerCacheTools(server, client2, cacheProvider = defaultCacheProvide
|
|
|
32896
32942
|
"untappd_cache_query",
|
|
32897
32943
|
{
|
|
32898
32944
|
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
|
|
32945
|
+
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 cache only, NO API call. Returns the matching check-ins plus cache freshness. Run untappd_sync_checkins first.",
|
|
32900
32946
|
annotations: toolAnnotations({ title: "Query cached check-ins with filters", readOnly: true, idempotent: true, openWorld: false }),
|
|
32901
32947
|
inputSchema: {
|
|
32902
32948
|
username: UsernameArg2,
|
|
@@ -32915,18 +32961,68 @@ function registerCacheTools(server, client2, cacheProvider = defaultCacheProvide
|
|
|
32915
32961
|
async ({ username, ...filters }) => {
|
|
32916
32962
|
const user = resolveUser2(username, client2.loginName);
|
|
32917
32963
|
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) });
|
|
32964
|
+
const rows = await cache.query(user, filters);
|
|
32965
|
+
return textResult({ username: user, count: rows.length, filters, results: rows, freshness: await freshness(cache, user) });
|
|
32920
32966
|
}
|
|
32921
32967
|
);
|
|
32922
32968
|
}
|
|
32923
|
-
|
|
32924
|
-
|
|
32925
|
-
|
|
32969
|
+
|
|
32970
|
+
// src/cache/db.ts
|
|
32971
|
+
import { DatabaseSync } from "node:sqlite";
|
|
32972
|
+
import { mkdirSync } from "node:fs";
|
|
32973
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
32974
|
+
import { homedir } from "node:os";
|
|
32975
|
+
var NodeSqlDriver = class {
|
|
32976
|
+
constructor(db) {
|
|
32977
|
+
this.db = db;
|
|
32978
|
+
}
|
|
32979
|
+
db;
|
|
32980
|
+
execScript(sql) {
|
|
32981
|
+
this.db.exec(sql);
|
|
32982
|
+
}
|
|
32983
|
+
run(sql, params) {
|
|
32984
|
+
this.db.prepare(sql).run(...params);
|
|
32985
|
+
}
|
|
32986
|
+
get(sql, params) {
|
|
32987
|
+
return this.db.prepare(sql).get(...params);
|
|
32988
|
+
}
|
|
32989
|
+
all(sql, params) {
|
|
32990
|
+
return this.db.prepare(sql).all(...params);
|
|
32991
|
+
}
|
|
32992
|
+
transaction(fn) {
|
|
32993
|
+
this.db.exec("BEGIN");
|
|
32994
|
+
try {
|
|
32995
|
+
fn();
|
|
32996
|
+
this.db.exec("COMMIT");
|
|
32997
|
+
} catch (e) {
|
|
32998
|
+
this.db.exec("ROLLBACK");
|
|
32999
|
+
throw e;
|
|
33000
|
+
}
|
|
33001
|
+
}
|
|
33002
|
+
};
|
|
33003
|
+
var CheckinCache = class _CheckinCache extends LocalCacheStore {
|
|
33004
|
+
constructor(db) {
|
|
33005
|
+
super(new CheckinStoreCore(new NodeSqlDriver(db)));
|
|
33006
|
+
this.db = db;
|
|
33007
|
+
}
|
|
33008
|
+
db;
|
|
33009
|
+
/** Open (creating parent dirs) a file-backed cache, or `:memory:` for tests. */
|
|
33010
|
+
static open(path) {
|
|
33011
|
+
if (path !== ":memory:") mkdirSync(dirname2(path), { recursive: true });
|
|
33012
|
+
return new _CheckinCache(new DatabaseSync(path));
|
|
33013
|
+
}
|
|
33014
|
+
close() {
|
|
33015
|
+
this.db.close();
|
|
33016
|
+
}
|
|
33017
|
+
};
|
|
33018
|
+
function defaultCachePath() {
|
|
33019
|
+
return readEnvVar("UNTAPPD_CACHE_DB") ?? join2(homedir(), ".untappd-mcp", "checkins.db");
|
|
32926
33020
|
}
|
|
32927
33021
|
|
|
32928
33022
|
// src/index.ts
|
|
32929
33023
|
var client = new UntappdClient();
|
|
33024
|
+
var nodeCache;
|
|
33025
|
+
var nodeCacheProvider = () => nodeCache ??= CheckinCache.open(defaultCachePath());
|
|
32930
33026
|
await runMcp({
|
|
32931
33027
|
name: "untappd-mcp",
|
|
32932
33028
|
version: VERSION,
|
|
@@ -32943,9 +33039,11 @@ await runMcp({
|
|
|
32943
33039
|
(s) => registerWishlistTools(s, client),
|
|
32944
33040
|
(s) => registerCheckinTools(s, client),
|
|
32945
33041
|
(s) => registerUtilityTools(s, client),
|
|
32946
|
-
//
|
|
32947
|
-
//
|
|
32948
|
-
//
|
|
32949
|
-
(s) => registerCacheTools(s, client)
|
|
33042
|
+
// The stdio server backs the cache with a local `node:sqlite` file; the
|
|
33043
|
+
// remote Cloudflare connector (src/worker.ts) backs the same cache tools
|
|
33044
|
+
// with a per-user Durable Object instead.
|
|
33045
|
+
(s) => registerCacheTools(s, client, nodeCacheProvider),
|
|
33046
|
+
// Keep last: logs the full registered toolset (count + names) at startup.
|
|
33047
|
+
(s) => logRegisteredTools(s, "stdio")
|
|
32950
33048
|
]
|
|
32951
33049
|
});
|