untappd-mcp 1.7.0 → 1.7.2

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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for Untappd — beers, breweries, venues, check-ins, wishlists, and your friend feed",
10
- "version": "1.7.0"
10
+ "version": "1.7.2"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Untappd",
16
16
  "source": "./",
17
17
  "description": "MCP server for Untappd — search beers/breweries/venues, read profiles/check-ins/wishlists, and post check-ins, toasts, and comments",
18
- "version": "1.7.0",
18
+ "version": "1.7.2",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "untappd-mcp",
3
3
  "displayName": "Untappd",
4
- "version": "1.7.0",
4
+ "version": "1.7.2",
5
5
  "description": "MCP server for Untappd — search beers/breweries/venues, read check-ins and wishlists, and post check-ins, toasts, and comments",
6
6
  "author": {
7
7
  "name": "Chris Hall",
@@ -17,6 +17,6 @@
17
17
  "checkins",
18
18
  "mcp"
19
19
  ],
20
- "skills": "./SKILL.md",
20
+ "skills": "./skills/",
21
21
  "mcp": "./.mcp.json"
22
22
  }
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.7.0";
31187
+ var VERSION = "1.7.2";
31188
31188
 
31189
31189
  // src/client.ts
31190
31190
  import { dirname, join } from "path";
@@ -31658,7 +31658,8 @@ var SCHEMA_STATEMENTS = [
31658
31658
  checkins_truncated INTEGER NOT NULL DEFAULT 0,
31659
31659
  beers_offset INTEGER,
31660
31660
  beers_total INTEGER,
31661
- beers_complete INTEGER NOT NULL DEFAULT 0
31661
+ beers_complete INTEGER NOT NULL DEFAULT 0,
31662
+ served_backfill_last INTEGER NOT NULL DEFAULT 0
31662
31663
  )`
31663
31664
  ];
31664
31665
  var MIGRATIONS = [
@@ -31666,7 +31667,8 @@ var MIGRATIONS = [
31666
31667
  "ALTER TABLE sync_state ADD COLUMN checkins_truncated INTEGER NOT NULL DEFAULT 0",
31667
31668
  "ALTER TABLE sync_state ADD COLUMN beers_offset INTEGER",
31668
31669
  "ALTER TABLE sync_state ADD COLUMN beers_total INTEGER",
31669
- "ALTER TABLE sync_state ADD COLUMN beers_complete INTEGER NOT NULL DEFAULT 0"
31670
+ "ALTER TABLE sync_state ADD COLUMN beers_complete INTEGER NOT NULL DEFAULT 0",
31671
+ "ALTER TABLE sync_state ADD COLUMN served_backfill_last INTEGER NOT NULL DEFAULT 0"
31670
31672
  ];
31671
31673
  var CheckinStoreCore = class {
31672
31674
  constructor(db) {
@@ -31787,7 +31789,8 @@ var CheckinStoreCore = class {
31787
31789
  checkins_truncated: Number(row.checkins_truncated) === 1,
31788
31790
  beers_offset: num(row.beers_offset),
31789
31791
  beers_total: num(row.beers_total),
31790
- beers_complete: Number(row.beers_complete) === 1
31792
+ beers_complete: Number(row.beers_complete) === 1,
31793
+ served_backfill_last: Number(row.served_backfill_last) === 1
31791
31794
  };
31792
31795
  }
31793
31796
  /**
@@ -31810,18 +31813,20 @@ var CheckinStoreCore = class {
31810
31813
  checkins_truncated: pick2("checkins_truncated", false),
31811
31814
  beers_offset: pick2("beers_offset", null),
31812
31815
  beers_total: pick2("beers_total", null),
31813
- beers_complete: pick2("beers_complete", false)
31816
+ beers_complete: pick2("beers_complete", false),
31817
+ served_backfill_last: pick2("served_backfill_last", false)
31814
31818
  };
31815
31819
  this.db.run(
31816
31820
  `INSERT INTO sync_state
