untappd-mcp 2.0.0 → 2.1.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.
@@ -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.0.0"
10
+ "version": "2.1.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.0.0",
18
+ "version": "2.1.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.0.0",
4
+ "version": "2.1.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/dist/bundle.js CHANGED
@@ -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;
@@ -24476,6 +24490,7 @@ function missingClientCapabilities(required2, declared) {
24476
24490
  return Object.keys(missing).length > 0 ? missing : void 0;
24477
24491
  }
24478
24492
  var FIRST_MODERN_PROTOCOL_VERSION = "2026-07-28";
24493
+ var SUPPORTED_MODERN_PROTOCOL_VERSIONS = [FIRST_MODERN_PROTOCOL_VERSION];
24479
24494
  function isModernProtocolVersion(version2) {
24480
24495
  return version2 >= FIRST_MODERN_PROTOCOL_VERSION;
24481
24496
  }
@@ -27034,6 +27049,25 @@ function isSpecNotificationMethod(method) {
27034
27049
  return ALL_CODECS.some((codec2) => codec2.hasNotificationMethod(method));
27035
27050
  }
27036
27051
  var ALL_CODECS = [rev2025Codec, rev2026Codec];
27052
+ function isPlainObject$3(value) {
27053
+ return value !== null && typeof value === "object" && !Array.isArray(value);
27054
+ }
27055
+ function requestMetaOf(params) {
27056
+ if (!isPlainObject$3(params)) return void 0;
27057
+ const meta3 = params["_meta"];
27058
+ return isPlainObject$3(meta3) ? meta3 : void 0;
27059
+ }
27060
+ function hasEnvelopeClaim(params) {
27061
+ const meta3 = requestMetaOf(params);
27062
+ return meta3 !== void 0 && PROTOCOL_VERSION_META_KEY in meta3;
27063
+ }
27064
+ function envelopeClaimVersion(params) {
27065
+ const value = requestMetaOf(params)?.[PROTOCOL_VERSION_META_KEY];
27066
+ return typeof value === "string" ? value : void 0;
27067
+ }
27068
+ function validateEnvelopeMeta(meta3) {
27069
+ return codecForVersion(MODERN_WIRE_REVISION).validateEnvelopeMeta(meta3);
27070
+ }
27037
27071
  var schemas_exports = /* @__PURE__ */ __exportAll({
27038
27072
  AnnotationsSchema: () => AnnotationsSchema,
27039
27073
  AudioContentSchema: () => AudioContentSchema,
@@ -27384,6 +27418,45 @@ var LADDER_ERROR_HTTP_STATUS = {
27384
27418
  [ProtocolErrorCode.MissingRequiredClientCapability]: 400,
27385
27419
  [HEADER_MISMATCH_ERROR_CODE]: 400
27386
27420
  };
27421
+ function rejection(rung, cell, httpStatus, error62, settled) {
27422
+ return {
27423
+ kind: "reject",
27424
+ rung,
27425
+ cell,
27426
+ httpStatus,
27427
+ code: error62.code,
27428
+ message: error62.message,
27429
+ ...error62.data !== void 0 && { data: error62.data },
27430
+ settled
27431
+ };
27432
+ }
27433
+ function carriesValidModernEnvelopeClaim(params) {
27434
+ if (!hasEnvelopeClaim(params)) return false;
27435
+ const claimedVersion = envelopeClaimVersion(params);
27436
+ if (claimedVersion === void 0 || !isModernProtocolVersion(claimedVersion)) return false;
27437
+ const meta3 = requestMetaOf(params);
27438
+ return meta3 !== void 0 && validateEnvelopeMeta(meta3).length === 0;
27439
+ }
27440
+ function modernOnlyStrictRejection(route, supportedVersions) {
27441
+ switch (route.reason) {
27442
+ case "http-method":
27443
+ return rejection("http-method", "modern-only-method-not-allowed", 405, new ProtocolError(-32e3, "Method not allowed."), true);
27444
+ case "batch":
27445
+ return rejection("jsonrpc-shape", "modern-only-batch-not-supported", 400, new ProtocolError(ProtocolErrorCode.InvalidRequest, "Bad Request: JSON-RPC batches are not supported by this endpoint"), true);
27446
+ case "response":
27447
+ return rejection("jsonrpc-shape", "modern-only-response-post", 400, new ProtocolError(ProtocolErrorCode.InvalidRequest, "Bad Request: JSON-RPC responses cannot be posted to this endpoint"), true);
27448
+ case "notification":
27449
+ return;
27450
+ case "initialize":
27451
+ case "no-claim": {
27452
+ const requested = route.requestedVersion;
27453
+ return rejection("era-classification", "modern-only-missing-envelope", 400, requested === void 0 ? new ProtocolError(ProtocolErrorCode.UnsupportedProtocolVersion, "Unsupported protocol version: the request did not name a protocol version", { supported: [...supportedVersions] }) : new UnsupportedProtocolVersionError({
27454
+ supported: [...supportedVersions],
27455
+ requested
27456
+ }), true);
27457
+ }
27458
+ }
27459
+ }
27387
27460
  function parseSchema(schema, data) {
27388
27461
  return z2.safeParse(schema, data);
27389
27462
  }
@@ -27945,6 +28018,9 @@ function withRequestStateValue(ctx, value) {
27945
28018
  };
27946
28019
  }
27947
28020
  var writeNegotiatedProtocolVersion;
28021
+ function setNegotiatedProtocolVersion(instance, version2) {
28022
+ writeNegotiatedProtocolVersion(instance, version2);
28023
+ }
27948
28024
  var Protocol = class {
27949
28025
  _transport;
27950
28026
  _requestMessageId = 0;
@@ -35888,6 +35964,190 @@ function getCompleter(schema) {
35888
35964
  return schema[COMPLETABLE_SYMBOL]?.complete;
35889
35965
  }
35890
35966
  var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
35967
+ function listenFilterAccepts(filter, event) {
35968
+ switch (event.kind) {
35969
+ case "tools_list_changed":
35970
+ return filter.toolsListChanged === true;
35971
+ case "prompts_list_changed":
35972
+ return filter.promptsListChanged === true;
35973
+ case "resources_list_changed":
35974
+ return filter.resourcesListChanged === true;
35975
+ case "resource_updated":
35976
+ return filter.resourceSubscriptions !== void 0 && filter.resourceSubscriptions.includes(event.uri);
35977
+ }
35978
+ }
35979
+ function honoredSubset(requested, capabilities) {
35980
+ const honored = {};
35981
+ const allow = (bit) => capabilities === void 0 || bit === true;
35982
+ if (requested.toolsListChanged === true && allow(capabilities?.tools?.listChanged)) honored.toolsListChanged = true;
35983
+ if (requested.promptsListChanged === true && allow(capabilities?.prompts?.listChanged)) honored.promptsListChanged = true;
35984
+ if (requested.resourcesListChanged === true && allow(capabilities?.resources?.listChanged)) honored.resourcesListChanged = true;
35985
+ if (requested.resourceSubscriptions !== void 0 && requested.resourceSubscriptions.length > 0 && allow(capabilities?.resources?.subscribe)) honored.resourceSubscriptions = [...requested.resourceSubscriptions];
35986
+ return honored;
35987
+ }
35988
+ var DEFAULT_MAX_SUBSCRIPTIONS = 1024;
35989
+ function stampSubscriptionId(notification, subscriptionId) {
35990
+ return {
35991
+ method: notification.method,
35992
+ params: {
35993
+ ...notification.params,
35994
+ _meta: {
35995
+ ...notification.params?._meta,
35996
+ [SUBSCRIPTION_ID_META_KEY]: subscriptionId
35997
+ }
35998
+ }
35999
+ };
36000
+ }
36001
+ function parseListenFilter(message) {
36002
+ const outcome = codecForVersion(MODERN_WIRE_REVISION).validateRequest("subscriptions/listen", message);
36003
+ return outcome.ok ? outcome.value.params?.notifications : void 0;
36004
+ }
36005
+ var CHANGE_NOTIFICATION_METHODS = /* @__PURE__ */ new Set([
36006
+ "notifications/tools/list_changed",
36007
+ "notifications/prompts/list_changed",
36008
+ "notifications/resources/list_changed",
36009
+ "notifications/resources/updated"
36010
+ ]);
36011
+ var StdioListenRouter = class {
36012
+ /** Active subscriptions, keyed by the listen request's JSON-RPC id verbatim. */
36013
+ _subs = /* @__PURE__ */ new Map();
36014
+ /**
36015
+ * The serving instance's declared capabilities. Filled in by the entry
36016
+ * once the modern instance is constructed (the router is created before
36017
+ * the instance exists), so the acknowledged filter is narrowed against
36018
+ * what the server can actually deliver.
36019
+ */
36020
+ _serverCapabilities;
36021
+ /**
36022
+ * The serving instance's identity, stamped onto the graceful-close
36023
+ * results' `_meta` (the spec's `SubscriptionsListenResultMeta` extends
36024
+ * `ResultMetaObject`). Handed over together with the capabilities.
36025
+ */
36026
+ _serverInfo;
36027
+ constructor(_maxSubscriptions = DEFAULT_MAX_SUBSCRIPTIONS, serverCapabilities, serverInfo) {
36028
+ this._maxSubscriptions = _maxSubscriptions;
36029
+ this._serverCapabilities = serverCapabilities;
36030
+ this._serverInfo = serverInfo;
36031
+ }
36032
+ /**
36033
+ * Record the serving instance's declared capabilities and identity once
36034
+ * it has been constructed. Called by `serveStdio`'s connect path;
36035
+ * subsequent `serve()` calls narrow the honored filter against the
36036
+ * capabilities, and `teardownAll()` stamps the identity.
36037
+ */
36038
+ setServerCapabilities(capabilities, serverInfo) {
36039
+ this._serverCapabilities = capabilities;
36040
+ if (serverInfo !== void 0) this._serverInfo = serverInfo;
36041
+ }
36042
+ /** Whether `id` is an active listen subscription on this connection. */
36043
+ has(id) {
36044
+ return this._subs.has(id);
36045
+ }
36046
+ /**
36047
+ * Serve one inbound `subscriptions/listen` request: registers the
36048
+ * subscription and returns the stamped acknowledged notification (or, on
36049
+ * capacity / params rejection, the in-band JSON-RPC error response).
36050
+ *
36051
+ * @throws when called before {@linkcode setServerCapabilities} (or the
36052
+ * constructor) has supplied the serving instance's capabilities. Honoring a
36053
+ * filter without knowing the server's advertised capabilities would fail
36054
+ * open (deliver unadvertised types); the entry guarantees capabilities are
36055
+ * set before any listen request is routed here.
36056
+ */
36057
+ serve(message) {
36058
+ if (this._serverCapabilities === void 0) throw new Error("StdioListenRouter.serve() called before setServerCapabilities(); refusing to honor a filter without capabilities");
36059
+ if (this._subs.size >= this._maxSubscriptions) return {
36060
+ jsonrpc: "2.0",
36061
+ id: message.id,
36062
+ error: {
36063
+ code: -32603,
36064
+ message: "Subscription limit reached"
36065
+ }
36066
+ };
36067
+ const filter = parseListenFilter(message);
36068
+ if (filter === void 0) return {
36069
+ jsonrpc: "2.0",
36070
+ id: message.id,
36071
+ error: {
36072
+ code: -32602,
36073
+ message: "Invalid params: 'notifications' is required and must be a valid SubscriptionFilter"
36074
+ }
36075
+ };
36076
+ const honored = honoredSubset(filter, this._serverCapabilities);
36077
+ this._subs.set(message.id, honored);
36078
+ return stampSubscriptionId({
36079
+ method: "notifications/subscriptions/acknowledged",
36080
+ params: { notifications: honored }
36081
+ }, message.id);
36082
+ }
36083
+ /**
36084
+ * Tear down one subscription (inbound `notifications/cancelled`). Returns
36085
+ * `true` when a subscription was removed. After this call NOTHING further
36086
+ * is delivered for that subscription id (the post-cancel hardening).
36087
+ */
36088
+ cancel(id) {
36089
+ return this._subs.delete(id);
36090
+ }
36091
+ /**
36092
+ * Route an outbound notification through the active subscriptions.
36093
+ *
36094
+ * - For a subscription-gated change notification, returns one stamped copy
36095
+ * per subscription that opted in to it (an empty array means it is
36096
+ * dropped — the modern era never delivers an un-requested change type).
36097
+ * - For any other outbound message, returns `'passthrough'` (the entry
36098
+ * forwards it as-is).
36099
+ */
36100
+ routeOutbound(message) {
36101
+ if (!CHANGE_NOTIFICATION_METHODS.has(message.method)) return "passthrough";
36102
+ const uriParam = message.params?.["uri"];
36103
+ const uri = typeof uriParam === "string" ? uriParam : void 0;
36104
+ const event = notificationToServerEvent(message.method, uri);
36105
+ const out = [];
36106
+ for (const [subscriptionId, filter] of this._subs) if (listenFilterAccepts(filter, event)) out.push(stampSubscriptionId({
36107
+ method: message.method,
36108
+ params: message.params ?? {}
36109
+ }, subscriptionId));
36110
+ return out;
36111
+ }
36112
+ /**
36113
+ * Server-side graceful teardown of every active subscription: returns the
36114
+ * empty `subscriptions/listen` JSON-RPC result for each subscription id —
36115
+ * the spec's graceful-close signal, `_meta` carrying the subscription id
36116
+ * and the serving instance's identity — for the entry to emit before
36117
+ * closing the wire. Clears the set so nothing further is delivered.
36118
+ */
36119
+ teardownAll() {
36120
+ const out = [];
36121
+ for (const id of this._subs.keys()) out.push({
36122
+ jsonrpc: "2.0",
36123
+ id,
36124
+ result: {
36125
+ resultType: "complete",
36126
+ _meta: {
36127
+ [SUBSCRIPTION_ID_META_KEY]: id,
36128
+ ...this._serverInfo !== void 0 && { [SERVER_INFO_META_KEY]: this._serverInfo }
36129
+ }
36130
+ }
36131
+ });
36132
+ this._subs.clear();
36133
+ return out;
36134
+ }
36135
+ };
36136
+ function notificationToServerEvent(method, uri) {
36137
+ switch (method) {
36138
+ case "notifications/tools/list_changed":
36139
+ return { kind: "tools_list_changed" };
36140
+ case "notifications/prompts/list_changed":
36141
+ return { kind: "prompts_list_changed" };
36142
+ case "notifications/resources/list_changed":
36143
+ return { kind: "resources_list_changed" };
36144
+ default:
36145
+ return {
36146
+ kind: "resource_updated",
36147
+ uri: uri ?? ""
36148
+ };
36149
+ }
36150
+ }
35891
36151
  var DEFAULT_LEGACY_SHIM_MAX_ROUNDS = 8;
35892
36152
  var DEFAULT_LEGACY_SHIM_ROUND_TIMEOUT_MS = 6e5;
35893
36153
  function resolveLegacyShimOptions(options) {
@@ -36025,6 +36285,12 @@ var INPUT_REQUIRED_CAPABLE_METHODS = /* @__PURE__ */ new Set([
36025
36285
  var writeClientIdentity;
36026
36286
  var installDiscoverHandler;
36027
36287
  var readServerIdentity;
36288
+ function installModernOnlyHandlers(server, servedModernVersions) {
36289
+ installDiscoverHandler(server, servedModernVersions);
36290
+ }
36291
+ function serverIdentityOf(server) {
36292
+ return readServerIdentity(server);
36293
+ }
36028
36294
  var Server = class extends Protocol {
36029
36295
  _clientCapabilities;
36030
36296
  _clientVersion;
@@ -37299,6 +37565,400 @@ var StdioServerTransport = class {
37299
37565
  });
37300
37566
  }
37301
37567
  };
37568
+ var DISCARD_ANSWER_TIMEOUT_MS = 3e3;
37569
+ var StdioConnectionChannel = class {
37570
+ onclose;
37571
+ onerror;
37572
+ onmessage;
37573
+ _closed = false;
37574
+ /** Request ids the entry delivered to the instance that the instance has not yet answered. */
37575
+ _pendingRequests = /* @__PURE__ */ new Set();
37576
+ _drainWaiters = [];
37577
+ constructor(_wire, _onInstanceClose, _outboundIntercept) {
37578
+ this._wire = _wire;
37579
+ this._onInstanceClose = _onInstanceClose;
37580
+ this._outboundIntercept = _outboundIntercept;
37581
+ }
37582
+ async start() {
37583
+ }
37584
+ async send(message, options) {
37585
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
37586
+ const { id } = message;
37587
+ if (id !== void 0) this._settle(id);
37588
+ }
37589
+ if (this._closed) return;
37590
+ if (this._outboundIntercept?.(message) === "handled") return;
37591
+ return this._wire.send(message, options);
37592
+ }
37593
+ setProtocolVersion = (version2) => {
37594
+ this._wire.setProtocolVersion?.(version2);
37595
+ };
37596
+ /** Forwards one inbound message to the connected instance. */
37597
+ deliver(message, extra) {
37598
+ if (this._closed) return;
37599
+ if (isJSONRPCRequest(message)) this._pendingRequests.add(message.id);
37600
+ else if (isJSONRPCNotification(message) && message.method === "notifications/cancelled") {
37601
+ const cancelledId = message.params?.requestId;
37602
+ if (cancelledId !== void 0) this._settle(cancelledId);
37603
+ }
37604
+ this.onmessage?.(message, extra);
37605
+ }
37606
+ /**
37607
+ * Resolves once every request delivered to the instance has been answered
37608
+ * through {@linkcode send}, settled by a delivered cancellation, or the
37609
+ * channel has been closed and nothing further can be answered. The wait is
37610
+ * bounded by `timeoutMs` as a backstop so no edge can hold the caller
37611
+ * indefinitely; resolves `false` only when the bound elapsed with requests
37612
+ * still unanswered. Used by the probe-discard path so a probe request the
37613
+ * entry accepted is never silently dropped.
37614
+ */
37615
+ async whenRequestsAnswered(timeoutMs) {
37616
+ if (this._closed || this._pendingRequests.size === 0) return true;
37617
+ return await new Promise((resolve) => {
37618
+ const waiter = () => {
37619
+ clearTimeout(timer);
37620
+ resolve(true);
37621
+ };
37622
+ const timer = setTimeout(() => {
37623
+ this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
37624
+ resolve(false);
37625
+ }, timeoutMs);
37626
+ this._drainWaiters.push(waiter);
37627
+ });
37628
+ }
37629
+ async close() {
37630
+ if (this._closed) return;
37631
+ this._closed = true;
37632
+ this._pendingRequests.clear();
37633
+ this._releaseDrainWaiters();
37634
+ try {
37635
+ this._onInstanceClose();
37636
+ } finally {
37637
+ this.onclose?.();
37638
+ }
37639
+ }
37640
+ _settle(id) {
37641
+ this._pendingRequests.delete(id);
37642
+ if (this._pendingRequests.size === 0) this._releaseDrainWaiters();
37643
+ }
37644
+ _releaseDrainWaiters() {
37645
+ const waiters = this._drainWaiters;
37646
+ this._drainWaiters = [];
37647
+ for (const waiter of waiters) waiter();
37648
+ }
37649
+ };
37650
+ function classifyOpeningMessage(message) {
37651
+ const params = message.params;
37652
+ if (message.method === "initialize" && !carriesValidModernEnvelopeClaim(params)) {
37653
+ const requestedVersion = params !== null && typeof params === "object" && typeof params.protocolVersion === "string" ? params.protocolVersion : void 0;
37654
+ return {
37655
+ kind: "legacy",
37656
+ reason: "initialize",
37657
+ ...requestedVersion !== void 0 && { requestedVersion }
37658
+ };
37659
+ }
37660
+ if (!hasEnvelopeClaim(params)) return {
37661
+ kind: "legacy",
37662
+ reason: "no-claim"
37663
+ };
37664
+ const meta3 = requestMetaOf(params);
37665
+ const firstIssue = (meta3 === void 0 ? [] : validateEnvelopeMeta(meta3))[0];
37666
+ if (firstIssue !== void 0) return {
37667
+ kind: "invalid-envelope",
37668
+ issue: firstIssue
37669
+ };
37670
+ const claimedVersion = envelopeClaimVersion(params);
37671
+ if (claimedVersion === void 0 || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedVersion)) return {
37672
+ kind: "unsupported-revision",
37673
+ requested: claimedVersion ?? "unknown"
37674
+ };
37675
+ return {
37676
+ kind: "modern",
37677
+ revision: claimedVersion,
37678
+ classification: {
37679
+ era: "modern",
37680
+ revision: claimedVersion
37681
+ }
37682
+ };
37683
+ }
37684
+ function serveStdio(factory, options = {}) {
37685
+ const legacyMode = options.legacy ?? "serve";
37686
+ const wire = options.transport ?? new StdioServerTransport();
37687
+ let state = { phase: "opening" };
37688
+ let discarding;
37689
+ let closing = false;
37690
+ const isTornDown = () => closing || state.phase === "closed";
37691
+ const reportError = (error62) => {
37692
+ try {
37693
+ options.onerror?.(error62);
37694
+ } catch {
37695
+ }
37696
+ };
37697
+ const writeErrorResponse = (id, code, message, data) => wire.send({
37698
+ jsonrpc: "2.0",
37699
+ id,
37700
+ error: {
37701
+ code,
37702
+ message,
37703
+ ...data !== void 0 && { data }
37704
+ }
37705
+ }).catch((error62) => reportError(toError(error62)));
37706
+ const listenRouter = new StdioListenRouter(options.maxSubscriptions ?? DEFAULT_MAX_SUBSCRIPTIONS);
37707
+ const modernOutboundIntercept = (message) => {
37708
+ if (!isJSONRPCNotification(message)) return void 0;
37709
+ const routed = listenRouter.routeOutbound(message);
37710
+ if (routed === "passthrough") return void 0;
37711
+ for (const stamped of routed) wire.send({
37712
+ jsonrpc: "2.0",
37713
+ ...stamped
37714
+ }).catch((error62) => reportError(toError(error62)));
37715
+ return "handled";
37716
+ };
37717
+ const tryServeListen = async (message) => {
37718
+ if (isJSONRPCRequest(message) && message.method === "subscriptions/listen") {
37719
+ const meta3 = requestMetaOf(message.params);
37720
+ const issue2 = hasEnvelopeClaim(message.params) ? (meta3 === void 0 ? [] : validateEnvelopeMeta(meta3))[0] : {
37721
+ key: "_meta",
37722
+ problem: "the per-request envelope is required on protocol revision 2026-07-28"
37723
+ };
37724
+ const claimedVersion = envelopeClaimVersion(message.params);
37725
+ let reply;
37726
+ if (issue2 !== void 0) reply = {
37727
+ jsonrpc: "2.0",
37728
+ id: message.id,
37729
+ error: {
37730
+ code: -32602,
37731
+ message: `Invalid _meta envelope: ${issue2.key}: ${issue2.problem}`
37732
+ }
37733
+ };
37734
+ else if (claimedVersion === void 0 || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedVersion)) {
37735
+ const error62 = new UnsupportedProtocolVersionError({
37736
+ supported: [...SUPPORTED_MODERN_PROTOCOL_VERSIONS],
37737
+ requested: claimedVersion ?? "unknown"
37738
+ });
37739
+ reply = {
37740
+ jsonrpc: "2.0",
37741
+ id: message.id,
37742
+ error: {
37743
+ code: error62.code,
37744
+ message: error62.message,
37745
+ data: error62.data
37746
+ }
37747
+ };
37748
+ } else reply = listenRouter.serve(message);
37749
+ await wire.send("error" in reply ? reply : {
37750
+ jsonrpc: "2.0",
37751
+ method: reply.method,
37752
+ params: reply.params
37753
+ }).catch((error62) => reportError(toError(error62)));
37754
+ return true;
37755
+ }
37756
+ if (isJSONRPCNotification(message) && message.method === "notifications/cancelled") {
37757
+ const cancelledId = message.params?.requestId;
37758
+ if (cancelledId !== void 0 && listenRouter.cancel(cancelledId)) return true;
37759
+ }
37760
+ return false;
37761
+ };
37762
+ const answerLegacyRejection = (request, reason, requestedVersion) => {
37763
+ const rejection2 = modernOnlyStrictRejection({
37764
+ kind: "legacy",
37765
+ reason,
37766
+ ...requestedVersion !== void 0 && { requestedVersion }
37767
+ }, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
37768
+ if (rejection2 === void 0) return Promise.resolve();
37769
+ reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection2.cell}): ${rejection2.message}`));
37770
+ return writeErrorResponse(request.id, rejection2.code, rejection2.message, rejection2.data);
37771
+ };
37772
+ const onInstanceClosed = (channel) => {
37773
+ if (closing || channel === discarding) return;
37774
+ closeAll();
37775
+ };
37776
+ const connectInstance = async (era, revision) => {
37777
+ const product = await factory({ era });
37778
+ const server = product instanceof McpServer ? product.server : product;
37779
+ if (era === "modern") {
37780
+ setNegotiatedProtocolVersion(server, revision);
37781
+ installModernOnlyHandlers(server, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
37782
+ listenRouter.setServerCapabilities(server.getCapabilities(), serverIdentityOf(server));
37783
+ }
37784
+ const channel = new StdioConnectionChannel(wire, () => onInstanceClosed(channel), era === "modern" ? modernOutboundIntercept : void 0);
37785
+ await product.connect(channel);
37786
+ return {
37787
+ product,
37788
+ channel
37789
+ };
37790
+ };
37791
+ const disposeLateInstance = (instance) => instance.product.close().catch((error62) => reportError(toError(error62)));
37792
+ const discardProbeInstance = async (instance) => {
37793
+ discarding = instance.channel;
37794
+ try {
37795
+ if (!await instance.channel.whenRequestsAnswered(DISCARD_ANSWER_TIMEOUT_MS)) reportError(/* @__PURE__ */ new Error(`Discarded the probe instance with requests still unanswered after ${DISCARD_ANSWER_TIMEOUT_MS}ms; continuing with the fallback`));
37796
+ await instance.product.close();
37797
+ } catch (error62) {
37798
+ reportError(toError(error62));
37799
+ } finally {
37800
+ discarding = void 0;
37801
+ }
37802
+ };
37803
+ const processMessage = async (message) => {
37804
+ if (state.phase === "closed") return;
37805
+ if (state.phase === "pinned") {
37806
+ if (state.era === "modern" && isJSONRPCRequest(message) && message.method === "initialize" && !carriesValidModernEnvelopeClaim(message.params)) {
37807
+ await answerLegacyRejection(message, "initialize", message.params !== null && typeof message.params === "object" && typeof message.params.protocolVersion === "string" ? message.params.protocolVersion : void 0);
37808
+ return;
37809
+ }
37810
+ if (state.era === "modern" && await tryServeListen(message)) return;
37811
+ state.instance.channel.deliver(message);
37812
+ return;
37813
+ }
37814
+ if (!isJSONRPCRequest(message) && !isJSONRPCNotification(message)) {
37815
+ reportError(/* @__PURE__ */ new Error("Discarded a JSON-RPC response received before the connection negotiated an era"));
37816
+ return;
37817
+ }
37818
+ const opening = classifyOpeningMessage(message);
37819
+ switch (opening.kind) {
37820
+ case "invalid-envelope": {
37821
+ const detail = `Invalid _meta envelope for protocol revision 2026-07-28: ${opening.issue.key}: ${opening.issue.problem}`;
37822
+ if (isJSONRPCRequest(message)) await writeErrorResponse(message.id, ProtocolErrorCode.InvalidParams, detail, { envelope: opening.issue });
37823
+ else reportError(/* @__PURE__ */ new Error(`Discarded a notification with a malformed envelope: ${detail}`));
37824
+ return;
37825
+ }
37826
+ case "unsupported-revision":
37827
+ if (isJSONRPCRequest(message)) {
37828
+ const error62 = new UnsupportedProtocolVersionError({
37829
+ supported: [...SUPPORTED_MODERN_PROTOCOL_VERSIONS],
37830
+ requested: opening.requested
37831
+ });
37832
+ reportError(error62);
37833
+ await writeErrorResponse(message.id, error62.code, error62.message, error62.data);
37834
+ } else reportError(/* @__PURE__ */ new Error(`Discarded a notification claiming unsupported protocol revision ${opening.requested}`));
37835
+ return;
37836
+ case "modern":
37837
+ if (isJSONRPCRequest(message) && message.method === "server/discover") {
37838
+ if (state.phase === "probe") {
37839
+ state.instance.channel.deliver(message, { classification: opening.classification });
37840
+ return;
37841
+ }
37842
+ const instance = await connectInstance("modern", opening.revision);
37843
+ if (isTornDown()) {
37844
+ await disposeLateInstance(instance);
37845
+ return;
37846
+ }
37847
+ state = {
37848
+ phase: "probe",
37849
+ instance
37850
+ };
37851
+ instance.channel.deliver(message, { classification: opening.classification });
37852
+ return;
37853
+ }
37854
+ if (state.phase === "probe") {
37855
+ if (isJSONRPCNotification(message)) {
37856
+ state.instance.channel.deliver(message, { classification: opening.classification });
37857
+ return;
37858
+ }
37859
+ state = {
37860
+ phase: "pinned",
37861
+ era: "modern",
37862
+ instance: state.instance
37863
+ };
37864
+ } else {
37865
+ const instance = await connectInstance("modern", opening.revision);
37866
+ if (isTornDown()) {
37867
+ await disposeLateInstance(instance);
37868
+ return;
37869
+ }
37870
+ state = {
37871
+ phase: "pinned",
37872
+ era: "modern",
37873
+ instance
37874
+ };
37875
+ }
37876
+ if (await tryServeListen(message)) return;
37877
+ state.instance.channel.deliver(message, { classification: opening.classification });
37878
+ return;
37879
+ case "legacy": {
37880
+ if (legacyMode === "reject") {
37881
+ if (isJSONRPCRequest(message)) await answerLegacyRejection(message, opening.reason, opening.requestedVersion);
37882
+ return;
37883
+ }
37884
+ if (state.phase === "probe") {
37885
+ await discardProbeInstance(state.instance);
37886
+ if (isTornDown()) return;
37887
+ state = { phase: "opening" };
37888
+ }
37889
+ const instance = await connectInstance("legacy");
37890
+ if (isTornDown()) {
37891
+ await disposeLateInstance(instance);
37892
+ return;
37893
+ }
37894
+ state = {
37895
+ phase: "pinned",
37896
+ era: "legacy",
37897
+ instance
37898
+ };
37899
+ state.instance.channel.deliver(message);
37900
+ return;
37901
+ }
37902
+ }
37903
+ };
37904
+ const queue = [];
37905
+ let pumping = false;
37906
+ const pump = async () => {
37907
+ if (pumping) return;
37908
+ pumping = true;
37909
+ try {
37910
+ while (queue.length > 0) {
37911
+ const message = queue.shift();
37912
+ try {
37913
+ await processMessage(message);
37914
+ } catch (error62) {
37915
+ if (isJSONRPCRequest(message)) await writeErrorResponse(message.id, ProtocolErrorCode.InternalError, "Internal server error");
37916
+ reportError(toError(error62));
37917
+ }
37918
+ }
37919
+ } finally {
37920
+ pumping = false;
37921
+ }
37922
+ };
37923
+ const closeAll = async () => {
37924
+ if (closing || state.phase === "closed") return;
37925
+ closing = true;
37926
+ const current = state;
37927
+ state = { phase: "closed" };
37928
+ for (const result of listenRouter.teardownAll()) await wire.send(result).catch((error62) => reportError(toError(error62)));
37929
+ if (current.phase === "probe" || current.phase === "pinned") await current.instance.product.close().catch((error62) => reportError(toError(error62)));
37930
+ await wire.close().catch((error62) => reportError(toError(error62)));
37931
+ };
37932
+ wire.onmessage = (message) => {
37933
+ queue.push(message);
37934
+ pump();
37935
+ };
37936
+ wire.onerror = (error62) => {
37937
+ reportError(error62);
37938
+ if (state.phase === "probe" || state.phase === "pinned") state.instance.channel.onerror?.(error62);
37939
+ };
37940
+ wire.onclose = () => {
37941
+ if (closing || state.phase === "closed") return;
37942
+ closing = true;
37943
+ const current = state;
37944
+ state = { phase: "closed" };
37945
+ if (current.phase === "probe" || current.phase === "pinned") current.instance.product.close().catch((error62) => reportError(toError(error62)));
37946
+ };
37947
+ const started = wire.start().catch((error62) => {
37948
+ reportError(toError(error62));
37949
+ throw error62;
37950
+ });
37951
+ started.catch(() => {
37952
+ });
37953
+ return { close: async () => {
37954
+ await started.catch(() => {
37955
+ });
37956
+ await closeAll();
37957
+ } };
37958
+ }
37959
+ function toError(value) {
37960
+ return value instanceof Error ? value : new Error(String(value));
37961
+ }
37302
37962
 
37303
37963
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
37304
37964
  var DEFAULT_ERROR_MESSAGE_MAX = 500;
@@ -57103,17 +57763,73 @@ Hint: ${err.hint}`);
57103
57763
  }
