untappd-mcp 2.1.3 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bundle.js CHANGED
@@ -23315,6 +23315,33 @@ var storage2 = new AsyncLocalStorage2();
23315
23315
  function withCallerCapabilities(capabilities, fn) {
23316
23316
  return capabilities ? storage2.run(capabilities, fn) : fn();
23317
23317
  }
23318
+ function currentCallerCapabilities() {
23319
+ return storage2.getStore();
23320
+ }
23321
+ var ENVELOPE_CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
23322
+ var ELICITATION_MODES = ["form", "url"];
23323
+ function isRecord(value) {
23324
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23325
+ }
23326
+ function callerCapabilities(ctx) {
23327
+ const envelope = isRecord(ctx) && isRecord(ctx.mcpReq) ? ctx.mcpReq.envelope : void 0;
23328
+ if (isRecord(envelope)) {
23329
+ const declared = envelope[ENVELOPE_CAPABILITIES_KEY];
23330
+ if (isRecord(declared))
23331
+ return declared;
23332
+ }
23333
+ return currentCallerCapabilities();
23334
+ }
23335
+ function callerAcceptsFormElicitation(ctx) {
23336
+ const capabilities = callerCapabilities(ctx);
23337
+ if (!capabilities)
23338
+ return void 0;
23339
+ const elicitation = capabilities.elicitation;
23340
+ if (!isRecord(elicitation))
23341
+ return false;
23342
+ const namedModes = ELICITATION_MODES.filter((mode) => mode in elicitation);
23343
+ return namedModes.length === 0 || namedModes.includes("form");
23344
+ }
23318
23345
 
23319
23346
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
23320
23347
  var __create2 = Object.create;
@@ -27713,6 +27740,37 @@ var inputRequired = Object.assign(buildInputRequired, {
27713
27740
  return { method: "roots/list" };
27714
27741
  }
27715
27742
  });
27743
+ function acceptedContent(responses, key, schema) {
27744
+ const view = inputResponse(responses, key);
27745
+ if (view.kind !== "elicit" || view.action !== "accept" || view.content === void 0) return void 0;
27746
+ if (schema === void 0) return view.content;
27747
+ const outcome = schema["~standard"].validate(view.content);
27748
+ if (outcome instanceof Promise) throw new TypeError("acceptedContent(responses, key, schema) requires a synchronously-validating schema");
27749
+ return outcome.issues === void 0 ? outcome.value : void 0;
27750
+ }
27751
+ function inputResponse(responses, key) {
27752
+ if (responses === void 0 || typeof responses !== "object" || responses === null) return { kind: "missing" };
27753
+ const entry = responses[key];
27754
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return { kind: "missing" };
27755
+ const candidate = entry;
27756
+ if (candidate["action"] === "accept" || candidate["action"] === "decline" || candidate["action"] === "cancel") {
27757
+ const content = candidate["content"];
27758
+ return {
27759
+ kind: "elicit",
27760
+ action: candidate["action"],
27761
+ ...content !== null && typeof content === "object" && !Array.isArray(content) && { content }
27762
+ };
27763
+ }
27764
+ if (Array.isArray(candidate["roots"])) return {
27765
+ kind: "roots",
27766
+ roots: candidate["roots"]
27767
+ };
27768
+ if (typeof candidate["role"] === "string" && candidate["content"] !== void 0) return {
27769
+ kind: "sampling",
27770
+ result: candidate
27771
+ };
27772
+ return { kind: "missing" };
27773
+ }
27716
27774
  var REQUEST_STATE_ONLY_LEG_PACING_MS = 250;
27717
27775
  function inputRequiredRoundsExceededMessage(method, maxRounds) {
27718
27776
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
@@ -37760,14 +37818,14 @@ function serveStdio(factory, options = {}) {
37760
37818
  return false;
37761
37819
  };
37762
37820
  const answerLegacyRejection = (request, reason, requestedVersion) => {
37763
- const rejection2 = modernOnlyStrictRejection({
37821
+ const rejection3 = modernOnlyStrictRejection({
37764
37822
  kind: "legacy",
37765
37823
  reason,
37766
37824
  ...requestedVersion !== void 0 && { requestedVersion }
37767
37825
  }, 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);
37826
+ if (rejection3 === void 0) return Promise.resolve();
37827
+ reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection3.cell}): ${rejection3.message}`));
37828
+ return writeErrorResponse(request.id, rejection3.code, rejection3.message, rejection3.data);
37771
37829
  };
37772
37830
  const onInstanceClosed = (channel) => {
37773
37831
  if (closing || channel === discarding) return;
@@ -57758,6 +57816,11 @@ var MEDIA_QUALIFIER = "(?:primary|secondary|main|default|cover|hero|profile|mast
57758
57816
  var MEDIA_KEY = new RegExp(`^(?:(?:${MEDIA_QUALIFIER}|${MEDIA_NOUN})[_-]?)?${MEDIA_NOUN}s?(?:[_-]?(?:link|uri|url|src)s?)?$`, "i");
57759
57817
 
57760
57818
  // node_modules/@chrischall/mcp-utils/dist/response/index.js
57819
+ function textResult(data) {
57820
+ return {
57821
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
57822
+ };
57823
+ }
57761
57824
  function errorResult(message) {
57762
57825
  return {
57763
57826
  content: [{ type: "text", text: redactSecrets(message) }],
@@ -57765,6 +57828,418 @@ function errorResult(message) {
57765
57828
  };
57766
57829
  }
57767
57830
 
57831
+ // node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
57832
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
57833
+
57834
+ // node_modules/@chrischall/mcp-utils/dist/server/canonical.js
57835
+ function canonicalJson(value) {
57836
+ if (value === null || typeof value === "string" || typeof value === "boolean")
57837
+ return JSON.stringify(value);
57838
+ if (value === void 0)
57839
+ return "null";
57840
+ if (typeof value === "number")
57841
+ return Number.isFinite(value) ? JSON.stringify(value) : `{"$num":"${String(value)}"}`;
57842
+ if (typeof value === "bigint")
57843
+ return `{"$bigint":"${value.toString()}"}`;
57844
+ if (typeof value === "function" || typeof value === "symbol") {
57845
+ throw new TypeError(`confirmation: cannot bind a ${typeof value} argument value.`);
57846
+ }
57847
+ if (Array.isArray(value))
57848
+ return `[${value.map(canonicalJson).join(",")}]`;
57849
+ if (value instanceof Date) {
57850
+ return `{"$date":${JSON.stringify(Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString())}}`;
57851
+ }
57852
+ if (ArrayBuffer.isView(value)) {
57853
+ const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
57854
+ return `{"$bytes":${JSON.stringify(bytes.toString("base64"))}}`;
57855
+ }
57856
+ if (value instanceof ArrayBuffer)
57857
+ return `{"$bytes":${JSON.stringify(Buffer.from(value).toString("base64"))}}`;
57858
+ if (value instanceof Map) {
57859
+ const entries2 = [...value.entries()].map(([k, v]) => `[${canonicalJson(k)},${canonicalJson(v)}]`).sort();
57860
+ return `{"$map":[${entries2.join(",")}]}`;
57861
+ }
57862
+ if (value instanceof Set) {
57863
+ return `{"$set":[${[...value].map(canonicalJson).sort().join(",")}]}`;
57864
+ }
57865
+ const proto = Object.getPrototypeOf(value);
57866
+ if (proto !== Object.prototype && proto !== null) {
57867
+ throw new TypeError("confirmation: cannot bind a non-plain object argument value (class instance).");
57868
+ }
57869
+ const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
57870
+ return `{${entries.map(([k, v]) => `${JSON.stringify(escapeKey(k))}:${canonicalJson(v)}`).join(",")}}`;
57871
+ }
57872
+ function escapeKey(key) {
57873
+ return key.startsWith("$") ? `$${key}` : key;
57874
+ }
57875
+
57876
+ // node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
57877
+ var DEFAULT_REQUEST_KEY = "confirmation";
57878
+ var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
57879
+ var UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
57880
+ var STATE_PREFIX = "mcpu.confirm.v1.";
57881
+ function bindingKey(binding) {
57882
+ if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
57883
+ throw new RangeError("requireConfirmation: binding.ttlSeconds must be a finite number greater than 0.");
57884
+ }
57885
+ const key = typeof binding.key === "string" ? Buffer.from(binding.key, "utf8") : Buffer.from(binding.key);
57886
+ if (key.length < 32) {
57887
+ throw new RangeError("requireConfirmation: binding.key must be at least 32 bytes.");
57888
+ }
57889
+ return key;
57890
+ }
57891
+ function commitment(action, args) {
57892
+ return createHash("sha256").update(`${action}\0${canonicalJson(args)}`).digest("base64url");
57893
+ }
57894
+ function mintState(key, action, binding) {
57895
+ const exp = Math.floor(Date.now() / 1e3) + (binding.ttlSeconds ?? 600);
57896
+ const body = Buffer.from(JSON.stringify({ c: commitment(action, binding.args), exp })).toString("base64url");
57897
+ const mac3 = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest("base64url");
57898
+ return `${STATE_PREFIX}${body}.${mac3}`;
57899
+ }
57900
+ function verifyState(key, action, binding, state) {
57901
+ if (typeof state !== "string" || !state.startsWith(STATE_PREFIX))
57902
+ return false;
57903
+ const rest = state.slice(STATE_PREFIX.length);
57904
+ const dot = rest.indexOf(".");
57905
+ if (dot < 0)
57906
+ return false;
57907
+ const body = rest.slice(0, dot);
57908
+ const mac3 = Buffer.from(rest.slice(dot + 1), "base64url");
57909
+ const expected = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest();
57910
+ if (mac3.length !== expected.length || !timingSafeEqual(mac3, expected))
57911
+ return false;
57912
+ let payload;
57913
+ try {
57914
+ payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
57915
+ } catch {
57916
+ return false;
57917
+ }
57918
+ if (typeof payload.exp !== "number" || payload.exp * 1e3 <= Date.now())
57919
+ return false;
57920
+ const want = Buffer.from(commitment(action, binding.args));
57921
+ const got = Buffer.from(typeof payload.c === "string" ? payload.c : "");
57922
+ return got.length === want.length && timingSafeEqual(got, want);
57923
+ }
57924
+ function echoedState(ctx) {
57925
+ const accessor = ctx.mcpReq.requestState;
57926
+ return typeof accessor === "function" ? accessor() : void 0;
57927
+ }
57928
+ function requireConfirmation(ctx, options) {
57929
+ const requestKey = options.requestKey ?? DEFAULT_REQUEST_KEY;
57930
+ const key = options.binding ? bindingKey(options.binding) : void 0;
57931
+ const confirmationSchema = external_exports.object({
57932
+ confirmed: external_exports.boolean().describe(options.confirmationLabel ?? DEFAULT_CONFIRMATION_LABEL)
57933
+ });
57934
+ const ask = () => {
57935
+ const preview = {
57936
+ action: options.action,
57937
+ ...options.details === void 0 ? {} : { details: options.details }
57938
+ };
57939
+ return inputRequired({
57940
+ inputRequests: {
57941
+ [requestKey]: inputRequired.elicit({
57942
+ message: `${options.message}
57943
+ ${JSON.stringify(preview, null, 2)}`,
57944
+ requestedSchema: confirmationSchema
57945
+ })
57946
+ },
57947
+ ...key && options.binding ? { requestState: mintState(key, options.action, options.binding) } : {}
57948
+ });
57949
+ };
57950
+ const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
57951
+ const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
57952
+ if (response.kind === "missing") {
57953
+ if (callerAcceptsFormElicitation(ctx) === false) {
57954
+ return textResult({
57955
+ confirmed: false,
57956
+ dispatched: false,
57957
+ action: options.action,
57958
+ reason: "confirmation-unsupported",
57959
+ note: options.unsupportedNote ? `${UNSUPPORTED_NOTE}. ${options.unsupportedNote}` : `${UNSUPPORTED_NOTE}.`
57960
+ });
57961
+ }
57962
+ return ask();
57963
+ }
57964
+ if (response.kind === "elicit" && response.action === "accept" && accepted?.confirmed === true) {
57965
+ if (key && options.binding) {
57966
+ const state = echoedState(ctx);
57967
+ if (state === void 0 || state === null) {
57968
+ return errorResult(`Confirmation for ${options.action} was accepted, but the retry carried no requestState, so it cannot be checked against the prompt that was shown. Nothing was done. The likely cause is that the MCP client or host does not round-trip requestState (it must echo the requestState from the input_required result back on the retry); asking again would loop.`);
57969
+ }
57970
+ if (!verifyState(key, options.action, options.binding, state))
57971
+ return ask();
57972
+ }
57973
+ return void 0;
57974
+ }
57975
+ return textResult({
57976
+ confirmed: false,
57977
+ cancelled: true,
57978
+ action: options.action,
57979
+ note: "Nothing was changed because the confirmation was declined, cancelled, or left unchecked."
57980
+ });
57981
+ }
57982
+
57983
+ // node_modules/@chrischall/mcp-utils/dist/server/confirm-token.js
57984
+ import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
57985
+ function createSpentTokenStore() {
57986
+ const spent = /* @__PURE__ */ new Map();
57987
+ return {
57988
+ has: (nonce) => spent.has(nonce),
57989
+ add: (nonce, exp) => {
57990
+ spent.set(nonce, exp);
57991
+ },
57992
+ prune: (now) => {
57993
+ for (const [nonce, exp] of spent)
57994
+ if (exp < now)
57995
+ spent.delete(nonce);
57996
+ },
57997
+ clear: () => spent.clear(),
57998
+ get size() {
57999
+ return spent.size;
58000
+ }
58001
+ };
58002
+ }
58003
+ var PROCESS_SPENT = createSpentTokenStore();
58004
+ var PREFIX = "mcpu.token.v1.";
58005
+ var DEFAULT_TTL_SECONDS = 600;
58006
+ function tokenKey(key) {
58007
+ const bytes = typeof key === "string" ? Buffer.from(key, "utf8") : Buffer.from(key);
58008
+ if (bytes.length < 32)
58009
+ throw new RangeError("confirm token: key must be at least 32 bytes.");
58010
+ return bytes;
58011
+ }
58012
+ function ttlOf(ttlSeconds) {
58013
+ if (ttlSeconds === void 0)
58014
+ return DEFAULT_TTL_SECONDS;
58015
+ if (!(Number.isFinite(ttlSeconds) && ttlSeconds > 0)) {
58016
+ throw new RangeError("confirm token: ttlSeconds must be a finite number greater than 0.");
58017
+ }
58018
+ return ttlSeconds;
58019
+ }
58020
+ function sign(key, body) {
58021
+ return createHmac2("sha256", key).update(`${PREFIX}${body}`).digest();
58022
+ }
58023
+ function hashConfirmPayload(payload) {
58024
+ return createHash2("sha256").update(canonicalJson(payload)).digest("base64url");
58025
+ }
58026
+ function issueConfirmToken(key, binding, options = {}) {
58027
+ const k = tokenKey(key);
58028
+ const now = options.now ?? Date.now();
58029
+ const exp = now + ttlOf(options.ttlSeconds) * 1e3;
58030
+ const claims = {
58031
+ t: binding.tool,
58032
+ ...binding.account === void 0 ? {} : { a: binding.account },
58033
+ g: binding.target,
58034
+ ...binding.revision === void 0 ? {} : { r: binding.revision },
58035
+ h: binding.payloadHash,
58036
+ exp,
58037
+ n: randomBytes(16).toString("base64url")
58038
+ };
58039
+ const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
58040
+ return { token: `${PREFIX}${body}.${sign(k, body).toString("base64url")}`, expiresAt: new Date(exp).toISOString() };
58041
+ }
58042
+ function parseClaims(body) {
58043
+ try {
58044
+ const claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
58045
+ return claims && typeof claims === "object" ? claims : void 0;
58046
+ } catch {
58047
+ return void 0;
58048
+ }
58049
+ }
58050
+ function verifyConfirmToken(key, token, binding, options = {}) {
58051
+ const k = tokenKey(key);
58052
+ const now = options.now ?? Date.now();
58053
+ const spent = options.spent ?? PROCESS_SPENT;
58054
+ spent.prune(now);
58055
+ if (!token.startsWith(PREFIX))
58056
+ return { ok: false, error: "TOKEN_INVALID" };
58057
+ const rest = token.slice(PREFIX.length);
58058
+ const dot = rest.indexOf(".");
58059
+ const body = dot < 0 ? "" : rest.slice(0, dot);
58060
+ const mac3 = Buffer.from(dot < 0 ? "" : rest.slice(dot + 1), "base64url");
58061
+ const expected = sign(k, body);
58062
+ if (!body || mac3.length !== expected.length || !timingSafeEqual2(mac3, expected))
58063
+ return { ok: false, error: "TOKEN_INVALID" };
58064
+ const claims = parseClaims(body);
58065
+ if (!claims)
58066
+ return { ok: false, error: "TOKEN_INVALID" };
58067
+ if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
58068
+ return { ok: false, error: "TOKEN_INVALID" };
58069
+ }
58070
+ if (spent.has(claims.n))
58071
+ return { ok: false, error: "TOKEN_REUSED" };
58072
+ if (now > claims.exp)
58073
+ return { ok: false, error: "TOKEN_EXPIRED" };
58074
+ if (claims.r !== binding.revision)
58075
+ return { ok: false, error: "DRAFT_CHANGED", reason: "revision-changed" };
58076
+ if (claims.h !== binding.payloadHash)
58077
+ return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
58078
+ spent.add(claims.n, claims.exp);
58079
+ return { ok: true };
58080
+ }
58081
+ var CONFIRM_TOKEN_INSTRUCTION = "Show this preview to the user verbatim and proceed only after they explicitly approve in chat. Then call again with confirmToken.";
58082
+ var confirmTokenParam = external_exports.string().optional().describe(`ONLY for the two-step confirmation fallback (a client without MCP elicitation). The confirmToken from this same tool's phase-1 "confirmation-required" response, passed back ONLY after the user has seen that preview and explicitly approved it in chat \u2014 never on the first call, never invented, never reused. Call again with the same arguments. Ignored when the client supports elicitation.`);
58083
+ var TOKEN_ERROR_NOTE = {
58084
+ TOKEN_EXPIRED: "Nothing was sent or changed: the confirmToken expired. Call again WITHOUT confirmToken for a fresh preview, and ask the user to approve it again.",
58085
+ TOKEN_REUSED: "Nothing was sent or changed by this call: this confirmToken was already used, and one approval acts once. If doing it again is really intended, call again WITHOUT confirmToken and get a new approval.",
58086
+ TOKEN_INVALID: "Nothing was sent or changed: this confirmToken was not issued by this server for this tool, account and target (or the server has restarted since). Call again WITHOUT confirmToken for a fresh preview and approval."
58087
+ };
58088
+ var CHANGED_NOTE = {
58089
+ "revision-changed": "Nothing was sent or changed: the target was edited since the user approved it (its version rotated), so what would happen is not what they saw.",
58090
+ "payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
58091
+ };
58092
+ function isToolResult(value) {
58093
+ return Array.isArray(value.content);
58094
+ }
58095
+ function rejection2(data) {
58096
+ return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
58097
+ }
58098
+ async function tokenConfirmation(action, fb) {
58099
+ const subject = await fb.subject();
58100
+ if (isToolResult(subject))
58101
+ return subject;
58102
+ const binding = {
58103
+ tool: fb.tool,
58104
+ ...fb.account === void 0 ? {} : { account: fb.account },
58105
+ target: subject.target,
58106
+ ...subject.revision === void 0 ? {} : { revision: subject.revision },
58107
+ payloadHash: hashConfirmPayload(subject.payload)
58108
+ };
58109
+ const phaseOne = () => {
58110
+ const { token, expiresAt } = issueConfirmToken(fb.key, binding, { ttlSeconds: fb.ttlSeconds });
58111
+ return {
58112
+ action,
58113
+ preview: subject.preview,
58114
+ confirmToken: token,
58115
+ expiresAt,
58116
+ ttlSeconds: ttlOf(fb.ttlSeconds),
58117
+ instruction: fb.instruction ?? CONFIRM_TOKEN_INSTRUCTION
58118
+ };
58119
+ };
58120
+ if (!fb.confirmToken) {
58121
+ return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
58122
+ }
58123
+ const verdict = verifyConfirmToken(fb.key, fb.confirmToken, binding, { spent: fb.spent });
58124
+ if (verdict.ok)
58125
+ return void 0;
58126
+ if (verdict.error === "DRAFT_CHANGED") {
58127
+ return rejection2({
58128
+ error: "DRAFT_CHANGED",
58129
+ reason: verdict.reason,
58130
+ note: `${CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
58131
+ ...phaseOne()
58132
+ });
58133
+ }
58134
+ return rejection2({ error: verdict.error, action, note: TOKEN_ERROR_NOTE[verdict.error] });
58135
+ }
58136
+ async function requireConfirmationWithFallback(ctx, options) {
58137
+ const { tokenFallback, ...confirmation } = options;
58138
+ if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
58139
+ return tokenConfirmation(options.action, tokenFallback);
58140
+ }
58141
+ return requireConfirmation(ctx, confirmation);
58142
+ }
58143
+
58144
+ // node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
58145
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "node:crypto";
58146
+
58147
+ // node_modules/@chrischall/mcp-utils/dist/config/index.js
58148
+ import { homedir } from "node:os";
58149
+ import { isAbsolute, join, resolve } from "node:path";
58150
+ var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
58151
+ function readEnvVar(key, opts = {}) {
58152
+ const env = opts.env ?? process.env;
58153
+ const raw = env[key];
58154
+ if (typeof raw === "string") {
58155
+ const trimmed = raw.trim();
58156
+ if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
58157
+ return trimmed;
58158
+ }
58159
+ }
58160
+ return opts.default;
58161
+ }
58162
+ function expandPath(p) {
58163
+ let expanded = p;
58164
+ if (p === "~") {
58165
+ expanded = homedir();
58166
+ } else if (p.startsWith("~/")) {
58167
+ expanded = join(homedir(), p.slice(2));
58168
+ }
58169
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
58170
+ }
58171
+ async function loadDotenvSafely(opts = {}) {
58172
+ try {
58173
+ const mod = await import(
58174
+ /* @vite-ignore */
58175
+ "dotenv"
58176
+ );
58177
+ const result = mod.config({
58178
+ ...opts.path !== void 0 ? { path: opts.path } : {},
58179
+ override: opts.override ?? false,
58180
+ quiet: true
58181
+ });
58182
+ return result.error === void 0;
58183
+ } catch {
58184
+ return false;
58185
+ }
58186
+ }
58187
+
58188
+ // node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
58189
+ var MODES = /* @__PURE__ */ new Set(["ask-user", "auto", "refuse"]);
58190
+ var DEFAULT_TTL_SECONDS2 = 600;
58191
+ var warned = /* @__PURE__ */ new Set();
58192
+ function readConfirmMode(env = process.env) {
58193
+ const raw = readEnvVar("MCP_CONFIRM_MODE", { env })?.trim().toLowerCase();
58194
+ if (!raw)
58195
+ return "ask-user";
58196
+ if (MODES.has(raw))
58197
+ return raw;
58198
+ if (!warned.has(raw)) {
58199
+ warned.add(raw);
58200
+ process.stderr.write(`MCP_CONFIRM_MODE="${raw}" is not one of ask-user, auto, refuse; treating it as refuse.
58201
+ `);
58202
+ }
58203
+ return "refuse";
58204
+ }
58205
+ function confirmTtlFromEnv(env = process.env) {
58206
+ const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
58207
+ return raw && /^[1-9]\d*$/.test(raw) ? Number(raw) : DEFAULT_TTL_SECONDS2;
58208
+ }
58209
+ var processKey;
58210
+ function confirmKeyFromEnv(env = process.env) {
58211
+ const secret = readEnvVar("MCP_CONFIRM_SECRET", { env });
58212
+ if (secret)
58213
+ return createHash3("sha256").update(secret, "utf8").digest();
58214
+ processKey ??= randomBytes2(32);
58215
+ return processKey;
58216
+ }
58217
+ var CONFIRM_TOKEN_AUTO_INSTRUCTION = "Nothing has been done yet. Review this preview; if it is what was intended, call again with the same arguments plus confirmToken. (This server runs with MCP_CONFIRM_MODE=auto, so the user's approval in chat is not required.)";
58218
+ var REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
58219
+ function confirmationFromEnv(options) {
58220
+ const { tool, account, confirmToken, subject, instruction, spent, env = process.env, ...confirmation } = options;
58221
+ const mode = readConfirmMode(env);
58222
+ if (mode === "refuse") {
58223
+ return {
58224
+ ...confirmation,
58225
+ unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${REFUSE_HINT}` : REFUSE_HINT
58226
+ };
58227
+ }
58228
+ return {
58229
+ ...confirmation,
58230
+ tokenFallback: {
58231
+ key: confirmKeyFromEnv(env),
58232
+ tool,
58233
+ ...account === void 0 ? {} : { account },
58234
+ ...confirmToken === void 0 ? {} : { confirmToken },
58235
+ subject,
58236
+ ttlSeconds: confirmTtlFromEnv(env),
58237
+ instruction: mode === "auto" ? CONFIRM_TOKEN_AUTO_INSTRUCTION : instruction ?? CONFIRM_TOKEN_INSTRUCTION,
58238
+ ...spent === void 0 ? {} : { spent }
58239
+ }
58240
+ };
58241
+ }
58242
+
57768
58243
  // node_modules/@chrischall/mcp-utils/dist/server/index.js
57769
58244
  var SERVER_PROTOCOL_VERSIONS = Object.freeze([
57770
58245
  "2026-07-28",
@@ -57940,47 +58415,6 @@ function runMcp(opts) {
57940
58415
  return handle;
57941
58416
  }
57942
58417
 
57943
- // node_modules/@chrischall/mcp-utils/dist/config/index.js
57944
- import { homedir } from "node:os";
57945
- import { isAbsolute, join, resolve } from "node:path";
57946
- var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
57947
- function readEnvVar(key, opts = {}) {
57948
- const env = opts.env ?? process.env;
57949
- const raw = env[key];
57950
- if (typeof raw === "string") {
57951
- const trimmed = raw.trim();
57952
- if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
57953
- return trimmed;
57954
- }
57955
- }
57956
- return opts.default;
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
- }
57967
- async function loadDotenvSafely(opts = {}) {
57968
- try {
57969
- const mod = await import(
57970
- /* @vite-ignore */
57971
- "dotenv"
57972
- );
57973
- const result = mod.config({
57974
- ...opts.path !== void 0 ? { path: opts.path } : {},
57975
- override: opts.override ?? false,
57976
- quiet: true
57977
- });
57978
- return result.error === void 0;
57979
- } catch {
57980
- return false;
57981
- }
57982
- }
57983
-
57984
58418
  // node_modules/@chrischall/mcp-utils/dist/fs/index.js
57985
58419
  import { openAsBlob } from "node:fs";
57986
58420
  import { open as open2 } from "node:fs/promises";
@@ -58126,7 +58560,7 @@ function toolAnnotations(opts = {}) {
58126
58560
  }
58127
58561
 
58128
58562
  // src/version.ts
58129
- var VERSION = "2.1.3";
58563
+ var VERSION = "2.2.0";
58130
58564
 
58131
58565
  // src/client.ts
58132
58566
  import { dirname as dirname2, join as join2 } from "path";
@@ -59667,6 +60101,22 @@ function registerDiscoverTools(server, client2) {
59667
60101
  );
59668
60102
  }
59669
60103
 
60104
+ // src/tools/confirm.ts
60105
+ var CONFIRM_FLOW = "Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).";
60106
+ function confirmWrite(ctx, c) {
60107
+ return requireConfirmationWithFallback(
60108
+ ctx,
60109
+ confirmationFromEnv({
60110
+ action: c.action,
60111
+ message: c.message,
60112
+ details: c.preview,
60113
+ tool: c.tool,
60114
+ confirmToken: c.confirmToken,
60115
+ subject: () => ({ target: String(c.target), payload: c.payload, preview: c.preview })
60116
+ })
60117
+ );
60118
+ }
60119
+
59670
60120
  // src/tools/friends.ts
59671
60121
  var TargetUidSchema = external_exports.number().int().positive().describe("The target user id (uid) \u2014 from a user profile (response.user.uid) or a friends-list item");
59672
60122
  var ACTIONS = [
@@ -59705,7 +60155,7 @@ function registerFriendActionTools(server, client2) {
59705
60155
  action.tool,
59706
60156
  {
59707
60157
  title: action.title,
59708
- 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.`,
60158
+ description: `${action.detail} Acts on YOUR account and affects a real relationship with another person. ${CONFIRM_FLOW} Note: this endpoint path is taken from the Untappd app but is not otherwise independently verified.`,
59709
60159
  // All four reach ANOTHER PERSON — the description says so itself: "affects
59710
60160
  // a real relationship with another person". A friend request cannot be
59711
60161
  // un-sent, and a rejection cannot be un-rejected, so none of them has an
@@ -59713,18 +60163,21 @@ function registerFriendActionTools(server, client2) {
59713
60163
  annotations: toolAnnotations({ title: action.title, readOnly: false, idempotent: true, openWorld: true, destructive: true }),
59714
60164
  inputSchema: external_exports.object({
59715
60165
  target_uid: TargetUidSchema,
59716
- confirm: schemaConfirm
60166
+ confirmToken: confirmTokenParam
59717
60167
  })
59718
60168
  },
59719
- async ({ target_uid, confirm }) => {
59720
- if (confirm !== true) {
59721
- return minifiedResult({
59722
- dryRun: true,
59723
- action: action.path,
59724
- target_uid,
59725
- note: `Dry run \u2014 re-run with confirm: true to ${action.verb} user ${target_uid}.`
59726
- });
59727
- }
60169
+ async ({ target_uid, confirmToken }, ctx) => {
60170
+ const request = { method: "POST", path: `/friend/${action.path}/${target_uid}` };
60171
+ const gate = await confirmWrite(ctx, {
60172
+ tool: action.tool,
60173
+ action: `untappd.friend_${action.path}`,
60174
+ message: `Review and confirm: ${action.verb} Untappd user ${target_uid}.`,
60175
+ confirmToken,
60176
+ target: target_uid,
60177
+ payload: request,
60178
+ preview: { action: action.path, target_uid, ...request, note: `Will ${action.verb} user ${target_uid}.` }
60179
+ });
60180
+ if (gate) return gate;
59728
60181
  const data = await client2.write("POST", `/friend/${action.path}/${target_uid}`);
59729
60182
  return minifiedResult({ done: true, action: action.path, target_uid, result: data?.result });
59730
60183
  }
@@ -59739,22 +60192,25 @@ function registerWishlistTools(server, client2) {
59739
60192
  "untappd_wishlist_add",
59740
60193
  {
59741
60194
  title: "Add a beer to your wishlist",
59742
- 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.",
60195
+ description: `Add a beer to YOUR Untappd wishlist by its bid. Writes to your account. ${CONFIRM_FLOW}`,
59743
60196
  annotations: toolAnnotations({ title: "Add a beer to your wishlist", readOnly: false, idempotent: true, openWorld: true, destructive: false }),
59744
60197
  inputSchema: external_exports.object({
59745
60198
  bid: BidSchema2,
59746
- confirm: schemaConfirm
60199
+ confirmToken: confirmTokenParam
59747
60200
  })
59748
60201
  },
59749
- async ({ bid, confirm }) => {
59750
- if (confirm !== true) {
59751
- return minifiedResult({
59752
- dryRun: true,
59753
- action: "wishlist_add",
59754
- bid,
59755
- note: "Dry run \u2014 re-run with confirm: true to add this beer to your wishlist."
59756
- });
59757
- }
60202
+ async ({ bid, confirmToken }, ctx) => {
60203
+ const request = { method: "GET", path: "/user/wishlist/add", query: { bid } };
60204
+ const gate = await confirmWrite(ctx, {
60205
+ tool: "untappd_wishlist_add",
60206
+ action: "untappd.wishlist_add",
60207
+ message: "Review and confirm adding this beer to your Untappd wishlist:",
60208
+ confirmToken,
60209
+ target: bid,
60210
+ payload: request,
60211
+ preview: { action: "wishlist_add", bid, ...request, note: "Adds this beer to your Untappd wishlist." }
60212
+ });
60213
+ if (gate) return gate;
59758
60214
  const data = await client2.write("GET", "/user/wishlist/add", { query: { bid } });
59759
60215
  return minifiedResult({ added: true, bid, result: data?.result });
59760
60216
  }
@@ -59763,22 +60219,25 @@ function registerWishlistTools(server, client2) {
59763
60219
  "untappd_wishlist_remove",
59764
60220
  {
59765
60221
  title: "Remove a beer from your wishlist",
59766
- 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.",
60222
+ description: `Remove a beer from YOUR Untappd wishlist by its bid. Writes to your account. ${CONFIRM_FLOW}`,
59767
60223
  annotations: toolAnnotations({ title: "Remove a beer from your wishlist", readOnly: false, idempotent: true, openWorld: true, destructive: false }),
59768
60224
  inputSchema: external_exports.object({
59769
60225
  bid: BidSchema2,
59770
- confirm: schemaConfirm
60226
+ confirmToken: confirmTokenParam
59771
60227
  })
59772
60228
  },
59773
- async ({ bid, confirm }) => {
59774
- if (confirm !== true) {
59775
- return minifiedResult({
59776
- dryRun: true,
59777
- action: "wishlist_remove",
59778
- bid,
59779
- note: "Dry run \u2014 re-run with confirm: true to remove this beer from your wishlist."
59780
- });
59781
- }
60229
+ async ({ bid, confirmToken }, ctx) => {
60230
+ const request = { method: "GET", path: "/user/wishlist/delete", query: { bid } };
60231
+ const gate = await confirmWrite(ctx, {
60232
+ tool: "untappd_wishlist_remove",
60233
+ action: "untappd.wishlist_remove",
60234
+ message: "Review and confirm removing this beer from your Untappd wishlist:",
60235
+ confirmToken,
60236
+ target: bid,
60237
+ payload: request,
60238
+ preview: { action: "wishlist_remove", bid, ...request, note: "Removes this beer from your Untappd wishlist." }
60239
+ });
60240
+ if (gate) return gate;
59782
60241
  const data = await client2.write("GET", "/user/wishlist/delete", { query: { bid } });
59783
60242
  return minifiedResult({ removed: true, bid, result: data?.result });
59784
60243
  }
@@ -59786,7 +60245,9 @@ function registerWishlistTools(server, client2) {
59786
60245
  }
59787
60246
 
59788
60247
  // src/tools/checkin.ts
59789
- import { realpathSync as realpathSync2, statSync } from "node:fs";
60248
+ import { createReadStream, realpathSync as realpathSync2, statSync } from "node:fs";
60249
+ import { createHash as createHash4 } from "node:crypto";
60250
+ import { pipeline } from "node:stream/promises";
59790
60251
  import { delimiter, extname } from "node:path";
59791
60252
  var CheckinIdSchema = external_exports.number().int().positive().describe("Untappd check-in id");
59792
60253
  var PHOTO_CONTENT_TYPES = { jpg: "image/jpeg", png: "image/png" };
@@ -59841,6 +60302,11 @@ async function checkPhoto(photoPath) {
59841
60302
  }
59842
60303
  return { path: real, size_bytes: size, ext, content_type: sniffed };
59843
60304
  }
60305
+ async function photoSha256(path) {
60306
+ const hash2 = createHash4("sha256");
60307
+ await pipeline(createReadStream(path), hash2);
60308
+ return hash2.digest("hex");
60309
+ }
59844
60310
  var RatingSchema = external_exports.number().min(0).max(5).refine((r) => Math.round(r * 4) === r * 4, { message: "rating must be a multiple of 0.25" });
59845
60311
  function gmtOffsetHours(timeZone, at) {
59846
60312
  const name = new Intl.DateTimeFormat("en-US", { timeZone, timeZoneName: "longOffset" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value;
@@ -59894,22 +60360,30 @@ function registerCheckinTools(server, client2) {
59894
60360
  "untappd_toast",
59895
60361
  {
59896
60362
  title: "Toast an Untappd check-in",
59897
- 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.",
60363
+ 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. Writes to your Untappd account and is visible to others. ${CONFIRM_FLOW}`,
59898
60364
  annotations: toolAnnotations({ title: "Toast an Untappd check-in", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
59899
60365
  inputSchema: external_exports.object({
59900
60366
  checkin_id: CheckinIdSchema,
59901
- confirm: schemaConfirm
60367
+ confirmToken: confirmTokenParam
59902
60368
  })
59903
60369
  },
59904
- async ({ checkin_id, confirm }) => {
59905
- if (confirm !== true) {
59906
- return minifiedResult({
59907
- dryRun: true,
60370
+ async ({ checkin_id, confirmToken }, ctx) => {
60371
+ const request = { method: "POST", path: `/checkin/toast/${checkin_id}` };
60372
+ const gate = await confirmWrite(ctx, {
60373
+ tool: "untappd_toast",
60374
+ action: "untappd.toast",
60375
+ message: "Review and confirm toggling your toast on this Untappd check-in:",
60376
+ confirmToken,
60377
+ target: checkin_id,
60378
+ payload: request,
60379
+ preview: {
59908
60380
  action: "toast",
59909
60381
  checkin_id,
59910
- note: "Dry run \u2014 re-run with confirm: true to toggle your toast on this check-in."
59911
- });
59912
- }
60382
+ ...request,
60383
+ note: "Toggles your toast on this check-in (removes it if you have already toasted)."
60384
+ }
60385
+ });
60386
+ if (gate) return gate;
59913
60387
  const data = await nonIdempotentWrite(
59914
60388
  () => client2.write("POST", `/checkin/toast/${checkin_id}`),
59915
60389
  `Untappd did not answer the toast request for check-in ${checkin_id} in time, so it may or may not have been applied. Toast is a TOGGLE: retrying could remove a toast that did land. Check with untappd_checkin_info (checkin_id ${checkin_id}) before retrying.`
@@ -59921,24 +60395,32 @@ function registerCheckinTools(server, client2) {
59921
60395
  "untappd_add_comment",
59922
60396
  {
59923
60397
  title: "Comment on an Untappd check-in",
59924
- 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.",
60398
+ description: "Post a comment on a check-in from YOUR account. Writes to your Untappd account and is visible to others. " + CONFIRM_FLOW,
59925
60399
  annotations: toolAnnotations({ title: "Comment on an Untappd check-in", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
59926
60400
  inputSchema: external_exports.object({
59927
60401
  checkin_id: CheckinIdSchema,
59928
60402
  comment: external_exports.string().min(1).max(2e3).describe("Comment text to post"),
59929
- confirm: schemaConfirm
60403
+ confirmToken: confirmTokenParam
59930
60404
  })
59931
60405
  },
59932
- async ({ checkin_id, comment, confirm }) => {
59933
- if (confirm !== true) {
59934
- return minifiedResult({
59935
- dryRun: true,
60406
+ async ({ checkin_id, comment, confirmToken }, ctx) => {
60407
+ const request = { method: "POST", path: `/checkin/addcomment/${checkin_id}`, form: { comment } };
60408
+ const gate = await confirmWrite(ctx, {
60409
+ tool: "untappd_add_comment",
60410
+ action: "untappd.add_comment",
60411
+ message: "Review and confirm posting this comment from your Untappd account:",
60412
+ confirmToken,
60413
+ target: checkin_id,
60414
+ payload: request,
60415
+ preview: {
59936
60416
  action: "add_comment",
59937
60417
  checkin_id,
59938
60418
  comment,
59939
- note: "Dry run \u2014 re-run with confirm: true to post this comment to your Untappd account."
59940
- });
59941
- }
60419
+ ...request,
60420
+ note: "Posts this comment to the check-in from your Untappd account, visible to others."
60421
+ }
60422
+ });
60423
+ if (gate) return gate;
59942
60424
  const data = await nonIdempotentWrite(
59943
60425
  () => client2.write("POST", `/checkin/addcomment/${checkin_id}`, { form: { comment } }),
59944
60426
  `Untappd did not answer in time, so the comment may have been posted already. Check the comments with untappd_checkin_info (checkin_id ${checkin_id}) before retrying, or it may be posted twice.`
@@ -59950,22 +60432,25 @@ function registerCheckinTools(server, client2) {
59950
60432
  "untappd_delete_comment",
59951
60433
  {
59952
60434
  title: "Delete a comment from an Untappd check-in",
59953
- 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.",
60435
+ description: `Delete one of YOUR comments by its comment id (the id from a check-in's comments list). ${CONFIRM_FLOW}`,
59954
60436
  annotations: toolAnnotations({ title: "Delete a comment from an Untappd check-in", readOnly: false, idempotent: true, openWorld: true, destructive: true }),
59955
60437
  inputSchema: external_exports.object({
59956
60438
  comment_id: external_exports.number().int().positive().describe("Untappd comment id (from a check-in's comments.items)"),
59957
- confirm: schemaConfirm
60439
+ confirmToken: confirmTokenParam
59958
60440
  })
59959
60441
  },
59960
- async ({ comment_id, confirm }) => {
59961
- if (confirm !== true) {
59962
- return minifiedResult({
59963
- dryRun: true,
59964
- action: "delete_comment",
59965
- comment_id,
59966
- note: "Dry run \u2014 re-run with confirm: true to delete this comment from your Untappd account."
59967
- });
59968
- }
60442
+ async ({ comment_id, confirmToken }, ctx) => {
60443
+ const request = { method: "POST", path: `/checkin/deletecomment/${comment_id}` };
60444
+ const gate = await confirmWrite(ctx, {
60445
+ tool: "untappd_delete_comment",
60446
+ action: "untappd.delete_comment",
60447
+ message: "Review and confirm deleting this comment from your Untappd account:",
60448
+ confirmToken,
60449
+ target: comment_id,
60450
+ payload: request,
60451
+ preview: { action: "delete_comment", comment_id, ...request, note: "Deletes this comment from your Untappd account." }
60452
+ });
60453
+ if (gate) return gate;
59969
60454
  const data = await client2.write("POST", `/checkin/deletecomment/${comment_id}`);
59970
60455
  return minifiedResult({ deleted: true, comment_id, result: data?.result });
59971
60456
  }
@@ -59974,22 +60459,30 @@ function registerCheckinTools(server, client2) {
59974
60459
  "untappd_delete_checkin",
59975
60460
  {
59976
60461
  title: "Delete an Untappd check-in",
59977
- 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.",
60462
+ description: `Permanently delete one of YOUR check-ins by its id. This is destructive and cannot be undone. ${CONFIRM_FLOW}`,
59978
60463
  annotations: toolAnnotations({ title: "Delete an Untappd check-in", readOnly: false, idempotent: true, openWorld: true, destructive: true }),
59979
60464
  inputSchema: external_exports.object({
59980
60465
  checkin_id: CheckinIdSchema,
59981
- confirm: schemaConfirm
60466
+ confirmToken: confirmTokenParam
59982
60467
  })
59983
60468
  },
59984
- async ({ checkin_id, confirm }) => {
59985
- if (confirm !== true) {
59986
- return minifiedResult({
59987
- dryRun: true,
60469
+ async ({ checkin_id, confirmToken }, ctx) => {
60470
+ const request = { method: "POST", path: `/checkin/delete/${checkin_id}` };
60471
+ const gate = await confirmWrite(ctx, {
60472
+ tool: "untappd_delete_checkin",
60473
+ action: "untappd.delete_checkin",
60474
+ message: "Review and confirm PERMANENTLY deleting this Untappd check-in:",
60475
+ confirmToken,
60476
+ target: checkin_id,
60477
+ payload: request,
60478
+ preview: {
59988
60479
  action: "delete_checkin",
59989
60480
  checkin_id,
59990
- note: "Dry run \u2014 re-run with confirm: true to PERMANENTLY delete this check-in. This cannot be undone."
59991
- });
59992
- }
60481
+ ...request,
60482
+ note: "PERMANENTLY deletes this check-in. This cannot be undone."
60483
+ }
60484
+ });
60485
+ if (gate) return gate;
59993
60486
  const data = await client2.write("POST", `/checkin/delete/${checkin_id}`);
59994
60487
  return minifiedResult({ deleted: true, checkin_id, result: data?.result });
59995
60488
  }
@@ -59998,7 +60491,7 @@ function registerCheckinTools(server, client2) {
59998
60491
  "untappd_checkin",
59999
60492
  {
60000
60493
  title: "Check in a beer on Untappd",
60001
- 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.",
60494
+ 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). The preview shows the exact fields and photo that will be posted. ${CONFIRM_FLOW}`,
60002
60495
  annotations: toolAnnotations({ title: "Check in a beer on Untappd", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
60003
60496
  inputSchema: external_exports.object({
60004
60497
  bid: external_exports.number().int().positive().describe("Untappd beer id to check in (from untappd_search_beer)"),
@@ -60006,7 +60499,7 @@ function registerCheckinTools(server, client2) {
60006
60499
  shout: external_exports.string().max(2e3).optional().describe("Optional shout / comment text for the check-in"),
60007
60500
  foursquare_id: external_exports.string().optional().describe("Optional Foursquare venue id to tag the check-in location"),
60008
60501
  photo_path: external_exports.string().optional().describe(
60009
- "Optional path to a local JPEG/PNG photo (max 15 MB) to attach \u2014 it is published publicly. Only use a file the user explicitly chose; the dry run shows the resolved path and size for them to confirm."
60502
+ "Optional path to a local JPEG/PNG photo (max 15 MB) to attach \u2014 it is published publicly. Only use a file the user explicitly chose; the preview shows the resolved path and size for them to confirm."
60010
60503
  ),
60011
60504
  geolat: external_exports.number().optional().describe("Optional latitude of the check-in"),
60012
60505
  geolng: external_exports.number().optional().describe("Optional longitude of the check-in"),
@@ -60014,12 +60507,13 @@ function registerCheckinTools(server, client2) {
60014
60507
  timezone: external_exports.string().min(1).optional().describe(
60015
60508
  "The drinker's IANA timezone (e.g. America/New_York), which sets the check-in's local time. Defaults to UNTAPPD_TIMEZONE, else the server's own zone \u2014 which on a hosted connector is usually UTC, so pass it when you know where the user is."
60016
60509
  ),
60017
- confirm: schemaConfirm
60510
+ confirmToken: confirmTokenParam
60018
60511
  })
60019
60512
  },
60020
- async ({ bid, rating, shout, foursquare_id, photo_path, geolat, geolng, container_id, timezone: requestedTz, confirm }) => {
60513
+ async ({ bid, rating, shout, foursquare_id, photo_path, geolat, geolng, container_id, timezone: requestedTz, confirmToken }, ctx) => {
60021
60514
  const { timezone, gmt_offset } = checkinTimezone(requestedTz);
60022
60515
  const photo = photo_path !== void 0 ? await checkPhoto(photo_path) : void 0;
60516
+ const photo_sha256 = photo ? await photoSha256(photo.path) : void 0;
60023
60517
  const form = {
60024
60518
  bid,
60025
60519
  rating: rating !== void 0 ? rating.toFixed(2) : void 0,
@@ -60034,15 +60528,22 @@ function registerCheckinTools(server, client2) {
60034
60528
  photo_file_ext: photo?.ext,
60035
60529
  platform: "ios"
60036
60530
  };
60037
- if (confirm !== true) {
60038
- return minifiedResult({
60039
- dryRun: true,
60531
+ const request = { method: "POST", path: "/checkin/add", form };
60532
+ const gate = await confirmWrite(ctx, {
60533
+ tool: "untappd_checkin",
60534
+ action: "untappd.checkin",
60535
+ message: "Review and confirm posting this check-in to your public Untappd feed:",
60536
+ confirmToken,
60537
+ target: bid,
60538
+ payload: { ...request, photo, photo_sha256 },
60539
+ preview: {
60040
60540
  action: "checkin",
60041
- form,
60541
+ ...request,
60042
60542
  photo: photo ? { ...photo, note: "this exact file will be uploaded PUBLICLY after the check-in is created" } : void 0,
60043
- note: "Dry run \u2014 re-run with confirm: true to POST this check-in to your public Untappd feed."
60044
- });
60045
- }
60543
+ note: "POSTs this check-in to your public Untappd feed."
60544
+ }
60545
+ });
60546
+ if (gate) return gate;
60046
60547
  let blob;
60047
60548
  if (photo) {
60048
60549
  blob = await fileBlob(photo.path, { maxBytes: MAX_PHOTO_BYTES, label: "Photo", allowedRoots: photoRoots() });