31817
- (username, oldest_max_id, newest_checkin_id, catchup_max_id, last_synced_at, backfill_complete, total_checkins, checkins_truncated, beers_offset, beers_total, beers_complete)
31818
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
31821
+ (username, oldest_max_id, newest_checkin_id, catchup_max_id, last_synced_at, backfill_complete, total_checkins, checkins_truncated, beers_offset, beers_total, beers_complete, served_backfill_last)
31822
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
31819
31823
  ON CONFLICT(username) DO UPDATE SET
31820
31824
  oldest_max_id=excluded.oldest_max_id, newest_checkin_id=excluded.newest_checkin_id,
31821
31825
  catchup_max_id=excluded.catchup_max_id, last_synced_at=excluded.last_synced_at,
31822
31826
  backfill_complete=excluded.backfill_complete, total_checkins=excluded.total_checkins,
31823
31827
  checkins_truncated=excluded.checkins_truncated, beers_offset=excluded.beers_offset,
31824
- beers_total=excluded.beers_total, beers_complete=excluded.beers_complete`,
31828
+ beers_total=excluded.beers_total, beers_complete=excluded.beers_complete,
31829
+ served_backfill_last=excluded.served_backfill_last`,
31825
31830
  [
31826
31831
  key,
31827
31832
  next.oldest_max_id,
@@ -31833,7 +31838,8 @@ var CheckinStoreCore = class {
31833
31838
  next.checkins_truncated ? 1 : 0,
31834
31839
  next.beers_offset,
31835
31840
  next.beers_total,
31836
- next.beers_complete ? 1 : 0
31841
+ next.beers_complete ? 1 : 0,
31842
+ next.served_backfill_last ? 1 : 0
31837
31843
  ]
31838
31844
  );
31839
31845
  }
@@ -32997,12 +33003,49 @@ async function syncCheckins(client2, cache, rawUsername, options = {}) {
32997
33003
  });
32998
33004
  }
32999
33005
  };
