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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +16 -2
- package/dist/bundle.js +630 -129
- package/dist/tools/checkin.js +110 -59
- package/dist/tools/confirm.js +20 -0
- package/dist/tools/friends.js +19 -15
- package/dist/tools/wishlist.js +32 -25
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
- package/skills/untappd-mcp/SKILL.md +10 -5
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
|
|
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 (
|
|
37769
|
-
reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${
|
|
37770
|
-
return writeErrorResponse(request.id,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
60166
|
+
confirmToken: confirmTokenParam
|
|
59717
60167
|
})
|
|
59718
60168
|
},
|
|
59719
|
-
async ({ target_uid,
|
|
59720
|
-
|
|
59721
|
-
|
|
59722
|
-
|
|
59723
|
-
|
|
59724
|
-
|
|
59725
|
-
|
|
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:
|
|
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
|
-
|
|
60199
|
+
confirmToken: confirmTokenParam
|
|
59747
60200
|
})
|
|
59748
60201
|
},
|
|
59749
|
-
async ({ bid,
|
|
59750
|
-
|
|
59751
|
-
|
|
59752
|
-
|
|
59753
|
-
|
|
59754
|
-
|
|
59755
|
-
|
|
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:
|
|
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
|
-
|
|
60226
|
+
confirmToken: confirmTokenParam
|
|
59771
60227
|
})
|
|
59772
60228
|
},
|
|
59773
|
-
async ({ bid,
|
|
59774
|
-
|
|
59775
|
-
|
|
59776
|
-
|
|
59777
|
-
|
|
59778
|
-
|
|
59779
|
-
|
|
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:
|
|
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
|
-
|
|
60367
|
+
confirmToken: confirmTokenParam
|
|
59902
60368
|
})
|
|
59903
60369
|
},
|
|
59904
|
-
async ({ checkin_id,
|
|
59905
|
-
|
|
59906
|
-
|
|
59907
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
60403
|
+
confirmToken: confirmTokenParam
|
|
59930
60404
|
})
|
|
59931
60405
|
},
|
|
59932
|
-
async ({ checkin_id, comment,
|
|
59933
|
-
|
|
59934
|
-
|
|
59935
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
60439
|
+
confirmToken: confirmTokenParam
|
|
59958
60440
|
})
|
|
59959
60441
|
},
|
|
59960
|
-
async ({ comment_id,
|
|
59961
|
-
|
|
59962
|
-
|
|
59963
|
-
|
|
59964
|
-
|
|
59965
|
-
|
|
59966
|
-
|
|
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:
|
|
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
|
-
|
|
60466
|
+
confirmToken: confirmTokenParam
|
|
59982
60467
|
})
|
|
59983
60468
|
},
|
|
59984
|
-
async ({ checkin_id,
|
|
59985
|
-
|
|
59986
|
-
|
|
59987
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
60510
|
+
confirmToken: confirmTokenParam
|
|
60018
60511
|
})
|
|
60019
60512
|
},
|
|
60020
|
-
async ({ bid, rating, shout, foursquare_id, photo_path, geolat, geolng, container_id, timezone: requestedTz,
|
|
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
|
-
|
|
60038
|
-
|
|
60039
|
-
|
|
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
|
-
|
|
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: "
|
|
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() });
|