myhotlunchbox-mcp 0.2.1 → 0.3.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/dist/auth.js +17 -5
- package/dist/bundle.js +402 -53
- package/dist/token-cache.js +63 -0
- package/dist/version.js +1 -1
- package/mint.yaml +24 -4
- package/package.json +4 -4
- package/server.json +16 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for My Hot Lunchbox — order school lunches, manage students, and track payments via natural language",
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.3.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "My Hot Lunchbox",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "MCP server for My Hot Lunchbox — school lunch calendar, ordering, and payments. Signs in server-side with the parent account credentials.",
|
|
18
|
-
"version": "0.
|
|
18
|
+
"version": "0.3.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myhotlunchbox-mcp",
|
|
3
3
|
"displayName": "My Hot Lunchbox",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "MCP server for My Hot Lunchbox — order school lunches, manage students, and track payments via natural language",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpToolError, truncateErrorMessage } from '@chrischall/mcp-utils';
|
|
2
2
|
import { TokenManager } from '@chrischall/mcp-utils/session';
|
|
3
3
|
import { API_PREFIX, OAUTH_SCOPE } from './config.js';
|
|
4
|
+
import { createTokenCache, reportCacheWriteFailure } from './token-cache.js';
|
|
4
5
|
/** Fallback lifetime when the server omits `expires_in`. */
|
|
5
6
|
const DEFAULT_TOKEN_LIFETIME_S = 3600;
|
|
6
7
|
/**
|
|
@@ -132,13 +133,24 @@ export class MhlbAuth {
|
|
|
132
133
|
if (this.loginInFlight)
|
|
133
134
|
return this.loginInFlight;
|
|
134
135
|
this.loginInFlight = (async () => {
|
|
135
|
-
const body = await this.passwordLogin();
|
|
136
136
|
const manager = new TokenManager({
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
// The FUNCTION form, so the cache is consulted before the password
|
|
138
|
+
// grant runs at all — the eager object form skips persistence, which
|
|
139
|
+
// would mean a cache that is written and never read.
|
|
140
|
+
initial: async () => {
|
|
141
|
+
const body = await this.passwordLogin();
|
|
142
|
+
return {
|
|
143
|
+
accessToken: this.assertAccessToken(body),
|
|
144
|
+
refreshToken: body.refresh_token,
|
|
145
|
+
expiresAt: MhlbAuth.expiryOf(body),
|
|
146
|
+
};
|
|
141
147
|
},
|
|
148
|
+
persistence: createTokenCache() ?? undefined,
|
|
149
|
+
onPersistError: reportCacheWriteFailure,
|
|
150
|
+
// `refresh` below already recovers from a revoked refresh token by
|
|
151
|
+
// falling back to a full login, so the library's own re-mint-on-revoked
|
|
152
|
+
// recovery would only add a third attempt after both have failed.
|
|
153
|
+
isRefreshRevoked: () => false,
|
|
142
154
|
refresh: async (refreshToken) => {
|
|
143
155
|
let next;
|
|
144
156
|
try {
|
package/dist/bundle.js
CHANGED
|
@@ -2984,7 +2984,7 @@ var require_compile = __commonJS({
|
|
|
2984
2984
|
const schOrFunc = root.refs[ref];
|
|
2985
2985
|
if (schOrFunc)
|
|
2986
2986
|
return schOrFunc;
|
|
2987
|
-
let _sch =
|
|
2987
|
+
let _sch = resolve3.call(this, root, ref);
|
|
2988
2988
|
if (_sch === void 0) {
|
|
2989
2989
|
const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
|
|
2990
2990
|
const { schemaId } = this.opts;
|
|
@@ -3011,7 +3011,7 @@ var require_compile = __commonJS({
|
|
|
3011
3011
|
function sameSchemaEnv(s1, s2) {
|
|
3012
3012
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3013
3013
|
}
|
|
3014
|
-
function
|
|
3014
|
+
function resolve3(root, ref) {
|
|
3015
3015
|
let sch;
|
|
3016
3016
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3017
3017
|
ref = sch;
|
|
@@ -3836,7 +3836,7 @@ var require_fast_uri = __commonJS({
|
|
|
3836
3836
|
}
|
|
3837
3837
|
return uri;
|
|
3838
3838
|
}
|
|
3839
|
-
function
|
|
3839
|
+
function resolve3(baseURI, relativeURI, options) {
|
|
3840
3840
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3841
3841
|
const {
|
|
3842
3842
|
parsed: baseParsed,
|
|
@@ -4198,7 +4198,7 @@ var require_fast_uri = __commonJS({
|
|
|
4198
4198
|
var fastUri = {
|
|
4199
4199
|
SCHEMES,
|
|
4200
4200
|
normalize,
|
|
4201
|
-
resolve:
|
|
4201
|
+
resolve: resolve3,
|
|
4202
4202
|
resolveComponent,
|
|
4203
4203
|
equal,
|
|
4204
4204
|
serialize,
|
|
@@ -29145,7 +29145,7 @@ var Protocol = class {
|
|
|
29145
29145
|
return;
|
|
29146
29146
|
}
|
|
29147
29147
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
29148
|
-
await new Promise((
|
|
29148
|
+
await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
|
|
29149
29149
|
options?.signal?.throwIfAborted();
|
|
29150
29150
|
}
|
|
29151
29151
|
} catch (error51) {
|
|
@@ -29162,7 +29162,7 @@ var Protocol = class {
|
|
|
29162
29162
|
*/
|
|
29163
29163
|
request(request, resultSchema, options) {
|
|
29164
29164
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
29165
|
-
return new Promise((
|
|
29165
|
+
return new Promise((resolve3, reject) => {
|
|
29166
29166
|
const earlyReject = (error51) => {
|
|
29167
29167
|
reject(error51);
|
|
29168
29168
|
};
|
|
@@ -29240,7 +29240,7 @@ var Protocol = class {
|
|
|
29240
29240
|
if (!parseResult.success) {
|
|
29241
29241
|
reject(parseResult.error);
|
|
29242
29242
|
} else {
|
|
29243
|
-
|
|
29243
|
+
resolve3(parseResult.data);
|
|
29244
29244
|
}
|
|
29245
29245
|
} catch (error51) {
|
|
29246
29246
|
reject(error51);
|
|
@@ -29501,12 +29501,12 @@ var Protocol = class {
|
|
|
29501
29501
|
}
|
|
29502
29502
|
} catch {
|
|
29503
29503
|
}
|
|
29504
|
-
return new Promise((
|
|
29504
|
+
return new Promise((resolve3, reject) => {
|
|
29505
29505
|
if (signal.aborted) {
|
|
29506
29506
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
29507
29507
|
return;
|
|
29508
29508
|
}
|
|
29509
|
-
const timeoutId = setTimeout(
|
|
29509
|
+
const timeoutId = setTimeout(resolve3, interval);
|
|
29510
29510
|
signal.addEventListener("abort", () => {
|
|
29511
29511
|
clearTimeout(timeoutId);
|
|
29512
29512
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -30597,7 +30597,7 @@ var McpServer = class {
|
|
|
30597
30597
|
let task = createTaskResult.task;
|
|
30598
30598
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
30599
30599
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
30600
|
-
await new Promise((
|
|
30600
|
+
await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
|
|
30601
30601
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
30602
30602
|
if (!updatedTask) {
|
|
30603
30603
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -31261,12 +31261,12 @@ var StdioServerTransport = class {
|
|
|
31261
31261
|
this.onclose?.();
|
|
31262
31262
|
}
|
|
31263
31263
|
send(message) {
|
|
31264
|
-
return new Promise((
|
|
31264
|
+
return new Promise((resolve3) => {
|
|
31265
31265
|
const json2 = serializeMessage(message);
|
|
31266
31266
|
if (this._stdout.write(json2)) {
|
|
31267
|
-
|
|
31267
|
+
resolve3();
|
|
31268
31268
|
} else {
|
|
31269
|
-
this._stdout.once("drain",
|
|
31269
|
+
this._stdout.once("drain", resolve3);
|
|
31270
31270
|
}
|
|
31271
31271
|
});
|
|
31272
31272
|
}
|
|
@@ -31440,6 +31440,20 @@ function readEnvVar(key, opts = {}) {
|
|
|
31440
31440
|
}
|
|
31441
31441
|
return opts.default;
|
|
31442
31442
|
}
|
|
31443
|
+
var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
31444
|
+
var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
31445
|
+
function parseBoolEnv(key, opts = {}) {
|
|
31446
|
+
const fallback = opts.default ?? false;
|
|
31447
|
+
const raw = readEnvVar(key, { env: opts.env });
|
|
31448
|
+
if (raw === void 0)
|
|
31449
|
+
return fallback;
|
|
31450
|
+
const token = raw.toLowerCase();
|
|
31451
|
+
if (TRUE_TOKENS.has(token))
|
|
31452
|
+
return true;
|
|
31453
|
+
if (FALSE_TOKENS.has(token))
|
|
31454
|
+
return false;
|
|
31455
|
+
return fallback;
|
|
31456
|
+
}
|
|
31443
31457
|
async function loadDotenvSafely(opts = {}) {
|
|
31444
31458
|
try {
|
|
31445
31459
|
const mod = await import(
|
|
@@ -31457,6 +31471,34 @@ async function loadDotenvSafely(opts = {}) {
|
|
|
31457
31471
|
}
|
|
31458
31472
|
}
|
|
31459
31473
|
|
|
31474
|
+
// node_modules/@chrischall/mcp-utils/dist/http/index.js
|
|
31475
|
+
var RateLimitedError = class extends Error {
|
|
31476
|
+
status = 429;
|
|
31477
|
+
constructor(service) {
|
|
31478
|
+
super(`Rate limited (429) by ${service} after retries.`);
|
|
31479
|
+
this.name = "RateLimitedError";
|
|
31480
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
31481
|
+
}
|
|
31482
|
+
};
|
|
31483
|
+
var RequestTimeoutError = class extends Error {
|
|
31484
|
+
timeoutMs;
|
|
31485
|
+
constructor(service, timeoutMs) {
|
|
31486
|
+
super(`Request to ${service} timed out after ${timeoutMs}ms.`);
|
|
31487
|
+
this.name = "RequestTimeoutError";
|
|
31488
|
+
this.timeoutMs = timeoutMs;
|
|
31489
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
31490
|
+
}
|
|
31491
|
+
};
|
|
31492
|
+
var ApiError = class extends Error {
|
|
31493
|
+
status;
|
|
31494
|
+
constructor(status, message) {
|
|
31495
|
+
super(message);
|
|
31496
|
+
this.name = "ApiError";
|
|
31497
|
+
this.status = status;
|
|
31498
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
31499
|
+
}
|
|
31500
|
+
};
|
|
31501
|
+
|
|
31460
31502
|
// node_modules/@chrischall/mcp-utils/dist/zod/index.js
|
|
31461
31503
|
var PositiveInt = external_exports.number().int().positive();
|
|
31462
31504
|
var NonNegInt = external_exports.number().int().nonnegative();
|
|
@@ -31485,57 +31527,313 @@ function toolAnnotations(opts = {}) {
|
|
|
31485
31527
|
}
|
|
31486
31528
|
|
|
31487
31529
|
// node_modules/@chrischall/mcp-utils/dist/session/index.js
|
|
31530
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
|
|
31531
|
+
import { dirname, join, resolve } from "node:path";
|
|
31532
|
+
import { homedir } from "node:os";
|
|
31533
|
+
import { randomBytes, createHmac } from "node:crypto";
|
|
31534
|
+
function bindingDigest(secret, salt) {
|
|
31535
|
+
return createHmac("sha256", salt).update(secret).digest("hex").slice(0, 32);
|
|
31536
|
+
}
|
|
31537
|
+
function isEnvelope(raw) {
|
|
31538
|
+
return raw !== null && typeof raw === "object" && raw.v === 1 && "state" in raw;
|
|
31539
|
+
}
|
|
31540
|
+
var StatePersistenceError = class extends Error {
|
|
31541
|
+
cause;
|
|
31542
|
+
constructor(cause) {
|
|
31543
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
31544
|
+
this.name = "StatePersistenceError";
|
|
31545
|
+
this.cause = cause;
|
|
31546
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
31547
|
+
}
|
|
31548
|
+
};
|
|
31549
|
+
function createFileStatePersistence(opts) {
|
|
31550
|
+
const { filePath, validate } = opts;
|
|
31551
|
+
const secret = opts.boundTo;
|
|
31552
|
+
return {
|
|
31553
|
+
load() {
|
|
31554
|
+
if (!existsSync(filePath))
|
|
31555
|
+
return null;
|
|
31556
|
+
try {
|
|
31557
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
31558
|
+
let state;
|
|
31559
|
+
if (isEnvelope(raw)) {
|
|
31560
|
+
const b = raw.boundTo;
|
|
31561
|
+
if (secret === void 0) {
|
|
31562
|
+
if (b !== void 0)
|
|
31563
|
+
return null;
|
|
31564
|
+
} else {
|
|
31565
|
+
if (b === void 0)
|
|
31566
|
+
return null;
|
|
31567
|
+
if (bindingDigest(secret, b.salt) !== b.digest)
|
|
31568
|
+
return null;
|
|
31569
|
+
}
|
|
31570
|
+
state = raw.state;
|
|
31571
|
+
} else {
|
|
31572
|
+
if (secret !== void 0)
|
|
31573
|
+
return null;
|
|
31574
|
+
state = raw;
|
|
31575
|
+
}
|
|
31576
|
+
if (validate !== void 0)
|
|
31577
|
+
return validate(state);
|
|
31578
|
+
return state;
|
|
31579
|
+
} catch {
|
|
31580
|
+
return null;
|
|
31581
|
+
}
|
|
31582
|
+
},
|
|
31583
|
+
save(state) {
|
|
31584
|
+
const dir = dirname(filePath);
|
|
31585
|
+
const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
|
|
31586
|
+
const dirExisted = existsSync(dir);
|
|
31587
|
+
try {
|
|
31588
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
31589
|
+
if (!dirExisted)
|
|
31590
|
+
chmodSync(dir, 448);
|
|
31591
|
+
let boundTo;
|
|
31592
|
+
if (secret !== void 0) {
|
|
31593
|
+
const salt = randomBytes(16).toString("hex");
|
|
31594
|
+
boundTo = { salt, digest: bindingDigest(secret, salt) };
|
|
31595
|
+
}
|
|
31596
|
+
const envelope = { v: 1, ...boundTo !== void 0 ? { boundTo } : {}, state };
|
|
31597
|
+
writeFileSync(tmp, JSON.stringify(envelope, null, 2), { mode: 384 });
|
|
31598
|
+
chmodSync(tmp, 384);
|
|
31599
|
+
renameSync(tmp, filePath);
|
|
31600
|
+
chmodSync(filePath, 384);
|
|
31601
|
+
} catch (err) {
|
|
31602
|
+
try {
|
|
31603
|
+
if (existsSync(tmp))
|
|
31604
|
+
unlinkSync(tmp);
|
|
31605
|
+
} catch {
|
|
31606
|
+
}
|
|
31607
|
+
throw err;
|
|
31608
|
+
}
|
|
31609
|
+
},
|
|
31610
|
+
clear() {
|
|
31611
|
+
try {
|
|
31612
|
+
if (existsSync(filePath))
|
|
31613
|
+
unlinkSync(filePath);
|
|
31614
|
+
} catch {
|
|
31615
|
+
}
|
|
31616
|
+
}
|
|
31617
|
+
};
|
|
31618
|
+
}
|
|
31619
|
+
function homeOf(env) {
|
|
31620
|
+
return readEnvVar("HOME", { env }) ?? homedir();
|
|
31621
|
+
}
|
|
31622
|
+
function resolveStateDir(opts = {}) {
|
|
31623
|
+
const env = opts.env;
|
|
31624
|
+
return join(readEnvVar("MCP_DATA_DIR", { env }) ?? homeOf(env), opts.subdir ?? "");
|
|
31625
|
+
}
|
|
31626
|
+
function resolveStateFile(opts) {
|
|
31627
|
+
const override = opts.envVar !== void 0 ? readEnvVar(opts.envVar, { env: opts.env }) : void 0;
|
|
31628
|
+
if (override !== void 0) {
|
|
31629
|
+
if (override === "~")
|
|
31630
|
+
return homeOf(opts.env);
|
|
31631
|
+
if (override.startsWith("~/"))
|
|
31632
|
+
return join(homeOf(opts.env), override.slice(2));
|
|
31633
|
+
return resolve(override);
|
|
31634
|
+
}
|
|
31635
|
+
const dirOpts = { env: opts.env };
|
|
31636
|
+
if (opts.subdir !== void 0)
|
|
31637
|
+
dirOpts.subdir = opts.subdir;
|
|
31638
|
+
return join(resolveStateDir(dirOpts), opts.fileName);
|
|
31639
|
+
}
|
|
31488
31640
|
var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
|
|
31641
|
+
function defaultIsRefreshRevoked(err) {
|
|
31642
|
+
if (err instanceof RateLimitedError || err instanceof RequestTimeoutError)
|
|
31643
|
+
return false;
|
|
31644
|
+
if (err instanceof ApiError && err.status >= 500)
|
|
31645
|
+
return false;
|
|
31646
|
+
return true;
|
|
31647
|
+
}
|
|
31648
|
+
function isBearerTokens(raw) {
|
|
31649
|
+
if (raw === null || typeof raw !== "object")
|
|
31650
|
+
return false;
|
|
31651
|
+
const t = raw;
|
|
31652
|
+
if (typeof t.accessToken !== "string" || t.accessToken === "")
|
|
31653
|
+
return false;
|
|
31654
|
+
if (typeof t.expiresAt !== "number" || !Number.isFinite(t.expiresAt))
|
|
31655
|
+
return false;
|
|
31656
|
+
return t.refreshToken === void 0 || typeof t.refreshToken === "string";
|
|
31657
|
+
}
|
|
31489
31658
|
var TokenManager = class {
|
|
31490
|
-
|
|
31491
|
-
|
|
31492
|
-
expiresAt;
|
|
31659
|
+
tokens;
|
|
31660
|
+
bootstrapFn;
|
|
31493
31661
|
refreshFn;
|
|
31494
31662
|
skewMs;
|
|
31663
|
+
persistence;
|
|
31664
|
+
now;
|
|
31665
|
+
isRefreshRevokedFn;
|
|
31666
|
+
onPersistErrorFn;
|
|
31495
31667
|
inFlight;
|
|
31668
|
+
bootstrapInFlight;
|
|
31669
|
+
/**
|
|
31670
|
+
* Persistence is consulted at most once per process. Without this the
|
|
31671
|
+
* revoked-token recovery below re-reads the SAME rejected record — `clear()`
|
|
31672
|
+
* is optional on {@link StatePersistence} and its failures are swallowed, so
|
|
31673
|
+
* recovery must not depend on it. After the first read the in-memory tokens
|
|
31674
|
+
* (or their deliberate absence) are the truth.
|
|
31675
|
+
*/
|
|
31676
|
+
persistenceRead = false;
|
|
31496
31677
|
constructor(opts) {
|
|
31497
|
-
|
|
31498
|
-
|
|
31499
|
-
|
|
31678
|
+
if (typeof opts.initial === "function") {
|
|
31679
|
+
this.bootstrapFn = opts.initial;
|
|
31680
|
+
} else {
|
|
31681
|
+
this.tokens = { ...opts.initial };
|
|
31682
|
+
}
|
|
31500
31683
|
this.refreshFn = opts.refresh;
|
|
31501
31684
|
this.skewMs = opts.skewMs ?? TOKEN_REFRESH_SKEW_MS;
|
|
31685
|
+
this.persistence = opts.persistence;
|
|
31686
|
+
this.now = opts.now ?? Date.now;
|
|
31687
|
+
this.isRefreshRevokedFn = opts.isRefreshRevoked ?? defaultIsRefreshRevoked;
|
|
31688
|
+
this.onPersistErrorFn = opts.onPersistError;
|
|
31502
31689
|
}
|
|
31503
31690
|
/** Whether the token is within the skew window of (or past) expiry. */
|
|
31504
31691
|
needsRefresh() {
|
|
31505
|
-
|
|
31692
|
+
if (this.tokens === void 0)
|
|
31693
|
+
return false;
|
|
31694
|
+
return this.now() >= this.tokens.expiresAt - this.skewMs;
|
|
31695
|
+
}
|
|
31696
|
+
/**
|
|
31697
|
+
* A stored token is worth using when it is still valid, OR when it carries a
|
|
31698
|
+
* refresh token — an expired-but-refreshable token still saves the login,
|
|
31699
|
+
* which is the expensive half.
|
|
31700
|
+
*/
|
|
31701
|
+
isUsable(t) {
|
|
31702
|
+
return this.now() < t.expiresAt - this.skewMs || t.refreshToken !== void 0;
|
|
31703
|
+
}
|
|
31704
|
+
/** Read persisted tokens, guarding shape and usability. Never throws. */
|
|
31705
|
+
async loadPersisted() {
|
|
31706
|
+
if (this.persistence === void 0 || this.persistenceRead)
|
|
31707
|
+
return null;
|
|
31708
|
+
this.persistenceRead = true;
|
|
31709
|
+
try {
|
|
31710
|
+
const raw = await this.persistence.load();
|
|
31711
|
+
if (!isBearerTokens(raw) || !this.isUsable(raw))
|
|
31712
|
+
return null;
|
|
31713
|
+
return raw;
|
|
31714
|
+
} catch {
|
|
31715
|
+
return null;
|
|
31716
|
+
}
|
|
31717
|
+
}
|
|
31718
|
+
/**
|
|
31719
|
+
* Write tokens. Silent by default (a lost write costs a future login, not this
|
|
31720
|
+
* request); throws a {@link StatePersistenceError} when `onPersistError` does.
|
|
31721
|
+
*/
|
|
31722
|
+
async persist(t) {
|
|
31723
|
+
if (this.persistence === void 0)
|
|
31724
|
+
return;
|
|
31725
|
+
try {
|
|
31726
|
+
await this.persistence.save(t);
|
|
31727
|
+
} catch (err) {
|
|
31728
|
+
if (this.onPersistErrorFn === void 0)
|
|
31729
|
+
return;
|
|
31730
|
+
try {
|
|
31731
|
+
this.onPersistErrorFn(err);
|
|
31732
|
+
} catch (hookErr) {
|
|
31733
|
+
throw new StatePersistenceError(hookErr);
|
|
31734
|
+
}
|
|
31735
|
+
}
|
|
31736
|
+
}
|
|
31737
|
+
/** Discard persisted tokens (a refresh they could not satisfy). Never throws. */
|
|
31738
|
+
async clearPersisted() {
|
|
31739
|
+
if (this.persistence?.clear === void 0)
|
|
31740
|
+
return;
|
|
31741
|
+
try {
|
|
31742
|
+
await this.persistence.clear();
|
|
31743
|
+
} catch {
|
|
31744
|
+
}
|
|
31745
|
+
}
|
|
31746
|
+
/** The current tokens, single-flighting the bootstrap if there are none. */
|
|
31747
|
+
ensureTokens() {
|
|
31748
|
+
if (this.tokens !== void 0)
|
|
31749
|
+
return Promise.resolve(this.tokens);
|
|
31750
|
+
if (this.bootstrapInFlight === void 0) {
|
|
31751
|
+
this.bootstrapInFlight = this.runBootstrap().finally(() => {
|
|
31752
|
+
this.bootstrapInFlight = void 0;
|
|
31753
|
+
});
|
|
31754
|
+
}
|
|
31755
|
+
return this.bootstrapInFlight;
|
|
31756
|
+
}
|
|
31757
|
+
/** One bootstrap attempt: persisted tokens if usable, else the login. */
|
|
31758
|
+
async runBootstrap() {
|
|
31759
|
+
const stored = await this.loadPersisted();
|
|
31760
|
+
if (stored !== null) {
|
|
31761
|
+
this.tokens = stored;
|
|
31762
|
+
return stored;
|
|
31763
|
+
}
|
|
31764
|
+
if (this.bootstrapFn === void 0) {
|
|
31765
|
+
throw new Error("TokenManager: no tokens and no bootstrap function to mint them.");
|
|
31766
|
+
}
|
|
31767
|
+
const fresh = await this.bootstrapFn();
|
|
31768
|
+
this.tokens = fresh;
|
|
31769
|
+
try {
|
|
31770
|
+
await this.persist(fresh);
|
|
31771
|
+
} catch (err) {
|
|
31772
|
+
throw err instanceof StatePersistenceError ? err.cause : err;
|
|
31773
|
+
}
|
|
31774
|
+
return fresh;
|
|
31506
31775
|
}
|
|
31507
31776
|
/**
|
|
31508
31777
|
* Single-flight refresh. Concurrent callers share one in-flight promise; it is
|
|
31509
31778
|
* cleared on settle (success or failure) so a subsequent refresh can proceed.
|
|
31510
31779
|
*/
|
|
31511
31780
|
refreshNow() {
|
|
31512
|
-
if (
|
|
31513
|
-
|
|
31514
|
-
if (rt === void 0) {
|
|
31515
|
-
return Promise.reject(new Error("TokenManager: cannot refresh \u2014 no refresh token is available."));
|
|
31516
|
-
}
|
|
31517
|
-
this.inFlight = (async () => {
|
|
31518
|
-
const tok = await this.refreshFn(rt);
|
|
31519
|
-
this.accessToken = tok.accessToken;
|
|
31520
|
-
if (tok.refreshToken !== void 0 && tok.refreshToken !== "") {
|
|
31521
|
-
this.refreshToken = tok.refreshToken;
|
|
31522
|
-
}
|
|
31523
|
-
this.expiresAt = tok.expiresAt;
|
|
31524
|
-
})().finally(() => {
|
|
31781
|
+
if (this.inFlight === void 0) {
|
|
31782
|
+
this.inFlight = this.runRefresh().finally(() => {
|
|
31525
31783
|
this.inFlight = void 0;
|
|
31526
31784
|
});
|
|
31527
31785
|
}
|
|
31528
31786
|
return this.inFlight;
|
|
31529
31787
|
}
|
|
31788
|
+
/** One refresh attempt against the current refresh token. */
|
|
31789
|
+
async runRefresh() {
|
|
31790
|
+
const current = this.tokens ?? await this.ensureTokens();
|
|
31791
|
+
const rt = current.refreshToken;
|
|
31792
|
+
if (rt === void 0) {
|
|
31793
|
+
throw new Error("TokenManager: cannot refresh \u2014 no refresh token is available.");
|
|
31794
|
+
}
|
|
31795
|
+
const tok = await this.refreshFn(rt);
|
|
31796
|
+
this.tokens = {
|
|
31797
|
+
accessToken: tok.accessToken,
|
|
31798
|
+
// Rotation is optional: keep the current refresh token when none comes back.
|
|
31799
|
+
refreshToken: tok.refreshToken !== void 0 && tok.refreshToken !== "" ? tok.refreshToken : rt,
|
|
31800
|
+
expiresAt: tok.expiresAt
|
|
31801
|
+
};
|
|
31802
|
+
await this.persist(this.tokens);
|
|
31803
|
+
}
|
|
31804
|
+
/**
|
|
31805
|
+
* Recover from a refresh the current credential could not satisfy — commonly
|
|
31806
|
+
* a refresh token restored from a previous process and revoked since. Without
|
|
31807
|
+
* a bootstrap to fall back on this is terminal; with one, re-minting beats
|
|
31808
|
+
* staying broken forever. Shared so the two entry points cannot diverge.
|
|
31809
|
+
*/
|
|
31810
|
+
async reBootstrap(err) {
|
|
31811
|
+
if (err instanceof StatePersistenceError)
|
|
31812
|
+
throw err.cause;
|
|
31813
|
+
if (this.bootstrapFn === void 0)
|
|
31814
|
+
throw err;
|
|
31815
|
+
if (!this.isRefreshRevokedFn(err))
|
|
31816
|
+
throw err;
|
|
31817
|
+
this.tokens = void 0;
|
|
31818
|
+
await this.clearPersisted();
|
|
31819
|
+
return this.ensureTokens();
|
|
31820
|
+
}
|
|
31530
31821
|
/** Get a valid access token, refreshing proactively inside the skew window. */
|
|
31531
31822
|
async getAccessToken() {
|
|
31532
|
-
|
|
31533
|
-
|
|
31534
|
-
|
|
31823
|
+
let tokens = this.tokens ?? await this.ensureTokens();
|
|
31824
|
+
if (this.needsRefresh()) {
|
|
31825
|
+
try {
|
|
31826
|
+
await this.refreshNow();
|
|
31827
|
+
} catch (err) {
|
|
31828
|
+
return (await this.reBootstrap(err)).accessToken;
|
|
31829
|
+
}
|
|
31830
|
+
tokens = this.tokens ?? tokens;
|
|
31831
|
+
}
|
|
31832
|
+
return tokens.accessToken;
|
|
31535
31833
|
}
|
|
31536
|
-
/** Current absolute expiry (epoch ms). */
|
|
31834
|
+
/** Current absolute expiry (epoch ms), or `0` before the first bootstrap. */
|
|
31537
31835
|
getExpiresAt() {
|
|
31538
|
-
return this.expiresAt;
|
|
31836
|
+
return this.tokens?.expiresAt ?? 0;
|
|
31539
31837
|
}
|
|
31540
31838
|
/**
|
|
31541
31839
|
* Run an authenticated request with reactive 401-replay. `call` receives a
|
|
@@ -31553,9 +31851,14 @@ var TokenManager = class {
|
|
|
31553
31851
|
const usedToken = await this.getAccessToken();
|
|
31554
31852
|
let res = await call(usedToken);
|
|
31555
31853
|
if (res.status === 401) {
|
|
31556
|
-
if (this.accessToken === usedToken)
|
|
31557
|
-
|
|
31558
|
-
|
|
31854
|
+
if (this.tokens?.accessToken === usedToken) {
|
|
31855
|
+
try {
|
|
31856
|
+
await this.refreshNow();
|
|
31857
|
+
} catch (err) {
|
|
31858
|
+
await this.reBootstrap(err);
|
|
31859
|
+
}
|
|
31860
|
+
}
|
|
31861
|
+
res = await call(this.tokens?.accessToken ?? usedToken);
|
|
31559
31862
|
}
|
|
31560
31863
|
return res;
|
|
31561
31864
|
}
|
|
@@ -31574,6 +31877,41 @@ function loadConfig(env = process.env) {
|
|
|
31574
31877
|
};
|
|
31575
31878
|
}
|
|
31576
31879
|
|
|
31880
|
+
// src/token-cache.ts
|
|
31881
|
+
function tokenCachePath(env = process.env) {
|
|
31882
|
+
return resolveStateFile({
|
|
31883
|
+
env,
|
|
31884
|
+
envVar: "MYHOTLUNCHBOX_TOKEN_FILE",
|
|
31885
|
+
subdir: ".myhotlunchbox-mcp",
|
|
31886
|
+
fileName: "token.json"
|
|
31887
|
+
});
|
|
31888
|
+
}
|
|
31889
|
+
function isTokens(raw) {
|
|
31890
|
+
if (raw === null || typeof raw !== "object") return false;
|
|
31891
|
+
const t = raw;
|
|
31892
|
+
return typeof t.accessToken === "string" && t.accessToken !== "" && typeof t.expiresAt === "number" && (t.refreshToken === void 0 || typeof t.refreshToken === "string");
|
|
31893
|
+
}
|
|
31894
|
+
function createTokenCache(env = process.env) {
|
|
31895
|
+
if (!parseBoolEnv("MYHOTLUNCHBOX_TOKEN_CACHE", { env, default: true })) return null;
|
|
31896
|
+
const username = readEnvVar("MYHOTLUNCHBOX_USERNAME", { env });
|
|
31897
|
+
const password = readEnvVar("MYHOTLUNCHBOX_PASSWORD", { env });
|
|
31898
|
+
if (username === void 0 || password === void 0) return null;
|
|
31899
|
+
return createFileStatePersistence({
|
|
31900
|
+
filePath: tokenCachePath(env),
|
|
31901
|
+
// Joined on a NUL, written as an escape rather than a literal byte: a
|
|
31902
|
+
// password may contain spaces, so a space-joined pair could collide with a
|
|
31903
|
+
// different pair by shifting the boundary between the two halves.
|
|
31904
|
+
boundTo: [username.trim().toLowerCase(), password].join("\0"),
|
|
31905
|
+
validate: (raw) => isTokens(raw) ? raw : null
|
|
31906
|
+
});
|
|
31907
|
+
}
|
|
31908
|
+
function reportCacheWriteFailure(err) {
|
|
31909
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
31910
|
+
console.error(
|
|
31911
|
+
`[myhotlunchbox-mcp] could not cache the OAuth tokens (${detail}); continuing without the cache \u2014 every restart will log in again until this is fixed.`
|
|
31912
|
+
);
|
|
31913
|
+
}
|
|
31914
|
+
|
|
31577
31915
|
// src/auth.ts
|
|
31578
31916
|
var DEFAULT_TOKEN_LIFETIME_S = 3600;
|
|
31579
31917
|
function scrubCredentials(text, secrets) {
|
|
@@ -31681,13 +32019,24 @@ var MhlbAuth = class _MhlbAuth {
|
|
|
31681
32019
|
if (this.manager) return this.manager;
|
|
31682
32020
|
if (this.loginInFlight) return this.loginInFlight;
|
|
31683
32021
|
this.loginInFlight = (async () => {
|
|
31684
|
-
const body = await this.passwordLogin();
|
|
31685
32022
|
const manager = new TokenManager({
|
|
31686
|
-
|
|
31687
|
-
|
|
31688
|
-
|
|
31689
|
-
|
|
32023
|
+
// The FUNCTION form, so the cache is consulted before the password
|
|
32024
|
+
// grant runs at all — the eager object form skips persistence, which
|
|
32025
|
+
// would mean a cache that is written and never read.
|
|
32026
|
+
initial: async () => {
|
|
32027
|
+
const body = await this.passwordLogin();
|
|
32028
|
+
return {
|
|
32029
|
+
accessToken: this.assertAccessToken(body),
|
|
32030
|
+
refreshToken: body.refresh_token,
|
|
32031
|
+
expiresAt: _MhlbAuth.expiryOf(body)
|
|
32032
|
+
};
|
|
31690
32033
|
},
|
|
32034
|
+
persistence: createTokenCache() ?? void 0,
|
|
32035
|
+
onPersistError: reportCacheWriteFailure,
|
|
32036
|
+
// `refresh` below already recovers from a revoked refresh token by
|
|
32037
|
+
// falling back to a full login, so the library's own re-mint-on-revoked
|
|
32038
|
+
// recovery would only add a third attempt after both have failed.
|
|
32039
|
+
isRefreshRevoked: () => false,
|
|
31691
32040
|
refresh: async (refreshToken) => {
|
|
31692
32041
|
let next;
|
|
31693
32042
|
try {
|
|
@@ -31891,7 +32240,7 @@ var MhlbClient = class {
|
|
|
31891
32240
|
};
|
|
31892
32241
|
|
|
31893
32242
|
// src/version.ts
|
|
31894
|
-
var VERSION = "0.
|
|
32243
|
+
var VERSION = "0.3.0";
|
|
31895
32244
|
|
|
31896
32245
|
// src/tools/_shared.ts
|
|
31897
32246
|
function preview(action, request, notes) {
|
|
@@ -32420,12 +32769,12 @@ function checkoutBody(args) {
|
|
|
32420
32769
|
}
|
|
32421
32770
|
|
|
32422
32771
|
// src/tools/reports.ts
|
|
32423
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
32424
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
32772
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
32773
|
+
import { isAbsolute, join as join2, resolve as resolve2 } from "node:path";
|
|
32425
32774
|
var MAX_INLINE_BYTES = 75e4;
|
|
32426
32775
|
function outputDir(env = process.env) {
|
|
32427
32776
|
const configured = readEnvVar("MYHOTLUNCHBOX_OUTPUT_DIR", { env });
|
|
32428
|
-
return configured ?
|
|
32777
|
+
return configured ? resolve2(configured) : process.cwd();
|
|
32429
32778
|
}
|
|
32430
32779
|
function* candidateNames(filename) {
|
|
32431
32780
|
const dot = filename.lastIndexOf(".");
|
|
@@ -32440,9 +32789,9 @@ function writeWithoutClobbering(dir, filename, bytes) {
|
|
|
32440
32789
|
for (const name of candidateNames(filename)) {
|
|
32441
32790
|
if (attempts >= MAX_NAME_ATTEMPTS) break;
|
|
32442
32791
|
attempts += 1;
|
|
32443
|
-
const candidate =
|
|
32792
|
+
const candidate = join2(dir, name);
|
|
32444
32793
|
try {
|
|
32445
|
-
|
|
32794
|
+
writeFileSync2(candidate, bytes, { flag: "wx" });
|
|
32446
32795
|
return candidate;
|
|
32447
32796
|
} catch (err) {
|
|
32448
32797
|
if (err.code !== "EEXIST") throw err;
|
|
@@ -32484,7 +32833,7 @@ function deliver(bytes, contentType, filename, inline) {
|
|
|
32484
32833
|
const dir = outputDir();
|
|
32485
32834
|
let path;
|
|
32486
32835
|
try {
|
|
32487
|
-
|
|
32836
|
+
mkdirSync2(dir, { recursive: true });
|
|
32488
32837
|
path = writeWithoutClobbering(dir, filename, bytes);
|
|
32489
32838
|
} catch (cause) {
|
|
32490
32839
|
if (cause instanceof McpToolError) throw cause;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createFileStatePersistence, resolveStateFile, } from '@chrischall/mcp-utils/session';
|
|
2
|
+
import { readEnvVar, parseBoolEnv } from '@chrischall/mcp-utils';
|
|
3
|
+
/** Where the OAuth token pair is cached between runs. */
|
|
4
|
+
export function tokenCachePath(env = process.env) {
|
|
5
|
+
return resolveStateFile({
|
|
6
|
+
env,
|
|
7
|
+
envVar: 'MYHOTLUNCHBOX_TOKEN_FILE',
|
|
8
|
+
subdir: '.myhotlunchbox-mcp',
|
|
9
|
+
fileName: 'token.json',
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/** Only a token pair is ever stored — never the username or password. */
|
|
13
|
+
function isTokens(raw) {
|
|
14
|
+
if (raw === null || typeof raw !== 'object')
|
|
15
|
+
return false;
|
|
16
|
+
const t = raw;
|
|
17
|
+
return (typeof t.accessToken === 'string' &&
|
|
18
|
+
t.accessToken !== '' &&
|
|
19
|
+
typeof t.expiresAt === 'number' &&
|
|
20
|
+
(t.refreshToken === undefined || typeof t.refreshToken === 'string'));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The token cache, or `null` when it is off or there are no credentials to
|
|
24
|
+
* bind a record to.
|
|
25
|
+
*
|
|
26
|
+
* A restart currently spends a full password grant for a token that is usually
|
|
27
|
+
* still valid — and, when it is not, a refresh would have done. Caching turns
|
|
28
|
+
* the first case into nothing and the second into one refresh, which matters on
|
|
29
|
+
* a host where a child idles out after ten minutes and every start is cold.
|
|
30
|
+
*
|
|
31
|
+
* The record is bound to the credentials that minted it, so rotating either
|
|
32
|
+
* discards it rather than leaving a token from the old password in play. Only a
|
|
33
|
+
* salted digest is written; neither value reaches the file.
|
|
34
|
+
*/
|
|
35
|
+
export function createTokenCache(env = process.env) {
|
|
36
|
+
if (!parseBoolEnv('MYHOTLUNCHBOX_TOKEN_CACHE', { env, default: true }))
|
|
37
|
+
return null;
|
|
38
|
+
const username = readEnvVar('MYHOTLUNCHBOX_USERNAME', { env });
|
|
39
|
+
const password = readEnvVar('MYHOTLUNCHBOX_PASSWORD', { env });
|
|
40
|
+
if (username === undefined || password === undefined)
|
|
41
|
+
return null;
|
|
42
|
+
return createFileStatePersistence({
|
|
43
|
+
filePath: tokenCachePath(env),
|
|
44
|
+
// Joined on a NUL, written as an escape rather than a literal byte: a
|
|
45
|
+
// password may contain spaces, so a space-joined pair could collide with a
|
|
46
|
+
// different pair by shifting the boundary between the two halves.
|
|
47
|
+
boundTo: [username.trim().toLowerCase(), password].join('\u0000'),
|
|
48
|
+
validate: (raw) => (isTokens(raw) ? raw : null),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Report a cache write that failed. Not fatal: the tokens are re-mintable from
|
|
53
|
+
* the credentials in the environment, so a lost write costs the next start a
|
|
54
|
+
* login rather than access. Worth saying, though — a read-only data dir
|
|
55
|
+
* otherwise looks exactly like a server that never caches.
|
|
56
|
+
*
|
|
57
|
+
* stderr only; stdout is the JSON-RPC channel.
|
|
58
|
+
*/
|
|
59
|
+
export function reportCacheWriteFailure(err) {
|
|
60
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
61
|
+
console.error(`[myhotlunchbox-mcp] could not cache the OAuth tokens (${detail}); continuing ` +
|
|
62
|
+
'without the cache — every restart will log in again until this is fixed.');
|
|
63
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Single source of truth for the server version; release-please rewrites it. */
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.3.0'; // x-release-please-version
|
package/mint.yaml
CHANGED
|
@@ -33,7 +33,10 @@ env:
|
|
|
33
33
|
help: >-
|
|
34
34
|
Password for that account. Used for a server-side OAuth2 password grant;
|
|
35
35
|
the refresh token it returns renews the session, so no browser or
|
|
36
|
-
extension is involved
|
|
36
|
+
extension is involved. The password itself is never written to disk — the
|
|
37
|
+
token pair it mints is cached at
|
|
38
|
+
$MCP_DATA_DIR/.myhotlunchbox-mcp/token.json (0600), bound to a salted
|
|
39
|
+
digest of these credentials so rotating either discards it.
|
|
37
40
|
|
|
38
41
|
- name: MYHOTLUNCHBOX_BASE_URL
|
|
39
42
|
secret: false
|
|
@@ -52,10 +55,27 @@ env:
|
|
|
52
55
|
the filesystem belongs to the runner, so prefer calling those tools with
|
|
53
56
|
inline: true and taking the bytes back in the result.
|
|
54
57
|
|
|
58
|
+
- name: MYHOTLUNCHBOX_TOKEN_CACHE
|
|
59
|
+
secret: false
|
|
60
|
+
required: false
|
|
61
|
+
help: >-
|
|
62
|
+
Set to false to disable the on-disk token cache and log in on every
|
|
63
|
+
process start. Defaults to enabled.
|
|
64
|
+
- name: MYHOTLUNCHBOX_TOKEN_FILE
|
|
65
|
+
secret: false
|
|
66
|
+
required: false
|
|
67
|
+
help: >-
|
|
68
|
+
Absolute path for the token cache file. Defaults to
|
|
69
|
+
$MCP_DATA_DIR/.myhotlunchbox-mcp/token.json.
|
|
55
70
|
state:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
dataDir: true
|
|
72
|
+
reason: >-
|
|
73
|
+
Caches the OAuth token pair at $MCP_DATA_DIR/.myhotlunchbox-mcp/token.json
|
|
74
|
+
(0600). This was dataDir:false while sessions lived only in memory, on the
|
|
75
|
+
reasoning that a cold start cost one login and lost nothing — true then, and
|
|
76
|
+
no longer: the cache is what turns those starts into nothing, and without a
|
|
77
|
+
persistent dir it is written to a rootfs the next idle-stop discards, so it
|
|
78
|
+
would never be read back.
|
|
59
79
|
|
|
60
80
|
identity:
|
|
61
81
|
# Credentials are per-registration secrets, not per-caller, so one shared
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myhotlunchbox-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/myhotlunchbox-mcp",
|
|
5
5
|
"description": "My Hot Lunchbox MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -38,14 +38,14 @@
|
|
|
38
38
|
"bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
|
|
39
39
|
"dev": "node dist/index.js",
|
|
40
40
|
"typecheck": "tsc --noEmit",
|
|
41
|
-
"test": "vitest run",
|
|
41
|
+
"test": "npm run typecheck && vitest run",
|
|
42
42
|
"test:watch": "vitest",
|
|
43
|
-
"test:coverage": "vitest run --coverage",
|
|
43
|
+
"test:coverage": "npm run typecheck && vitest run --coverage",
|
|
44
44
|
"verify:reads": "node scripts/verify-reads.mjs",
|
|
45
45
|
"capture:writes": "node scripts/capture-writes.mjs"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@chrischall/mcp-utils": "^0.
|
|
48
|
+
"@chrischall/mcp-utils": "^0.17.1",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
50
|
"dotenv": "^17.4.0",
|
|
51
51
|
"zod": "^4.4.2"
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/myhotlunchbox-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.3.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "myhotlunchbox-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.3.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -43,6 +43,20 @@
|
|
|
43
43
|
"isRequired": false,
|
|
44
44
|
"format": "string",
|
|
45
45
|
"isSecret": false
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "MYHOTLUNCHBOX_TOKEN_CACHE",
|
|
49
|
+
"description": "Set to false to disable the on-disk token cache and re-authenticate on every process start. Defaults to enabled.",
|
|
50
|
+
"isRequired": false,
|
|
51
|
+
"format": "string",
|
|
52
|
+
"isSecret": false
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "MYHOTLUNCHBOX_TOKEN_FILE",
|
|
56
|
+
"description": "Absolute path for the token cache file. Defaults to $MCP_DATA_DIR/.myhotlunchbox-mcp/token.json.",
|
|
57
|
+
"isRequired": false,
|
|
58
|
+
"format": "string",
|
|
59
|
+
"isSecret": false
|
|
46
60
|
}
|
|
47
61
|
]
|
|
48
62
|
}
|