untappd-mcp 2.2.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +11 -2
- package/dist/bundle.js +92 -6
- package/dist/cache/db.js +41 -4
- package/dist/cache/store.js +19 -0
- package/dist/tools/cache.js +42 -1
- package/dist/tools/checkin.js +9 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/untappd-mcp/SKILL.md +10 -4
|
@@ -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.2.
|
|
10
|
+
"version": "2.2.1"
|
|
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.2.
|
|
18
|
+
"version": "2.2.1",
|
|
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.2.
|
|
4
|
+
"version": "2.2.1",
|
|
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/README.md
CHANGED
|
@@ -36,7 +36,7 @@ it goes stale.
|
|
|
36
36
|
| `UNTAPPD_USER_AGENT` | no | Override the User-Agent (default mimics the app). |
|
|
37
37
|
| `UNTAPPD_TIMEZONE` | no | IANA timezone (e.g. `America/New_York`) that check-ins are stamped with when the call doesn't pass `timezone`. Set it when the server runs somewhere other than the drinker's zone (e.g. a hosted connector, usually UTC). Defaults to the server process's zone. |
|
|
38
38
|
| `UNTAPPD_PHOTO_DIR` | no | Restrict `untappd_checkin`'s `photo_path` to files inside this directory (several allowed, separated by `:`). Recommended wherever the model can be steered by untrusted content. |
|
|
39
|
-
| `UNTAPPD_CACHE_DB` | no | Path to the local check-in cache SQLite file (default `~/.untappd-mcp/checkins.db`). Local/stdio only. |
|
|
39
|
+
| `UNTAPPD_CACHE_DB` | no | Path to the local check-in cache SQLite file (default `~/.untappd-mcp/checkins.db`; created owner-only — dir `0700`, file `0600`). Local/stdio only. |
|
|
40
40
|
|
|
41
41
|
Copy `.env.example` to `.env` and fill it in for local use.
|
|
42
42
|
|
|
@@ -86,7 +86,7 @@ Writes (each asks you to confirm first — see [Confirmations](#confirmations)):
|
|
|
86
86
|
|
|
87
87
|
Check-in cache: `untappd_sync_checkins`, `untappd_sync_user_beers`,
|
|
88
88
|
`untappd_cache_has_had`, `untappd_cache_has_had_many`, `untappd_cache_not_had`,
|
|
89
|
-
`untappd_cache_query`, `untappd_top_not_had`.
|
|
89
|
+
`untappd_cache_query`, `untappd_top_not_had`, `untappd_cache_forget`.
|
|
90
90
|
|
|
91
91
|
## Check-in cache
|
|
92
92
|
|
|
@@ -149,6 +149,15 @@ Syncing **another** user goes through the same authed endpoint as
|
|
|
149
149
|
account is public or your friend. Otherwise the sync returns a clear error
|
|
150
150
|
telling you to add them as a friend first.
|
|
151
151
|
|
|
152
|
+
**Retention.** Nothing in the cache expires: synced rows — including another
|
|
153
|
+
user's dated check-ins, comments and venues — stay until you remove them. The
|
|
154
|
+
file is created owner-only (directory `0700`, database `0600`, and older
|
|
155
|
+
installs are tightened on open). `untappd_cache_forget` deletes one user's
|
|
156
|
+
cached check-ins, distinct-beers list and sync state after a confirmation
|
|
157
|
+
(preview shows the username and row counts); it touches only the local cache,
|
|
158
|
+
never Untappd, and a later sync can re-fetch. Deleting the file removes
|
|
159
|
+
everything.
|
|
160
|
+
|
|
152
161
|
A cache holds only the check-ins the account it belongs to was allowed to
|
|
153
162
|
fetch. `untappd_healthcheck` reports the running version and the exact tool set
|
|
154
163
|
(count + names + a stable hash), so you can confirm which build is serving.
|
package/dist/bundle.js
CHANGED
|
@@ -58560,7 +58560,7 @@ function toolAnnotations(opts = {}) {
|
|
|
58560
58560
|
}
|
|
58561
58561
|
|
|
58562
58562
|
// src/version.ts
|
|
58563
|
-
var VERSION = "2.2.
|
|
58563
|
+
var VERSION = "2.2.1";
|
|
58564
58564
|
|
|
58565
58565
|
// src/client.ts
|
|
58566
58566
|
import { dirname as dirname2, join as join2 } from "path";
|
|
@@ -59378,6 +59378,22 @@ var CheckinStoreCore = class {
|
|
|
59378
59378
|
[...params, limit]
|
|
59379
59379
|
);
|
|
59380
59380
|
}
|
|
59381
|
+
/** Delete one user's cached rows (case-insensitive) in a single transaction. */
|
|
59382
|
+
forgetUser(username) {
|
|
59383
|
+
const key = username.toLowerCase();
|
|
59384
|
+
const count2 = (table) => Number(this.db.get(`SELECT COUNT(*) AS n FROM ${table} WHERE username = ?`, [key])?.n ?? 0);
|
|
59385
|
+
const removed = {
|
|
59386
|
+
checkins: count2("checkins"),
|
|
59387
|
+
distinct_beers: count2("distinct_beers"),
|
|
59388
|
+
sync_state: count2("sync_state")
|
|
59389
|
+
};
|
|
59390
|
+
this.db.transaction(() => {
|
|
59391
|
+
this.db.run("DELETE FROM checkins WHERE username = ?", [key]);
|
|
59392
|
+
this.db.run("DELETE FROM distinct_beers WHERE username = ?", [key]);
|
|
59393
|
+
this.db.run("DELETE FROM sync_state WHERE username = ?", [key]);
|
|
59394
|
+
});
|
|
59395
|
+
return removed;
|
|
59396
|
+
}
|
|
59381
59397
|
getBeerMeta(bids) {
|
|
59382
59398
|
if (bids.length === 0) return [];
|
|
59383
59399
|
const placeholders = bids.map(() => "?").join(", ");
|
|
@@ -59458,6 +59474,9 @@ var LocalCacheStore = class {
|
|
|
59458
59474
|
async upsertBeerMeta(rows) {
|
|
59459
59475
|
this.core.upsertBeerMeta(rows);
|
|
59460
59476
|
}
|
|
59477
|
+
async forgetUser(username) {
|
|
59478
|
+
return this.core.forgetUser(username);
|
|
59479
|
+
}
|
|
59461
59480
|
};
|
|
59462
59481
|
function escapeLike(s) {
|
|
59463
59482
|
return s.replace(/[%_]/g, " ");
|
|
@@ -60293,7 +60312,13 @@ async function checkPhoto(photoPath) {
|
|
|
60293
60312
|
hint: "Attach a normal-sized JPEG or PNG photo."
|
|
60294
60313
|
});
|
|
60295
60314
|
}
|
|
60296
|
-
|
|
60315
|
+
let head;
|
|
60316
|
+
try {
|
|
60317
|
+
head = await readFileHead(real, 16, { allowedRoots: roots });
|
|
60318
|
+
} catch {
|
|
60319
|
+
throw new McpToolError("Photo file not found or not readable.");
|
|
60320
|
+
}
|
|
60321
|
+
const sniffed = sniffMimeBytes(head);
|
|
60297
60322
|
if (sniffed !== PHOTO_CONTENT_TYPES[ext]) {
|
|
60298
60323
|
throw createHelpfulError(
|
|
60299
60324
|
sniffed === "image/jpeg" || sniffed === "image/png" ? `Photo content (${sniffed}) does not match its .${ext} extension.` : "The file is not a JPEG or PNG image.",
|
|
@@ -61288,11 +61313,48 @@ function registerCacheTools(server, client2, cacheProvider) {
|
|
|
61288
61313
|
});
|
|
61289
61314
|
}
|
|
61290
61315
|
);
|
|
61316
|
+
server.registerTool(
|
|
61317
|
+
"untappd_cache_forget",
|
|
61318
|
+
{
|
|
61319
|
+
title: "Forget a user's cached check-ins",
|
|
61320
|
+
description: `Delete everything the LOCAL cache holds for one user \u2014 their cached check-ins (beer, rating, comment, venue, date), distinct-beers list and sync state \u2014 e.g. after syncing a friend you no longer want a history of. Only the local cache is touched; nothing on Untappd changes, and a later sync can re-fetch it. Shared beer metadata is kept. The preview shows the username and exactly how many rows will be removed. Omit username for your own account. ${CONFIRM_FLOW}`,
|
|
61321
|
+
annotations: toolAnnotations({ title: "Forget a user's cached check-ins", readOnly: false, idempotent: true, openWorld: false, destructive: true }),
|
|
61322
|
+
inputSchema: external_exports.object({
|
|
61323
|
+
username: UsernameArg2,
|
|
61324
|
+
confirmToken: confirmTokenParam
|
|
61325
|
+
})
|
|
61326
|
+
},
|
|
61327
|
+
async ({ username, confirmToken }, ctx) => {
|
|
61328
|
+
const user = resolveUser2(username, client2.loginName);
|
|
61329
|
+
const cache = cacheProvider();
|
|
61330
|
+
const counts = {
|
|
61331
|
+
checkins: await cache.cachedCount(user),
|
|
61332
|
+
distinct_beers: await cache.distinctBeersCount(user)
|
|
61333
|
+
};
|
|
61334
|
+
const gate = await confirmWrite(ctx, {
|
|
61335
|
+
tool: "untappd_cache_forget",
|
|
61336
|
+
action: "untappd.cache_forget",
|
|
61337
|
+
message: `Review and confirm deleting ${user}'s data from the local Untappd cache:`,
|
|
61338
|
+
confirmToken,
|
|
61339
|
+
target: user.toLowerCase(),
|
|
61340
|
+
payload: { username: user.toLowerCase(), ...counts },
|
|
61341
|
+
preview: {
|
|
61342
|
+
action: "cache_forget",
|
|
61343
|
+
username: user,
|
|
61344
|
+
...counts,
|
|
61345
|
+
note: "Deletes these rows from the LOCAL cache only; Untappd itself is unchanged and a re-sync can restore them."
|
|
61346
|
+
}
|
|
61347
|
+
});
|
|
61348
|
+
if (gate) return gate;
|
|
61349
|
+
const removed = await cache.forgetUser(user);
|
|
61350
|
+
return minifiedResult({ forgotten: true, username: user, removed });
|
|
61351
|
+
}
|
|
61352
|
+
);
|
|
61291
61353
|
}
|
|
61292
61354
|
|
|
61293
61355
|
// src/cache/db.ts
|
|
61294
61356
|
import { DatabaseSync } from "node:sqlite";
|
|
61295
|
-
import { mkdirSync } from "node:fs";
|
|
61357
|
+
import { chmodSync, closeSync, existsSync as existsSync2, mkdirSync, openSync } from "node:fs";
|
|
61296
61358
|
import { dirname as dirname3, join as join3 } from "node:path";
|
|
61297
61359
|
import { homedir as homedir2 } from "node:os";
|
|
61298
61360
|
var NodeSqlDriver = class {
|
|
@@ -61329,15 +61391,39 @@ var CheckinCache = class _CheckinCache extends LocalCacheStore {
|
|
|
61329
61391
|
this.db = db;
|
|
61330
61392
|
}
|
|
61331
61393
|
db;
|
|
61332
|
-
/**
|
|
61394
|
+
/**
|
|
61395
|
+
* Open (creating parent dirs) a file-backed cache, or `:memory:` for tests.
|
|
61396
|
+
* The file holds other people's dated check-in and venue history, so it is
|
|
61397
|
+
* kept owner-only: dirs 0700, the db (and any -wal/-shm/-journal) 0600.
|
|
61398
|
+
*/
|
|
61333
61399
|
static open(path) {
|
|
61334
|
-
if (path !== ":memory:")
|
|
61335
|
-
|
|
61400
|
+
if (path !== ":memory:") preparePrivateFile(path);
|
|
61401
|
+
const cache = new _CheckinCache(new DatabaseSync(path));
|
|
61402
|
+
if (path !== ":memory:") tightenSidecars(path);
|
|
61403
|
+
return cache;
|
|
61336
61404
|
}
|
|
61337
61405
|
close() {
|
|
61338
61406
|
this.db.close();
|
|
61339
61407
|
}
|
|
61340
61408
|
};
|
|
61409
|
+
function preparePrivateFile(path) {
|
|
61410
|
+
const dir = dirname3(path);
|
|
61411
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
61412
|
+
bestEffortChmod(dir, 448);
|
|
61413
|
+
if (!existsSync2(path)) closeSync(openSync(path, "a", 384));
|
|
61414
|
+
bestEffortChmod(path, 384);
|
|
61415
|
+
}
|
|
61416
|
+
function tightenSidecars(path) {
|
|
61417
|
+
for (const suffix of ["-wal", "-shm", "-journal"]) {
|
|
61418
|
+
if (existsSync2(path + suffix)) bestEffortChmod(path + suffix, 384);
|
|
61419
|
+
}
|
|
61420
|
+
}
|
|
61421
|
+
function bestEffortChmod(path, mode) {
|
|
61422
|
+
try {
|
|
61423
|
+
chmodSync(path, mode);
|
|
61424
|
+
} catch {
|
|
61425
|
+
}
|
|
61426
|
+
}
|
|
61341
61427
|
function defaultCachePath() {
|
|
61342
61428
|
return readEnvVar("UNTAPPD_CACHE_DB") ?? join3(homedir2(), ".untappd-mcp", "checkins.db");
|
|
61343
61429
|
}
|
package/dist/cache/db.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { readEnvVar } from '@chrischall/mcp-utils';
|
|
@@ -46,16 +46,53 @@ export class CheckinCache extends LocalCacheStore {
|
|
|
46
46
|
super(new CheckinStoreCore(new NodeSqlDriver(db)));
|
|
47
47
|
this.db = db;
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Open (creating parent dirs) a file-backed cache, or `:memory:` for tests.
|
|
51
|
+
* The file holds other people's dated check-in and venue history, so it is
|
|
52
|
+
* kept owner-only: dirs 0700, the db (and any -wal/-shm/-journal) 0600.
|
|
53
|
+
*/
|
|
50
54
|
static open(path) {
|
|
51
55
|
if (path !== ':memory:')
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
preparePrivateFile(path);
|
|
57
|
+
const cache = new CheckinCache(new DatabaseSync(path));
|
|
58
|
+
if (path !== ':memory:')
|
|
59
|
+
tightenSidecars(path);
|
|
60
|
+
return cache;
|
|
54
61
|
}
|
|
55
62
|
close() {
|
|
56
63
|
this.db.close();
|
|
57
64
|
}
|
|
58
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Create the cache's directory (0700 — `mode` applies to every dir mkdir
|
|
68
|
+
* creates) and the db file itself (0600) BEFORE SQLite opens it, so the file is
|
|
69
|
+
* never briefly umask-readable. Pre-existing loose entries from older versions
|
|
70
|
+
* are tightened too; chmod failures (e.g. a shared dir the user doesn't own)
|
|
71
|
+
* are best-effort, never fatal.
|
|
72
|
+
*/
|
|
73
|
+
function preparePrivateFile(path) {
|
|
74
|
+
const dir = dirname(path);
|
|
75
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
76
|
+
bestEffortChmod(dir, 0o700);
|
|
77
|
+
if (!existsSync(path))
|
|
78
|
+
closeSync(openSync(path, 'a', 0o600));
|
|
79
|
+
bestEffortChmod(path, 0o600);
|
|
80
|
+
}
|
|
81
|
+
/** SQLite side files inherit the db's mode, but re-assert in case a loose one lingers. */
|
|
82
|
+
function tightenSidecars(path) {
|
|
83
|
+
for (const suffix of ['-wal', '-shm', '-journal']) {
|
|
84
|
+
if (existsSync(path + suffix))
|
|
85
|
+
bestEffortChmod(path + suffix, 0o600);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function bestEffortChmod(path, mode) {
|
|
89
|
+
try {
|
|
90
|
+
chmodSync(path, mode);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* best-effort — not every filesystem supports POSIX modes */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
59
96
|
/** Default on-disk cache path: `$UNTAPPD_CACHE_DB` or `~/.untappd-mcp/checkins.db`. */
|
|
60
97
|
export function defaultCachePath() {
|
|
61
98
|
return readEnvVar('UNTAPPD_CACHE_DB') ?? join(homedir(), '.untappd-mcp', 'checkins.db');
|
package/dist/cache/store.js
CHANGED
|
@@ -466,6 +466,22 @@ export class CheckinStoreCore {
|
|
|
466
466
|
const limit = Math.min(Math.max(filters.limit ?? 25, 1), 200);
|
|
467
467
|
return this.db.all(`SELECT * FROM checkins WHERE ${where.join(' AND ')} ORDER BY ${order} LIMIT ?`, [...params, limit]);
|
|
468
468
|
}
|
|
469
|
+
/** Delete one user's cached rows (case-insensitive) in a single transaction. */
|
|
470
|
+
forgetUser(username) {
|
|
471
|
+
const key = username.toLowerCase();
|
|
472
|
+
const count = (table) => Number(this.db.get(`SELECT COUNT(*) AS n FROM ${table} WHERE username = ?`, [key])?.n ?? 0);
|
|
473
|
+
const removed = {
|
|
474
|
+
checkins: count('checkins'),
|
|
475
|
+
distinct_beers: count('distinct_beers'),
|
|
476
|
+
sync_state: count('sync_state'),
|
|
477
|
+
};
|
|
478
|
+
this.db.transaction(() => {
|
|
479
|
+
this.db.run('DELETE FROM checkins WHERE username = ?', [key]);
|
|
480
|
+
this.db.run('DELETE FROM distinct_beers WHERE username = ?', [key]);
|
|
481
|
+
this.db.run('DELETE FROM sync_state WHERE username = ?', [key]);
|
|
482
|
+
});
|
|
483
|
+
return removed;
|
|
484
|
+
}
|
|
469
485
|
getBeerMeta(bids) {
|
|
470
486
|
if (bids.length === 0)
|
|
471
487
|
return [];
|
|
@@ -540,6 +556,9 @@ export class LocalCacheStore {
|
|
|
540
556
|
async upsertBeerMeta(rows) {
|
|
541
557
|
this.core.upsertBeerMeta(rows);
|
|
542
558
|
}
|
|
559
|
+
async forgetUser(username) {
|
|
560
|
+
return this.core.forgetUser(username);
|
|
561
|
+
}
|
|
543
562
|
}
|
|
544
563
|
// Escape LIKE wildcards in user input so a literal % or _ isn't treated as a
|
|
545
564
|
// wildcard. Our LIKE patterns don't set an ESCAPE clause, so we simply strip the
|
package/dist/tools/cache.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { RateLimitError, createHelpfulError, minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
2
|
+
import { RateLimitError, confirmTokenParam, createHelpfulError, minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
3
3
|
import { beerMetaFrom } from '../cache/store.js';
|
|
4
4
|
import { syncCheckins } from '../cache/sync.js';
|
|
5
5
|
import { syncUserBeers } from '../cache/sync-beers.js';
|
|
6
|
+
import { CONFIRM_FLOW, confirmWrite } from './confirm.js';
|
|
6
7
|
// Re-fetch cached beer metadata at most this often; a hit newer than this skips
|
|
7
8
|
// the beer/info API call.
|
|
8
9
|
const BEER_META_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
@@ -386,4 +387,44 @@ export function registerCacheTools(server, client, cacheProvider) {
|
|
|
386
387
|
freshness: await freshness(cache, user),
|
|
387
388
|
});
|
|
388
389
|
});
|
|
390
|
+
server.registerTool('untappd_cache_forget', {
|
|
391
|
+
title: "Forget a user's cached check-ins",
|
|
392
|
+
description: "Delete everything the LOCAL cache holds for one user — their cached check-ins (beer, rating, comment, venue, " +
|
|
393
|
+
'date), distinct-beers list and sync state — e.g. after syncing a friend you no longer want a history of. Only ' +
|
|
394
|
+
'the local cache is touched; nothing on Untappd changes, and a later sync can re-fetch it. Shared beer ' +
|
|
395
|
+
'metadata is kept. The preview shows the username and exactly how many rows will be removed. Omit username ' +
|
|
396
|
+
`for your own account. ${CONFIRM_FLOW}`,
|
|
397
|
+
annotations: toolAnnotations({ title: "Forget a user's cached check-ins", readOnly: false, idempotent: true, openWorld: false, destructive: true }),
|
|
398
|
+
inputSchema: z.object({
|
|
399
|
+
username: UsernameArg,
|
|
400
|
+
confirmToken: confirmTokenParam,
|
|
401
|
+
}),
|
|
402
|
+
}, async ({ username, confirmToken }, ctx) => {
|
|
403
|
+
const user = resolveUser(username, client.loginName);
|
|
404
|
+
const cache = cacheProvider();
|
|
405
|
+
// Bound into the token: if a sync adds rows after the preview, the
|
|
406
|
+
// token no longer matches and the user re-confirms the real counts.
|
|
407
|
+
const counts = {
|
|
408
|
+
checkins: await cache.cachedCount(user),
|
|
409
|
+
distinct_beers: await cache.distinctBeersCount(user),
|
|
410
|
+
};
|
|
411
|
+
const gate = await confirmWrite(ctx, {
|
|
412
|
+
tool: 'untappd_cache_forget',
|
|
413
|
+
action: 'untappd.cache_forget',
|
|
414
|
+
message: `Review and confirm deleting ${user}'s data from the local Untappd cache:`,
|
|
415
|
+
confirmToken,
|
|
416
|
+
target: user.toLowerCase(),
|
|
417
|
+
payload: { username: user.toLowerCase(), ...counts },
|
|
418
|
+
preview: {
|
|
419
|
+
action: 'cache_forget',
|
|
420
|
+
username: user,
|
|
421
|
+
...counts,
|
|
422
|
+
note: 'Deletes these rows from the LOCAL cache only; Untappd itself is unchanged and a re-sync can restore them.',
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
if (gate)
|
|
426
|
+
return gate;
|
|
427
|
+
const removed = await cache.forgetUser(user);
|
|
428
|
+
return minifiedResult({ forgotten: true, username: user, removed });
|
|
429
|
+
});
|
|
389
430
|
}
|
package/dist/tools/checkin.js
CHANGED
|
@@ -64,7 +64,15 @@ async function checkPhoto(photoPath) {
|
|
|
64
64
|
hint: 'Attach a normal-sized JPEG or PNG photo.',
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
// Re-check UNTAPPD_PHOTO_DIR at open time, so a swap after the check above can't escape it.
|
|
68
|
+
let head;
|
|
69
|
+
try {
|
|
70
|
+
head = await readFileHead(real, 16, { allowedRoots: roots });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw new McpToolError('Photo file not found or not readable.');
|
|
74
|
+
}
|
|
75
|
+
const sniffed = sniffMimeBytes(head);
|
|
68
76
|
if (sniffed !== PHOTO_CONTENT_TYPES[ext]) {
|
|
69
77
|
throw createHelpfulError(sniffed === 'image/jpeg' || sniffed === 'image/png'
|
|
70
78
|
? `Photo content (${sniffed}) does not match its .${ext} extension.`
|
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.2.
|
|
6
|
+
export const VERSION = '2.2.1'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "untappd-mcp",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
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.2.
|
|
9
|
+
"version": "2.2.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "untappd-mcp",
|
|
14
|
-
"version": "2.2.
|
|
14
|
+
"version": "2.2.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -42,7 +42,7 @@ Most user tools default `username` to your configured account when omitted.
|
|
|
42
42
|
|
|
43
43
|
## Response shape (`view`)
|
|
44
44
|
|
|
45
|
-
Thirteen of this server's
|
|
45
|
+
Thirteen of this server's 46 tools take `view: "compact" | "full"`, and
|
|
46
46
|
**`compact` is the DEFAULT** — the slim rung is what you get without asking
|
|
47
47
|
for it.
|
|
48
48
|
|
|
@@ -104,14 +104,14 @@ silently manufactured.
|
|
|
104
104
|
`full` already IS the upstream response and a third value would silently alias
|
|
105
105
|
one that exists.
|
|
106
106
|
|
|
107
|
-
### The
|
|
107
|
+
### The 33 tools without `view`
|
|
108
108
|
|
|
109
109
|
Each for its own reason — and none of them will tell you it ignored the
|
|
110
110
|
parameter, because an undeclared key is dropped by zod without a warning:
|
|
111
111
|
|
|
112
|
-
- **The
|
|
112
|
+
- **The 12 confirmation-gated writes** (`untappd_checkin`, `untappd_toast`,
|
|
113
113
|
`untappd_add_comment`, the two deletes, the wishlist pair, the four friend
|
|
114
|
-
actions) answer with a confirmation preview or a receipt. Nothing in a receipt is
|
|
114
|
+
actions, and the local `untappd_cache_forget`) answer with a confirmation preview or a receipt. Nothing in a receipt is
|
|
115
115
|
decoration.
|
|
116
116
|
- **`untappd_sync_checkins` / `untappd_sync_user_beers`** answer with sync
|
|
117
117
|
PROGRESS — pages walked, `another_run_needed`, `backfill_complete`. Slimming
|
|
@@ -190,6 +190,12 @@ Query tools (has-had ones consult BOTH sources — a hit in either = had):
|
|
|
190
190
|
- `untappd_cache_query` — filter cached check-ins by brewery, style, `min_rating`,
|
|
191
191
|
venue, and date range.
|
|
192
192
|
|
|
193
|
+
**Retention.** The cache keeps what you sync until you remove it — including
|
|
194
|
+
friends' dated check-ins, comments and venues. `untappd_cache_forget` (confirmation-gated)
|
|
195
|
+
deletes one user's check-ins, distinct beers and sync state from the LOCAL cache
|
|
196
|
+
only; Untappd is untouched and a later sync can re-fetch. The file is created
|
|
197
|
+
owner-only (dir `0700`, file `0600`).
|
|
198
|
+
|
|
193
199
|
Every cache read returns a `freshness` block reporting each source's completeness
|
|
194
200
|
separately (plus `coverage_complete` and a `caveat` when incomplete), so you can
|
|
195
201
|
flag a "not found" as possibly a false negative until the relevant sync finishes.
|