untappd-mcp 2.5.0 → 2.5.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +43 -13
- package/dist/cache/db.js +62 -14
- package/dist/cache/sync-brewery-likes.js +3 -2
- package/dist/compact.js +3 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Untappd — beers, breweries, venues, check-ins, wishlists, and your friend feed",
|
|
10
|
-
"version": "2.5.
|
|
10
|
+
"version": "2.5.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": "2.5.
|
|
18
|
+
"version": "2.5.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": "2.5.
|
|
4
|
+
"version": "2.5.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",
|
package/dist/bundle.js
CHANGED
|
@@ -59906,7 +59906,7 @@ function toolAnnotations(opts = {}) {
|
|
|
59906
59906
|
}
|
|
59907
59907
|
|
|
59908
59908
|
// src/version.ts
|
|
59909
|
-
var VERSION = "2.5.
|
|
59909
|
+
var VERSION = "2.5.2";
|
|
59910
59910
|
|
|
59911
59911
|
// src/client.ts
|
|
59912
59912
|
import { dirname as dirname2, join as join4 } from "path";
|
|
@@ -60503,7 +60503,9 @@ function compactBreweryBeerList(r, page, favorites) {
|
|
|
60503
60503
|
source: r.source,
|
|
60504
60504
|
fetched_at: r.fetched_at,
|
|
60505
60505
|
expires_at: r.expires_at,
|
|
60506
|
-
|
|
60506
|
+
// Always present (null when favorites aren't known: no cache, or no username) so
|
|
60507
|
+
// the cached and no-cache paths return the same keys.
|
|
60508
|
+
favorite: favorites ? favorites.brewery : null,
|
|
60507
60509
|
pagination: { offset: page.offset, limit: page.limit, next_offset: more ? page.offset + count2 : null },
|
|
60508
60510
|
beers: {
|
|
60509
60511
|
count: count2,
|
|
@@ -64755,7 +64757,7 @@ async function syncBreweryLikes(client2, cache, rawUsername, maxPages = 10) {
|
|
|
64755
64757
|
added += await cache.upsertBreweryLikes(rawUsername, rows, passStamp);
|
|
64756
64758
|
const next = offsetOf(data?.pagination?.offset);
|
|
64757
64759
|
const advanced = next !== null && next > offset ? next : offset + items.length;
|
|
64758
|
-
complete = items.length === 0 ||
|
|
64760
|
+
complete = items.length === 0 || total !== null && advanced >= total;
|
|
64759
64761
|
offset = advanced;
|
|
64760
64762
|
if (complete) removed = await cache.pruneBreweryLikes(rawUsername, passStamp);
|
|
64761
64763
|
await cache.setListState(rawUsername, BREWERY_LIKES_LIST, {
|
|
@@ -66700,7 +66702,7 @@ var NodeSqlDriver = class {
|
|
|
66700
66702
|
return this.prepared(sql).all(...params);
|
|
66701
66703
|
}
|
|
66702
66704
|
transaction(fn) {
|
|
66703
|
-
this.db.exec("BEGIN IMMEDIATE");
|
|
66705
|
+
retryBusy(() => this.db.exec("BEGIN IMMEDIATE"));
|
|
66704
66706
|
try {
|
|
66705
66707
|
fn();
|
|
66706
66708
|
this.db.exec("COMMIT");
|
|
@@ -66726,19 +66728,25 @@ var CheckinCache = class _CheckinCache extends LocalCacheStore {
|
|
|
66726
66728
|
static open(path) {
|
|
66727
66729
|
if (path !== ":memory:") preparePrivateFile(path);
|
|
66728
66730
|
const db = new DatabaseSync(path);
|
|
66729
|
-
|
|
66730
|
-
|
|
66731
|
-
|
|
66732
|
-
|
|
66731
|
+
try {
|
|
66732
|
+
db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
66733
|
+
if (path !== ":memory:") {
|
|
66734
|
+
retryBusy(() => {
|
|
66735
|
+
if (journalModeOf(db) !== "wal") db.exec("PRAGMA journal_mode = WAL");
|
|
66736
|
+
});
|
|
66737
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
66738
|
+
}
|
|
66739
|
+
const cache = new _CheckinCache(db, path);
|
|
66740
|
+
if (path !== ":memory:") tightenSidecars(path);
|
|
66741
|
+
return cache;
|
|
66742
|
+
} catch (e) {
|
|
66743
|
+
db.close();
|
|
66744
|
+
throw e;
|
|
66733
66745
|
}
|
|
66734
|
-
const cache = new _CheckinCache(db, path);
|
|
66735
|
-
if (path !== ":memory:") tightenSidecars(path);
|
|
66736
|
-
return cache;
|
|
66737
66746
|
}
|
|
66738
66747
|
/** The SQLite journal mode actually in effect (`wal` for a file, `memory` for :memory:). */
|
|
66739
66748
|
journalMode() {
|
|
66740
|
-
|
|
66741
|
-
return String(row?.journal_mode ?? "unknown");
|
|
66749
|
+
return journalModeOf(this.db);
|
|
66742
66750
|
}
|
|
66743
66751
|
/** On-disk footprint: the db file plus its -wal/-shm side files. */
|
|
66744
66752
|
fileInfo() {
|
|
@@ -66777,6 +66785,28 @@ function bestEffortChmod(path, mode) {
|
|
|
66777
66785
|
}
|
|
66778
66786
|
}
|
|
66779
66787
|
var BUSY_TIMEOUT_MS = 5e3;
|
|
66788
|
+
function journalModeOf(db) {
|
|
66789
|
+
const row = db.prepare("PRAGMA journal_mode").get();
|
|
66790
|
+
return String(row?.journal_mode ?? "unknown");
|
|
66791
|
+
}
|
|
66792
|
+
function isBusy(e) {
|
|
66793
|
+
const code2 = e?.errcode;
|
|
66794
|
+
return typeof code2 === "number" && ((code2 & 255) === 5 || (code2 & 255) === 6);
|
|
66795
|
+
}
|
|
66796
|
+
var sleepCell = new Int32Array(new SharedArrayBuffer(4));
|
|
66797
|
+
function retryBusy(fn, timeoutMs = BUSY_TIMEOUT_MS) {
|
|
66798
|
+
const deadline = Date.now() + timeoutMs;
|
|
66799
|
+
for (let attempt = 0; ; attempt++) {
|
|
66800
|
+
try {
|
|
66801
|
+
return fn();
|
|
66802
|
+
} catch (e) {
|
|
66803
|
+
const left = deadline - Date.now();
|
|
66804
|
+
if (!isBusy(e) || left <= 0) throw e;
|
|
66805
|
+
const wait = Math.min(100, 5 * 2 ** Math.min(attempt, 5)) + Math.random() * 5;
|
|
66806
|
+
Atomics.wait(sleepCell, 0, 0, Math.min(wait, left));
|
|
66807
|
+
}
|
|
66808
|
+
}
|
|
66809
|
+
}
|
|
66780
66810
|
function defaultCachePath() {
|
|
66781
66811
|
const file2 = readEnvVar("UNTAPPD_CACHE_DB");
|
|
66782
66812
|
if (file2) return file2 === ":memory:" ? file2 : absolutise(file2);
|
package/dist/cache/db.js
CHANGED
|
@@ -47,7 +47,9 @@ class NodeSqlDriver {
|
|
|
47
47
|
// IMMEDIATE takes the write lock up front. A DEFERRED transaction that
|
|
48
48
|
// reads and then writes can hit SQLITE_BUSY on the lock upgrade, which
|
|
49
49
|
// busy_timeout does NOT retry — the failure mode of two hosts open at once.
|
|
50
|
-
|
|
50
|
+
// BEGIN itself can still get a handler-less SQLITE_BUSY (e.g. while another
|
|
51
|
+
// process recovers the WAL), so it is retried too.
|
|
52
|
+
retryBusy(() => this.db.exec('BEGIN IMMEDIATE'));
|
|
51
53
|
try {
|
|
52
54
|
fn();
|
|
53
55
|
this.db.exec('COMMIT');
|
|
@@ -78,23 +80,36 @@ export class CheckinCache extends LocalCacheStore {
|
|
|
78
80
|
if (path !== ':memory:')
|
|
79
81
|
preparePrivateFile(path);
|
|
80
82
|
const db = new DatabaseSync(path);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
try {
|
|
84
|
+
// Set before the schema/migration transaction so even that waits out a
|
|
85
|
+
// second process instead of failing. WAL lets readers and one writer run
|
|
86
|
+
// concurrently (two hosts open at once); NORMAL sync is the WAL-safe default.
|
|
87
|
+
db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
88
|
+
if (path !== ':memory:') {
|
|
89
|
+
// Switching a fresh file to WAL needs an exclusive lock, and when another
|
|
90
|
+
// process is opening it too SQLite answers SQLITE_BUSY WITHOUT calling
|
|
91
|
+
// the busy handler (#256) — so busy_timeout never applies and this has
|
|
92
|
+
// to retry itself. Once the file is WAL (the switch is persistent) a
|
|
93
|
+
// later open skips the switch entirely.
|
|
94
|
+
retryBusy(() => {
|
|
95
|
+
if (journalModeOf(db) !== 'wal')
|
|
96
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
97
|
+
});
|
|
98
|
+
db.exec('PRAGMA synchronous = NORMAL');
|
|
99
|
+
}
|
|
100
|
+
const cache = new CheckinCache(db, path);
|
|
101
|
+
if (path !== ':memory:')
|
|
102
|
+
tightenSidecars(path);
|
|
103
|
+
return cache;
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
db.close();
|
|
107
|
+
throw e;
|
|
88
108
|
}
|
|
89
|
-
const cache = new CheckinCache(db, path);
|
|
90
|
-
if (path !== ':memory:')
|
|
91
|
-
tightenSidecars(path);
|
|
92
|
-
return cache;
|
|
93
109
|
}
|
|
94
110
|
/** The SQLite journal mode actually in effect (`wal` for a file, `memory` for :memory:). */
|
|
95
111
|
journalMode() {
|
|
96
|
-
|
|
97
|
-
return String(row?.journal_mode ?? 'unknown');
|
|
112
|
+
return journalModeOf(this.db);
|
|
98
113
|
}
|
|
99
114
|
/** On-disk footprint: the db file plus its -wal/-shm side files. */
|
|
100
115
|
fileInfo() {
|
|
@@ -152,6 +167,39 @@ function bestEffortChmod(path, mode) {
|
|
|
152
167
|
}
|
|
153
168
|
// How long a write waits for another process's lock before failing.
|
|
154
169
|
const BUSY_TIMEOUT_MS = 5000;
|
|
170
|
+
function journalModeOf(db) {
|
|
171
|
+
const row = db.prepare('PRAGMA journal_mode').get();
|
|
172
|
+
return String(row?.journal_mode ?? 'unknown');
|
|
173
|
+
}
|
|
174
|
+
/** SQLITE_BUSY / SQLITE_LOCKED, including their extended codes (e.g. BUSY_RECOVERY). */
|
|
175
|
+
function isBusy(e) {
|
|
176
|
+
const code = e?.errcode;
|
|
177
|
+
return typeof code === 'number' && ((code & 0xff) === 5 || (code & 0xff) === 6);
|
|
178
|
+
}
|
|
179
|
+
const sleepCell = new Int32Array(new SharedArrayBuffer(4));
|
|
180
|
+
/**
|
|
181
|
+
* Run `fn`, retrying a SQLITE_BUSY that SQLite raised without consulting the
|
|
182
|
+
* busy handler, until {@link BUSY_TIMEOUT_MS} after the FIRST attempt. A BUSY
|
|
183
|
+
* that already waited out busy_timeout lands past that deadline and is thrown
|
|
184
|
+
* as-is, so the total wait never exceeds busy_timeout's. The driver is
|
|
185
|
+
* synchronous, so the backoff blocks (Atomics.wait) like busy_timeout does.
|
|
186
|
+
*/
|
|
187
|
+
export function retryBusy(fn, timeoutMs = BUSY_TIMEOUT_MS) {
|
|
188
|
+
const deadline = Date.now() + timeoutMs;
|
|
189
|
+
for (let attempt = 0;; attempt++) {
|
|
190
|
+
try {
|
|
191
|
+
return fn();
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
const left = deadline - Date.now();
|
|
195
|
+
if (!isBusy(e) || left <= 0)
|
|
196
|
+
throw e;
|
|
197
|
+
// 5, 10, 20 … ms, capped at 100, plus jitter so colliding opens spread out.
|
|
198
|
+
const wait = Math.min(100, 5 * 2 ** Math.min(attempt, 5)) + Math.random() * 5;
|
|
199
|
+
Atomics.wait(sleepCell, 0, 0, Math.min(wait, left));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
155
203
|
/**
|
|
156
204
|
* The on-disk cache path, always ABSOLUTE: `$UNTAPPD_CACHE_DB` (a file), else
|
|
157
205
|
* `$UNTAPPD_CACHE_DIR/checkins.db`, else `~/.untappd-mcp/checkins.db`.
|
|
@@ -83,8 +83,9 @@ export async function syncBreweryLikes(client, cache, rawUsername, maxPages = 10
|
|
|
83
83
|
// pagination.offset is ABSENT on an empty list (live, 6a) — fall back to offset + items.
|
|
84
84
|
const next = offsetOf(data?.pagination?.offset);
|
|
85
85
|
const advanced = next !== null && next > offset ? next : offset + items.length;
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
// Without a total, a short page is not proof of the end (a route may serve fewer
|
|
87
|
+
// than it was asked for), so only an empty page ends the pass.
|
|
88
|
+
complete = items.length === 0 || (total !== null && advanced >= total);
|
|
88
89
|
offset = advanced;
|
|
89
90
|
if (complete)
|
|
90
91
|
removed = await cache.pruneBreweryLikes(rawUsername, passStamp);
|
package/dist/compact.js
CHANGED
|
@@ -122,7 +122,9 @@ export function compactBreweryBeerList(r, page, favorites) {
|
|
|
122
122
|
source: r.source,
|
|
123
123
|
fetched_at: r.fetched_at,
|
|
124
124
|
expires_at: r.expires_at,
|
|
125
|
-
|
|
125
|
+
// Always present (null when favorites aren't known: no cache, or no username) so
|
|
126
|
+
// the cached and no-cache paths return the same keys.
|
|
127
|
+
favorite: favorites ? favorites.brewery : null,
|
|
126
128
|
pagination: { offset: page.offset, limit: page.limit, next_offset: more ? page.offset + count : null },
|
|
127
129
|
beers: {
|
|
128
130
|
count,
|
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 = '2.5.
|
|
6
|
+
export const VERSION = '2.5.2'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "untappd-mcp",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.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>",
|
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": "2.5.
|
|
9
|
+
"version": "2.5.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "untappd-mcp",
|
|
14
|
-
"version": "2.5.
|
|
14
|
+
"version": "2.5.2",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|