33000
- if (priorNewest !== null) {
33006
+ let usedFirstFetch = false;
33007
+ const nextFetch = async (maxId) => {
33008
+ if (!usedFirstFetch) {
33009
+ usedFirstFetch = true;
33010
+ return firstFetch(maxId);
33011
+ }
33012
+ return fetchPage(client2, encodedUser, maxId);
33013
+ };
33014
+ const catchupPending = catchupMaxId !== null;
33015
+ const wantPhase1 = priorNewest !== null;
33016
+ const wantPhase2 = !backfillComplete && !checkinsTruncated && !catchupPending;
33017
+ let phase1Budget;
33018
+ let phase2Budget;
33019
+ let servedBackfillThisRun = priorState?.served_backfill_last ?? false;
33020
+ if (wantPhase1 && wantPhase2) {
33021
+ phase1Budget = Math.ceil(maxPages / 2);
33022
+ phase2Budget = maxPages - phase1Budget;
33023
+ if (phase2Budget === 0) {
33024
+ if (priorState?.served_backfill_last) {
33025
+ phase1Budget = 1;
33026
+ phase2Budget = 0;
33027
+ servedBackfillThisRun = false;
33028
+ } else {
33029
+ phase1Budget = 0;
33030
+ phase2Budget = 1;
33031
+ servedBackfillThisRun = true;
33032
+ }
33033
+ }
33034
+ } else if (wantPhase1) {
33035
+ phase1Budget = maxPages;
33036
+ phase2Budget = 0;
33037
+ } else {
33038
+ phase1Budget = 0;
33039
+ phase2Budget = maxPages;
33040
+ }
33041
+ if (priorNewest !== null && phase1Budget > 0) {
33001
33042
  let maxId = catchupMaxId ?? void 0;
33002
33043
  let caughtUp = false;
33003
- for (; pages < maxPages; ) {
33004
- const page = pages === 0 ? await firstFetch(maxId) : await fetchPage(client2, encodedUser, maxId);
33044
+ let phase1Pages = 0;
33045
+ for (; phase1Pages < phase1Budget; ) {
33046
+ const page = await nextFetch(maxId);
33005
33047
  pages++;
33048
+ phase1Pages++;
33006
33049
  if (page.items.length === 0) {
33007
33050
  caughtUp = true;
33008
33051
  break;
@@ -33046,12 +33089,13 @@ async function syncCheckins(client2, cache, rawUsername, options = {}) {
33046
33089
  catchupInProgress = true;
33047
33090
  }
33048
33091
  }
33049
- if (!backfillComplete && !catchupInProgress && !checkinsTruncated) {
33092
+ if (!backfillComplete && !catchupInProgress && !checkinsTruncated && phase2Budget > 0) {
33050
33093
  let maxId = oldestMaxId ?? void 0;
33051
- for (; pages < maxPages; ) {
33052
- const first = pages === 0;
33053
- const page = first ? await firstFetch(maxId) : await fetchPage(client2, encodedUser, maxId);
33094
+ let phase2Pages = 0;
33095
+ for (; phase2Pages < phase2Budget; ) {
33096
+ const page = await nextFetch(maxId);
33054
33097
  pages++;
33098
+ phase2Pages++;
33055
33099
  if (page.items.length === 0) {
33056
33100
  backfillComplete = reachedFullCoverage(await cache.cachedCount(rawUsername), total);
33057
33101
  checkinsTruncated = !backfillComplete;
@@ -33088,6 +33132,7 @@ async function syncCheckins(client2, cache, rawUsername, options = {}) {
33088
33132
  backfill_complete: backfillComplete,
33089
33133
  checkins_truncated: checkinsTruncated,
33090
33134
  total_checkins: total,
33135
+ served_backfill_last: servedBackfillThisRun,
33091
33136
  last_synced_at: now()
33092
33137
  });
33093
33138
  const backfillPercent = total && total > 0 ? Math.min(100, Math.round(cached2 / total * 100)) : backfillComplete && !catchupInProgress ? 100 : null;
@@ -174,7 +174,8 @@ export const SCHEMA_STATEMENTS = [
174
174
  checkins_truncated INTEGER NOT NULL DEFAULT 0,
175
175
  beers_offset INTEGER,
176
176
  beers_total INTEGER,
177
- beers_complete INTEGER NOT NULL DEFAULT 0
177
+ beers_complete INTEGER NOT NULL DEFAULT 0,
178
+ served_backfill_last INTEGER NOT NULL DEFAULT 0
178
179
  )`,
179
180
  ];
180
181
  /** Columns added after the initial release, each applied idempotently on open. */
@@ -184,6 +185,7 @@ export const MIGRATIONS = [
184
185
  'ALTER TABLE sync_state ADD COLUMN beers_offset INTEGER',
185
186
  'ALTER TABLE sync_state ADD COLUMN beers_total INTEGER',
186
187
  'ALTER TABLE sync_state ADD COLUMN beers_complete INTEGER NOT NULL DEFAULT 0',
188
+ 'ALTER TABLE sync_state ADD COLUMN served_backfill_last INTEGER NOT NULL DEFAULT 0',
187
189
  ];
188
190
  /**
189
191
  * The cache logic over a synchronous {@link SqlDriver}. Usernames are keyed
@@ -295,6 +297,7 @@ export class CheckinStoreCore {
295
297
  beers_offset: num(row.beers_offset),
296
298
  beers_total: num(row.beers_total),
297
299
  beers_complete: Number(row.beers_complete) === 1,
300
+ served_backfill_last: Number(row.served_backfill_last) === 1,
298
301
  };
299
302
  }
300
303
  /**
@@ -318,19 +321,21 @@ export class CheckinStoreCore {
318
321
  beers_offset: pick('beers_offset', null),
319
322
  beers_total: pick('beers_total', null),
320
323
  beers_complete: pick('beers_complete', false),
324
+ served_backfill_last: pick('served_backfill_last', false),
321
325
  };
322
326
  this.db.run(`INSERT INTO sync_state
323
- (username, oldest_max_id, newest_checkin_id, catchup_max_id, last_synced_at, backfill_complete, total_checkins, checkins_truncated, beers_offset, beers_total, beers_complete)
324
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
327
+ (username, oldest_max_id, newest_checkin_id, catchup_max_id, last_synced_at, backfill_complete, total_checkins, checkins_truncated, beers_offset, beers_total, beers_complete, served_backfill_last)
328
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
325
329
  ON CONFLICT(username) DO UPDATE SET
326
330
  oldest_max_id=excluded.oldest_max_id, newest_checkin_id=excluded.newest_checkin_id,
327
331
  catchup_max_id=excluded.catchup_max_id, last_synced_at=excluded.last_synced_at,
328
332
  backfill_complete=excluded.backfill_complete, total_checkins=excluded.total_checkins,
329
333
  checkins_truncated=excluded.checkins_truncated, beers_offset=excluded.beers_offset,
330
- beers_total=excluded.beers_total, beers_complete=excluded.beers_complete`, [
334
+ beers_total=excluded.beers_total, beers_complete=excluded.beers_complete,
335
+ served_backfill_last=excluded.served_backfill_last`, [
331
336
  key, next.oldest_max_id, next.newest_checkin_id, next.catchup_max_id, next.last_synced_at,
332
337
  next.backfill_complete ? 1 : 0, next.total_checkins, next.checkins_truncated ? 1 : 0,
333
- next.beers_offset, next.beers_total, next.beers_complete ? 1 : 0,
338
+ next.beers_offset, next.beers_total, next.beers_complete ? 1 : 0, next.served_backfill_last ? 1 : 0,
334
339
  ]);
335
340
  }
336
341
  /**
@@ -108,13 +108,71 @@ export async function syncCheckins(client, cache, rawUsername, options = {}) {
108
108
  });
109
109
  }
110
110
  };
111
+ // Only the very first network call of the whole run gets the friendlier
112
+ // wrapped error; every call after that uses the plain fetch.
113
+ let usedFirstFetch = false;
114
+ const nextFetch = async (maxId) => {
115
+ if (!usedFirstFetch) {
116
+ usedFirstFetch = true;
117
+ return firstFetch(maxId);
118
+ }
119
+ return fetchPage(client, encodedUser, maxId);
120
+ };
121
+ // ── Split the shared per-call page budget across the two phases ──
122
+ // `maxPages` is the TOTAL page budget for this call (the documented
123
+ // rate-limit contract), NOT a per-phase budget. Phase 1 (catch-up) and
124
+ // Phase 2 (backfill) share it. The bug being fixed: a single shared page
125
+ // counter always ran Phase 1 first, and Phase 1 spends at least one page just
126
+ // to reconfirm "nothing new above the cached top" — so a large catch-up burst
127
+ // (or a small maxPages) could leave Phase 2 with ZERO pages every run,
128
+ // forever. The gap below oldest_max_id was then never re-fetched,
129
+ // backfill_complete never became true, and has_had silently false-negatived
130
+ // every bid in that unhealed gap. Fix: give each phase a slice of the shared
131
+ // budget (ceil to catch-up, the remainder to backfill) so both make progress,
132
+ // while the TOTAL fetched stays ≤ maxPages.
133
+ const catchupPending = catchupMaxId !== null; // an interrupted catch-up must finish first
134
+ const wantPhase1 = priorNewest !== null;
135
+ const wantPhase2 = !backfillComplete && !checkinsTruncated && !catchupPending;
136
+ let phase1Budget;
137
+ let phase2Budget;
138
+ // Preserved unless a max_pages=1 tie below flips it.
139
+ let servedBackfillThisRun = priorState?.served_backfill_last ?? false;
140
+ if (wantPhase1 && wantPhase2) {
141
+ phase1Budget = Math.ceil(maxPages / 2);
142
+ phase2Budget = maxPages - phase1Budget;
143
+ if (phase2Budget === 0) {
144
+ // maxPages === 1: the split zeroes one phase. Alternate which phase gets
145
+ // the single page each run (persisted turn flag) so BOTH converge over
146
+ // repeated runs and neither is permanently starved.
147
+ if (priorState?.served_backfill_last) {
148
+ phase1Budget = 1;
149
+ phase2Budget = 0;
150
+ servedBackfillThisRun = false; // catch-up's turn this run
151
+ }
152
+ else {
153
+ phase1Budget = 0;
154
+ phase2Budget = 1;
155
+ servedBackfillThisRun = true; // backfill's turn this run
156
+ }
157
+ }
158
+ }
159
+ else if (wantPhase1) {
160
+ phase1Budget = maxPages;
161
+ phase2Budget = 0;
162
+ }
163
+ else {
164
+ phase1Budget = 0;
165
+ phase2Budget = maxPages;
166
+ }
111
167
  // ── Phase 1: incremental catch-up of the new-top region (resumable) ──
112
- if (priorNewest !== null) {
168
+ if (priorNewest !== null && phase1Budget > 0) {
113
169
  let maxId = catchupMaxId ?? undefined;
114
170
  let caughtUp = false;
115
- for (; pages < maxPages;) {
116
- const page = pages === 0 ? await firstFetch(maxId) : await fetchPage(client, encodedUser, maxId);
171
+ let phase1Pages = 0;
172
+ for (; phase1Pages < phase1Budget;) {
173
+ const page = await nextFetch(maxId);
117
174
  pages++;
175
+ phase1Pages++;
118
176
  if (page.items.length === 0) {
119
177
  caughtUp = true;
120
178
  break;
@@ -162,13 +220,14 @@ export async function syncCheckins(client, cache, rawUsername, options = {}) {
162
220
  catchupInProgress = true; // budget spent mid-catch-up; next run resumes
163
221
  }
164
222
  }
165
- // ── Phase 2: backfill older history (resumable) ──
166
- if (!backfillComplete && !catchupInProgress && !checkinsTruncated) {
223
+ // ── Phase 2: backfill older history (resumable), its slice of the budget ──
224
+ if (!backfillComplete && !catchupInProgress && !checkinsTruncated && phase2Budget > 0) {
167
225
  let maxId = oldestMaxId ?? undefined;
168
- for (; pages < maxPages;) {
169
- const first = pages === 0;
170
- const page = first ? await firstFetch(maxId) : await fetchPage(client, encodedUser, maxId);
226
+ let phase2Pages = 0;
227
+ for (; phase2Pages < phase2Budget;) {
228
+ const page = await nextFetch(maxId);
171
229
  pages++;
230
+ phase2Pages++;
172
231
  if (page.items.length === 0) {
173
232
  // Reached the end — but only "complete" if coverage is ~full.
174
233
  backfillComplete = reachedFullCoverage(await cache.cachedCount(rawUsername), total);
@@ -211,6 +270,7 @@ export async function syncCheckins(client, cache, rawUsername, options = {}) {
211
270
  backfill_complete: backfillComplete,
212
271
  checkins_truncated: checkinsTruncated,
213
272
  total_checkins: total,
273
+ served_backfill_last: servedBackfillThisRun,
214
274
  last_synced_at: now(),
215
275
  });
216
276
  const backfillPercent = total && total > 0
package/dist/version.js CHANGED
@@ -3,4 +3,4 @@
3
3
  // json's `extra-files`), and `versionSyncTest` guards that it stays equal to
4
4
  // package.json. Import VERSION wherever the version is needed rather than
5
5
  // re-declaring it.
6
- export const VERSION = '1.7.0'; // x-release-please-version
6
+ export const VERSION = '1.7.2'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "untappd-mcp",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "mcpName": "io.github.chrischall/untappd-mcp",
5
5
  "description": "Untappd MCP server for Claude — developed and maintained by AI (Claude Code)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -27,7 +27,7 @@
27
27
  "files": [
28
28
  "dist",
29
29
  ".claude-plugin",
30
- "SKILL.md",
30
+ "skills/",
31
31
  ".mcp.json",
32
32
  "server.json"
33
33
  ],
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/untappd-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.7.0",
9
+ "version": "1.7.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "untappd-mcp",
14
- "version": "1.7.0",
14
+ "version": "1.7.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
File without changes