57104
57764
  throw err;
57105
57765
  }
57766
+ function mcpRequestFrom(args) {
57767
+ const ctx = args.at(-1);
57768
+ if (typeof ctx !== "object" || ctx === null)
57769
+ return void 0;
57770
+ const req = ctx.mcpReq;
57771
+ if (typeof req !== "object" || req === null)
57772
+ return void 0;
57773
+ return typeof req.notify === "function" ? req : void 0;
57774
+ }
57775
+ function callSignalFrom(args) {
57776
+ const ctx = args.at(-1);
57777
+ if (typeof ctx !== "object" || ctx === null)
57778
+ return void 0;
57779
+ const req = ctx.mcpReq;
57780
+ if (typeof req !== "object" || req === null)
57781
+ return void 0;
57782
+ const signal = req.signal;
57783
+ return signal instanceof AbortSignal ? signal : void 0;
57784
+ }
57785
+ function declaredCapabilitiesFrom(server) {
57786
+ const inner = server.server;
57787
+ if (typeof inner !== "object" || inner === null)
57788
+ return void 0;
57789
+ const read = inner.getClientCapabilities;
57790
+ if (typeof read !== "function")
57791
+ return void 0;
57792
+ try {
57793
+ const declared = read.call(inner);
57794
+ return typeof declared === "object" && declared !== null ? declared : void 0;
57795
+ } catch {
57796
+ return void 0;
57797
+ }
57798
+ }
57106
57799
  function surfaceToolHints(server) {
57107
57800
  const register2 = server.registerTool.bind(server);
57108
- server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => {
57109
- let result;
57110
- try {
57111
- result = cb(...args);
57112
- } catch (err) {
57113
- return hintResultOrRethrow(err);
57114
- }
57115
- return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
57116
- });
57801
+ server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => (
57802
+ // THE CALLER'S CANCELLATION, made ambient for the whole handler
57803
+ // (`cancel/index.ts`). The SDK delivers it and the fleet ignored it:
57804
+ // measured against @modelcontextprotocol/server 2.0.0, a cancelled
57805
+ // call aborts `ctx.mcpReq.signal` with the caller's reason and the
57806
+ // handler runs to completion regardless — so the HTTP request stays
57807
+ // in flight, the child keeps burning metered CPU, and the upstream
57808
+ // keeps being hit for somebody who has gone. claude.ai sent 101
57809
+ // cancellations in the week to 2026-09-20.
57810
+ //
57811
+ // Here because this is the one wrapper every tool already passes
57812
+ // through: threading the signal by hand would mean editing several
57813
+ // hundred handlers and missing exactly the ones nobody edits.
57814
+ withCallSignal(callSignalFrom(args), () => (
57815
+ // WHAT THE CALLER CAN DO, made ambient for the same reason and in
57816
+ // the same place (`caller/index.ts`). Only the 2025-era half needs
57817
+ // the wrapper — a 2026-07-28 request answers for itself off its own
57818
+ // envelope — but a guarded tool that cannot tell whether the caller
57819
+ // can be shown a prompt returns one the SDK then refuses to deliver,
57820
+ // and that refusal reaches the caller as an unexplained protocol
57821
+ // error it had no chance to catch.
57822
+ withCallerCapabilities(declaredCapabilitiesFrom(server), () => {
57823
+ let result;
57824
+ try {
57825
+ result = cb(...args);
57826
+ } catch (err) {
57827
+ return hintResultOrRethrow(err);
57828
+ }
57829
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
57830
+ })
57831
+ ), mcpRequestFrom(args))
57832
+ ));
57117
57833
  }
57118
57834
  async function createMcpServer(opts) {
57119
57835
  const server = new McpServer({ name: opts.name, version: opts.version }, { supportedProtocolVersions: [...SERVER_PROTOCOL_VERSIONS] });
@@ -57128,7 +57844,7 @@ async function createMcpServer(opts) {
57128
57844
  }
57129
57845
  return server;
57130
57846
  }
57131
- function withGracefulShutdown(server, opts = {}) {
57847
+ function withGracefulShutdown(target, opts = {}) {
57132
57848
  const shouldExit = opts.exit ?? true;
57133
57849
  let shuttingDown = false;
57134
57850
  const handler = (signal) => {
@@ -57139,7 +57855,7 @@ function withGracefulShutdown(server, opts = {}) {
57139
57855
  try {
57140
57856
  if (opts.onSignal)
57141
57857
  await opts.onSignal(signal);
57142
- await server.close();
57858
+ await target.close();
57143
57859
  } catch (err) {
57144
57860
  console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
57145
57861
  } finally {
@@ -57151,16 +57867,26 @@ function withGracefulShutdown(server, opts = {}) {
57151
57867
  process.on("SIGINT", () => handler("SIGINT"));
57152
57868
  process.on("SIGTERM", () => handler("SIGTERM"));
57153
57869
  }
57154
- async function runMcp(opts) {
57155
- const server = await createMcpServer(opts);
57870
+ function runMcp(opts) {
57871
+ if (opts.banner !== void 0) {
57872
+ console.error(opts.banner);
57873
+ }
57874
+ const spec = opts.transport ?? "stdio";
57875
+ const handle = serveStdio(() => createMcpServer({ ...opts, banner: void 0 }), {
57876
+ legacy: opts.legacy ?? "serve",
57877
+ onerror: opts.onerror ?? ((error62) => {
57878
+ console.error(`[mcp-utils] stdio server error: ${redactSecrets(error62.message)}`);
57879
+ }),
57880
+ // `'stdio'` means "let the entry make its own StdioServerTransport over
57881
+ // this process"; anything else is the caller's transport, which the entry
57882
+ // then owns (it starts it, pumps it, and closes it with the handle).
57883
+ ...spec === "stdio" ? {} : { transport: spec }
57884
+ });
57156
57885
  const shutdown = opts.shutdown ?? true;
57157
57886
  if (shutdown !== false) {
57158
- withGracefulShutdown(server, shutdown === true ? {} : shutdown);
57887
+ withGracefulShutdown(handle, shutdown === true ? {} : shutdown);
57159
57888
  }
57160
- const spec = opts.transport ?? "stdio";
57161
- const transport = spec === "stdio" ? new StdioServerTransport() : spec;
57162
- await server.connect(transport);
57163
- return server;
57889
+ return handle;
57164
57890
  }
57165
57891
 
57166
57892
  // node_modules/@chrischall/mcp-utils/dist/config/index.js
@@ -57256,12 +57982,13 @@ function toolAnnotations(opts = {}) {
57256
57982
  ...opts.title !== void 0 ? { title: opts.title } : {},
57257
57983
  readOnlyHint: opts.readOnly ?? true,
57258
57984
  ...opts.idempotent !== void 0 ? { idempotentHint: opts.idempotent } : {},
57985
+ ...opts.destructive !== void 0 ? { destructiveHint: opts.destructive } : {},
57259
57986
  ...opts.openWorld !== void 0 ? { openWorldHint: opts.openWorld } : {}
57260
57987
  };
57261
57988
  }
57262
57989
 
57263
57990
  // src/version.ts
57264
- var VERSION = "2.0.0";
57991
+ var VERSION = "2.1.1";
57265
57992
 
57266
57993
  // src/client.ts
57267
57994
  import { dirname, join } from "path";
@@ -58830,7 +59557,11 @@ function registerFriendActionTools(server, client2) {
58830
59557
  {
58831
59558
  title: action.title,
58832
59559
  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.`,
58833
- annotations: toolAnnotations({ title: action.title, readOnly: false, idempotent: true, openWorld: true }),
59560
+ // All four reach ANOTHER PERSON — the description says so itself: "affects
59561
+ // a real relationship with another person". A friend request cannot be
59562
+ // un-sent, and a rejection cannot be un-rejected, so none of them has an
59563
+ // inverse in this tool set.
59564
+ annotations: toolAnnotations({ title: action.title, readOnly: false, idempotent: true, openWorld: true, destructive: true }),
58834
59565
  inputSchema: external_exports.object({
58835
59566
  target_uid: TargetUidSchema,
58836
59567
  confirm: schemaConfirm
@@ -58860,7 +59591,7 @@ function registerWishlistTools(server, client2) {
58860
59591
  {
58861
59592
  title: "Add a beer to your wishlist",
58862
59593
  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.",
58863
- annotations: toolAnnotations({ title: "Add a beer to your wishlist", readOnly: false, idempotent: true, openWorld: true }),
59594
+ annotations: toolAnnotations({ title: "Add a beer to your wishlist", readOnly: false, idempotent: true, openWorld: true, destructive: false }),
58864
59595
  inputSchema: external_exports.object({
58865
59596
  bid: BidSchema2,
58866
59597
  confirm: schemaConfirm
@@ -58884,7 +59615,7 @@ function registerWishlistTools(server, client2) {
58884
59615
  {
58885
59616
  title: "Remove a beer from your wishlist",
58886
59617
  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.",
58887
- annotations: toolAnnotations({ title: "Remove a beer from your wishlist", readOnly: false, idempotent: true, openWorld: true }),
59618
+ annotations: toolAnnotations({ title: "Remove a beer from your wishlist", readOnly: false, idempotent: true, openWorld: true, destructive: false }),
58888
59619
  inputSchema: external_exports.object({
58889
59620
  bid: BidSchema2,
58890
59621
  confirm: schemaConfirm
@@ -58926,7 +59657,7 @@ function registerCheckinTools(server, client2) {
58926
59657
  {
58927
59658
  title: "Toast an Untappd check-in",
58928
59659
  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.",
58929
- annotations: toolAnnotations({ title: "Toast an Untappd check-in", readOnly: false, idempotent: false, openWorld: true }),
59660
+ annotations: toolAnnotations({ title: "Toast an Untappd check-in", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
58930
59661
  inputSchema: external_exports.object({
58931
59662
  checkin_id: CheckinIdSchema,
58932
59663
  confirm: schemaConfirm
@@ -58950,7 +59681,7 @@ function registerCheckinTools(server, client2) {
58950
59681
  {
58951
59682
  title: "Comment on an Untappd check-in",
58952
59683
  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.",
58953
- annotations: toolAnnotations({ title: "Comment on an Untappd check-in", readOnly: false, idempotent: false, openWorld: true }),
59684
+ annotations: toolAnnotations({ title: "Comment on an Untappd check-in", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
58954
59685
  inputSchema: external_exports.object({
58955
59686
  checkin_id: CheckinIdSchema,
58956
59687
  comment: external_exports.string().min(1).max(2e3).describe("Comment text to post"),
@@ -58976,7 +59707,7 @@ function registerCheckinTools(server, client2) {
58976
59707
  {
58977
59708
  title: "Delete a comment from an Untappd check-in",
58978
59709
  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.",
58979
- annotations: toolAnnotations({ title: "Delete a comment from an Untappd check-in", readOnly: false, idempotent: true, openWorld: true }),
59710
+ annotations: toolAnnotations({ title: "Delete a comment from an Untappd check-in", readOnly: false, idempotent: true, openWorld: true, destructive: true }),
58980
59711
  inputSchema: external_exports.object({
58981
59712
  comment_id: external_exports.number().int().positive().describe("Untappd comment id (from a check-in's comments.items)"),
58982
59713
  confirm: schemaConfirm
@@ -59000,7 +59731,7 @@ function registerCheckinTools(server, client2) {
59000
59731
  {
59001
59732
  title: "Delete an Untappd check-in",
59002
59733
  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.",
59003
- annotations: toolAnnotations({ title: "Delete an Untappd check-in", readOnly: false, idempotent: true, openWorld: true }),
59734
+ annotations: toolAnnotations({ title: "Delete an Untappd check-in", readOnly: false, idempotent: true, openWorld: true, destructive: true }),
59004
59735
  inputSchema: external_exports.object({
59005
59736
  checkin_id: CheckinIdSchema,
59006
59737
  confirm: schemaConfirm
@@ -59024,7 +59755,7 @@ function registerCheckinTools(server, client2) {
59024
59755
  {
59025
59756
  title: "Check in a beer on Untappd",
59026
59757
  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.",
59027
- annotations: toolAnnotations({ title: "Check in a beer on Untappd", readOnly: false, idempotent: false, openWorld: true }),
59758
+ annotations: toolAnnotations({ title: "Check in a beer on Untappd", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
59028
59759
  inputSchema: external_exports.object({
59029
59760
  bid: external_exports.number().int().positive().describe("Untappd beer id to check in (from untappd_search_beer)"),
59030
59761
  rating: RatingSchema.optional().describe("Rating 0\u20135 in 0.25 increments (omit for no rating)"),
@@ -59538,7 +60269,7 @@ function registerCacheTools(server, client2, cacheProvider) {
59538
60269
  {
59539
60270
  title: "Sync a user's check-ins into the cache",
59540
60271
  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.",
59541
- annotations: toolAnnotations({ title: "Sync a user's check-ins into the cache", readOnly: false, idempotent: false, openWorld: true }),
60272
+ annotations: toolAnnotations({ title: "Sync a user's check-ins into the cache", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
59542
60273
  inputSchema: external_exports.object({
59543
60274
  username: UsernameArg2,
59544
60275
  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."),
@@ -59559,7 +60290,7 @@ function registerCacheTools(server, client2, cacheProvider) {
59559
60290
  {
59560
60291
  title: "Sync a user's complete distinct-beers list into the cache",
59561
60292
  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.`,
59562
- annotations: toolAnnotations({ title: "Sync a user's complete distinct-beers list into the cache", readOnly: false, idempotent: false, openWorld: true }),
60293
+ annotations: toolAnnotations({ title: "Sync a user's complete distinct-beers list into the cache", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
59563
60294
  inputSchema: external_exports.object({
59564
60295
  username: UsernameArg2,
59565
60296
  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.")
@@ -59693,7 +60424,7 @@ function registerCacheTools(server, client2, cacheProvider) {
59693
60424
  {
59694
60425
  title: "Top-rated beers a user has NOT had, from a candidate list",
59695
60426
  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.',
59696
- annotations: toolAnnotations({ title: "Top-rated beers a user has NOT had, from a candidate list", readOnly: false, idempotent: false, openWorld: true }),
60427
+ annotations: toolAnnotations({ title: "Top-rated beers a user has NOT had, from a candidate list", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
59697
60428
  inputSchema: external_exports.object({
59698
60429
  username: UsernameArg2,
59699
60430
  bids: external_exports.array(external_exports.number().int().positive()).min(1).max(100).describe("Candidate beer ids (1\u2013100)"),
@@ -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.0.0'; // x-release-please-version
6
+ export const VERSION = '2.1.1'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "untappd-mcp",
3
- "version": "2.0.0",
3
+ "version": "2.1.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>",
@@ -45,7 +45,7 @@
45
45
  "typecheck": "tsc -p tsconfig.json --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@chrischall/mcp-utils": "^0.28.0",
48
+ "@chrischall/mcp-utils": "^2.2.0",
49
49
  "@modelcontextprotocol/server": "^2.0.0",
50
50
  "ai": "^7.0.19",
51
51
  "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": "2.0.0",
9
+ "version": "2.1.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "untappd-mcp",
14
- "version": "2.0.0",
14
+ "version": "2.1.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },