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/dist/bundle.js CHANGED
@@ -31184,7 +31184,7 @@ function toolAnnotations(opts = {}) {
31184
31184
  }
31185
31185
 
31186
31186
  // src/version.ts
31187
- var VERSION = "1.3.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/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";
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 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;
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.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"]) {
32463
+ for (const stmt of SCHEMA_STATEMENTS) this.db.execScript(stmt);
32464
+ for (const mig of MIGRATIONS) {
32449
32465
  try {
32450
- this.db.exec(`ALTER TABLE sync_state ADD COLUMN ${col}`);
32466
+ this.db.execScript(mig);
32451
32467
  } catch {
32452
32468
  }
32453
32469
  }
32454
32470
  }
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
- }
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 stmt = this.db.prepare(`
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
- stmt.run(
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
- this.db.exec("COMMIT");
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.prepare("SELECT COUNT(*) AS n FROM checkins WHERE username = ?").get(username.toLowerCase());
32511
- return row.n;
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.prepare("SELECT COUNT(DISTINCT bid) AS n FROM checkins WHERE username = ? AND bid IS NOT NULL").get(username.toLowerCase());
32516
- return row.n;
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.prepare("SELECT MAX(checkin_id) AS m FROM checkins WHERE username = ?").get(username.toLowerCase());
32521
- return row.m;
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.prepare("SELECT * FROM sync_state WHERE username = ?").get(username.toLowerCase());
32530
+ const row = this.db.get("SELECT * FROM sync_state WHERE username = ?", [username.toLowerCase()]);
32525
32531
  if (!row) return void 0;
32526
- return { ...row, username: row.username, backfill_complete: row.backfill_complete === 1 };
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.prepare(
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
- 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
+ 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.prepare(`SELECT COUNT(*) AS n, MAX(rating) AS best FROM checkins WHERE ${clause}`).get(...params);
32578
- const matches = this.db.prepare(
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
- FROM checkins WHERE ${clause} ORDER BY checkin_id DESC LIMIT 25`
32581
- ).all(...params);
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: agg.n > 0,
32584
- count: agg.n,
32600
+ had: count2 > 0,
32601
+ count: count2,
32585
32602
  last_date: matches[0]?.created_at ?? null,
32586
- best_rating: agg.best,
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
- const rows = this.db.prepare(`SELECT * FROM checkins WHERE ${where.join(" AND ")} ORDER BY ${order} LIMIT ?`).all(...params, limit);
32628
- return rows;
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 = defaultCacheProvider()) {
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 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 }),
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 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 }),
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 local cache",
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 local cache", readOnly: true, idempotent: true, openWorld: false }),
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 LOCAL cache only, NO API call. Returns the matching check-ins plus cache freshness. Run untappd_sync_checkins first.",
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
- function defaultCacheProvider() {
32924
- let cache;
32925
- return () => cache ??= CheckinCache.open(defaultCachePath());
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
- // 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)
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
  });