untappd-mcp 1.8.7 → 1.9.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.
@@ -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.8.7"
10
+ "version": "1.9.0"
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.8.7",
18
+ "version": "1.9.0",
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.8.7",
4
+ "version": "1.9.0",
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
@@ -26,8 +26,9 @@ it goes stale.
26
26
 
27
27
  | Variable | Required | Description |
28
28
  | --- | --- | --- |
29
- | `UNTAPPD_USERNAME` | yes | Your Untappd username or login email. |
30
- | `UNTAPPD_PASSWORD` | yes | Your Untappd password (used only for the xauth login). |
29
+ | `UNTAPPD_ACCESS_TOKEN` | no | An access token you already hold. Supply this and **no password is needed** — the xauth login is skipped entirely. |
30
+ | `UNTAPPD_USERNAME` | if no token | Your Untappd username or login email. |
31
+ | `UNTAPPD_PASSWORD` | if no token | Your Untappd password (used only for the xauth login that mints a token). |
31
32
  | `UNTAPPD_CLIENT_ID` | yes | The Untappd mobile app client id (see below). |
32
33
  | `UNTAPPD_CLIENT_SECRET` | yes | The Untappd mobile app client secret. |
33
34
  | `UNTAPPD_DEVICE_ID` | no | Stable device UUID the token is keyed to (a default is provided). |
package/dist/bundle.js CHANGED
@@ -31000,53 +31000,6 @@ var StdioServerTransport = class {
31000
31000
  }
31001
31001
  };
31002
31002
 
31003
- // node_modules/@chrischall/mcp-utils/dist/server/index.js
31004
- async function createMcpServer(opts) {
31005
- const server = new McpServer({ name: opts.name, version: opts.version });
31006
- if (opts.banner !== void 0) {
31007
- console.error(opts.banner);
31008
- }
31009
- const deps = opts.deps;
31010
- for (const register of opts.tools) {
31011
- await register(server, deps);
31012
- }
31013
- return server;
31014
- }
31015
- function withGracefulShutdown(server, opts = {}) {
31016
- const shouldExit = opts.exit ?? true;
31017
- let shuttingDown = false;
31018
- const handler = (signal) => {
31019
- if (shuttingDown)
31020
- return;
31021
- shuttingDown = true;
31022
- void (async () => {
31023
- try {
31024
- if (opts.onSignal)
31025
- await opts.onSignal(signal);
31026
- await server.close();
31027
- } catch (err) {
31028
- console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
31029
- } finally {
31030
- if (shouldExit)
31031
- process.exit(0);
31032
- }
31033
- })();
31034
- };
31035
- process.on("SIGINT", () => handler("SIGINT"));
31036
- process.on("SIGTERM", () => handler("SIGTERM"));
31037
- }
31038
- async function runMcp(opts) {
31039
- const server = await createMcpServer(opts);
31040
- const shutdown = opts.shutdown ?? true;
31041
- if (shutdown !== false) {
31042
- withGracefulShutdown(server, shutdown === true ? {} : shutdown);
31043
- }
31044
- const spec = opts.transport ?? "stdio";
31045
- const transport = spec === "stdio" ? new StdioServerTransport() : spec;
31046
- await server.connect(transport);
31047
- return server;
31048
- }
31049
-
31050
31003
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
31051
31004
  var DEFAULT_ERROR_MESSAGE_MAX = 500;
31052
31005
  var McpToolError = class extends Error {
@@ -31133,6 +31086,81 @@ function textResult(data) {
31133
31086
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
31134
31087
  };
31135
31088
  }
