untappd-mcp 2.1.0 → 2.1.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": "2.1.0"
10
+ "version": "2.1.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.1.0",
18
+ "version": "2.1.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.1.0",
4
+ "version": "2.1.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
@@ -2282,7 +2282,7 @@ var require_versions = __commonJS({
2282
2282
  exports.version = {
2283
2283
  major: 4,
2284
2284
  minor: 6,
2285
- patch: 4
2285
+ patch: 5
2286
2286
  };
2287
2287
  (function() {
2288
2288
  var keys = Object.getOwnPropertyNames(exports);
@@ -4851,7 +4851,7 @@ var require_memoizer = __commonJS({
4851
4851
  var NONE2 = 0;
4852
4852
  var ASSUMED2 = 1;
4853
4853
  var PROVEN2 = 2;
4854
- function isRecursive2(inst, stack, resolve) {
4854
+ function isRecursive2(inst, stack, resolve3) {
4855
4855
  const cached2 = recursive2.get(inst);
4856
4856
  if (cached2 !== void 0)
4857
4857
  return cached2 ? PROVEN2 : NONE2;
@@ -4861,7 +4861,7 @@ var require_memoizer = __commonJS({
4861
4861
  let result = NONE2;
4862
4862
  const check2 = (child) => {
4863
4863
  if (result !== PROVEN2 && child?._zod) {
4864
- const answer = isRecursive2(child, stack, resolve);
4864
+ const answer = isRecursive2(child, stack, resolve3);
4865
4865
  if (answer > result)
4866
4866
  result = answer;
4867
4867
  }
@@ -4872,7 +4872,7 @@ var require_memoizer = __commonJS({
4872
4872
  const desc = Object.getOwnPropertyDescriptor(sh, key);
4873
4873
  if (spread && !desc.enumerable)
4874
4874
  continue;
4875
- const child = desc.get ? ASSUMED2 : desc.value?._zod ? isRecursive2(desc.value, stack, resolve) : NONE2;
4875
+ const child = desc.get ? ASSUMED2 : desc.value?._zod ? isRecursive2(desc.value, stack, resolve3) : NONE2;
4876
4876
  if (child > answer)
4877
4877
  answer = child;
4878
4878
  }
@@ -4936,7 +4936,7 @@ var require_memoizer = __commonJS({
4936
4936
  break;
4937
4937
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
4938
4938
  case "lazy": {
4939
- const inner = def._cachedInner ?? (resolve ? inst._zod.innerType : void 0);
4939
+ const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
4940
4940
  merge2(inner ? isRecursive2(inner, stack, false) : ASSUMED2);
4941
4941
  break;
4942
4942
  }
@@ -23302,6 +23302,20 @@ var require_v4 = __commonJS({
23302
23302
  }
23303
23303
  });
23304
23304
 
23305
+ // node_modules/@chrischall/mcp-utils/dist/cancel/index.js
23306
+ import { AsyncLocalStorage } from "node:async_hooks";
23307
+ var storage = new AsyncLocalStorage();
23308
+ function withCallSignal(signal, fn, request) {
23309
+ return signal ? storage.run({ signal, ...request ? { request } : {} }, fn) : fn();
23310
+ }
23311
+
23312
+ // node_modules/@chrischall/mcp-utils/dist/caller/index.js
23313
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
23314
+ var storage2 = new AsyncLocalStorage2();
23315
+ function withCallerCapabilities(capabilities, fn) {
23316
+ return capabilities ? storage2.run(capabilities, fn) : fn();
23317
+ }
23318
+
23305
23319
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
23306
23320
  var __create2 = Object.create;
23307
23321
  var __defProp2 = Object.defineProperty;
@@ -27704,14 +27718,14 @@ function inputRequiredRoundsExceededMessage(method, maxRounds) {
27704
27718
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
27705
27719
  }
27706
27720
  function sleep(ms, signal) {
27707
- return new Promise((resolve, reject) => {
27721
+ return new Promise((resolve3, reject) => {
27708
27722
  if (signal?.aborted) {
27709
27723
  reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
27710
27724
  return;
27711
27725
  }
27712
27726
  const timer = setTimeout(() => {
27713
27727
  signal?.removeEventListener("abort", onAbort);
27714
- resolve();
27728
+ resolve3();
27715
27729
  }, ms);
27716
27730
  const onAbort = () => {
27717
27731
  clearTimeout(timer);
@@ -28505,7 +28519,7 @@ var Protocol = class {
28505
28519
  const flowStartedAt = Date.now();
28506
28520
  let onAbort;
28507
28521
  let cleanupMessageId;
28508
- return new Promise((resolve, reject) => {
28522
+ return new Promise((resolve3, reject) => {
28509
28523
  const earlyReject = (error62) => {
28510
28524
  reject(error62);
28511
28525
  };
@@ -28573,7 +28587,7 @@ var Protocol = class {
28573
28587
  }
28574
28588
  if (decoded.kind === "invalid") return reject(decoded.error);
28575
28589
  if (decoded.kind === "input_required") {
28576
- if (options?.allowInputRequired === true) return resolve(manualInputRequiredValue(decoded));
28590
+ if (options?.allowInputRequired === true) return resolve3(manualInputRequiredValue(decoded));
28577
28591
  const flow = {
28578
28592
  codec: codec2,
28579
28593
  request,
@@ -28585,11 +28599,11 @@ var Protocol = class {
28585
28599
  params
28586
28600
  }, resultSchema, legOptions)
28587
28601
  };
28588
- return resolve(this._resolveNonCompleteResult(decoded, flow));
28602
+ return resolve3(this._resolveNonCompleteResult(decoded, flow));
28589
28603
  }
28590
28604
  const result = decoded.result;
28591
28605
  validateStandardSchema(resultSchema, result).then((parseResult) => {
28592
- if (parseResult.success) resolve(parseResult.data);
28606
+ if (parseResult.success) resolve3(parseResult.data);
28593
28607
  else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
28594
28608
  }, reject);
28595
28609
  });
@@ -31501,7 +31515,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin(((exports) => {
31501
31515
  ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
31502
31516
  const schOrFunc = root.refs[ref];
31503
31517
  if (schOrFunc) return schOrFunc;
31504
- let _sch = resolve.call(this, root, ref);
31518
+ let _sch = resolve3.call(this, root, ref);
31505
31519
  if (_sch === void 0) {
31506
31520
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
31507
31521
  const { schemaId } = this.opts;
@@ -31527,7 +31541,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin(((exports) => {
31527
31541
  function sameSchemaEnv(s1, s2) {
31528
31542
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
31529
31543
  }
31530
- function resolve(root, ref) {
31544
+ function resolve3(root, ref) {
31531
31545
  let sch;
31532
31546
  while (typeof (sch = this.refs[ref]) == "string") ref = sch;
31533
31547
  return sch || this.schemas[ref] || resolveSchema.call(this, root, ref);
@@ -31977,47 +31991,47 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
31977
31991
  else if (typeof uri === "object") uri = parse3(serialize(uri, options), options);
31978
31992
  return uri;
31979
31993
  }
31980
- function resolve(baseURI, relativeURI, options) {
31994
+ function resolve3(baseURI, relativeURI, options) {
31981
31995
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
31982
31996
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
31983
31997
  schemelessOptions.skipEscape = true;
31984
31998
  return serialize(resolved, schemelessOptions);
31985
31999
  }
31986
- function resolveComponent(base, relative, options, skipNormalization) {
32000
+ function resolveComponent(base, relative2, options, skipNormalization) {
31987
32001
  const target = {};
31988
32002
  if (!skipNormalization) {
31989
32003
  base = parse3(serialize(base, options), options);
31990
- relative = parse3(serialize(relative, options), options);
32004
+ relative2 = parse3(serialize(relative2, options), options);
31991
32005
  }
31992
32006
  options = options || {};
31993
- if (!options.tolerant && relative.scheme) {
31994
- target.scheme = relative.scheme;
31995
- target.userinfo = relative.userinfo;
31996
- target.host = relative.host;
31997
- target.port = relative.port;
31998
- target.path = removeDotSegments(relative.path || "");
31999
- target.query = relative.query;
32007
+ if (!options.tolerant && relative2.scheme) {
32008
+ target.scheme = relative2.scheme;
32009
+ target.userinfo = relative2.userinfo;
32010
+ target.host = relative2.host;
32011
+ target.port = relative2.port;
32012
+ target.path = removeDotSegments(relative2.path || "");
32013
+ target.query = relative2.query;
32000
32014
  } else {
32001
- if (relative.userinfo !== void 0 || relative.host !== void 0 || relative.port !== void 0) {
32002
- target.userinfo = relative.userinfo;
32003
- target.host = relative.host;
32004
- target.port = relative.port;
32005
- target.path = removeDotSegments(relative.path || "");
32006
- target.query = relative.query;
32015
+ if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
32016
+ target.userinfo = relative2.userinfo;
32017
+ target.host = relative2.host;
32018
+ target.port = relative2.port;
32019
+ target.path = removeDotSegments(relative2.path || "");
32020
+ target.query = relative2.query;
32007
32021
  } else {
32008
- if (!relative.path) {
32022
+ if (!relative2.path) {
32009
32023
  target.path = base.path;
32010
- if (relative.query !== void 0) target.query = relative.query;
32024
+ if (relative2.query !== void 0) target.query = relative2.query;
32011
32025
  else target.query = base.query;
32012
32026
  } else {
32013
- if (relative.path[0] === "/") target.path = removeDotSegments(relative.path);
32027
+ if (relative2.path[0] === "/") target.path = removeDotSegments(relative2.path);
32014
32028
  else {
32015
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative.path;
32016
- else if (!base.path) target.path = relative.path;
32017
- else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
32029
+ if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative2.path;
32030
+ else if (!base.path) target.path = relative2.path;
32031
+ else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
32018
32032
  target.path = removeDotSegments(target.path);
32019
32033
  }
32020
- target.query = relative.query;
32034
+ target.query = relative2.query;
32021
32035
  }
32022
32036
  target.userinfo = base.userinfo;
32023
32037
  target.host = base.host;
@@ -32025,7 +32039,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
32025
32039
  }
32026
32040
  target.scheme = base.scheme;
32027
32041
  }
32028
- target.fragment = relative.fragment;
32042
+ target.fragment = relative2.fragment;
32029
32043
  return target;
32030
32044
  }
32031
32045
  function equal(uriA, uriB, options) {
@@ -32151,7 +32165,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
32151
32165
  const fastUri = {
32152
32166
  SCHEMES,
32153
32167
  normalize,
32154
- resolve,
32168
+ resolve: resolve3,
32155
32169
  resolveComponent,
32156
32170
  equal,
32157
32171
  serialize,
@@ -37524,7 +37538,7 @@ var StdioServerTransport = class {
37524
37538
  }
37525
37539
  send(message) {
37526
37540
  if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
37527
- return new Promise((resolve, reject) => {
37541
+ return new Promise((resolve3, reject) => {
37528
37542
  const json2 = serializeMessage(message);
37529
37543
  let settled = false;
37530
37544
  const onError = (error62) => {
@@ -37539,14 +37553,14 @@ var StdioServerTransport = class {
37539
37553
  settled = true;
37540
37554
  this._stdout.off("error", onError);
37541
37555
  this._stdout.off("drain", onDrain);
37542
- resolve();
37556
+ resolve3();
37543
37557
  };
37544
37558
  this._stdout.once("error", onError);
37545
37559
  if (this._stdout.write(json2)) {
37546
37560
  if (settled) return;
37547
37561
  settled = true;
37548
37562
  this._stdout.off("error", onError);
37549
- resolve();
37563
+ resolve3();
37550
37564
  } else if (!settled) this._stdout.once("drain", onDrain);
37551
37565
  });
37552
37566
  }
@@ -37600,14 +37614,14 @@ var StdioConnectionChannel = class {
37600
37614
  */
37601
37615
  async whenRequestsAnswered(timeoutMs) {
37602
37616
  if (this._closed || this._pendingRequests.size === 0) return true;
37603
- return await new Promise((resolve) => {
37617
+ return await new Promise((resolve3) => {
37604
37618
  const waiter = () => {
37605
37619
  clearTimeout(timer);
37606
- resolve(true);
37620
+ resolve3(true);
37607
37621
  };
37608
37622
  const timer = setTimeout(() => {
37609
37623
  this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
37610
- resolve(false);
37624
+ resolve3(false);
37611
37625
  }, timeoutMs);
37612
37626
  this._drainWaiters.push(waiter);
37613
37627
  });
@@ -38005,13 +38019,28 @@ var API_KEY_RE = new RegExp([
38005
38019
  "whsec_[A-Za-z0-9]{16,}\\b"
38006
38020
  // webhook signing secret (Stripe-style)
38007
38021
  ].map((p) => `\\b${p}`).join("|"), "g");
38008
- var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
38022
+ var QUERY_SECRET_RE = /([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
38023
+ var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
38024
+ var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
38025
+ var HEADER_SECRET_RE = /(\b(?:x[-_][a-z0-9_-]*?(?:api[-_]?key|token|secret|auth)[a-z0-9_-]*|api[-_]?key)\s*:\s*)[^\s,;"'<>`]+/gi;
38009
38026
  var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
38010
- var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
38011
- var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
38012
- var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
38027
+ var JSON_SECRET_KEYS = [
38028
+ "(?:access|refresh|id|auth|session|bearer|csrf|xsrf)[_-]?token",
38029
+ "client[_-]?secret",
38030
+ "api[_-]?secret",
38031
+ "(?:x[_-])?api[_-]?key",
38032
+ "private[_-]?key",
38033
+ "x[_-][a-z0-9_-]*?(?:api[_-]?key|token|secret|auth)[a-z0-9_-]*",
38034
+ "(?:proxy-)?authorization",
38035
+ "password",
38036
+ "passwd",
38037
+ "secret",
38038
+ "token"
38039
+ ].join("|");
38040
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")(?:[^"\\\\]|\\\\.)*(")`, "gi");
38041
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')(?:[^'\\\\]|\\\\.)*(')`, "gi");
38013
38042
  function redactSecrets(text) {
38014
- return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
38043
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(HEADER_SECRET_RE, "$1[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(OAUTH_CODE_RE, "$1[REDACTED]").replace(FORM_PASSWORD_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
38015
38044
  }
38016
38045
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
38017
38046
  const str2 = text === null || text === void 0 ? "" : String(text);
@@ -40616,7 +40645,7 @@ ${content.join("\n")}
40616
40645
  var version = {
40617
40646
  major: 4,
40618
40647
  minor: 6,
40619
- patch: 4
40648
+ patch: 5
40620
40649
  };
40621
40650
 
40622
40651
  // node_modules/zod/v4/core/schemas.js
@@ -43052,7 +43081,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
43052
43081
  var NONE = 0;
43053
43082
  var ASSUMED = 1;
43054
43083
  var PROVEN = 2;
43055
- function isRecursive(inst, stack, resolve) {
43084
+ function isRecursive(inst, stack, resolve3) {
43056
43085
  const cached2 = recursive.get(inst);
43057
43086
  if (cached2 !== void 0)
43058
43087
  return cached2 ? PROVEN : NONE;
@@ -43062,7 +43091,7 @@ function isRecursive(inst, stack, resolve) {
43062
43091
  let result = NONE;
43063
43092
  const check2 = (child) => {
43064
43093
  if (result !== PROVEN && child?._zod) {
43065
- const answer = isRecursive(child, stack, resolve);
43094
+ const answer = isRecursive(child, stack, resolve3);
43066
43095
  if (answer > result)
43067
43096
  result = answer;
43068
43097
  }
@@ -43073,7 +43102,7 @@ function isRecursive(inst, stack, resolve) {
43073
43102
  const desc = Object.getOwnPropertyDescriptor(sh, key);
43074
43103
  if (spread && !desc.enumerable)
43075
43104
  continue;
43076
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve) : NONE;
43105
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve3) : NONE;
43077
43106
  if (child > answer)
43078
43107
  answer = child;
43079
43108
  }
@@ -43137,7 +43166,7 @@ function isRecursive(inst, stack, resolve) {
43137
43166
  break;
43138
43167
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
43139
43168
  case "lazy": {
43140
- const inner = def._cachedInner ?? (resolve ? inst._zod.innerType : void 0);
43169
+ const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
43141
43170
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
43142
43171
  break;
43143
43172
  }
@@ -57749,17 +57778,73 @@ Hint: ${err.hint}`);
57749
57778
  }
57750
57779
  throw err;
57751
57780
  }
57781
+ function mcpRequestFrom(args) {
57782
+ const ctx = args.at(-1);
57783
+ if (typeof ctx !== "object" || ctx === null)
57784
+ return void 0;
57785
+ const req = ctx.mcpReq;
57786
+ if (typeof req !== "object" || req === null)
57787
+ return void 0;
57788
+ return typeof req.notify === "function" ? req : void 0;
57789
+ }
57790
+ function callSignalFrom(args) {
57791
+ const ctx = args.at(-1);
57792
+ if (typeof ctx !== "object" || ctx === null)
57793
+ return void 0;
57794
+ const req = ctx.mcpReq;
57795
+ if (typeof req !== "object" || req === null)
57796
+ return void 0;
57797
+ const signal = req.signal;
57798
+ return signal instanceof AbortSignal ? signal : void 0;
57799
+ }
57800
+ function declaredCapabilitiesFrom(server) {
57801
+ const inner = server.server;
57802
+ if (typeof inner !== "object" || inner === null)
57803
+ return void 0;
57804
+ const read = inner.getClientCapabilities;
57805
+ if (typeof read !== "function")
57806
+ return void 0;
57807
+ try {
57808
+ const declared = read.call(inner);
57809
+ return typeof declared === "object" && declared !== null ? declared : void 0;
57810
+ } catch {
57811
+ return void 0;
57812
+ }
57813
+ }
57752
57814
  function surfaceToolHints(server) {
57753
57815
  const register2 = server.registerTool.bind(server);
57754
- server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => {
57755
- let result;
57756
- try {
57757
- result = cb(...args);
57758
- } catch (err) {
57759
- return hintResultOrRethrow(err);
57760
- }
57761
- return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
57762
- });
57816
+ server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => (
57817
+ // THE CALLER'S CANCELLATION, made ambient for the whole handler
57818
+ // (`cancel/index.ts`). The SDK delivers it and the fleet ignored it:
57819
+ // measured against @modelcontextprotocol/server 2.0.0, a cancelled
57820
+ // call aborts `ctx.mcpReq.signal` with the caller's reason and the
57821
+ // handler runs to completion regardless — so the HTTP request stays
57822
+ // in flight, the child keeps burning metered CPU, and the upstream
57823
+ // keeps being hit for somebody who has gone. claude.ai sent 101
57824
+ // cancellations in the week to 2026-09-20.
57825
+ //
57826
+ // Here because this is the one wrapper every tool already passes
57827
+ // through: threading the signal by hand would mean editing several
57828
+ // hundred handlers and missing exactly the ones nobody edits.
57829
+ withCallSignal(callSignalFrom(args), () => (
57830
+ // WHAT THE CALLER CAN DO, made ambient for the same reason and in
57831
+ // the same place (`caller/index.ts`). Only the 2025-era half needs
57832
+ // the wrapper — a 2026-07-28 request answers for itself off its own
57833
+ // envelope — but a guarded tool that cannot tell whether the caller
57834
+ // can be shown a prompt returns one the SDK then refuses to deliver,
57835
+ // and that refusal reaches the caller as an unexplained protocol
57836
+ // error it had no chance to catch.
57837
+ withCallerCapabilities(declaredCapabilitiesFrom(server), () => {
57838
+ let result;
57839
+ try {
57840
+ result = cb(...args);
57841
+ } catch (err) {
57842
+ return hintResultOrRethrow(err);
57843
+ }
57844
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
57845
+ })
57846
+ ), mcpRequestFrom(args))
57847
+ ));
57763
57848
  }
57764
57849
  async function createMcpServer(opts) {
57765
57850
  const server = new McpServer({ name: opts.name, version: opts.version }, { supportedProtocolVersions: [...SERVER_PROTOCOL_VERSIONS] });
@@ -57774,13 +57859,47 @@ async function createMcpServer(opts) {
57774
57859
  }
57775
57860
  return server;
57776
57861
  }
57862
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5e3;
57863
+ var DEFAULT_REPEAT_SIGNAL_GRACE_MS = 500;
57777
57864
  function withGracefulShutdown(target, opts = {}) {
57778
57865
  const shouldExit = opts.exit ?? true;
57866
+ if (opts.timeoutMs !== void 0 && !(Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0)) {
57867
+ throw new RangeError("withGracefulShutdown: timeoutMs must be a finite number greater than 0.");
57868
+ }
57869
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
57870
+ if (opts.repeatSignalGraceMs !== void 0 && !(Number.isFinite(opts.repeatSignalGraceMs) && opts.repeatSignalGraceMs >= 0)) {
57871
+ throw new RangeError("withGracefulShutdown: repeatSignalGraceMs must be a finite number >= 0.");
57872
+ }
57873
+ const repeatGraceMs = opts.repeatSignalGraceMs ?? DEFAULT_REPEAT_SIGNAL_GRACE_MS;
57779
57874
  let shuttingDown = false;
57875
+ let firstSignalAt = 0;
57876
+ let exited = false;
57877
+ const exitOnce = () => {
57878
+ if (exited)
57879
+ return;
57880
+ exited = true;
57881
+ process.exit(0);
57882
+ };
57780
57883
  const handler = (signal) => {
57781
- if (shuttingDown)
57884
+ if (shuttingDown) {
57885
+ if (Date.now() - firstSignalAt < repeatGraceMs)
57886
+ return;
57887
+ if (shouldExit) {
57888
+ console.error(`[mcp-utils] second ${signal} during shutdown \u2014 exiting now`);
57889
+ exitOnce();
57890
+ }
57782
57891
  return;
57892
+ }
57783
57893
  shuttingDown = true;
57894
+ firstSignalAt = Date.now();
57895
+ let timer;
57896
+ if (shouldExit) {
57897
+ timer = setTimeout(() => {
57898
+ console.error(`[mcp-utils] graceful shutdown on ${signal} did not finish in ${timeoutMs}ms \u2014 exiting`);
57899
+ exitOnce();
57900
+ }, timeoutMs);
57901
+ timer.unref?.();
57902
+ }
57784
57903
  void (async () => {
57785
57904
  try {
57786
57905
  if (opts.onSignal)
@@ -57789,8 +57908,10 @@ function withGracefulShutdown(target, opts = {}) {
57789
57908
  } catch (err) {
57790
57909
  console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
57791
57910
  } finally {
57911
+ if (timer !== void 0)
57912
+ clearTimeout(timer);
57792
57913
  if (shouldExit)
57793
- process.exit(0);
57914
+ exitOnce();
57794
57915
  }
57795
57916
  })();
57796
57917
  };
@@ -57820,6 +57941,8 @@ function runMcp(opts) {
57820
57941
  }
57821
57942
 
57822
57943
  // node_modules/@chrischall/mcp-utils/dist/config/index.js
57944
+ import { homedir } from "node:os";
57945
+ import { isAbsolute, join, resolve } from "node:path";
57823
57946
  var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
57824
57947
  function readEnvVar(key, opts = {}) {
57825
57948
  const env = opts.env ?? process.env;
@@ -57832,6 +57955,15 @@ function readEnvVar(key, opts = {}) {
57832
57955
  }
57833
57956
  return opts.default;
57834
57957
  }
57958
+ function expandPath(p) {
57959
+ let expanded = p;
57960
+ if (p === "~") {
57961
+ expanded = homedir();
57962
+ } else if (p.startsWith("~/")) {
57963
+ expanded = join(homedir(), p.slice(2));
57964
+ }
57965
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
57966
+ }
57835
57967
  async function loadDotenvSafely(opts = {}) {
57836
57968
  try {
57837
57969
  const mod = await import(
@@ -57851,10 +57983,48 @@ async function loadDotenvSafely(opts = {}) {
57851
57983
 
57852
57984
  // node_modules/@chrischall/mcp-utils/dist/fs/index.js
57853
57985
  import { openAsBlob } from "node:fs";
57986
+
57987
+ // node_modules/@chrischall/mcp-utils/dist/fs/confine.js
57988
+ import { existsSync, realpathSync } from "node:fs";
57989
+ import { dirname, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "node:path";
57990
+ function isInside(root, target) {
57991
+ const rel = relative(root, target);
57992
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute2(rel);
57993
+ }
57994
+ function realPathAllowingMissing(p) {
57995
+ const abs = resolve2(p);
57996
+ let existing = abs;
57997
+ const tail = [];
57998
+ while (!existsSync(existing)) {
57999
+ const parent = dirname(existing);
58000
+ if (parent === existing)
58001
+ break;
58002
+ tail.unshift(existing.slice(parent.length).replace(/^[/\\]+/, ""));
58003
+ existing = parent;
58004
+ }
58005
+ let real;
58006
+ try {
58007
+ real = realpathSync(existing);
58008
+ } catch {
58009
+ real = existing;
58010
+ }
58011
+ return tail.length > 0 ? resolve2(real, ...tail) : real;
58012
+ }
58013
+ function assertPathWithinRoots(path, roots) {
58014
+ const target = realPathAllowingMissing(expandPath(path));
58015
+ for (const root of roots) {
58016
+ if (isInside(realPathAllowingMissing(expandPath(root)), target))
58017
+ return target;
58018
+ }
58019
+ throw new Error(`Path is outside the allowed directories: ${path}`);
58020
+ }
58021
+
58022
+ // node_modules/@chrischall/mcp-utils/dist/fs/index.js
57854
58023
  async function fileBlob(path, opts = {}) {
58024
+ const target = opts.allowedRoots ? assertPathWithinRoots(path, opts.allowedRoots) : path;
57855
58025
  let blob;
57856
58026
  try {
57857
- blob = await openAsBlob(path, opts.type !== void 0 ? { type: opts.type } : void 0);
58027
+ blob = await openAsBlob(target, opts.type !== void 0 ? { type: opts.type } : void 0);
57858
58028
  } catch {
57859
58029
  throw new Error(`Cannot read file for upload: ${path}`);
57860
58030
  }
@@ -57908,23 +58078,27 @@ var pageSchema = {
57908
58078
  page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
57909
58079
  };
57910
58080
  function toolAnnotations(opts = {}) {
58081
+ if (opts.readOnly === true && opts.destructive === true) {
58082
+ throw new Error("toolAnnotations: readOnly: true contradicts destructive: true \u2014 a destructive tool is not read-only.");
58083
+ }
57911
58084
  return {
57912
58085
  ...opts.title !== void 0 ? { title: opts.title } : {},
57913
- readOnlyHint: opts.readOnly ?? true,
58086
+ readOnlyHint: opts.readOnly ?? opts.destructive === void 0,
57914
58087
  ...opts.idempotent !== void 0 ? { idempotentHint: opts.idempotent } : {},
58088
+ ...opts.destructive !== void 0 ? { destructiveHint: opts.destructive } : {},
57915
58089
  ...opts.openWorld !== void 0 ? { openWorldHint: opts.openWorld } : {}
57916
58090
  };
57917
58091
  }
57918
58092
 
57919
58093
  // src/version.ts
57920
- var VERSION = "2.1.0";
58094
+ var VERSION = "2.1.2";
57921
58095
 
57922
58096
  // src/client.ts
57923
- import { dirname, join } from "path";
58097
+ import { dirname as dirname2, join as join2 } from "path";
57924
58098
  import { fileURLToPath } from "url";
57925
58099
  if (import.meta.url) {
57926
- const __dirname = dirname(fileURLToPath(import.meta.url));
57927
- await loadDotenvSafely({ path: join(__dirname, "..", ".env"), override: false });
58100
+ const __dirname = dirname2(fileURLToPath(import.meta.url));
58101
+ await loadDotenvSafely({ path: join2(__dirname, "..", ".env"), override: false });
57928
58102
  }
57929
58103
  var BASE_URL = "https://api.untappd.com/v4";
57930
58104
  var SERVICE = "Untappd";
@@ -59486,7 +59660,11 @@ function registerFriendActionTools(server, client2) {
59486
59660
  {
59487
59661
  title: action.title,
59488
59662
  description: `${action.detail} Acts on YOUR account and affects a real relationship with another person. Without confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it performs the action. Note: this endpoint path is taken from the Untappd app but is not otherwise independently verified.`,
59489
- annotations: toolAnnotations({ title: action.title, readOnly: false, idempotent: true, openWorld: true }),
59663
+ // All four reach ANOTHER PERSON — the description says so itself: "affects
59664
+ // a real relationship with another person". A friend request cannot be
59665
+ // un-sent, and a rejection cannot be un-rejected, so none of them has an
59666
+ // inverse in this tool set.
59667
+ annotations: toolAnnotations({ title: action.title, readOnly: false, idempotent: true, openWorld: true, destructive: true }),
59490
59668
  inputSchema: external_exports.object({
59491
59669
  target_uid: TargetUidSchema,
59492
59670
  confirm: schemaConfirm
@@ -59516,7 +59694,7 @@ function registerWishlistTools(server, client2) {
59516
59694
  {
59517
59695
  title: "Add a beer to your wishlist",
59518
59696
  description: "Add a beer to YOUR Untappd wishlist by its bid. Without confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it adds. Writes to your account.",
59519
- annotations: toolAnnotations({ title: "Add a beer to your wishlist", readOnly: false, idempotent: true, openWorld: true }),
59697
+ annotations: toolAnnotations({ title: "Add a beer to your wishlist", readOnly: false, idempotent: true, openWorld: true, destructive: false }),
59520
59698
  inputSchema: external_exports.object({
59521
59699
  bid: BidSchema2,
59522
59700
  confirm: schemaConfirm
@@ -59540,7 +59718,7 @@ function registerWishlistTools(server, client2) {
59540
59718
  {
59541
59719
  title: "Remove a beer from your wishlist",
59542
59720
  description: "Remove a beer from YOUR Untappd wishlist by its bid. Without confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it removes. Writes to your account.",
59543
- annotations: toolAnnotations({ title: "Remove a beer from your wishlist", readOnly: false, idempotent: true, openWorld: true }),
59721
+ annotations: toolAnnotations({ title: "Remove a beer from your wishlist", readOnly: false, idempotent: true, openWorld: true, destructive: false }),
59544
59722
  inputSchema: external_exports.object({
59545
59723
  bid: BidSchema2,
59546
59724
  confirm: schemaConfirm
@@ -59562,7 +59740,7 @@ function registerWishlistTools(server, client2) {
59562
59740
  }
59563
59741
 
59564
59742
  // src/tools/checkin.ts
59565
- import { existsSync } from "node:fs";
59743
+ import { existsSync as existsSync2 } from "node:fs";
59566
59744
  import { extname } from "node:path";
59567
59745
  var CheckinIdSchema = external_exports.number().int().positive().describe("Untappd check-in id");
59568
59746
  var PHOTO_CONTENT_TYPES = { jpg: "image/jpeg", png: "image/png" };
@@ -59582,7 +59760,7 @@ function registerCheckinTools(server, client2) {
59582
59760
  {
59583
59761
  title: "Toast an Untappd check-in",
59584
59762
  description: "Toast (like) a check-in on YOUR account. This endpoint is a TOGGLE: calling it on a check-in you have already toasted removes the toast. Without confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it posts. Writes to your Untappd account and is visible to others.",
59585
- annotations: toolAnnotations({ title: "Toast an Untappd check-in", readOnly: false, idempotent: false, openWorld: true }),
59763
+ annotations: toolAnnotations({ title: "Toast an Untappd check-in", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
59586
59764
  inputSchema: external_exports.object({
59587
59765
  checkin_id: CheckinIdSchema,
59588
59766
  confirm: schemaConfirm
@@ -59606,7 +59784,7 @@ function registerCheckinTools(server, client2) {
59606
59784
  {
59607
59785
  title: "Comment on an Untappd check-in",
59608
59786
  description: "Post a comment on a check-in from YOUR account. Without confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it posts. Writes to your Untappd account and is visible to others.",
59609
- annotations: toolAnnotations({ title: "Comment on an Untappd check-in", readOnly: false, idempotent: false, openWorld: true }),
59787
+ annotations: toolAnnotations({ title: "Comment on an Untappd check-in", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
59610
59788
  inputSchema: external_exports.object({
59611
59789
  checkin_id: CheckinIdSchema,
59612
59790
  comment: external_exports.string().min(1).max(2e3).describe("Comment text to post"),
@@ -59632,7 +59810,7 @@ function registerCheckinTools(server, client2) {
59632
59810
  {
59633
59811
  title: "Delete a comment from an Untappd check-in",
59634
59812
  description: "Delete one of YOUR comments by its comment id (the id from a check-in's comments list). Without confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it deletes.",
59635
- annotations: toolAnnotations({ title: "Delete a comment from an Untappd check-in", readOnly: false, idempotent: true, openWorld: true }),
59813
+ annotations: toolAnnotations({ title: "Delete a comment from an Untappd check-in", readOnly: false, idempotent: true, openWorld: true, destructive: true }),
59636
59814
  inputSchema: external_exports.object({
59637
59815
  comment_id: external_exports.number().int().positive().describe("Untappd comment id (from a check-in's comments.items)"),
59638
59816
  confirm: schemaConfirm
@@ -59656,7 +59834,7 @@ function registerCheckinTools(server, client2) {
59656
59834
  {
59657
59835
  title: "Delete an Untappd check-in",
59658
59836
  description: "Permanently delete one of YOUR check-ins by its id. This is destructive and cannot be undone. Without confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it deletes.",
59659
- annotations: toolAnnotations({ title: "Delete an Untappd check-in", readOnly: false, idempotent: true, openWorld: true }),
59837
+ annotations: toolAnnotations({ title: "Delete an Untappd check-in", readOnly: false, idempotent: true, openWorld: true, destructive: true }),
59660
59838
  inputSchema: external_exports.object({
59661
59839
  checkin_id: CheckinIdSchema,
59662
59840
  confirm: schemaConfirm
@@ -59680,7 +59858,7 @@ function registerCheckinTools(server, client2) {
59680
59858
  {
59681
59859
  title: "Check in a beer on Untappd",
59682
59860
  description: "Post a NEW beer check-in to YOUR Untappd account \u2014 this publishes to your public feed. Provide the beer id (bid) from untappd_search_beer; optionally a rating (0\u20135 in 0.25 steps), a shout (comment), a venue via foursquare_id, and a local photo via photo_path (JPEG/PNG). Without confirm: true it returns a dry-run preview of the exact fields and makes NO network call; with confirm: true it posts.",
59683
- annotations: toolAnnotations({ title: "Check in a beer on Untappd", readOnly: false, idempotent: false, openWorld: true }),
59861
+ annotations: toolAnnotations({ title: "Check in a beer on Untappd", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
59684
59862
  inputSchema: external_exports.object({
59685
59863
  bid: external_exports.number().int().positive().describe("Untappd beer id to check in (from untappd_search_beer)"),
59686
59864
  rating: RatingSchema.optional().describe("Rating 0\u20135 in 0.25 increments (omit for no rating)"),
@@ -59729,7 +59907,7 @@ function registerCheckinTools(server, client2) {
59729
59907
  }
59730
59908
  let blob;
59731
59909
  if (photo_path !== void 0) {
59732
- if (!existsSync(photo_path)) throw new McpToolError(`Photo file not found: ${photo_path}`);
59910
+ if (!existsSync2(photo_path)) throw new McpToolError(`Photo file not found: ${photo_path}`);
59733
59911
  blob = await fileBlob(photo_path);
59734
59912
  }
59735
59913
  const data = await client2.write("POST", "/checkin/add", { form });
@@ -60194,7 +60372,7 @@ function registerCacheTools(server, client2, cacheProvider) {
60194
60372
  {
60195
60373
  title: "Sync a user's check-ins into the cache",
60196
60374
  description: "Fetch a user's detailed check-ins (venue, date, comment) into the cache from user/checkins. Incremental and resumable: pages backwards up to max_pages per call, persisting progress every page; run again until another_run_needed is false. NOTE: Untappd only returns the ~50 most recent check-ins for accounts other than your own and will not page further \u2014 such a sync reports history_truncated and you should use untappd_sync_user_beers for full has-had coverage. backfill_complete is only reported once ~all of total_checkins is cached. Pass force_backfill: true to reset a cache wrongly marked complete and re-page the whole history (cached rows are kept). Omit username for your own account.",
60197
- annotations: toolAnnotations({ title: "Sync a user's check-ins into the cache", readOnly: false, idempotent: false, openWorld: true }),
60375
+ annotations: toolAnnotations({ title: "Sync a user's check-ins into the cache", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
60198
60376
  inputSchema: external_exports.object({
60199
60377
  username: UsernameArg2,
60200
60378
  max_pages: external_exports.number().int().min(1).max(50).optional().describe("Pages (50 check-ins each) to fetch this run (default 10). Keep modest to respect the ~100 calls/hour rate limit."),
@@ -60215,7 +60393,7 @@ function registerCacheTools(server, client2, cacheProvider) {
60215
60393
  {
60216
60394
  title: "Sync a user's complete distinct-beers list into the cache",
60217
60395
  description: `Fetch a user's COMPLETE distinct-beers list (every unique beer they've ever had, with their rating, times-had count, and first/last dates) into the cache from user/beers. This is the cheapest way to get full "has had" coverage \u2014 thousands of beers instead of tens of thousands of check-ins \u2014 and, unlike user/checkins, it pages fully for any public/friend account. Offset-paged and resumable: fetches max_pages per call and persists progress; run again until another_run_needed is false. Feeds the same untappd_cache_has_had / has_had_many / not_had tools. Omit username for your own account.`,
60218
- annotations: toolAnnotations({ title: "Sync a user's complete distinct-beers list into the cache", readOnly: false, idempotent: false, openWorld: true }),
60396
+ annotations: toolAnnotations({ title: "Sync a user's complete distinct-beers list into the cache", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
60219
60397
  inputSchema: external_exports.object({
60220
60398
  username: UsernameArg2,
60221
60399
  max_pages: external_exports.number().int().min(1).max(50).optional().describe("Pages (50 beers each) to fetch this run (default 10). Keep modest to respect the ~100 calls/hour rate limit.")
@@ -60349,7 +60527,7 @@ function registerCacheTools(server, client2, cacheProvider) {
60349
60527
  {
60350
60528
  title: "Top-rated beers a user has NOT had, from a candidate list",
60351
60529
  description: 'The "what should I order off this tap list?" tool. From a list of candidate beer ids, return the top N the user has NOT yet had, ranked by Untappd global rating, with an optional style filter. Not-had filtering uses the cache only (both sources, no API call). Beer ratings/styles come from a metadata cache; a beer/info API call is made only on a cache miss or if the cached metadata is >30 days old, capped at api_budget calls per run (~100 calls/hour limit) \u2014 if more are needed it returns partial: true / another_run_needed: true, so re-running fills the rest. Reports the same freshness/caveat block as untappd_cache_not_had. Omit username for your own account.',
60352
- annotations: toolAnnotations({ title: "Top-rated beers a user has NOT had, from a candidate list", readOnly: false, idempotent: false, openWorld: true }),
60530
+ annotations: toolAnnotations({ title: "Top-rated beers a user has NOT had, from a candidate list", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
60353
60531
  inputSchema: external_exports.object({
60354
60532
  username: UsernameArg2,
60355
60533
  bids: external_exports.array(external_exports.number().int().positive()).min(1).max(100).describe("Candidate beer ids (1\u2013100)"),
@@ -60443,8 +60621,8 @@ function registerCacheTools(server, client2, cacheProvider) {
60443
60621
  // src/cache/db.ts
60444
60622
  import { DatabaseSync } from "node:sqlite";
60445
60623
  import { mkdirSync } from "node:fs";
60446
- import { dirname as dirname2, join as join2 } from "node:path";
60447
- import { homedir } from "node:os";
60624
+ import { dirname as dirname3, join as join3 } from "node:path";
60625
+ import { homedir as homedir2 } from "node:os";
60448
60626
  var NodeSqlDriver = class {
60449
60627
  constructor(db) {
60450
60628
  this.db = db;
@@ -60481,7 +60659,7 @@ var CheckinCache = class _CheckinCache extends LocalCacheStore {
60481
60659
  db;
60482
60660
  /** Open (creating parent dirs) a file-backed cache, or `:memory:` for tests. */
60483
60661
  static open(path) {
60484
- if (path !== ":memory:") mkdirSync(dirname2(path), { recursive: true });
60662
+ if (path !== ":memory:") mkdirSync(dirname3(path), { recursive: true });
60485
60663
  return new _CheckinCache(new DatabaseSync(path));
60486
60664
  }
60487
60665
  close() {
@@ -60489,7 +60667,7 @@ var CheckinCache = class _CheckinCache extends LocalCacheStore {
60489
60667
  }
60490
60668
  };
60491
60669
  function defaultCachePath() {
60492
- return readEnvVar("UNTAPPD_CACHE_DB") ?? join2(homedir(), ".untappd-mcp", "checkins.db");
60670
+ return readEnvVar("UNTAPPD_CACHE_DB") ?? join3(homedir2(), ".untappd-mcp", "checkins.db");
60493
60671
  }
60494
60672
 
60495
60673
  // src/index.ts
@@ -92,7 +92,7 @@ export function registerCacheTools(server, client, cacheProvider) {
92
92
  'untappd_sync_user_beers for full has-had coverage. backfill_complete is only reported once ~all of ' +
93
93
  'total_checkins is cached. Pass force_backfill: true to reset a cache wrongly marked complete and re-page ' +
94
94
  'the whole history (cached rows are kept). Omit username for your own account.',
95
- annotations: toolAnnotations({ title: "Sync a user's check-ins into the cache", readOnly: false, idempotent: false, openWorld: true }),
95
+ annotations: toolAnnotations({ title: "Sync a user's check-ins into the cache", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
96
96
  inputSchema: z.object({
97
97
  username: UsernameArg,
98
98
  max_pages: z
@@ -123,7 +123,7 @@ export function registerCacheTools(server, client, cacheProvider) {
123
123
  'user/checkins, it pages fully for any public/friend account. Offset-paged and resumable: fetches max_pages ' +
124
124
  'per call and persists progress; run again until another_run_needed is false. Feeds the same ' +
125
125
  'untappd_cache_has_had / has_had_many / not_had tools. Omit username for your own account.',
126
- annotations: toolAnnotations({ title: "Sync a user's complete distinct-beers list into the cache", readOnly: false, idempotent: false, openWorld: true }),
126
+ annotations: toolAnnotations({ title: "Sync a user's complete distinct-beers list into the cache", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
127
127
  inputSchema: z.object({
128
128
  username: UsernameArg,
129
129
  max_pages: z
@@ -271,7 +271,7 @@ export function registerCacheTools(server, client, cacheProvider) {
271
271
  'run (~100 calls/hour limit) — if more are needed it returns partial: true / another_run_needed: true, so ' +
272
272
  're-running fills the rest. Reports the same freshness/caveat block as untappd_cache_not_had. Omit username ' +
273
273
  'for your own account.',
274
- annotations: toolAnnotations({ title: 'Top-rated beers a user has NOT had, from a candidate list', readOnly: false, idempotent: false, openWorld: true }),
274
+ annotations: toolAnnotations({ title: 'Top-rated beers a user has NOT had, from a candidate list', readOnly: false, idempotent: false, openWorld: true, destructive: false }),
275
275
  inputSchema: z.object({
276
276
  username: UsernameArg,
277
277
  bids: z.array(z.number().int().positive()).min(1).max(100).describe('Candidate beer ids (1–100)'),
@@ -28,7 +28,7 @@ export function registerCheckinTools(server, client) {
28
28
  description: "Toast (like) a check-in on YOUR account. This endpoint is a TOGGLE: calling it on a check-in you have " +
29
29
  'already toasted removes the toast. Without confirm: true it returns a dry-run preview and makes NO network ' +
30
30
  'call; with confirm: true it posts. Writes to your Untappd account and is visible to others.',
31
- annotations: toolAnnotations({ title: 'Toast an Untappd check-in', readOnly: false, idempotent: false, openWorld: true }),
31
+ annotations: toolAnnotations({ title: 'Toast an Untappd check-in', readOnly: false, idempotent: false, openWorld: true, destructive: false }),
32
32
  inputSchema: z.object({
33
33
  checkin_id: CheckinIdSchema,
34
34
  confirm: schemaConfirm,
@@ -49,7 +49,7 @@ export function registerCheckinTools(server, client) {
49
49
  title: 'Comment on an Untappd check-in',
50
50
  description: 'Post a comment on a check-in from YOUR account. Without confirm: true it returns a dry-run preview and ' +
51
51
  'makes NO network call; with confirm: true it posts. Writes to your Untappd account and is visible to others.',
52
- annotations: toolAnnotations({ title: 'Comment on an Untappd check-in', readOnly: false, idempotent: false, openWorld: true }),
52
+ annotations: toolAnnotations({ title: 'Comment on an Untappd check-in', readOnly: false, idempotent: false, openWorld: true, destructive: false }),
53
53
  inputSchema: z.object({
54
54
  checkin_id: CheckinIdSchema,
55
55
  comment: z.string().min(1).max(2000).describe('Comment text to post'),
@@ -72,7 +72,7 @@ export function registerCheckinTools(server, client) {
72
72
  title: 'Delete a comment from an Untappd check-in',
73
73
  description: 'Delete one of YOUR comments by its comment id (the id from a check-in\'s comments list). Without ' +
74
74
  'confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it deletes.',
75
- annotations: toolAnnotations({ title: 'Delete a comment from an Untappd check-in', readOnly: false, idempotent: true, openWorld: true }),
75
+ annotations: toolAnnotations({ title: 'Delete a comment from an Untappd check-in', readOnly: false, idempotent: true, openWorld: true, destructive: true }),
76
76
  inputSchema: z.object({
77
77
  comment_id: z.number().int().positive().describe('Untappd comment id (from a check-in\'s comments.items)'),
78
78
  confirm: schemaConfirm,
@@ -93,7 +93,7 @@ export function registerCheckinTools(server, client) {
93
93
  title: 'Delete an Untappd check-in',
94
94
  description: 'Permanently delete one of YOUR check-ins by its id. This is destructive and cannot be undone. Without ' +
95
95
  'confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it deletes.',
96
- annotations: toolAnnotations({ title: 'Delete an Untappd check-in', readOnly: false, idempotent: true, openWorld: true }),
96
+ annotations: toolAnnotations({ title: 'Delete an Untappd check-in', readOnly: false, idempotent: true, openWorld: true, destructive: true }),
97
97
  inputSchema: z.object({
98
98
  checkin_id: CheckinIdSchema,
99
99
  confirm: schemaConfirm,
@@ -116,7 +116,7 @@ export function registerCheckinTools(server, client) {
116
116
  '(bid) from untappd_search_beer; optionally a rating (0–5 in 0.25 steps), a shout (comment), a venue via ' +
117
117
  'foursquare_id, and a local photo via photo_path (JPEG/PNG). Without confirm: true it returns a dry-run ' +
118
118
  'preview of the exact fields and makes NO network call; with confirm: true it posts.',
119
- annotations: toolAnnotations({ title: 'Check in a beer on Untappd', readOnly: false, idempotent: false, openWorld: true }),
119
+ annotations: toolAnnotations({ title: 'Check in a beer on Untappd', readOnly: false, idempotent: false, openWorld: true, destructive: false }),
120
120
  inputSchema: z.object({
121
121
  bid: z.number().int().positive().describe('Untappd beer id to check in (from untappd_search_beer)'),
122
122
  rating: RatingSchema.optional().describe('Rating 0–5 in 0.25 increments (omit for no rating)'),
@@ -50,7 +50,11 @@ export function registerFriendActionTools(server, client) {
50
50
  'Without confirm: true it returns a dry-run preview and makes NO network call; with confirm: true it ' +
51
51
  'performs the action. Note: this endpoint path is taken from the Untappd app but is not otherwise ' +
52
52
  'independently verified.',
53
- annotations: toolAnnotations({ title: action.title, readOnly: false, idempotent: true, openWorld: true }),
53
+ // All four reach ANOTHER PERSON — the description says so itself: "affects
54
+ // a real relationship with another person". A friend request cannot be
55
+ // un-sent, and a rejection cannot be un-rejected, so none of them has an
56
+ // inverse in this tool set.
57
+ annotations: toolAnnotations({ title: action.title, readOnly: false, idempotent: true, openWorld: true, destructive: true }),
54
58
  inputSchema: z.object({
55
59
  target_uid: TargetUidSchema,
56
60
  confirm: schemaConfirm,
@@ -6,7 +6,7 @@ export function registerWishlistTools(server, client) {
6
6
  title: 'Add a beer to your wishlist',
7
7
  description: 'Add a beer to YOUR Untappd wishlist by its bid. Without confirm: true it returns a dry-run preview and ' +
8
8
  'makes NO network call; with confirm: true it adds. Writes to your account.',
9
- annotations: toolAnnotations({ title: 'Add a beer to your wishlist', readOnly: false, idempotent: true, openWorld: true }),
9
+ annotations: toolAnnotations({ title: 'Add a beer to your wishlist', readOnly: false, idempotent: true, openWorld: true, destructive: false }),
10
10
  inputSchema: z.object({
11
11
  bid: BidSchema,
12
12
  confirm: schemaConfirm,
@@ -27,7 +27,7 @@ export function registerWishlistTools(server, client) {
27
27
  title: 'Remove a beer from your wishlist',
28
28
  description: 'Remove a beer from YOUR Untappd wishlist by its bid. Without confirm: true it returns a dry-run preview ' +
29
29
  'and makes NO network call; with confirm: true it removes. Writes to your account.',
30
- annotations: toolAnnotations({ title: 'Remove a beer from your wishlist', readOnly: false, idempotent: true, openWorld: true }),
30
+ annotations: toolAnnotations({ title: 'Remove a beer from your wishlist', readOnly: false, idempotent: true, openWorld: true, destructive: false }),
31
31
  inputSchema: z.object({
32
32
  bid: BidSchema,
33
33
  confirm: schemaConfirm,
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.1.0'; // x-release-please-version
6
+ export const VERSION = '2.1.2'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "untappd-mcp",
3
- "version": "2.1.0",
3
+ "version": "2.1.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>",
@@ -45,11 +45,11 @@
45
45
  "typecheck": "tsc -p tsconfig.json --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@chrischall/mcp-utils": "^1.0.0",
48
+ "@chrischall/mcp-utils": "^2.4.0",
49
49
  "@modelcontextprotocol/server": "^2.0.0",
50
50
  "ai": "^7.0.19",
51
- "dotenv": "^17.4.0",
52
- "zod": "^4.6.2"
51
+ "dotenv": "^18.0.1",
52
+ "zod": "^4.6.5"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@modelcontextprotocol/client": "^2.0.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": "2.1.0",
9
+ "version": "2.1.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "untappd-mcp",
14
- "version": "2.1.0",
14
+ "version": "2.1.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },