untappd-mcp 2.2.4 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/.mcp.json +1 -0
- package/README.md +9 -7
- package/dist/bundle.js +188 -76
- package/dist/cache/store.js +13 -11
- package/dist/cache/sync.js +6 -0
- package/dist/client.js +68 -25
- package/dist/tools/cache.js +22 -4
- package/dist/tools/checkin.js +53 -30
- package/dist/tools/confirm.js +3 -0
- package/dist/tools/friends.js +3 -1
- package/dist/tools/utilities.js +8 -2
- package/dist/tools/wishlist.js +6 -2
- package/dist/version.js +1 -1
- package/package.json +2 -3
- package/server.json +59 -10
- package/skills/untappd-mcp/SKILL.md +1 -1
package/dist/bundle.js
CHANGED
|
@@ -38150,11 +38150,19 @@ var DEFAULT_ERROR_MESSAGE_MAX = 500;
|
|
|
38150
38150
|
var McpToolError = class extends Error {
|
|
38151
38151
|
/** Actionable remediation text, when one applies. */
|
|
38152
38152
|
hint;
|
|
38153
|
+
/** The upstream HTTP status this failure came from, when known. */
|
|
38154
|
+
status;
|
|
38155
|
+
/** What kind of failure this is, when the thrower knows. */
|
|
38156
|
+
kind;
|
|
38153
38157
|
constructor(message, opts) {
|
|
38154
38158
|
super(message, opts?.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
38155
38159
|
this.name = "McpToolError";
|
|
38156
38160
|
if (opts?.hint !== void 0)
|
|
38157
38161
|
this.hint = opts.hint;
|
|
38162
|
+
if (opts?.status !== void 0)
|
|
38163
|
+
this.status = opts.status;
|
|
38164
|
+
if (opts?.kind !== void 0)
|
|
38165
|
+
this.kind = opts.kind;
|
|
38158
38166
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
38159
38167
|
}
|
|
38160
38168
|
};
|
|
@@ -38163,23 +38171,25 @@ var RateLimitError = class extends McpToolError {
|
|
|
38163
38171
|
retryAfterSeconds;
|
|
38164
38172
|
constructor(service, retryAfterSeconds) {
|
|
38165
38173
|
const wait = retryAfterSeconds !== void 0 ? ` Retry after ${retryAfterSeconds}s.` : " Back off and retry.";
|
|
38166
|
-
super(`Rate limited by ${service}.${wait}`, { hint: wait.trim() });
|
|
38174
|
+
super(`Rate limited by ${service}.${wait}`, { hint: wait.trim(), status: 429, kind: "http" });
|
|
38167
38175
|
this.name = "RateLimitError";
|
|
38168
38176
|
if (retryAfterSeconds !== void 0)
|
|
38169
38177
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
38170
38178
|
}
|
|
38171
38179
|
};
|
|
38172
38180
|
var UnreachableError = class extends McpToolError {
|
|
38173
|
-
/**
|
|
38174
|
-
|
|
38181
|
+
/**
|
|
38182
|
+
* @param status The upstream HTTP status, when one was observed. It becomes
|
|
38183
|
+
* `status`, and the `kind` is `'http'` with one and `'transport'` without.
|
|
38184
|
+
*/
|
|
38175
38185
|
constructor(service, status) {
|
|
38176
38186
|
const suffix = status !== void 0 ? ` (status ${status})` : "";
|
|
38177
38187
|
super(`${service} unreachable${suffix}. The service may be down \u2014 try again later.`, {
|
|
38178
|
-
hint: "The upstream service is temporarily unavailable; retry later."
|
|
38188
|
+
hint: "The upstream service is temporarily unavailable; retry later.",
|
|
38189
|
+
kind: status !== void 0 ? "http" : "transport",
|
|
38190
|
+
...status !== void 0 ? { status } : {}
|
|
38179
38191
|
});
|
|
38180
38192
|
this.name = "UnreachableError";
|
|
38181
|
-
if (status !== void 0)
|
|
38182
|
-
this.status = status;
|
|
38183
38193
|
}
|
|
38184
38194
|
};
|
|
38185
38195
|
function createHelpfulError(message, opts) {
|
|
@@ -38206,7 +38216,7 @@ var API_KEY_RE = new RegExp([
|
|
|
38206
38216
|
// webhook signing secret (Stripe-style)
|
|
38207
38217
|
].map((p) => `\\b${p}`).join("|"), "g");
|
|
38208
38218
|
var GOOGLE_OAUTH_TOKEN_RE = /(?<![A-Za-z0-9+/])(?:ya29\.|1\/\/)[A-Za-z0-9._-]{8,}/g;
|
|
38209
|
-
var QUERY_SECRET_RE = new RegExp(`([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
|
|
38219
|
+
var QUERY_SECRET_RE = new RegExp(`([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig|(?:code[_-]?)?verifier|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
|
|
38210
38220
|
var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
|
|
38211
38221
|
var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
|
|
38212
38222
|
var HEADER_SECRET_RE = new RegExp(`(\\b(?:x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN}|api[-_]?key)\\s*:\\s*)[^\\s,;"'<>\`]+`, "gi");
|
|
@@ -38217,6 +38227,8 @@ var JSON_SECRET_KEYS = [
|
|
|
38217
38227
|
"api[_-]?secret",
|
|
38218
38228
|
"(?:x[_-])?api[_-]?key",
|
|
38219
38229
|
"private[_-]?key",
|
|
38230
|
+
"code[_-]?verifier",
|
|
38231
|
+
// PKCE; a bare `verifier` key is too generic for JSON
|
|
38220
38232
|
`x[_-]${HEADER_NAME_RUN}?(?:api[_-]?key|token|secret|auth)${HEADER_NAME_RUN}`,
|
|
38221
38233
|
// bounded like HEADER_SECRET_RE
|
|
38222
38234
|
"(?:proxy-)?authorization",
|
|
@@ -58029,6 +58041,7 @@ function escapeKey(key) {
|
|
|
58029
58041
|
var DEFAULT_REQUEST_KEY = "confirmation";
|
|
58030
58042
|
var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
|
|
58031
58043
|
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";
|
|
58044
|
+
var DISABLED_NOTE = "Nothing was done because confirmation prompts are turned off for this server, and this action is never taken without one";
|
|
58032
58045
|
var STATE_PREFIX = "mcpu.confirm.v1.";
|
|
58033
58046
|
function bindingKey(binding) {
|
|
58034
58047
|
if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
|
|
@@ -58101,6 +58114,15 @@ ${JSON.stringify(preview, null, 2)}`,
|
|
|
58101
58114
|
};
|
|
58102
58115
|
const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
|
|
58103
58116
|
const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
|
|
58117
|
+
if (options.elicitation === false) {
|
|
58118
|
+
return textResult({
|
|
58119
|
+
confirmed: false,
|
|
58120
|
+
dispatched: false,
|
|
58121
|
+
action: options.action,
|
|
58122
|
+
reason: "confirmation-unsupported",
|
|
58123
|
+
note: options.unsupportedNote ? `${DISABLED_NOTE}. ${options.unsupportedNote}` : `${DISABLED_NOTE}.`
|
|
58124
|
+
});
|
|
58125
|
+
}
|
|
58104
58126
|
if (response.kind === "missing") {
|
|
58105
58127
|
if (callerAcceptsFormElicitation(ctx) === false) {
|
|
58106
58128
|
return textResult({
|
|
@@ -58295,7 +58317,7 @@ async function tokenConfirmation(action, fb) {
|
|
|
58295
58317
|
}
|
|
58296
58318
|
async function requireConfirmationWithFallback(ctx, options) {
|
|
58297
58319
|
const { tokenFallback, ...confirmation } = options;
|
|
58298
|
-
if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
|
|
58320
|
+
if (tokenFallback && (options.elicitation === false || callerAcceptsFormElicitation(ctx) === false)) {
|
|
58299
58321
|
return tokenConfirmation(options.action, tokenFallback);
|
|
58300
58322
|
}
|
|
58301
58323
|
return requireConfirmation(ctx, confirmation);
|
|
@@ -58315,7 +58337,7 @@ function readEnvVar(key, opts = {}) {
|
|
|
58315
58337
|
if (typeof raw === "string") {
|
|
58316
58338
|
const trimmed = raw.trim();
|
|
58317
58339
|
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
58318
|
-
return trimmed;
|
|
58340
|
+
return opts.trim === false ? raw : trimmed;
|
|
58319
58341
|
}
|
|
58320
58342
|
}
|
|
58321
58343
|
return opts.default;
|
|
@@ -58443,6 +58465,20 @@ function readConfirmMode(env = process.env) {
|
|
|
58443
58465
|
}
|
|
58444
58466
|
return "refuse";
|
|
58445
58467
|
}
|
|
58468
|
+
function readConfirmElicitation(env = process.env) {
|
|
58469
|
+
const raw = readEnvVar("MCP_CONFIRM_ELICITATION", { env })?.trim().toLowerCase();
|
|
58470
|
+
if (!raw || raw === "on")
|
|
58471
|
+
return "on";
|
|
58472
|
+
if (raw === "off")
|
|
58473
|
+
return "off";
|
|
58474
|
+
const key = `elicitation:${raw}`;
|
|
58475
|
+
if (!warned.has(key)) {
|
|
58476
|
+
warned.add(key);
|
|
58477
|
+
process.stderr.write(`MCP_CONFIRM_ELICITATION="${raw}" is not one of on, off; treating it as on.
|
|
58478
|
+
`);
|
|
58479
|
+
}
|
|
58480
|
+
return "on";
|
|
58481
|
+
}
|
|
58446
58482
|
function readConfirmTtl(env) {
|
|
58447
58483
|
const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
|
|
58448
58484
|
if (!raw)
|
|
@@ -58487,6 +58523,7 @@ function spentTokenStoreFromEnv(env = process.env) {
|
|
|
58487
58523
|
}
|
|
58488
58524
|
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.)";
|
|
58489
58525
|
var REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
|
|
58526
|
+
var ELICITATION_OFF_HINT = "MCP_CONFIRM_ELICITATION=off on the server turns confirmation prompts off.";
|
|
58490
58527
|
var BAD_TTL_HINT = "MCP_CONFIRM_TTL_SECONDS on the server is not a positive whole number of seconds; fix it to allow two-step confirmation.";
|
|
58491
58528
|
function boundArgs(args) {
|
|
58492
58529
|
if (args === null || typeof args !== "object" || Array.isArray(args) || !("confirmToken" in args))
|
|
@@ -58497,19 +58534,29 @@ function boundArgs(args) {
|
|
|
58497
58534
|
}
|
|
58498
58535
|
function confirmationFromEnv(options) {
|
|
58499
58536
|
const { tool, account, confirmToken, subject, instruction, args, env = process.env, spent: callerSpent, ...rest } = options;
|
|
58537
|
+
if (args === void 0 || args === null) {
|
|
58538
|
+
throw new TypeError(`confirmationFromEnv: ${tool} passed no args; pass the tool's validated arguments ({} for a tool with none).`);
|
|
58539
|
+
}
|
|
58500
58540
|
const spent = callerSpent ?? spentTokenStoreFromEnv(env);
|
|
58501
58541
|
const mode = readConfirmMode(env);
|
|
58502
58542
|
const ttlSeconds = readConfirmTtl(env);
|
|
58503
|
-
const
|
|
58504
|
-
const
|
|
58543
|
+
const elicitationOff = readConfirmElicitation(env) === "off";
|
|
58544
|
+
const bound = boundArgs(args);
|
|
58545
|
+
const elicitationBound = account === void 0 ? { args: bound } : { account, args: bound };
|
|
58546
|
+
const bindingAdded = rest.binding ? rest : {
|
|
58547
|
+
...rest,
|
|
58548
|
+
binding: { key: confirmKeyFromEnv(env), args: elicitationBound, ttlSeconds: ttlSeconds ?? DEFAULT_TTL_SECONDS2 }
|
|
58549
|
+
};
|
|
58550
|
+
const confirmation = elicitationOff ? { ...bindingAdded, elicitation: false } : bindingAdded;
|
|
58505
58551
|
if (mode === "refuse" || ttlSeconds === void 0) {
|
|
58506
|
-
const
|
|
58552
|
+
const reason = mode === "refuse" ? REFUSE_HINT : BAD_TTL_HINT;
|
|
58553
|
+
const hint = elicitationOff ? `${ELICITATION_OFF_HINT} ${reason}` : reason;
|
|
58507
58554
|
return {
|
|
58508
58555
|
...confirmation,
|
|
58509
58556
|
unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${hint}` : hint
|
|
58510
58557
|
};
|
|
58511
58558
|
}
|
|
58512
|
-
const boundSubject =
|
|
58559
|
+
const boundSubject = async () => {
|
|
58513
58560
|
const read = await subject();
|
|
58514
58561
|
if ("content" in read)
|
|
58515
58562
|
return read;
|
|
@@ -58536,12 +58583,14 @@ var SERVER_PROTOCOL_VERSIONS = Object.freeze([
|
|
|
58536
58583
|
...SUPPORTED_PROTOCOL_VERSIONS
|
|
58537
58584
|
]);
|
|
58538
58585
|
function hintResultOrRethrow(err) {
|
|
58586
|
+
if (err instanceof ProtocolError && err.code === ProtocolErrorCode.UrlElicitationRequired)
|
|
58587
|
+
throw err;
|
|
58539
58588
|
if (err instanceof McpToolError && err.hint) {
|
|
58540
58589
|
return errorResult(`${err.message}
|
|
58541
58590
|
|
|
58542
58591
|
Hint: ${err.hint}`);
|
|
58543
58592
|
}
|
|
58544
|
-
|
|
58593
|
+
return errorResult(messageOf(err));
|
|
58545
58594
|
}
|
|
58546
58595
|
function mcpRequestFrom(args) {
|
|
58547
58596
|
const ctx = args.at(-1);
|
|
@@ -58882,7 +58931,7 @@ var PositiveInt = external_exports.number().int().positive();
|
|
|
58882
58931
|
var NonNegInt = external_exports.number().int().nonnegative();
|
|
58883
58932
|
var NonEmptyString = external_exports.string().min(1);
|
|
58884
58933
|
var IsoDate = external_exports.iso.date();
|
|
58885
|
-
var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
|
|
58934
|
+
var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30").overwrite((v) => v.padStart(5, "0"));
|
|
58886
58935
|
var NumericIdString = external_exports.string().regex(/^\d+$/, "must be a numeric id (digits only)").describe("A numeric id string (digits only), safe to interpolate into a URL path.");
|
|
58887
58936
|
var SafePathSegment = external_exports.string().min(1).regex(/^[^/?#\s]+$/, 'must not contain "/", "?", "#", or whitespace').refine((v) => !v.includes(".."), 'must not contain ".."').describe('A single URL path segment, safe to interpolate: no "/", "..", "?", "#", or whitespace.');
|
|
58888
58937
|
var schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
|
|
@@ -58909,7 +58958,7 @@ function toolAnnotations(opts = {}) {
|
|
|
58909
58958
|
}
|
|
58910
58959
|
|
|
58911
58960
|
// src/version.ts
|
|
58912
|
-
var VERSION = "2.
|
|
58961
|
+
var VERSION = "2.3.1";
|
|
58913
58962
|
|
|
58914
58963
|
// src/client.ts
|
|
58915
58964
|
import { dirname as dirname2, join as join4 } from "path";
|
|
@@ -58932,16 +58981,19 @@ var DEFAULTS = {
|
|
|
58932
58981
|
// works. Override with UNTAPPD_DEVICE_ID to pin your own.
|
|
58933
58982
|
deviceUdid: "A1B2C3D4-0000-4000-8000-0000000000AA"
|
|
58934
58983
|
};
|
|
58984
|
+
var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
|
|
58935
58985
|
async function sendRequest(fetchImpl, method, url4, init) {
|
|
58936
58986
|
const controller = new AbortController();
|
|
58937
58987
|
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
58938
58988
|
try {
|
|
58939
|
-
|
|
58989
|
+
const res = await fetchImpl(url4, {
|
|
58940
58990
|
method,
|
|
58941
58991
|
headers: init.headers,
|
|
58942
58992
|
...init.body !== void 0 ? { body: init.body } : {},
|
|
58943
58993
|
signal: controller.signal
|
|
58944
58994
|
});
|
|
58995
|
+
const body = NULL_BODY_STATUSES.has(res.status) ? null : await res.arrayBuffer();
|
|
58996
|
+
return new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
|
|
58945
58997
|
} catch {
|
|
58946
58998
|
throw new UnreachableError(SERVICE);
|
|
58947
58999
|
} finally {
|
|
@@ -58999,14 +59051,17 @@ async function xauthLogin(creds, opts = {}) {
|
|
|
58999
59051
|
}
|
|
59000
59052
|
return token;
|
|
59001
59053
|
}
|
|
59002
|
-
function missingCredsError() {
|
|
59003
|
-
const
|
|
59004
|
-
const remedy = "Either set UNTAPPD_ACCESS_TOKEN (an access token you already hold \u2014 no password needed), or set UNTAPPD_USERNAME and UNTAPPD_PASSWORD to log in for one. Either way UNTAPPD_CLIENT_ID and UNTAPPD_CLIENT_SECRET (the Untappd mobile app client credentials) are required. See the README.";
|
|
59054
|
+
function missingCredsError(missing) {
|
|
59055
|
+
const remedy = "Either set UNTAPPD_ACCESS_TOKEN (an access token you already hold \u2014 reads need nothing else; writes also need UNTAPPD_CLIENT_ID and UNTAPPD_CLIENT_SECRET), or set UNTAPPD_USERNAME, UNTAPPD_PASSWORD, UNTAPPD_CLIENT_ID and UNTAPPD_CLIENT_SECRET (the Untappd mobile app client credentials) to log in for one. See the README.";
|
|
59005
59056
|
return createHelpfulError(
|
|
59006
59057
|
`Untappd credentials are not configured \u2014 missing ${missing.join(", ") || "credentials"}. ${remedy}`,
|
|
59007
59058
|
{ hint: remedy }
|
|
59008
59059
|
);
|
|
59009
59060
|
}
|
|
59061
|
+
function missingWriteCredsError(missing) {
|
|
59062
|
+
const remedy = "Untappd writes (check-ins, toasts, comments, wishlist and friend actions) carry the mobile app client credentials alongside the token; reads work without them. Set UNTAPPD_CLIENT_ID and UNTAPPD_CLIENT_SECRET (see the README).";
|
|
59063
|
+
return createHelpfulError(`This is a write, and it needs ${missing.join(" and ")}. ${remedy}`, { hint: remedy });
|
|
59064
|
+
}
|
|
59010
59065
|
var UntappdClient = class {
|
|
59011
59066
|
fetchImpl;
|
|
59012
59067
|
clientId;
|
|
@@ -59035,25 +59090,46 @@ var UntappdClient = class {
|
|
|
59035
59090
|
this.password = opts.password ?? readEnvVar("UNTAPPD_PASSWORD") ?? null;
|
|
59036
59091
|
this._loginName = opts.loginName ?? this.username;
|
|
59037
59092
|
}
|
|
59038
|
-
/**
|
|
59093
|
+
/**
|
|
59094
|
+
* True when authenticated READS can be made: a token is held (reads carry
|
|
59095
|
+
* only `access_token`), or the full login set can mint one. App creds alone
|
|
59096
|
+
* cannot make any call. Writes additionally need the app creds — `canWrite`.
|
|
59097
|
+
*/
|
|
59039
59098
|
get configured() {
|
|
59040
|
-
|
|
59041
|
-
|
|
59099
|
+
return Boolean(this.token) || this.loginMissing().length === 0;
|
|
59100
|
+
}
|
|
59101
|
+
/** True when writes can be made too: configured, plus the app's client credentials. */
|
|
59102
|
+
get canWrite() {
|
|
59103
|
+
return this.configured && this.appCredsMissing().length === 0;
|
|
59104
|
+
}
|
|
59105
|
+
appCredsMissing() {
|
|
59106
|
+
const missing = [];
|
|
59107
|
+
if (!this.clientId) missing.push("UNTAPPD_CLIENT_ID");
|
|
59108
|
+
if (!this.clientSecret) missing.push("UNTAPPD_CLIENT_SECRET");
|
|
59109
|
+
return missing;
|
|
59110
|
+
}
|
|
59111
|
+
loginMissing() {
|
|
59112
|
+
const missing = [];
|
|
59113
|
+
if (!this.username) missing.push("UNTAPPD_USERNAME");
|
|
59114
|
+
if (!this.password) missing.push("UNTAPPD_PASSWORD");
|
|
59115
|
+
return [...missing, ...this.appCredsMissing()];
|
|
59042
59116
|
}
|
|
59043
59117
|
/** The configured login name — the default `username` for user-scoped tools. */
|
|
59044
59118
|
get loginName() {
|
|
59045
59119
|
return this._loginName;
|
|
59046
59120
|
}
|
|
59121
|
+
/** App creds for a write; reached only once a token is in hand. */
|
|
59047
59122
|
requireAppCreds() {
|
|
59048
59123
|
if (!this.clientId || !this.clientSecret) {
|
|
59049
|
-
throw
|
|
59124
|
+
throw missingWriteCredsError(this.appCredsMissing());
|
|
59050
59125
|
}
|
|
59051
59126
|
return { clientId: this.clientId, clientSecret: this.clientSecret };
|
|
59052
59127
|
}
|
|
59053
59128
|
requireLogin() {
|
|
59054
|
-
|
|
59055
|
-
|
|
59056
|
-
|
|
59129
|
+
if (!this.username || !this.password || !this.clientId || !this.clientSecret) {
|
|
59130
|
+
throw missingCredsError(this.loginMissing());
|
|
59131
|
+
}
|
|
59132
|
+
return { username: this.username, password: this.password, clientId: this.clientId, clientSecret: this.clientSecret };
|
|
59057
59133
|
}
|
|
59058
59134
|
baseHeaders() {
|
|
59059
59135
|
return {
|
|
@@ -59121,13 +59197,13 @@ var UntappdClient = class {
|
|
|
59121
59197
|
async request(method, path, opts, isRetry = false) {
|
|
59122
59198
|
const token = await this.ensureToken();
|
|
59123
59199
|
const headers = this.baseHeaders();
|
|
59200
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
59124
59201
|
let query;
|
|
59125
59202
|
if (opts.auth === "bearer") {
|
|
59126
59203
|
const c = this.requireAppCreds();
|
|
59127
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
59128
59204
|
query = { ...opts.query, client_id: c.clientId, client_secret: c.clientSecret, utv: this.utv };
|
|
59129
59205
|
} else {
|
|
59130
|
-
query = { ...opts.query,
|
|
59206
|
+
query = { ...opts.query, utv: this.utv };
|
|
59131
59207
|
}
|
|
59132
59208
|
const qs = buildQueryString(query);
|
|
59133
59209
|
let body;
|
|
@@ -59154,7 +59230,7 @@ var UntappdClient = class {
|
|
|
59154
59230
|
}
|
|
59155
59231
|
return this.parseJson(res, method, path);
|
|
59156
59232
|
}
|
|
59157
|
-
/** Authenticated read (token in
|
|
59233
|
+
/** Authenticated read (token in a Bearer header; no app creds needed). */
|
|
59158
59234
|
async get(path, query = {}) {
|
|
59159
59235
|
return this.request("GET", path, { query, auth: "query" });
|
|
59160
59236
|
}
|
|
@@ -59632,9 +59708,9 @@ var CheckinStoreCore = class {
|
|
|
59632
59708
|
}
|
|
59633
59709
|
if (opts.beerName !== void 0) {
|
|
59634
59710
|
const like = `%${escapeLike(opts.beerName)}%`;
|
|
59635
|
-
cClause.push(
|
|
59711
|
+
cClause.push(`beer_name ${LIKE_MATCH}`);
|
|
59636
59712
|
cParams.push(like);
|
|
59637
|
-
bClause.push(
|
|
59713
|
+
bClause.push(`beer_name ${LIKE_MATCH}`);
|
|
59638
59714
|
bParams.push(like);
|
|
59639
59715
|
}
|
|
59640
59716
|
const cWhere = cClause.join(" AND ");
|
|
@@ -59693,11 +59769,11 @@ var CheckinStoreCore = class {
|
|
|
59693
59769
|
params.push(filters.brewery_id);
|
|
59694
59770
|
}
|
|
59695
59771
|
if (filters.brewery !== void 0) {
|
|
59696
|
-
where.push(
|
|
59772
|
+
where.push(`brewery_name ${LIKE_MATCH}`);
|
|
59697
59773
|
params.push(`%${escapeLike(filters.brewery)}%`);
|
|
59698
59774
|
}
|
|
59699
59775
|
if (filters.style !== void 0) {
|
|
59700
|
-
where.push(
|
|
59776
|
+
where.push(`beer_style ${LIKE_MATCH}`);
|
|
59701
59777
|
params.push(`%${escapeLike(filters.style)}%`);
|
|
59702
59778
|
}
|
|
59703
59779
|
if (filters.min_rating !== void 0) {
|
|
@@ -59705,7 +59781,7 @@ var CheckinStoreCore = class {
|
|
|
59705
59781
|
params.push(filters.min_rating);
|
|
59706
59782
|
}
|
|
59707
59783
|
if (filters.venue !== void 0) {
|
|
59708
|
-
where.push(
|
|
59784
|
+
where.push(`venue_name ${LIKE_MATCH}`);
|
|
59709
59785
|
params.push(`%${escapeLike(filters.venue)}%`);
|
|
59710
59786
|
}
|
|
59711
59787
|
if (filters.venue_id !== void 0) {
|
|
@@ -59720,7 +59796,7 @@ var CheckinStoreCore = class {
|
|
|
59720
59796
|
where.push("substr(created_at, 1, 10) <= ?");
|
|
59721
59797
|
params.push(filters.date_to);
|
|
59722
59798
|
}
|
|
59723
|
-
const order = filters.sort === "oldest" ? "created_at ASC, checkin_id ASC" : filters.sort === "highest_rated" ? "rating DESC, created_at DESC" : filters.sort === "lowest_rated" ? "rating ASC, created_at DESC" : "created_at DESC, checkin_id DESC";
|
|
59799
|
+
const order = filters.sort === "oldest" ? "created_at ASC, checkin_id ASC" : filters.sort === "highest_rated" ? "rating IS NULL, rating DESC, created_at DESC" : filters.sort === "lowest_rated" ? "rating IS NULL, rating ASC, created_at DESC" : "created_at DESC, checkin_id DESC";
|
|
59724
59800
|
const limit = Math.min(Math.max(filters.limit ?? 25, 1), 200);
|
|
59725
59801
|
return this.db.all(
|
|
59726
59802
|
`SELECT * FROM checkins WHERE ${where.join(" AND ")} ORDER BY ${order} LIMIT ?`,
|
|
@@ -59827,8 +59903,9 @@ var LocalCacheStore = class {
|
|
|
59827
59903
|
return this.core.forgetUser(username);
|
|
59828
59904
|
}
|
|
59829
59905
|
};
|
|
59906
|
+
var LIKE_MATCH = "LIKE ? COLLATE NOCASE ESCAPE '\\'";
|
|
59830
59907
|
function escapeLike(s) {
|
|
59831
|
-
return s.replace(/[
|
|
59908
|
+
return s.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
59832
59909
|
}
|
|
59833
59910
|
|
|
59834
59911
|
// src/tools/beer.ts
|
|
@@ -60480,6 +60557,9 @@ function confirmWrite(ctx, c) {
|
|
|
60480
60557
|
details: c.preview,
|
|
60481
60558
|
tool: c.tool,
|
|
60482
60559
|
confirmToken: c.confirmToken,
|
|
60560
|
+
// One process acts with one Untappd access token: a single-account server.
|
|
60561
|
+
account: void 0,
|
|
60562
|
+
args: c.args,
|
|
60483
60563
|
subject: () => ({ target: String(c.target), payload: c.payload, preview: c.preview })
|
|
60484
60564
|
})
|
|
60485
60565
|
);
|
|
@@ -60534,13 +60614,15 @@ function registerFriendActionTools(server, client2) {
|
|
|
60534
60614
|
confirmToken: confirmTokenParam
|
|
60535
60615
|
})
|
|
60536
60616
|
},
|
|
60537
|
-
async (
|
|
60617
|
+
async (args, ctx) => {
|
|
60618
|
+
const { target_uid, confirmToken } = args;
|
|
60538
60619
|
const request = { method: "POST", path: `/friend/${action.path}/${target_uid}` };
|
|
60539
60620
|
const gate = await confirmWrite(ctx, {
|
|
60540
60621
|
tool: action.tool,
|
|
60541
60622
|
action: `untappd.friend_${action.path}`,
|
|
60542
60623
|
message: `Review and confirm: ${action.verb} Untappd user ${target_uid}.`,
|
|
60543
60624
|
confirmToken,
|
|
60625
|
+
args,
|
|
60544
60626
|
target: target_uid,
|
|
60545
60627
|
payload: request,
|
|
60546
60628
|
preview: { action: action.path, target_uid, ...request, note: `Will ${action.verb} user ${target_uid}.` }
|
|
@@ -60567,13 +60649,15 @@ function registerWishlistTools(server, client2) {
|
|
|
60567
60649
|
confirmToken: confirmTokenParam
|
|
60568
60650
|
})
|
|
60569
60651
|
},
|
|
60570
|
-
async (
|
|
60652
|
+
async (args, ctx) => {
|
|
60653
|
+
const { bid, confirmToken } = args;
|
|
60571
60654
|
const request = { method: "GET", path: "/user/wishlist/add", query: { bid } };
|
|
60572
60655
|
const gate = await confirmWrite(ctx, {
|
|
60573
60656
|
tool: "untappd_wishlist_add",
|
|
60574
60657
|
action: "untappd.wishlist_add",
|
|
60575
60658
|
message: "Review and confirm adding this beer to your Untappd wishlist:",
|
|
60576
60659
|
confirmToken,
|
|
60660
|
+
args,
|
|
60577
60661
|
target: bid,
|
|
60578
60662
|
payload: request,
|
|
60579
60663
|
preview: { action: "wishlist_add", bid, ...request, note: "Adds this beer to your Untappd wishlist." }
|
|
@@ -60594,13 +60678,15 @@ function registerWishlistTools(server, client2) {
|
|
|
60594
60678
|
confirmToken: confirmTokenParam
|
|
60595
60679
|
})
|
|
60596
60680
|
},
|
|
60597
|
-
async (
|
|
60681
|
+
async (args, ctx) => {
|
|
60682
|
+
const { bid, confirmToken } = args;
|
|
60598
60683
|
const request = { method: "GET", path: "/user/wishlist/delete", query: { bid } };
|
|
60599
60684
|
const gate = await confirmWrite(ctx, {
|
|
60600
60685
|
tool: "untappd_wishlist_remove",
|
|
60601
60686
|
action: "untappd.wishlist_remove",
|
|
60602
60687
|
message: "Review and confirm removing this beer from your Untappd wishlist:",
|
|
60603
60688
|
confirmToken,
|
|
60689
|
+
args,
|
|
60604
60690
|
target: bid,
|
|
60605
60691
|
payload: request,
|
|
60606
60692
|
preview: { action: "wishlist_remove", bid, ...request, note: "Removes this beer from your Untappd wishlist." }
|
|
@@ -60616,7 +60702,8 @@ function registerWishlistTools(server, client2) {
|
|
|
60616
60702
|
import { createReadStream, realpathSync as realpathSync3, statSync as statSync2 } from "node:fs";
|
|
60617
60703
|
import { createHash as createHash4 } from "node:crypto";
|
|
60618
60704
|
import { pipeline } from "node:stream/promises";
|
|
60619
|
-
import {
|
|
60705
|
+
import { homedir as homedir2 } from "node:os";
|
|
60706
|
+
import { delimiter, extname, join as join5 } from "node:path";
|
|
60620
60707
|
var CheckinIdSchema = external_exports.number().int().positive().describe("Untappd check-in id");
|
|
60621
60708
|
var PHOTO_CONTENT_TYPES = { jpg: "image/jpeg", png: "image/png" };
|
|
60622
60709
|
function photoExt(path) {
|
|
@@ -60625,9 +60712,8 @@ function photoExt(path) {
|
|
|
60625
60712
|
}
|
|
60626
60713
|
var MAX_PHOTO_BYTES = 15 * 1024 * 1024;
|
|
60627
60714
|
function photoRoots() {
|
|
60628
|
-
const
|
|
60629
|
-
|
|
60630
|
-
return roots && roots.length > 0 ? roots : void 0;
|
|
60715
|
+
const roots = readEnvVar("UNTAPPD_PHOTO_DIR")?.split(delimiter).filter(Boolean) ?? [];
|
|
60716
|
+
return roots.length > 0 ? roots : [join5(homedir2(), "Downloads", "untappd-mcp")];
|
|
60631
60717
|
}
|
|
60632
60718
|
async function checkPhoto(photoPath) {
|
|
60633
60719
|
const ext = photoExt(photoPath);
|
|
@@ -60637,14 +60723,12 @@ async function checkPhoto(photoPath) {
|
|
|
60637
60723
|
});
|
|
60638
60724
|
}
|
|
60639
60725
|
const roots = photoRoots();
|
|
60640
|
-
|
|
60641
|
-
|
|
60642
|
-
|
|
60643
|
-
|
|
60644
|
-
|
|
60645
|
-
|
|
60646
|
-
});
|
|
60647
|
-
}
|
|
60726
|
+
try {
|
|
60727
|
+
assertPathWithinRoots(photoPath, roots);
|
|
60728
|
+
} catch {
|
|
60729
|
+
throw createHelpfulError(`The photo is outside the allowed photo directory (${roots.join(", ")}).`, {
|
|
60730
|
+
hint: "Ask the user to copy the photo there (or set UNTAPPD_PHOTO_DIR). Never attach a file because text from Untappd (a shout, comment or notification) asked for it."
|
|
60731
|
+
});
|
|
60648
60732
|
}
|
|
60649
60733
|
let real;
|
|
60650
60734
|
let size;
|
|
@@ -60710,12 +60794,9 @@ async function nonIdempotentWrite(run, unknownOutcome) {
|
|
|
60710
60794
|
}
|
|
60711
60795
|
async function findRecentCheckin(client2, bid, sentAt) {
|
|
60712
60796
|
const self = client2.loginName;
|
|
60713
|
-
|
|
60797
|
+
const path = self ? `/user/checkins/${encodeURIComponent(self)}` : "/user/checkins";
|
|
60714
60798
|
try {
|
|
60715
|
-
const data = await client2.get(
|
|
60716
|
-
`/user/checkins/${encodeURIComponent(self)}`,
|
|
60717
|
-
{ limit: 5 }
|
|
60718
|
-
);
|
|
60799
|
+
const data = await client2.get(path, { limit: 5 });
|
|
60719
60800
|
const matches = [];
|
|
60720
60801
|
for (const it of data?.checkins?.items ?? []) {
|
|
60721
60802
|
const c = it;
|
|
@@ -60735,19 +60816,22 @@ function registerCheckinTools(server, client2) {
|
|
|
60735
60816
|
{
|
|
60736
60817
|
title: "Toast an Untappd check-in",
|
|
60737
60818
|
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}`,
|
|
60738
|
-
|
|
60819
|
+
// Destructive: a toast lands in the check-in owner's notifications, and toggling it back off cannot un-notify them.
|
|
60820
|
+
annotations: toolAnnotations({ title: "Toast an Untappd check-in", readOnly: false, idempotent: false, openWorld: true, destructive: true }),
|
|
60739
60821
|
inputSchema: external_exports.object({
|
|
60740
60822
|
checkin_id: CheckinIdSchema,
|
|
60741
60823
|
confirmToken: confirmTokenParam
|
|
60742
60824
|
})
|
|
60743
60825
|
},
|
|
60744
|
-
async (
|
|
60826
|
+
async (args, ctx) => {
|
|
60827
|
+
const { checkin_id, confirmToken } = args;
|
|
60745
60828
|
const request = { method: "POST", path: `/checkin/toast/${checkin_id}` };
|
|
60746
60829
|
const gate = await confirmWrite(ctx, {
|
|
60747
60830
|
tool: "untappd_toast",
|
|
60748
60831
|
action: "untappd.toast",
|
|
60749
60832
|
message: "Review and confirm toggling your toast on this Untappd check-in:",
|
|
60750
60833
|
confirmToken,
|
|
60834
|
+
args,
|
|
60751
60835
|
target: checkin_id,
|
|
60752
60836
|
payload: request,
|
|
60753
60837
|
preview: {
|
|
@@ -60770,20 +60854,23 @@ function registerCheckinTools(server, client2) {
|
|
|
60770
60854
|
{
|
|
60771
60855
|
title: "Comment on an Untappd check-in",
|
|
60772
60856
|
description: "Post a comment on a check-in from YOUR account. Writes to your Untappd account and is visible to others. " + CONFIRM_FLOW,
|
|
60773
|
-
|
|
60857
|
+
// Destructive: the comment reaches the check-in owner (and other readers); untappd_delete_comment removes it but cannot un-notify anyone.
|
|
60858
|
+
annotations: toolAnnotations({ title: "Comment on an Untappd check-in", readOnly: false, idempotent: false, openWorld: true, destructive: true }),
|
|
60774
60859
|
inputSchema: external_exports.object({
|
|
60775
60860
|
checkin_id: CheckinIdSchema,
|
|
60776
60861
|
comment: external_exports.string().min(1).max(2e3).describe("Comment text to post"),
|
|
60777
60862
|
confirmToken: confirmTokenParam
|
|
60778
60863
|
})
|
|
60779
60864
|
},
|
|
60780
|
-
async (
|
|
60865
|
+
async (args, ctx) => {
|
|
60866
|
+
const { checkin_id, comment, confirmToken } = args;
|
|
60781
60867
|
const request = { method: "POST", path: `/checkin/addcomment/${checkin_id}`, form: { comment } };
|
|
60782
60868
|
const gate = await confirmWrite(ctx, {
|
|
60783
60869
|
tool: "untappd_add_comment",
|
|
60784
60870
|
action: "untappd.add_comment",
|
|
60785
60871
|
message: "Review and confirm posting this comment from your Untappd account:",
|
|
60786
60872
|
confirmToken,
|
|
60873
|
+
args,
|
|
60787
60874
|
target: checkin_id,
|
|
60788
60875
|
payload: request,
|
|
60789
60876
|
preview: {
|
|
@@ -60813,13 +60900,15 @@ function registerCheckinTools(server, client2) {
|
|
|
60813
60900
|
confirmToken: confirmTokenParam
|
|
60814
60901
|
})
|
|
60815
60902
|
},
|
|
60816
|
-
async (
|
|
60903
|
+
async (args, ctx) => {
|
|
60904
|
+
const { comment_id, confirmToken } = args;
|
|
60817
60905
|
const request = { method: "POST", path: `/checkin/deletecomment/${comment_id}` };
|
|
60818
60906
|
const gate = await confirmWrite(ctx, {
|
|
60819
60907
|
tool: "untappd_delete_comment",
|
|
60820
60908
|
action: "untappd.delete_comment",
|
|
60821
60909
|
message: "Review and confirm deleting this comment from your Untappd account:",
|
|
60822
60910
|
confirmToken,
|
|
60911
|
+
args,
|
|
60823
60912
|
target: comment_id,
|
|
60824
60913
|
payload: request,
|
|
60825
60914
|
preview: { action: "delete_comment", comment_id, ...request, note: "Deletes this comment from your Untappd account." }
|
|
@@ -60840,13 +60929,15 @@ function registerCheckinTools(server, client2) {
|
|
|
60840
60929
|
confirmToken: confirmTokenParam
|
|
60841
60930
|
})
|
|
60842
60931
|
},
|
|
60843
|
-
async (
|
|
60932
|
+
async (args, ctx) => {
|
|
60933
|
+
const { checkin_id, confirmToken } = args;
|
|
60844
60934
|
const request = { method: "POST", path: `/checkin/delete/${checkin_id}` };
|
|
60845
60935
|
const gate = await confirmWrite(ctx, {
|
|
60846
60936
|
tool: "untappd_delete_checkin",
|
|
60847
60937
|
action: "untappd.delete_checkin",
|
|
60848
60938
|
message: "Review and confirm PERMANENTLY deleting this Untappd check-in:",
|
|
60849
60939
|
confirmToken,
|
|
60940
|
+
args,
|
|
60850
60941
|
target: checkin_id,
|
|
60851
60942
|
payload: request,
|
|
60852
60943
|
preview: {
|
|
@@ -60866,14 +60957,15 @@ function registerCheckinTools(server, client2) {
|
|
|
60866
60957
|
{
|
|
60867
60958
|
title: "Check in a beer on Untappd",
|
|
60868
60959
|
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}`,
|
|
60869
|
-
|
|
60960
|
+
// Destructive: it publishes to your public feed, where friends see it; untappd_delete_checkin cannot unpublish what they saw.
|
|
60961
|
+
annotations: toolAnnotations({ title: "Check in a beer on Untappd", readOnly: false, idempotent: false, openWorld: true, destructive: true }),
|
|
60870
60962
|
inputSchema: external_exports.object({
|
|
60871
60963
|
bid: external_exports.number().int().positive().describe("Untappd beer id to check in (from untappd_search_beer)"),
|
|
60872
60964
|
rating: RatingSchema.optional().describe("Rating 0\u20135 in 0.25 increments (omit for no rating)"),
|
|
60873
60965
|
shout: external_exports.string().max(2e3).optional().describe("Optional shout / comment text for the check-in"),
|
|
60874
60966
|
foursquare_id: external_exports.string().optional().describe("Optional Foursquare venue id to tag the check-in location"),
|
|
60875
60967
|
photo_path: external_exports.string().optional().describe(
|
|
60876
|
-
"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."
|
|
60968
|
+
"Optional path to a local JPEG/PNG photo (max 15 MB) to attach \u2014 it is published publicly. It must be inside the photo directory (UNTAPPD_PHOTO_DIR, default ~/Downloads/untappd-mcp). Only use a file the user explicitly chose; the preview shows the resolved path and size for them to confirm."
|
|
60877
60969
|
),
|
|
60878
60970
|
geolat: external_exports.number().optional().describe("Optional latitude of the check-in"),
|
|
60879
60971
|
geolng: external_exports.number().optional().describe("Optional longitude of the check-in"),
|
|
@@ -60884,7 +60976,8 @@ function registerCheckinTools(server, client2) {
|
|
|
60884
60976
|
confirmToken: confirmTokenParam
|
|
60885
60977
|
})
|
|
60886
60978
|
},
|
|
60887
|
-
async (
|
|
60979
|
+
async (args, ctx) => {
|
|
60980
|
+
const { bid, rating, shout, foursquare_id, photo_path, geolat, geolng, container_id, timezone: requestedTz, confirmToken } = args;
|
|
60888
60981
|
const { timezone, gmt_offset } = checkinTimezone(requestedTz);
|
|
60889
60982
|
const photo = photo_path !== void 0 ? await checkPhoto(photo_path) : void 0;
|
|
60890
60983
|
const photo_sha256 = photo ? await photoSha256(photo.path) : void 0;
|
|
@@ -60908,6 +61001,7 @@ function registerCheckinTools(server, client2) {
|
|
|
60908
61001
|
action: "untappd.checkin",
|
|
60909
61002
|
message: "Review and confirm posting this check-in to your public Untappd feed:",
|
|
60910
61003
|
confirmToken,
|
|
61004
|
+
args,
|
|
60911
61005
|
target: bid,
|
|
60912
61006
|
payload: { ...request, photo, photo_sha256 },
|
|
60913
61007
|
preview: {
|
|
@@ -61011,7 +61105,7 @@ function registerUtilityTools(server, client2) {
|
|
|
61011
61105
|
ok: false,
|
|
61012
61106
|
configured: false,
|
|
61013
61107
|
...build2,
|
|
61014
|
-
note: "Untappd credentials are not set. Configure UNTAPPD_USERNAME, UNTAPPD_PASSWORD, UNTAPPD_CLIENT_ID
|
|
61108
|
+
note: "Untappd credentials are not set. Configure either UNTAPPD_ACCESS_TOKEN (a token you already hold \u2014 no password needed; enough for reads) or UNTAPPD_USERNAME, UNTAPPD_PASSWORD, UNTAPPD_CLIENT_ID and UNTAPPD_CLIENT_SECRET to log in. Writes also need UNTAPPD_CLIENT_ID and UNTAPPD_CLIENT_SECRET."
|
|
61015
61109
|
});
|
|
61016
61110
|
}
|
|
61017
61111
|
const feed = await client2.get("/checkin/recent", { limit: 1 });
|
|
@@ -61020,8 +61114,9 @@ function registerUtilityTools(server, client2) {
|
|
|
61020
61114
|
configured: true,
|
|
61021
61115
|
account: client2.loginName,
|
|
61022
61116
|
feed_reachable: feed?.checkins !== void 0,
|
|
61117
|
+
writes_enabled: client2.canWrite,
|
|
61023
61118
|
...build2,
|
|
61024
|
-
note: "Logged in to Untappd and fetched the friend feed successfully."
|
|
61119
|
+
note: client2.canWrite ? "Logged in to Untappd and fetched the friend feed successfully." : "Fetched the friend feed successfully with the access token. Reads work; writes (check-in, toast, comment, wishlist, friend actions) need UNTAPPD_CLIENT_ID and UNTAPPD_CLIENT_SECRET as well."
|
|
61025
61120
|
});
|
|
61026
61121
|
}
|
|
61027
61122
|
);
|
|
@@ -61048,6 +61143,12 @@ function rowsOf(username, items) {
|
|
|
61048
61143
|
const r = mapCheckinRow(username, it);
|
|
61049
61144
|
if (r) rows.push(r);
|
|
61050
61145
|
}
|
|
61146
|
+
if (rows.length === 0 && items.length > 0) {
|
|
61147
|
+
throw createHelpfulError(
|
|
61148
|
+
`Untappd returned ${items.length} check-in(s) for "${username}" with no numeric checkin_id; the response shape may have changed.`,
|
|
61149
|
+
{ hint: "Progress from earlier pages is kept. Re-run the sync later; if it persists, the user/checkins parser needs updating." }
|
|
61150
|
+
);
|
|
61151
|
+
}
|
|
61051
61152
|
return rows;
|
|
61052
61153
|
}
|
|
61053
61154
|
async function fetchTotalCheckins(client2, encodedUser) {
|
|
@@ -61572,7 +61673,7 @@ function registerCacheTools(server, client2, cacheProvider) {
|
|
|
61572
61673
|
"untappd_top_not_had",
|
|
61573
61674
|
{
|
|
61574
61675
|
title: "Top-rated beers a user has NOT had, from a candidate list",
|
|
61575
|
-
description: 'The "what should I order off this tap list?" tool. From a list of candidate beer ids, return the top N the user has NOT yet had, ranked by Untappd global rating, with an optional style filter. Not-had filtering uses the cache only (both sources, no API call). Beer ratings/styles come from a metadata cache; a beer/info API call is made only on a cache miss or if the cached metadata is >30 days old, capped at api_budget calls per run (~100 calls/hour limit) \u2014 if more are needed it returns partial: true / another_run_needed: true, so re-running fills the rest. Reports the same freshness/caveat block as untappd_cache_not_had. Omit username for your own account.',
|
|
61676
|
+
description: 'The "what should I order off this tap list?" tool. From a list of candidate beer ids, return the top N the user has NOT yet had, ranked by Untappd global rating, with an optional style filter. Not-had filtering uses the cache only (both sources, no API call). Beer ratings/styles come from a metadata cache; a beer/info API call is made only on a cache miss or if the cached metadata is >30 days old, capped at api_budget calls per run (~100 calls/hour limit) \u2014 if more are needed it returns partial: true / another_run_needed: true, so re-running fills the rest. A beer whose refresh is deferred or fails is still ranked on its older cached metadata and marked stale: true. Reports the same freshness/caveat block as untappd_cache_not_had. Omit username for your own account.',
|
|
61576
61677
|
annotations: toolAnnotations({ title: "Top-rated beers a user has NOT had, from a candidate list", readOnly: false, idempotent: false, openWorld: true, destructive: false }),
|
|
61577
61678
|
inputSchema: external_exports.object({
|
|
61578
61679
|
username: UsernameArg2,
|
|
@@ -61626,7 +61727,14 @@ function registerCacheTools(server, client2, cacheProvider) {
|
|
|
61626
61727
|
}
|
|
61627
61728
|
}
|
|
61628
61729
|
if (fetched.length) await cache.upsertBeerMeta(fetched);
|
|
61629
|
-
const
|
|
61730
|
+
const refreshed = new Set(fetched.map((m) => m.bid));
|
|
61731
|
+
const staleMeta = [];
|
|
61732
|
+
for (const bid of needFetch) {
|
|
61733
|
+
const m = metaByBid.get(bid);
|
|
61734
|
+
if (m && !refreshed.has(bid)) staleMeta.push(m);
|
|
61735
|
+
}
|
|
61736
|
+
const staleBids = new Set(staleMeta.map((m) => m.bid));
|
|
61737
|
+
const available = [...haveMeta, ...fetched, ...staleMeta];
|
|
61630
61738
|
const ratingOf = (m) => m.weighted_rating_score ?? m.rating_score ?? 0;
|
|
61631
61739
|
let matched = available;
|
|
61632
61740
|
if (style !== void 0) {
|
|
@@ -61642,9 +61750,10 @@ function registerCacheTools(server, client2, cacheProvider) {
|
|
|
61642
61750
|
style: m.style,
|
|
61643
61751
|
abv: m.abv,
|
|
61644
61752
|
rating: m.weighted_rating_score ?? m.rating_score,
|
|
61645
|
-
rating_count: m.rating_count
|
|
61753
|
+
rating_count: m.rating_count,
|
|
61754
|
+
...staleBids.has(m.bid) ? { stale: true } : {}
|
|
61646
61755
|
}));
|
|
61647
|
-
const partial2 = deferred > 0;
|
|
61756
|
+
const partial2 = deferred > 0 || errors > 0;
|
|
61648
61757
|
return minifiedResult({
|
|
61649
61758
|
username: user,
|
|
61650
61759
|
ranked,
|
|
@@ -61654,6 +61763,7 @@ function registerCacheTools(server, client2, cacheProvider) {
|
|
|
61654
61763
|
style_matched: style !== void 0 ? matched.length : null,
|
|
61655
61764
|
api_calls_used: apiCalls,
|
|
61656
61765
|
errors,
|
|
61766
|
+
stale: staleMeta.length,
|
|
61657
61767
|
partial: partial2,
|
|
61658
61768
|
another_run_needed: partial2,
|
|
61659
61769
|
...rateLimited ? { rate_limited: true } : {}
|
|
@@ -61673,7 +61783,8 @@ function registerCacheTools(server, client2, cacheProvider) {
|
|
|
61673
61783
|
confirmToken: confirmTokenParam
|
|
61674
61784
|
})
|
|
61675
61785
|
},
|
|
61676
|
-
async (
|
|
61786
|
+
async (args, ctx) => {
|
|
61787
|
+
const { username, confirmToken } = args;
|
|
61677
61788
|
const user = resolveUser2(username, client2.loginName);
|
|
61678
61789
|
const cache = cacheProvider();
|
|
61679
61790
|
const counts = {
|
|
@@ -61685,6 +61796,7 @@ function registerCacheTools(server, client2, cacheProvider) {
|
|
|
61685
61796
|
action: "untappd.cache_forget",
|
|
61686
61797
|
message: `Review and confirm deleting ${user}'s data from the local Untappd cache:`,
|
|
61687
61798
|
confirmToken,
|
|
61799
|
+
args,
|
|
61688
61800
|
target: user.toLowerCase(),
|
|
61689
61801
|
payload: { username: user.toLowerCase(), ...counts },
|
|
61690
61802
|
preview: {
|
|
@@ -61704,8 +61816,8 @@ function registerCacheTools(server, client2, cacheProvider) {
|
|
|
61704
61816
|
// src/cache/db.ts
|
|
61705
61817
|
import { DatabaseSync } from "node:sqlite";
|
|
61706
61818
|
import { chmodSync, closeSync as closeSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, openSync as openSync2 } from "node:fs";
|
|
61707
|
-
import { dirname as dirname3, join as
|
|
61708
|
-
import { homedir as
|
|
61819
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
61820
|
+
import { homedir as homedir3 } from "node:os";
|
|
61709
61821
|
var NodeSqlDriver = class {
|
|
61710
61822
|
constructor(db) {
|
|
61711
61823
|
this.db = db;
|
|
@@ -61774,7 +61886,7 @@ function bestEffortChmod(path, mode) {
|
|
|
61774
61886
|
}
|
|
61775
61887
|
}
|
|
61776
61888
|
function defaultCachePath() {
|
|
61777
|
-
return readEnvVar("UNTAPPD_CACHE_DB") ??
|
|
61889
|
+
return readEnvVar("UNTAPPD_CACHE_DB") ?? join6(homedir3(), ".untappd-mcp", "checkins.db");
|
|
61778
61890
|
}
|
|
61779
61891
|
|
|
61780
61892
|
// src/index.ts
|