31089
+ function errorResult(message) {
31090
+ return {
31091
+ content: [{ type: "text", text: redactSecrets(message) }],
31092
+ isError: true
31093
+ };
31094
+ }
31095
+
31096
+ // node_modules/@chrischall/mcp-utils/dist/server/index.js
31097
+ function hintResultOrRethrow(err) {
31098
+ if (err instanceof McpToolError && err.hint) {
31099
+ return errorResult(`${err.message}
31100
+
31101
+ Hint: ${err.hint}`);
31102
+ }
31103
+ throw err;
31104
+ }
31105
+ function surfaceToolHints(server) {
31106
+ const register = server.registerTool.bind(server);
31107
+ server.registerTool = (name, config2, cb) => register(name, config2, (...args) => {
31108
+ let result;
31109
+ try {
31110
+ result = cb(...args);
31111
+ } catch (err) {
31112
+ return hintResultOrRethrow(err);
31113
+ }
31114
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
31115
+ });
31116
+ }
31117
+ async function createMcpServer(opts) {
31118
+ const server = new McpServer({ name: opts.name, version: opts.version });
31119
+ if (opts.surfaceHints !== false)
31120
+ surfaceToolHints(server);
31121
+ if (opts.banner !== void 0) {
31122
+ console.error(opts.banner);
31123
+ }
31124
+ const deps = opts.deps;
31125
+ for (const register of opts.tools) {
31126
+ await register(server, deps);
31127
+ }
31128
+ return server;
31129
+ }
31130
+ function withGracefulShutdown(server, opts = {}) {
31131
+ const shouldExit = opts.exit ?? true;
31132
+ let shuttingDown = false;
31133
+ const handler = (signal) => {
31134
+ if (shuttingDown)
31135
+ return;
31136
+ shuttingDown = true;
31137
+ void (async () => {
31138
+ try {
31139
+ if (opts.onSignal)
31140
+ await opts.onSignal(signal);
31141
+ await server.close();
31142
+ } catch (err) {
31143
+ console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
31144
+ } finally {
31145
+ if (shouldExit)
31146
+ process.exit(0);
31147
+ }
31148
+ })();
31149
+ };
31150
+ process.on("SIGINT", () => handler("SIGINT"));
31151
+ process.on("SIGTERM", () => handler("SIGTERM"));
31152
+ }
31153
+ async function runMcp(opts) {
31154
+ const server = await createMcpServer(opts);
31155
+ const shutdown = opts.shutdown ?? true;
31156
+ if (shutdown !== false) {
31157
+ withGracefulShutdown(server, shutdown === true ? {} : shutdown);
31158
+ }
31159
+ const spec = opts.transport ?? "stdio";
31160
+ const transport = spec === "stdio" ? new StdioServerTransport() : spec;
31161
+ await server.connect(transport);
31162
+ return server;
31163
+ }
31136
31164
 
31137
31165
  // node_modules/@chrischall/mcp-utils/dist/config/index.js
31138
31166
  var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
@@ -31232,7 +31260,7 @@ function toolAnnotations(opts = {}) {
31232
31260
  }
31233
31261
 
31234
31262
  // src/version.ts
31235
- var VERSION = "1.8.7";
31263
+ var VERSION = "1.9.0";
31236
31264
 
31237
31265
  // src/client.ts
31238
31266
  import { dirname, join } from "path";
@@ -31324,11 +31352,10 @@ async function xauthLogin(creds, opts = {}) {
31324
31352
  }
31325
31353
  function missingCredsError() {
31326
31354
  const missing = ["UNTAPPD_USERNAME", "UNTAPPD_PASSWORD", "UNTAPPD_CLIENT_ID", "UNTAPPD_CLIENT_SECRET"].filter((k) => !readEnvVar(k));
31355
+ const remedy = "Either set UNTAPPD_ACCESS_TOKEN (an access token you already hold \u2014 no password needed), or set UNTAPPD_USERNAME and UNTAPPD_PASSWORD to log in for one. Either way UNTAPPD_CLIENT_ID and UNTAPPD_CLIENT_SECRET (the Untappd mobile app client credentials) are required. See the README.";
31327
31356
  return createHelpfulError(
31328
- `Untappd credentials are not configured \u2014 missing ${missing.join(", ") || "credentials"}.`,
31329
- {
31330
- hint: "Set UNTAPPD_USERNAME and UNTAPPD_PASSWORD (your Untappd login), plus UNTAPPD_CLIENT_ID and UNTAPPD_CLIENT_SECRET (the Untappd mobile app client credentials). See the README for how to obtain them."
31331
- }
31357
+ `Untappd credentials are not configured \u2014 missing ${missing.join(", ") || "credentials"}. ${remedy}`,
31358
+ { hint: remedy }
31332
31359
  );
31333
31360
  }
31334
31361
  var UntappdClient = class {
@@ -31343,6 +31370,7 @@ var UntappdClient = class {
31343
31370
  userAgent = readEnvVar("UNTAPPD_USER_AGENT") ?? DEFAULTS.userAgent;
31344
31371
  token;
31345
31372
  loginInFlight = null;
31373
+ tokenIsSupplied;
31346
31374
  /**
31347
31375
  * Defer the config error so the server can still start (and respond to the
31348
31376
  * host's install-time tools/list smoke test) when credentials aren't set yet.
@@ -31350,7 +31378,8 @@ var UntappdClient = class {
31350
31378
  */
31351
31379
  constructor(opts = {}) {
31352
31380
  this.fetchImpl = opts.fetchImpl ?? fetch;
31353
- this.token = opts.token ?? null;
31381
+ this.token = opts.token ?? readEnvVar("UNTAPPD_ACCESS_TOKEN") ?? null;
31382
+ this.tokenIsSupplied = this.token !== null;
31354
31383
  this.clientId = opts.clientId ?? readEnvVar("UNTAPPD_CLIENT_ID") ?? null;
31355
31384
  this.clientSecret = opts.clientSecret ?? readEnvVar("UNTAPPD_CLIENT_SECRET") ?? null;
31356
31385
  this.username = opts.username ?? readEnvVar("UNTAPPD_USERNAME") ?? null;
@@ -31413,6 +31442,7 @@ var UntappdClient = class {
31413
31442
  userAgent: this.userAgent
31414
31443
  });
31415
31444
  this.token = token;
31445
+ this.tokenIsSupplied = false;
31416
31446
  return token;
31417
31447
  }
31418
31448
  async parseJson(res, method, path) {
@@ -31462,6 +31492,14 @@ var UntappdClient = class {
31462
31492
  }
31463
31493
  const res = await this.send(method, `${BASE_URL}${path}${qs}`, { headers, body });
31464
31494
  if (res.status === 401 && !isRetry) {
31495
+ if (this.tokenIsSupplied && !(this.username && this.password)) {
31496
+ throw createHelpfulError(
31497
+ "Untappd rejected the access token (401) \u2014 UNTAPPD_ACCESS_TOKEN has expired or been revoked. Supply a fresh token, or set UNTAPPD_USERNAME and UNTAPPD_PASSWORD so a new one can be minted automatically.",
31498
+ {
31499
+ hint: "The token is stale, not missing. A pre-seeded token cannot be refreshed without a login."
31500
+ }
31501
+ );
31502
+ }
31465
31503
  this.token = null;
31466
31504
  return this.request(method, path, opts, true);
31467
31505
  }
package/dist/client.js CHANGED
@@ -112,10 +112,13 @@ export async function xauthLogin(creds, opts = {}) {
112
112
  }
113
113
  function missingCredsError() {
114
114
  const missing = ['UNTAPPD_USERNAME', 'UNTAPPD_PASSWORD', 'UNTAPPD_CLIENT_ID', 'UNTAPPD_CLIENT_SECRET'].filter((k) => !readEnvVar(k));
115
- return createHelpfulError(`Untappd credentials are not configured missing ${missing.join(', ') || 'credentials'}.`, {
116
- hint: 'Set UNTAPPD_USERNAME and UNTAPPD_PASSWORD (your Untappd login), plus UNTAPPD_CLIENT_ID and ' +
117
- 'UNTAPPD_CLIENT_SECRET (the Untappd mobile app client credentials). See the README for how to obtain them.',
118
- });
115
+ // Both paths in the MESSAGE, not only `hint`: MCP serialization surfaces the
116
+ // message and drops the hint, so a token path mentioned only there is
117
+ // invisible exactly when someone is deciding what to configure.
118
+ const remedy = 'Either set UNTAPPD_ACCESS_TOKEN (an access token you already hold — no password needed), or set ' +
119
+ 'UNTAPPD_USERNAME and UNTAPPD_PASSWORD to log in for one. Either way UNTAPPD_CLIENT_ID and ' +
120
+ 'UNTAPPD_CLIENT_SECRET (the Untappd mobile app client credentials) are required. See the README.';
121
+ return createHelpfulError(`Untappd credentials are not configured — missing ${missing.join(', ') || 'credentials'}. ${remedy}`, { hint: remedy });
119
122
  }
120
123
  export class UntappdClient {
121
124
  fetchImpl;
@@ -129,6 +132,7 @@ export class UntappdClient {
129
132
  userAgent = readEnvVar('UNTAPPD_USER_AGENT') ?? DEFAULTS.userAgent;
130
133
  token;
131
134
  loginInFlight = null;
135
+ tokenIsSupplied;
132
136
  /**
133
137
  * Defer the config error so the server can still start (and respond to the
134
138
  * host's install-time tools/list smoke test) when credentials aren't set yet.
@@ -136,7 +140,15 @@ export class UntappdClient {
136
140
  */
137
141
  constructor(opts = {}) {
138
142
  this.fetchImpl = opts.fetchImpl ?? fetch;
139
- this.token = opts.token ?? null;
143
+ // Path 1 of the ladder: a token the consumer already holds. Reachable from
144
+ // the environment, not just from code — otherwise the only configuration a
145
+ // deployment can express is "hand over the password", which is the shape
146
+ // this exists to avoid.
147
+ this.token = opts.token ?? readEnvVar('UNTAPPD_ACCESS_TOKEN') ?? null;
148
+ // Whether that token was GIVEN to us rather than minted by a login. It
149
+ // decides what a later 401 means: a minted token can simply be re-minted,
150
+ // a supplied one can only be replaced by whoever supplied it.
151
+ this.tokenIsSupplied = this.token !== null;
140
152
  this.clientId = opts.clientId ?? readEnvVar('UNTAPPD_CLIENT_ID') ?? null;
141
153
  this.clientSecret = opts.clientSecret ?? readEnvVar('UNTAPPD_CLIENT_SECRET') ?? null;
142
154
  this.username = opts.username ?? readEnvVar('UNTAPPD_USERNAME') ?? null;
@@ -203,6 +215,7 @@ export class UntappdClient {
203
215
  userAgent: this.userAgent,
204
216
  });
205
217
  this.token = token;
218
+ this.tokenIsSupplied = false;
206
219
  return token;
207
220
  }
208
221
  async parseJson(res, method, path) {
@@ -259,8 +272,19 @@ export class UntappdClient {
259
272
  body = f.toString();
260
273
  }
261
274
  const res = await this.send(method, `${BASE_URL}${path}${qs}`, { headers, body });
262
- // A 401 means the cached token went stale drop it and log in once more.
275
+ // A 401 means the token went stale. Re-minting one needs a login, and a
276
+ // deployment that SUPPLIED the token may have no username/password to log
277
+ // in with — that fell through to `missingCredsError()` and told the
278
+ // operator to set the very variable they had set (CLAUDE.md, "a pre-seeded
279
+ // token is one-way"). Name what actually happened instead, and don't spend
280
+ // a second request discovering there is nothing to retry with.
263
281
  if (res.status === 401 && !isRetry) {
282
+ if (this.tokenIsSupplied && !(this.username && this.password)) {
283
+ throw createHelpfulError('Untappd rejected the access token (401) — UNTAPPD_ACCESS_TOKEN has expired or been revoked. ' +
284
+ 'Supply a fresh token, or set UNTAPPD_USERNAME and UNTAPPD_PASSWORD so a new one can be minted automatically.', {
285
+ hint: 'The token is stale, not missing. A pre-seeded token cannot be refreshed without a login.',
286
+ });
287
+ }
264
288
  this.token = null;
265
289
  return this.request(method, path, opts, true);
266
290
  }
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.8.7'; // x-release-please-version
6
+ export const VERSION = '1.9.0'; // x-release-please-version
package/mint.yaml ADDED
@@ -0,0 +1,68 @@
1
+ version: 1
2
+ name: Untappd
3
+ slug: untappd
4
+ summary: >-
5
+ Search Untappd beers, breweries, and venues; read profiles, check-ins,
6
+ wishlists, and your friend feed; and post check-ins, toasts, and comments
7
+ to your account
8
+ env:
9
+ - name: UNTAPPD_USERNAME
10
+ required: true
11
+ help: >-
12
+ Your Untappd username or login email.
13
+ - name: UNTAPPD_PASSWORD
14
+ secret: true
15
+ required: true
16
+ help: >-
17
+ Your Untappd account password (used only for xauth login).
18
+ - name: UNTAPPD_CLIENT_ID
19
+ secret: true
20
+ required: true
21
+ help: >-
22
+ Untappd mobile app client id (obtained by intercepting the app; see
23
+ README).
24
+ - name: UNTAPPD_CLIENT_SECRET
25
+ secret: true
26
+ required: true
27
+ help: >-
28
+ Untappd mobile app client secret.
29
+ - name: UNTAPPD_CACHE_DB
30
+ required: false
31
+ help: >-
32
+ Path to the on-disk check-in cache (defaults to
33
+ ~/.untappd-mcp/checkins.db). Relates to state.dataDir below.
34
+ - name: UNTAPPD_DEVICE_ID
35
+ required: false
36
+ help: >-
37
+ A stable UUID that Untappd keys the returned access token to. Any stable
38
+ value works; set it to pin your own instead of the built-in default.
39
+ - name: UNTAPPD_USER_AGENT
40
+ required: false
41
+ help: >-
42
+ Overrides the User-Agent sent upstream. Leave unset unless the service
43
+ requires a specific one.
44
+ - name: UNTAPPD_UTV
45
+ required: false
46
+ help: >-
47
+ Overrides the Untappd app version string, which is sent on every request
48
+ (not only at login). Leave unset unless a version bump is needed to keep
49
+ the API accepting requests.
50
+ state:
51
+ dataDir: true
52
+ reason: >-
53
+ Persists the check-in cache SQLite database (UNTAPPD_CACHE_DB, default
54
+ ~/.untappd-mcp/checkins.db) under $HOME. Without it every cold start begins
55
+ with an empty cache and has to re-sync from the API. The access token is
56
+ NOT persisted — it is held in memory only — so this is about avoiding
57
+ repeated syncs, not about surviving authentication.
58
+ egress:
59
+ allow:
60
+ # Only hosts the SERVER process actually fetches. Hosts that appear
61
+ # solely in a URL this server BUILDS and returns are excluded.
62
+ - api.untappd.com
63
+ #
64
+ # NOTE — this server also reaches at least one host determined at
65
+ # RUNTIME, which cannot appear as a literal here:
66
+ # - a signed upload URL returned by the API (often storage.googleapis.com / s3)
67
+ # Confirm the concrete host(s) against a live run and add them, or
68
+ # the isolated tier will block the call.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "untappd-mcp",
3
- "version": "1.8.7",
3
+ "version": "1.9.0",
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>",
@@ -29,18 +29,20 @@
29
29
  ".claude-plugin",
30
30
  "skills/",
31
31
  ".mcp.json",
32
- "server.json"
32
+ "server.json",
33
+ "mint.yaml"
33
34
  ],
34
35
  "scripts": {
35
36
  "build": "tsc && npm run bundle",
36
37
  "bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
37
38
  "dev": "node dist/index.js",
38
- "test": "vitest run",
39
+ "test": "npm run typecheck && vitest run",
39
40
  "test:watch": "vitest",
40
- "test:coverage": "vitest run --coverage"
41
+ "test:coverage": "npm run typecheck && vitest run --coverage",
42
+ "typecheck": "tsc -p tsconfig.json --noEmit"
41
43
  },
42
44
  "dependencies": {
43
- "@chrischall/mcp-utils": "^0.14.0",
45
+ "@chrischall/mcp-utils": "^0.15.0",
44
46
  "@modelcontextprotocol/sdk": "^1.29.0",
45
47
  "ai": "^7.0.19",
46
48
  "dotenv": "^17.4.0",
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.8.7",
9
+ "version": "1.9.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "untappd-mcp",
14
- "version": "1.8.7",
14
+ "version": "1.9.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },