myhotlunchbox-mcp 0.3.0 → 0.5.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 +3 -1
- package/dist/bundle.js +226 -42
- package/dist/index.js +2 -0
- package/dist/tools/_shared.js +8 -3
- package/dist/tools/account.js +3 -3
- package/dist/tools/billing.js +12 -12
- package/dist/tools/calendar.js +3 -3
- package/dist/tools/checkout.js +3 -3
- package/dist/tools/health.js +65 -0
- package/dist/tools/orders.js +9 -9
- package/dist/tools/reports.js +3 -3
- package/dist/tools/students.js +7 -7
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/server.json +2 -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.5.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.5.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.5.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/README.md
CHANGED
|
@@ -38,10 +38,12 @@ still works; the configuration error surfaces on the first tool call.
|
|
|
38
38
|
|
|
39
39
|
## Tools
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
35 tools, all prefixed `mhlb_`. All 20 read tools are verified live against a real parent account (`node scripts/verify-reads.mjs`); the 14 write tools are not — see below.
|
|
42
42
|
|
|
43
43
|
**Account** — `mhlb_whoami`, `mhlb_session_reset`
|
|
44
44
|
|
|
45
|
+
**Health** — `mhlb_healthcheck` (is this connector working? reports whether the credential resolved, whether My Hot Lunchbox accepted it, and what to fix — unlike `mhlb_whoami`, which throws instead of answering)
|
|
46
|
+
|
|
45
47
|
**Students** — `mhlb_list_students`, `mhlb_get_student_form`,
|
|
46
48
|
`mhlb_new_student_form`, `mhlb_create_student`, `mhlb_update_student`,
|
|
47
49
|
`mhlb_delete_student`
|
package/dist/bundle.js
CHANGED
|
@@ -31343,14 +31343,22 @@ function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
|
|
|
31343
31343
|
return redacted;
|
|
31344
31344
|
return `${redacted.slice(0, max)}\u2026 [truncated]`;
|
|
31345
31345
|
}
|
|
31346
|
+
function messageOf(err) {
|
|
31347
|
+
if (err instanceof Error)
|
|
31348
|
+
return err.message;
|
|
31349
|
+
return String(err);
|
|
31350
|
+
}
|
|
31346
31351
|
|
|
31347
|
-
// node_modules/@chrischall/mcp-utils/dist/response/
|
|
31348
|
-
function
|
|
31349
|
-
return {
|
|
31350
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
31351
|
-
};
|
|
31352
|
+
// node_modules/@chrischall/mcp-utils/dist/response/view.js
|
|
31353
|
+
function minifiedResult(data) {
|
|
31354
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
31352
31355
|
}
|
|
31353
|
-
|
|
31356
|
+
|
|
31357
|
+
// node_modules/@chrischall/mcp-utils/dist/response/media.js
|
|
31358
|
+
var MEDIA_NOUN = "(?:avatar|tall_avatar|cover_photo|cover_image|picture|photo|thumbnail|thumb|image|icon|banner|profile_pic(?:ture)?|logo)";
|
|
31359
|
+
var MEDIA_KEY = new RegExp(`^${MEDIA_NOUN}s?(?:(?:link|uri|url)s?)?$`, "i");
|
|
31360
|
+
|
|
31361
|
+
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
31354
31362
|
function errorResult(message) {
|
|
31355
31363
|
return {
|
|
31356
31364
|
content: [{ type: "text", text: redactSecrets(message) }],
|
|
@@ -32240,11 +32248,11 @@ var MhlbClient = class {
|
|
|
32240
32248
|
};
|
|
32241
32249
|
|
|
32242
32250
|
// src/version.ts
|
|
32243
|
-
var VERSION = "0.
|
|
32251
|
+
var VERSION = "0.5.0";
|
|
32244
32252
|
|
|
32245
32253
|
// src/tools/_shared.ts
|
|
32246
32254
|
function preview(action, request, notes) {
|
|
32247
|
-
return
|
|
32255
|
+
return minifiedResult({
|
|
32248
32256
|
dryRun: true,
|
|
32249
32257
|
action,
|
|
32250
32258
|
wouldSend: request,
|
|
@@ -32265,7 +32273,7 @@ function registerAccountTools(server, client2) {
|
|
|
32265
32273
|
annotations: toolAnnotations({ title: "Who am I", openWorld: true }),
|
|
32266
32274
|
inputSchema: {}
|
|
32267
32275
|
},
|
|
32268
|
-
async () =>
|
|
32276
|
+
async () => minifiedResult(await client2.get("/auth/userinfo"))
|
|
32269
32277
|
);
|
|
32270
32278
|
server.registerTool(
|
|
32271
32279
|
"mhlb_session_reset",
|
|
@@ -32277,7 +32285,7 @@ function registerAccountTools(server, client2) {
|
|
|
32277
32285
|
async () => {
|
|
32278
32286
|
const wasAuthenticated = client2.isAuthenticated;
|
|
32279
32287
|
client2.resetSession();
|
|
32280
|
-
return
|
|
32288
|
+
return minifiedResult({ reset: true, hadActiveSession: wasAuthenticated });
|
|
32281
32289
|
}
|
|
32282
32290
|
);
|
|
32283
32291
|
}
|
|
@@ -32292,7 +32300,7 @@ function registerStudentTools(server, client2) {
|
|
|
32292
32300
|
annotations: toolAnnotations({ title: "List students", openWorld: true }),
|
|
32293
32301
|
inputSchema: {}
|
|
32294
32302
|
},
|
|
32295
|
-
async () =>
|
|
32303
|
+
async () => minifiedResult(await client2.get("/parent/childrenInfo"))
|
|
32296
32304
|
);
|
|
32297
32305
|
server.registerTool(
|
|
32298
32306
|
"mhlb_get_student_form",
|
|
@@ -32301,7 +32309,7 @@ function registerStudentTools(server, client2) {
|
|
|
32301
32309
|
annotations: toolAnnotations({ title: "Get student form", openWorld: true }),
|
|
32302
32310
|
inputSchema: { studentId: PositiveInt.describe("Student id from mhlb_list_students.") }
|
|
32303
32311
|
},
|
|
32304
|
-
async ({ studentId }) =>
|
|
32312
|
+
async ({ studentId }) => minifiedResult(await client2.get("/parent/editChild", { childId: studentId }))
|
|
32305
32313
|
);
|
|
32306
32314
|
server.registerTool(
|
|
32307
32315
|
"mhlb_new_student_form",
|
|
@@ -32310,7 +32318,7 @@ function registerStudentTools(server, client2) {
|
|
|
32310
32318
|
annotations: toolAnnotations({ title: "New student form", openWorld: true }),
|
|
32311
32319
|
inputSchema: {}
|
|
32312
32320
|
},
|
|
32313
|
-
async () =>
|
|
32321
|
+
async () => minifiedResult(await client2.get("/parent/createChild"))
|
|
32314
32322
|
);
|
|
32315
32323
|
server.registerTool(
|
|
32316
32324
|
"mhlb_create_student",
|
|
@@ -32321,7 +32329,7 @@ function registerStudentTools(server, client2) {
|
|
|
32321
32329
|
},
|
|
32322
32330
|
async ({ student, confirm }) => {
|
|
32323
32331
|
if (!confirm) return preview("Create student", { method: "POST", path: "/parent/createChild", body: student });
|
|
32324
|
-
return
|
|
32332
|
+
return minifiedResult(await client2.write("/parent/createChild", student));
|
|
32325
32333
|
}
|
|
32326
32334
|
);
|
|
32327
32335
|
server.registerTool(
|
|
@@ -32337,7 +32345,7 @@ function registerStudentTools(server, client2) {
|
|
|
32337
32345
|
"This is a whole-record replace: any field missing from `student` is cleared, not preserved."
|
|
32338
32346
|
]);
|
|
32339
32347
|
}
|
|
32340
|
-
return
|
|
32348
|
+
return minifiedResult(await client2.write("/parent/editChild", student));
|
|
32341
32349
|
}
|
|
32342
32350
|
);
|
|
32343
32351
|
server.registerTool(
|
|
@@ -32358,7 +32366,7 @@ function registerStudentTools(server, client2) {
|
|
|
32358
32366
|
["Deleting a student is not reversible through this API."]
|
|
32359
32367
|
);
|
|
32360
32368
|
}
|
|
32361
|
-
return
|
|
32369
|
+
return minifiedResult(await client2.write("/parent/deleteChild", void 0, { id: studentId }));
|
|
32362
32370
|
}
|
|
32363
32371
|
);
|
|
32364
32372
|
}
|
|
@@ -32378,7 +32386,7 @@ function registerCalendarTools(server, client2) {
|
|
|
32378
32386
|
// Field names are `start`/`end`, captured from the live app. Sending
|
|
32379
32387
|
// `startDate`/`endDate` returns 200 with an empty `events` array — a silent
|
|
32380
32388
|
// wrong answer, not an error.
|
|
32381
|
-
async ({ startDate, endDate }) =>
|
|
32389
|
+
async ({ startDate, endDate }) => minifiedResult(await client2.write("/calendar/studentSchoolData", { start: startDate, end: endDate }))
|
|
32382
32390
|
);
|
|
32383
32391
|
server.registerTool(
|
|
32384
32392
|
"mhlb_get_day",
|
|
@@ -32390,7 +32398,7 @@ function registerCalendarTools(server, client2) {
|
|
|
32390
32398
|
date: IsoDate.describe("The day to look at (YYYY-MM-DD).")
|
|
32391
32399
|
}
|
|
32392
32400
|
},
|
|
32393
|
-
async ({ studentId, date: date5 }) =>
|
|
32401
|
+
async ({ studentId, date: date5 }) => minifiedResult(await client2.get("/calendar/studentOrderItems", { studentId, date: date5 }))
|
|
32394
32402
|
);
|
|
32395
32403
|
}
|
|
32396
32404
|
|
|
@@ -32426,7 +32434,7 @@ function registerOrderTools(server, client2) {
|
|
|
32426
32434
|
studentId: PositiveInt.optional().describe("Limit to one student.")
|
|
32427
32435
|
}
|
|
32428
32436
|
},
|
|
32429
|
-
async ({ orderStatus, period, studentId }) =>
|
|
32437
|
+
async ({ orderStatus, period, studentId }) => minifiedResult(
|
|
32430
32438
|
await client2.get("/event/shoppingCart", {
|
|
32431
32439
|
orderStatus,
|
|
32432
32440
|
selectedPeriod: period,
|
|
@@ -32445,7 +32453,7 @@ function registerOrderTools(server, client2) {
|
|
|
32445
32453
|
studentId: PositiveInt.optional().describe("Limit to one student.")
|
|
32446
32454
|
}
|
|
32447
32455
|
},
|
|
32448
|
-
async ({ tabName, period, studentId }) =>
|
|
32456
|
+
async ({ tabName, period, studentId }) => minifiedResult(
|
|
32449
32457
|
await client2.get("/event/ShoppingCartBaseData", {
|
|
32450
32458
|
shopingCartTabsName: tabName,
|
|
32451
32459
|
selectedPeriod: period,
|
|
@@ -32463,7 +32471,7 @@ function registerOrderTools(server, client2) {
|
|
|
32463
32471
|
date: IsoDate.describe("The lunch date (YYYY-MM-DD).")
|
|
32464
32472
|
}
|
|
32465
32473
|
},
|
|
32466
|
-
async ({ studentId, date: date5 }) =>
|
|
32474
|
+
async ({ studentId, date: date5 }) => minifiedResult(await client2.get("/event/orderBaseData", { studentId, eventDate: date5 }))
|
|
32467
32475
|
);
|
|
32468
32476
|
server.registerTool(
|
|
32469
32477
|
"mhlb_get_order_form",
|
|
@@ -32475,7 +32483,7 @@ function registerOrderTools(server, client2) {
|
|
|
32475
32483
|
studentId: PositiveInt.describe("Student id from mhlb_list_students.")
|
|
32476
32484
|
}
|
|
32477
32485
|
},
|
|
32478
|
-
async ({ eventId, studentId }) =>
|
|
32486
|
+
async ({ eventId, studentId }) => minifiedResult(await client2.get("/event/createOrder", { eventId, studentId }))
|
|
32479
32487
|
);
|
|
32480
32488
|
server.registerTool(
|
|
32481
32489
|
"mhlb_get_order",
|
|
@@ -32488,7 +32496,7 @@ function registerOrderTools(server, client2) {
|
|
|
32488
32496
|
studentId: PositiveInt.optional().describe("Student id.")
|
|
32489
32497
|
}
|
|
32490
32498
|
},
|
|
32491
|
-
async ({ orderId, eventId, studentId }) =>
|
|
32499
|
+
async ({ orderId, eventId, studentId }) => minifiedResult(await client2.get("/event/editOrder", { orderId, eventId, studentId }))
|
|
32492
32500
|
);
|
|
32493
32501
|
server.registerTool(
|
|
32494
32502
|
"mhlb_create_order",
|
|
@@ -32503,7 +32511,7 @@ function registerOrderTools(server, client2) {
|
|
|
32503
32511
|
"This adds the lunch to the cart. Payment is a separate step (mhlb_checkout)."
|
|
32504
32512
|
]);
|
|
32505
32513
|
}
|
|
32506
|
-
return
|
|
32514
|
+
return minifiedResult(await client2.write("/event/createOrder", order));
|
|
32507
32515
|
}
|
|
32508
32516
|
);
|
|
32509
32517
|
server.registerTool(
|
|
@@ -32519,7 +32527,7 @@ function registerOrderTools(server, client2) {
|
|
|
32519
32527
|
"This is a whole-order replace: items missing from `order` are removed, not preserved."
|
|
32520
32528
|
]);
|
|
32521
32529
|
}
|
|
32522
|
-
return
|
|
32530
|
+
return minifiedResult(await client2.write("/event/editOrder", order));
|
|
32523
32531
|
}
|
|
32524
32532
|
);
|
|
32525
32533
|
server.registerTool(
|
|
@@ -32537,7 +32545,7 @@ function registerOrderTools(server, client2) {
|
|
|
32537
32545
|
"isRepeated: true removes the whole recurring series, not just this date."
|
|
32538
32546
|
]);
|
|
32539
32547
|
}
|
|
32540
|
-
return
|
|
32548
|
+
return minifiedResult(await client2.write("/event/deleteOrder", body));
|
|
32541
32549
|
}
|
|
32542
32550
|
);
|
|
32543
32551
|
}
|
|
@@ -32554,7 +32562,7 @@ function registerBillingTools(server, client2) {
|
|
|
32554
32562
|
studentId: PositiveInt.optional().describe("Limit to one student.")
|
|
32555
32563
|
}
|
|
32556
32564
|
},
|
|
32557
|
-
async ({ period, studentId }) =>
|
|
32565
|
+
async ({ period, studentId }) => minifiedResult(await client2.get("/event/transactionsList", { selectedPeriod: period, selectedStudentId: studentId }))
|
|
32558
32566
|
);
|
|
32559
32567
|
server.registerTool(
|
|
32560
32568
|
"mhlb_get_transaction",
|
|
@@ -32564,7 +32572,7 @@ function registerBillingTools(server, client2) {
|
|
|
32564
32572
|
inputSchema: { transactionId: PositiveInt.describe("Transaction id (the `id` field from mhlb_list_transactions).") }
|
|
32565
32573
|
},
|
|
32566
32574
|
// The query parameter is `id`, not `transactionId` — verified live.
|
|
32567
|
-
async ({ transactionId }) =>
|
|
32575
|
+
async ({ transactionId }) => minifiedResult(await client2.get("/event/transactionDetails", { id: transactionId }))
|
|
32568
32576
|
);
|
|
32569
32577
|
server.registerTool(
|
|
32570
32578
|
"mhlb_list_subscriptions",
|
|
@@ -32573,7 +32581,7 @@ function registerBillingTools(server, client2) {
|
|
|
32573
32581
|
annotations: toolAnnotations({ title: "List subscriptions", openWorld: true }),
|
|
32574
32582
|
inputSchema: { period: external_exports.string().optional().describe("Ordering period to scope to.") }
|
|
32575
32583
|
},
|
|
32576
|
-
async ({ period }) =>
|
|
32584
|
+
async ({ period }) => minifiedResult(await client2.get("/event/upcomingSubscriptions", { period }))
|
|
32577
32585
|
);
|
|
32578
32586
|
server.registerTool(
|
|
32579
32587
|
"mhlb_get_subscription_settings",
|
|
@@ -32582,7 +32590,7 @@ function registerBillingTools(server, client2) {
|
|
|
32582
32590
|
annotations: toolAnnotations({ title: "Get subscription settings", openWorld: true }),
|
|
32583
32591
|
inputSchema: {}
|
|
32584
32592
|
},
|
|
32585
|
-
async () =>
|
|
32593
|
+
async () => minifiedResult(await client2.get("/event/subscription"))
|
|
32586
32594
|
);
|
|
32587
32595
|
server.registerTool(
|
|
32588
32596
|
"mhlb_set_subscription_enabled",
|
|
@@ -32602,7 +32610,7 @@ function registerBillingTools(server, client2) {
|
|
|
32602
32610
|
enabled ? ["Enabling means future lunches are ordered and charged automatically."] : []
|
|
32603
32611
|
);
|
|
32604
32612
|
}
|
|
32605
|
-
return
|
|
32613
|
+
return minifiedResult(
|
|
32606
32614
|
await client2.write("/parent/changeSubscriptionStatus", void 0, { isEnableSubscription: enabled })
|
|
32607
32615
|
);
|
|
32608
32616
|
}
|
|
@@ -32624,7 +32632,7 @@ function registerBillingTools(server, client2) {
|
|
|
32624
32632
|
"isRepeated: true stops the whole recurring series, not just this date."
|
|
32625
32633
|
]);
|
|
32626
32634
|
}
|
|
32627
|
-
return
|
|
32635
|
+
return minifiedResult(await client2.write("/event/unsubcribeOrder", body));
|
|
32628
32636
|
}
|
|
32629
32637
|
);
|
|
32630
32638
|
server.registerTool(
|
|
@@ -32634,7 +32642,7 @@ function registerBillingTools(server, client2) {
|
|
|
32634
32642
|
annotations: toolAnnotations({ title: "List gift cards", openWorld: true }),
|
|
32635
32643
|
inputSchema: {}
|
|
32636
32644
|
},
|
|
32637
|
-
async () =>
|
|
32645
|
+
async () => minifiedResult(await client2.get("/parent/giftCardDataTables"))
|
|
32638
32646
|
);
|
|
32639
32647
|
server.registerTool(
|
|
32640
32648
|
"mhlb_apply_gift_card",
|
|
@@ -32647,7 +32655,7 @@ function registerBillingTools(server, client2) {
|
|
|
32647
32655
|
if (!confirm) {
|
|
32648
32656
|
return preview("Apply gift card", { method: "POST", path: "/parent/applyGiftCard", query: { giftCardCode: code } });
|
|
32649
32657
|
}
|
|
32650
|
-
return
|
|
32658
|
+
return minifiedResult(await client2.write("/parent/applyGiftCard", void 0, { giftCardCode: code }));
|
|
32651
32659
|
}
|
|
32652
32660
|
);
|
|
32653
32661
|
server.registerTool(
|
|
@@ -32657,7 +32665,7 @@ function registerBillingTools(server, client2) {
|
|
|
32657
32665
|
annotations: toolAnnotations({ title: "Get applied coupon", openWorld: true }),
|
|
32658
32666
|
inputSchema: {}
|
|
32659
32667
|
},
|
|
32660
|
-
async () =>
|
|
32668
|
+
async () => minifiedResult(await client2.get("/parent/coupon"))
|
|
32661
32669
|
);
|
|
32662
32670
|
server.registerTool(
|
|
32663
32671
|
"mhlb_apply_coupon",
|
|
@@ -32670,7 +32678,7 @@ function registerBillingTools(server, client2) {
|
|
|
32670
32678
|
if (!confirm) {
|
|
32671
32679
|
return preview("Apply coupon", { method: "POST", path: "/parent/applyCoupon", query: { couponCode: code } });
|
|
32672
32680
|
}
|
|
32673
|
-
return
|
|
32681
|
+
return minifiedResult(await client2.write("/parent/applyCoupon", void 0, { couponCode: code }));
|
|
32674
32682
|
}
|
|
32675
32683
|
);
|
|
32676
32684
|
server.registerTool(
|
|
@@ -32682,7 +32690,7 @@ function registerBillingTools(server, client2) {
|
|
|
32682
32690
|
},
|
|
32683
32691
|
async ({ confirm }) => {
|
|
32684
32692
|
if (!confirm) return preview("Remove coupon", { method: "POST", path: "/parent/removeCoupon" });
|
|
32685
|
-
return
|
|
32693
|
+
return minifiedResult(await client2.write("/parent/removeCoupon"));
|
|
32686
32694
|
}
|
|
32687
32695
|
);
|
|
32688
32696
|
}
|
|
@@ -32707,7 +32715,7 @@ function registerCheckoutTools(server, client2) {
|
|
|
32707
32715
|
"This step prices the cart and returns payment options. It does not charge a card."
|
|
32708
32716
|
]);
|
|
32709
32717
|
}
|
|
32710
|
-
return
|
|
32718
|
+
return minifiedResult(await client2.write("/payment/initCheckout", body));
|
|
32711
32719
|
}
|
|
32712
32720
|
);
|
|
32713
32721
|
server.registerTool(
|
|
@@ -32747,7 +32755,7 @@ function registerCheckoutTools(server, client2) {
|
|
|
32747
32755
|
});
|
|
32748
32756
|
}
|
|
32749
32757
|
const result = await client2.write("/payment/checkout", body);
|
|
32750
|
-
return
|
|
32758
|
+
return minifiedResult({ result, expectedTotal, idempotencyKey: key });
|
|
32751
32759
|
}
|
|
32752
32760
|
);
|
|
32753
32761
|
}
|
|
@@ -32823,7 +32831,7 @@ function deliver(bytes, contentType, filename, inline) {
|
|
|
32823
32831
|
{ hint: "Call again with inline: false to write it to a file instead." }
|
|
32824
32832
|
);
|
|
32825
32833
|
}
|
|
32826
|
-
return
|
|
32834
|
+
return minifiedResult({
|
|
32827
32835
|
filename,
|
|
32828
32836
|
contentType,
|
|
32829
32837
|
bytes: bytes.byteLength,
|
|
@@ -32842,7 +32850,7 @@ function deliver(bytes, contentType, filename, inline) {
|
|
|
32842
32850
|
cause
|
|
32843
32851
|
});
|
|
32844
32852
|
}
|
|
32845
|
-
return
|
|
32853
|
+
return minifiedResult({ path, contentType, bytes: bytes.byteLength });
|
|
32846
32854
|
}
|
|
32847
32855
|
function midpoint(start, end) {
|
|
32848
32856
|
const mid = new Date((Date.parse(start) + Date.parse(end)) / 2);
|
|
@@ -32919,6 +32927,181 @@ function registerReportTools(server, client2) {
|
|
|
32919
32927
|
);
|
|
32920
32928
|
}
|
|
32921
32929
|
|
|
32930
|
+
// node_modules/@chrischall/mcp-utils/dist/healthcheck/index.js
|
|
32931
|
+
function statusOf(err) {
|
|
32932
|
+
if (typeof err !== "object" || err === null)
|
|
32933
|
+
return void 0;
|
|
32934
|
+
const s = err.status ?? err.statusCode;
|
|
32935
|
+
return typeof s === "number" ? s : void 0;
|
|
32936
|
+
}
|
|
32937
|
+
var CREDENTIAL_ARMS = /* @__PURE__ */ new Set([
|
|
32938
|
+
"ok",
|
|
32939
|
+
"no_credential",
|
|
32940
|
+
"credential_rejected",
|
|
32941
|
+
"timeout",
|
|
32942
|
+
"http",
|
|
32943
|
+
"transport",
|
|
32944
|
+
"unknown"
|
|
32945
|
+
]);
|
|
32946
|
+
function isArm(kind) {
|
|
32947
|
+
return kind !== void 0 && CREDENTIAL_ARMS.has(kind);
|
|
32948
|
+
}
|
|
32949
|
+
function credentialHint(arm, prefix, hostLabel, source) {
|
|
32950
|
+
switch (arm) {
|
|
32951
|
+
case "ok":
|
|
32952
|
+
return `Credential from '${source}' works: ${hostLabel} accepted an authenticated request. If a real tool still fails, the problem is that tool, not auth.`;
|
|
32953
|
+
case "no_credential":
|
|
32954
|
+
return `No credential resolved. Nothing was available to authenticate with \u2014 sign in and reconnect the connector so ${prefix} receives a token, or set the documented environment variable.`;
|
|
32955
|
+
case "credential_rejected":
|
|
32956
|
+
return `${hostLabel} rejected the credential from '${source}'. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
|
|
32957
|
+
case "timeout":
|
|
32958
|
+
return `The credential from '${source}' resolved, but ${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
|
|
32959
|
+
case "http":
|
|
32960
|
+
return `${hostLabel} answered with an error status that is not an auth rejection. That is USUALLY a ${hostLabel}-side problem rather than an auth one \u2014 but a 404 here more often means the probe path is wrong than that ${hostLabel} is broken, so check error.message and probe.url before concluding anything about the credential.`;
|
|
32961
|
+
case "transport":
|
|
32962
|
+
return `Could not reach ${hostLabel} at all. Check network egress; the credential itself was never judged.`;
|
|
32963
|
+
default:
|
|
32964
|
+
return `Unexpected failure \u2014 see error.message.`;
|
|
32965
|
+
}
|
|
32966
|
+
}
|
|
32967
|
+
function registerCredentialHealthcheckTool(args) {
|
|
32968
|
+
const { server, prefix, hostLabel, probePath, resolveCredential, probeFn, classifyThrown, hints } = args;
|
|
32969
|
+
const probeUrl = probePath ? `https://${hostLabel}${probePath}` : void 0;
|
|
32970
|
+
server.registerTool(`${prefix}_healthcheck`, {
|
|
32971
|
+
title: "Verify credentials and upstream reachability",
|
|
32972
|
+
description: `Resolves the credential the way real tools do, then makes one authenticated request to ${hostLabel}. Reports which source supplied the credential, whether ${hostLabel} accepted it, the round-trip time, and a plain-English hint distinguishing 'no credential' from 'credential rejected' from 'a ${hostLabel}-side problem'. Call this when a real tool fails and you want to know which hop broke. Read-only; never returns the credential itself.`,
|
|
32973
|
+
annotations: {
|
|
32974
|
+
title: "Verify credentials and upstream reachability",
|
|
32975
|
+
readOnlyHint: true,
|
|
32976
|
+
idempotentHint: true,
|
|
32977
|
+
openWorldHint: true
|
|
32978
|
+
},
|
|
32979
|
+
inputSchema: {}
|
|
32980
|
+
}, async () => {
|
|
32981
|
+
let probeStarted = 0;
|
|
32982
|
+
let state;
|
|
32983
|
+
try {
|
|
32984
|
+
state = await resolveCredential();
|
|
32985
|
+
} catch (e) {
|
|
32986
|
+
const classified = classifyThrown?.(e);
|
|
32987
|
+
const result2 = {
|
|
32988
|
+
ok: false,
|
|
32989
|
+
// Still false, and still no source: a classification explains WHY
|
|
32990
|
+
// nothing resolved, it does not invent a credential that did.
|
|
32991
|
+
credential: { source: null, resolved: false },
|
|
32992
|
+
// No `url`: nothing was probed, and naming one implies it was tried.
|
|
32993
|
+
probe: { elapsed_ms: 0 },
|
|
32994
|
+
error: {
|
|
32995
|
+
kind: classified?.kind ?? "no_credential",
|
|
32996
|
+
message: truncateErrorMessage(messageOf(e)),
|
|
32997
|
+
...classified?.detail !== void 0 ? { detail: classified.detail } : {}
|
|
32998
|
+
},
|
|
32999
|
+
// The hint must follow the KIND beside it. Falling back to
|
|
33000
|
+
// `no_credential`'s copy under a classified kind would state a cause
|
|
33001
|
+
// the kind contradicts — the same disagreement this path exists to
|
|
33002
|
+
// remove. So: an inline hint wins; else the classified arm's own
|
|
33003
|
+
// copy (consumer override first); else, for a kind this module has
|
|
33004
|
+
// no copy for, the neutral `unknown` text rather than one that
|
|
33005
|
+
// asserts a cause; else the unclassified `no_credential` default.
|
|
33006
|
+
hint: classified?.hint ?? (isArm(classified?.kind) ? hints?.[classified.kind] ?? credentialHint(classified.kind, prefix, hostLabel, null) : classified !== void 0 ? credentialHint("unknown", prefix, hostLabel, null) : hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null))
|
|
33007
|
+
};
|
|
33008
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
33009
|
+
}
|
|
33010
|
+
const credential = {
|
|
33011
|
+
source: state.source,
|
|
33012
|
+
resolved: state.source !== null,
|
|
33013
|
+
...state.detail !== void 0 ? { detail: state.detail } : {}
|
|
33014
|
+
};
|
|
33015
|
+
if (!credential.resolved) {
|
|
33016
|
+
const result2 = {
|
|
33017
|
+
ok: false,
|
|
33018
|
+
credential,
|
|
33019
|
+
probe: { elapsed_ms: 0 },
|
|
33020
|
+
error: { kind: "no_credential", message: "no credential source resolved" },
|
|
33021
|
+
hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
|
|
33022
|
+
};
|
|
33023
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
33024
|
+
}
|
|
33025
|
+
let arm = "ok";
|
|
33026
|
+
let error51;
|
|
33027
|
+
let status;
|
|
33028
|
+
let customHint;
|
|
33029
|
+
probeStarted = Date.now();
|
|
33030
|
+
try {
|
|
33031
|
+
await probeFn();
|
|
33032
|
+
} catch (e) {
|
|
33033
|
+
status = statusOf(e);
|
|
33034
|
+
const aborted2 = e instanceof Error && e.name === "AbortError";
|
|
33035
|
+
arm = status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
|
|
33036
|
+
let kind = arm;
|
|
33037
|
+
let detail;
|
|
33038
|
+
const custom2 = classifyThrown?.(e);
|
|
33039
|
+
if (custom2) {
|
|
33040
|
+
kind = custom2.kind;
|
|
33041
|
+
customHint = custom2.hint;
|
|
33042
|
+
detail = custom2.detail;
|
|
33043
|
+
}
|
|
33044
|
+
error51 = {
|
|
33045
|
+
kind,
|
|
33046
|
+
// Redacted AND bounded before it reaches the result: an upstream
|
|
33047
|
+
// failure routinely quotes what it was sent, and a healthcheck is
|
|
33048
|
+
// the tool people paste into a chat when something is broken.
|
|
33049
|
+
message: truncateErrorMessage(messageOf(e)),
|
|
33050
|
+
...detail !== void 0 ? { detail } : {}
|
|
33051
|
+
};
|
|
33052
|
+
}
|
|
33053
|
+
const result = {
|
|
33054
|
+
ok: error51 === void 0,
|
|
33055
|
+
credential,
|
|
33056
|
+
probe: {
|
|
33057
|
+
...probeUrl ? { url: probeUrl } : {},
|
|
33058
|
+
elapsed_ms: Date.now() - probeStarted,
|
|
33059
|
+
...status !== void 0 ? { status } : {}
|
|
33060
|
+
},
|
|
33061
|
+
...error51 ? { error: error51 } : {},
|
|
33062
|
+
hint: customHint ?? hints?.[arm] ?? credentialHint(arm, prefix, hostLabel, state.source)
|
|
33063
|
+
};
|
|
33064
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
33065
|
+
});
|
|
33066
|
+
}
|
|
33067
|
+
|
|
33068
|
+
// src/tools/health.ts
|
|
33069
|
+
var NOT_CONFIGURED = "credentials are not configured";
|
|
33070
|
+
function classifyMhlbError(err) {
|
|
33071
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33072
|
+
if (msg.includes(NOT_CONFIGURED)) return { kind: "no_credential" };
|
|
33073
|
+
if (msg.includes("Could not reach My Hot Lunchbox")) {
|
|
33074
|
+
return {
|
|
33075
|
+
kind: "unreachable",
|
|
33076
|
+
hint: "Could not reach My Hot Lunchbox at all, so the credential was never tested. Check network connectivity, or MYHOTLUNCHBOX_BASE_URL if the app has moved."
|
|
33077
|
+
};
|
|
33078
|
+
}
|
|
33079
|
+
if (msg.includes("rejected the sign-in")) {
|
|
33080
|
+
return {
|
|
33081
|
+
kind: "credential_rejected",
|
|
33082
|
+
hint: "My Hot Lunchbox rejected the sign-in. Check MYHOTLUNCHBOX_USERNAME / MYHOTLUNCHBOX_PASSWORD against https://ordernow.myhotlunchbox.com \u2014 but do NOT re-run this with guesses: repeated failures can lock the account or force a CAPTCHA that blocks server-side sign-in entirely."
|
|
33083
|
+
};
|
|
33084
|
+
}
|
|
33085
|
+
return void 0;
|
|
33086
|
+
}
|
|
33087
|
+
function registerHealthcheckTools(server, client2, readConfig = () => loadConfig()) {
|
|
33088
|
+
registerCredentialHealthcheckTool({
|
|
33089
|
+
server,
|
|
33090
|
+
prefix: "mhlb",
|
|
33091
|
+
hostLabel: "ordernow.myhotlunchbox.com",
|
|
33092
|
+
probePath: "/api/auth/userinfo",
|
|
33093
|
+
resolveCredential: async () => {
|
|
33094
|
+
const { username, password, baseUrl } = readConfig();
|
|
33095
|
+
const source = username && password ? "MYHOTLUNCHBOX_USERNAME+MYHOTLUNCHBOX_PASSWORD" : null;
|
|
33096
|
+
return { source, detail: { base_url: baseUrl } };
|
|
33097
|
+
},
|
|
33098
|
+
// The same authenticated read `mhlb_whoami` makes: cheap, and it changes
|
|
33099
|
+
// nothing — no order is placed, no balance moved.
|
|
33100
|
+
probeFn: () => client2.get("/auth/userinfo"),
|
|
33101
|
+
classifyThrown: classifyMhlbError
|
|
33102
|
+
});
|
|
33103
|
+
}
|
|
33104
|
+
|
|
32922
33105
|
// src/index.ts
|
|
32923
33106
|
await loadDotenvSafely();
|
|
32924
33107
|
var client = new MhlbClient();
|
|
@@ -32934,6 +33117,7 @@ await runMcp({
|
|
|
32934
33117
|
registerOrderTools,
|
|
32935
33118
|
registerBillingTools,
|
|
32936
33119
|
registerCheckoutTools,
|
|
32937
|
-
registerReportTools
|
|
33120
|
+
registerReportTools,
|
|
33121
|
+
registerHealthcheckTools
|
|
32938
33122
|
]
|
|
32939
33123
|
});
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { registerOrderTools } from './tools/orders.js';
|
|
|
9
9
|
import { registerBillingTools } from './tools/billing.js';
|
|
10
10
|
import { registerCheckoutTools } from './tools/checkout.js';
|
|
11
11
|
import { registerReportTools } from './tools/reports.js';
|
|
12
|
+
import { registerHealthcheckTools } from './tools/health.js';
|
|
12
13
|
await loadDotenvSafely();
|
|
13
14
|
// Built here, in the caller, so the deferred-config-error pattern holds: the
|
|
14
15
|
// server still boots (and answers the host's install-time tools/list probe)
|
|
@@ -27,5 +28,6 @@ await runMcp({
|
|
|
27
28
|
registerBillingTools,
|
|
28
29
|
registerCheckoutTools,
|
|
29
30
|
registerReportTools,
|
|
31
|
+
registerHealthcheckTools,
|
|
30
32
|
],
|
|
31
33
|
});
|
package/dist/tools/_shared.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { minifiedResult } from '@chrischall/mcp-utils';
|
|
2
2
|
/**
|
|
3
3
|
* Standard dry-run envelope for a confirm-gated write.
|
|
4
4
|
*
|
|
@@ -6,7 +6,7 @@ import { jsonResult, textResult } from '@chrischall/mcp-utils';
|
|
|
6
6
|
* returns exactly what it would have sent, so the caller can inspect it first.
|
|
7
7
|
*/
|
|
8
8
|
export function preview(action, request, notes) {
|
|
9
|
-
return
|
|
9
|
+
return minifiedResult({
|
|
10
10
|
dryRun: true,
|
|
11
11
|
action,
|
|
12
12
|
wouldSend: request,
|
|
@@ -16,7 +16,12 @@ export function preview(action, request, notes) {
|
|
|
16
16
|
],
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
// `minifiedResult` only. This seam re-exported both for a while, and every
|
|
20
|
+
// tool went on importing `jsonResult` — @chrischall/mcp-utils' alias for the
|
|
21
|
+
// PRETTY `textResult` — so the re-export was the only trace of a minification
|
|
22
|
+
// that never happened. Exporting one name makes the wrong choice unavailable
|
|
23
|
+
// rather than merely discouraged.
|
|
24
|
+
export { minifiedResult };
|
|
20
25
|
/**
|
|
21
26
|
* Marker appended to the description of every tool whose request shape was
|
|
22
27
|
* derived from the web app's compiled client but never exercised against a
|
package/dist/tools/account.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toolAnnotations } from '@chrischall/mcp-utils';
|
|
2
|
-
import {
|
|
2
|
+
import { minifiedResult } from './_shared.js';
|
|
3
3
|
export function registerAccountTools(server, client) {
|
|
4
4
|
server.registerTool('mhlb_whoami', {
|
|
5
5
|
description: 'Get the signed-in My Hot Lunchbox account: name, email, role, number of students, ' +
|
|
@@ -7,7 +7,7 @@ export function registerAccountTools(server, client) {
|
|
|
7
7
|
'Start here to confirm the session works.',
|
|
8
8
|
annotations: toolAnnotations({ title: 'Who am I', openWorld: true }),
|
|
9
9
|
inputSchema: {},
|
|
10
|
-
}, async () =>
|
|
10
|
+
}, async () => minifiedResult(await client.get('/auth/userinfo')));
|
|
11
11
|
server.registerTool('mhlb_session_reset', {
|
|
12
12
|
description: 'Discard the cached My Hot Lunchbox access token so the next tool call signs in again. ' +
|
|
13
13
|
'Use after changing credentials, or if calls start failing with stale-session errors.',
|
|
@@ -16,6 +16,6 @@ export function registerAccountTools(server, client) {
|
|
|
16
16
|
}, async () => {
|
|
17
17
|
const wasAuthenticated = client.isAuthenticated;
|
|
18
18
|
client.resetSession();
|
|
19
|
-
return
|
|
19
|
+
return minifiedResult({ reset: true, hadActiveSession: wasAuthenticated });
|
|
20
20
|
});
|
|
21
21
|
}
|
package/dist/tools/billing.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { toolAnnotations, PositiveInt, NonEmptyString, schemaConfirm } from '@chrischall/mcp-utils';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import { UNVERIFIED, minifiedResult, preview } from './_shared.js';
|
|
4
4
|
import { OrderRefShape, orderRefBody } from './orders.js';
|
|
5
5
|
export function registerBillingTools(server, client) {
|
|
6
6
|
server.registerTool('mhlb_list_transactions', {
|
|
@@ -10,24 +10,24 @@ export function registerBillingTools(server, client) {
|
|
|
10
10
|
period: z.string().optional().describe('Ordering period to scope to, from mhlb_get_cart_tabs.'),
|
|
11
11
|
studentId: PositiveInt.optional().describe('Limit to one student.'),
|
|
12
12
|
},
|
|
13
|
-
}, async ({ period, studentId }) =>
|
|
13
|
+
}, async ({ period, studentId }) => minifiedResult(await client.get('/event/transactionsList', { selectedPeriod: period, selectedStudentId: studentId })));
|
|
14
14
|
server.registerTool('mhlb_get_transaction', {
|
|
15
15
|
description: 'Get the line-item detail of one transaction — which lunches it paid for.',
|
|
16
16
|
annotations: toolAnnotations({ title: 'Get transaction', openWorld: true }),
|
|
17
17
|
inputSchema: { transactionId: PositiveInt.describe('Transaction id (the `id` field from mhlb_list_transactions).') },
|
|
18
18
|
},
|
|
19
19
|
// The query parameter is `id`, not `transactionId` — verified live.
|
|
20
|
-
async ({ transactionId }) =>
|
|
20
|
+
async ({ transactionId }) => minifiedResult(await client.get('/event/transactionDetails', { id: transactionId })));
|
|
21
21
|
server.registerTool('mhlb_list_subscriptions', {
|
|
22
22
|
description: 'List upcoming lunch subscriptions — the recurring orders that will be placed and charged automatically.',
|
|
23
23
|
annotations: toolAnnotations({ title: 'List subscriptions', openWorld: true }),
|
|
24
24
|
inputSchema: { period: z.string().optional().describe('Ordering period to scope to.') },
|
|
25
|
-
}, async ({ period }) =>
|
|
25
|
+
}, async ({ period }) => minifiedResult(await client.get('/event/upcomingSubscriptions', { period })));
|
|
26
26
|
server.registerTool('mhlb_get_subscription_settings', {
|
|
27
27
|
description: 'Get the account’s subscription configuration — whether recurring ordering is on, and its terms.',
|
|
28
28
|
annotations: toolAnnotations({ title: 'Get subscription settings', openWorld: true }),
|
|
29
29
|
inputSchema: {},
|
|
30
|
-
}, async () =>
|
|
30
|
+
}, async () => minifiedResult(await client.get('/event/subscription')));
|
|
31
31
|
server.registerTool('mhlb_set_subscription_enabled', {
|
|
32
32
|
description: 'Turn recurring lunch subscriptions on or off for the account. Turning it ON means future lunches are ' +
|
|
33
33
|
'ordered and charged automatically.' + UNVERIFIED,
|
|
@@ -40,7 +40,7 @@ export function registerBillingTools(server, client) {
|
|
|
40
40
|
if (!confirm) {
|
|
41
41
|
return preview(`${enabled ? 'Enable' : 'Disable'} subscriptions`, { method: 'POST', path: '/parent/changeSubscriptionStatus', query: { isEnableSubscription: enabled } }, enabled ? ['Enabling means future lunches are ordered and charged automatically.'] : []);
|
|
42
42
|
}
|
|
43
|
-
return
|
|
43
|
+
return minifiedResult(await client.write('/parent/changeSubscriptionStatus', undefined, { isEnableSubscription: enabled }));
|
|
44
44
|
});
|
|
45
45
|
server.registerTool('mhlb_unsubscribe_order', {
|
|
46
46
|
description: 'Stop a recurring subscription for a specific lunch order.' + UNVERIFIED,
|
|
@@ -57,13 +57,13 @@ export function registerBillingTools(server, client) {
|
|
|
57
57
|
'isRepeated: true stops the whole recurring series, not just this date.',
|
|
58
58
|
]);
|
|
59
59
|
}
|
|
60
|
-
return
|
|
60
|
+
return minifiedResult(await client.write('/event/unsubcribeOrder', body));
|
|
61
61
|
});
|
|
62
62
|
server.registerTool('mhlb_list_gift_cards', {
|
|
63
63
|
description: 'List gift cards on the account — codes, balances and status.',
|
|
64
64
|
annotations: toolAnnotations({ title: 'List gift cards', openWorld: true }),
|
|
65
65
|
inputSchema: {},
|
|
66
|
-
}, async () =>
|
|
66
|
+
}, async () => minifiedResult(await client.get('/parent/giftCardDataTables')));
|
|
67
67
|
server.registerTool('mhlb_apply_gift_card', {
|
|
68
68
|
description: 'Redeem a gift card code onto the account balance.' + UNVERIFIED,
|
|
69
69
|
annotations: toolAnnotations({ title: 'Apply gift card', readOnly: false, openWorld: true }),
|
|
@@ -72,13 +72,13 @@ export function registerBillingTools(server, client) {
|
|
|
72
72
|
if (!confirm) {
|
|
73
73
|
return preview('Apply gift card', { method: 'POST', path: '/parent/applyGiftCard', query: { giftCardCode: code } });
|
|
74
74
|
}
|
|
75
|
-
return
|
|
75
|
+
return minifiedResult(await client.write('/parent/applyGiftCard', undefined, { giftCardCode: code }));
|
|
76
76
|
});
|
|
77
77
|
server.registerTool('mhlb_get_coupon', {
|
|
78
78
|
description: 'Get the coupon currently applied to the account, if any.',
|
|
79
79
|
annotations: toolAnnotations({ title: 'Get applied coupon', openWorld: true }),
|
|
80
80
|
inputSchema: {},
|
|
81
|
-
}, async () =>
|
|
81
|
+
}, async () => minifiedResult(await client.get('/parent/coupon')));
|
|
82
82
|
server.registerTool('mhlb_apply_coupon', {
|
|
83
83
|
description: 'Apply a coupon code to the account.' + UNVERIFIED,
|
|
84
84
|
annotations: toolAnnotations({ title: 'Apply coupon', readOnly: false, openWorld: true }),
|
|
@@ -87,7 +87,7 @@ export function registerBillingTools(server, client) {
|
|
|
87
87
|
if (!confirm) {
|
|
88
88
|
return preview('Apply coupon', { method: 'POST', path: '/parent/applyCoupon', query: { couponCode: code } });
|
|
89
89
|
}
|
|
90
|
-
return
|
|
90
|
+
return minifiedResult(await client.write('/parent/applyCoupon', undefined, { couponCode: code }));
|
|
91
91
|
});
|
|
92
92
|
server.registerTool('mhlb_remove_coupon', {
|
|
93
93
|
description: 'Remove the coupon currently applied to the account.' + UNVERIFIED,
|
|
@@ -96,6 +96,6 @@ export function registerBillingTools(server, client) {
|
|
|
96
96
|
}, async ({ confirm }) => {
|
|
97
97
|
if (!confirm)
|
|
98
98
|
return preview('Remove coupon', { method: 'POST', path: '/parent/removeCoupon' });
|
|
99
|
-
return
|
|
99
|
+
return minifiedResult(await client.write('/parent/removeCoupon'));
|
|
100
100
|
});
|
|
101
101
|
}
|
package/dist/tools/calendar.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toolAnnotations, PositiveInt, IsoDate } from '@chrischall/mcp-utils';
|
|
2
|
-
import {
|
|
2
|
+
import { minifiedResult } from './_shared.js';
|
|
3
3
|
export function registerCalendarTools(server, client) {
|
|
4
4
|
server.registerTool('mhlb_get_calendar', {
|
|
5
5
|
description: 'Get the lunch calendar for the account over a date range: which days are open for ordering, already ' +
|
|
@@ -13,7 +13,7 @@ export function registerCalendarTools(server, client) {
|
|
|
13
13
|
// Field names are `start`/`end`, captured from the live app. Sending
|
|
14
14
|
// `startDate`/`endDate` returns 200 with an empty `events` array — a silent
|
|
15
15
|
// wrong answer, not an error.
|
|
16
|
-
async ({ startDate, endDate }) =>
|
|
16
|
+
async ({ startDate, endDate }) => minifiedResult(await client.write('/calendar/studentSchoolData', { start: startDate, end: endDate })));
|
|
17
17
|
server.registerTool('mhlb_get_day', {
|
|
18
18
|
description: 'Get what a student has ordered on one specific date — the items, sizes, quantities, add-ons and prices.',
|
|
19
19
|
annotations: toolAnnotations({ title: 'Get a day’s order', openWorld: true }),
|
|
@@ -21,5 +21,5 @@ export function registerCalendarTools(server, client) {
|
|
|
21
21
|
studentId: PositiveInt.describe('Student id from mhlb_list_students.'),
|
|
22
22
|
date: IsoDate.describe('The day to look at (YYYY-MM-DD).'),
|
|
23
23
|
},
|
|
24
|
-
}, async ({ studentId, date }) =>
|
|
24
|
+
}, async ({ studentId, date }) => minifiedResult(await client.get('/calendar/studentOrderItems', { studentId, date })));
|
|
25
25
|
}
|
package/dist/tools/checkout.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { McpToolError, toolAnnotations, PositiveInt, schemaConfirm } from '@chrischall/mcp-utils';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import {
|
|
4
|
+
import { UNVERIFIED, minifiedResult, preview } from './_shared.js';
|
|
5
5
|
/**
|
|
6
6
|
* Checkout is the only pair of tools that moves money. Both are confirm-gated
|
|
7
7
|
* like every other write.
|
|
@@ -30,7 +30,7 @@ export function registerCheckoutTools(server, client) {
|
|
|
30
30
|
'This step prices the cart and returns payment options. It does not charge a card.',
|
|
31
31
|
]);
|
|
32
32
|
}
|
|
33
|
-
return
|
|
33
|
+
return minifiedResult(await client.write('/payment/initCheckout', body));
|
|
34
34
|
});
|
|
35
35
|
server.registerTool('mhlb_checkout', {
|
|
36
36
|
description: 'PAY for the lunches in the cart. This charges a real payment method on the My Hot Lunchbox account. ' +
|
|
@@ -86,7 +86,7 @@ export function registerCheckoutTools(server, client) {
|
|
|
86
86
|
// would mangle a non-object and would let `expectedTotal`/`idempotencyKey`
|
|
87
87
|
// silently shadow same-named server fields.
|
|
88
88
|
const result = await client.write('/payment/checkout', body);
|
|
89
|
-
return
|
|
89
|
+
return minifiedResult({ result, expectedTotal, idempotencyKey: key });
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { registerCredentialHealthcheckTool } from '@chrischall/mcp-utils/healthcheck';
|
|
2
|
+
import { loadConfig } from '../config.js';
|
|
3
|
+
/**
|
|
4
|
+
* `mhlb_healthcheck` — the one call that answers "is this connector
|
|
5
|
+
* working?", and the only tool here that reports a failure as DATA rather
|
|
6
|
+
* than throwing.
|
|
7
|
+
*
|
|
8
|
+
* My Hot Lunchbox had none. `mhlb_whoami` looks like one — its own
|
|
9
|
+
* description says "Start here to confirm the session works" — and is not: it
|
|
10
|
+
* is a plain data read that THROWS when auth fails, so the caller gets an
|
|
11
|
+
* exception, not an answer, and cannot tell a bad password from an
|
|
12
|
+
* unreachable host from a lockout.
|
|
13
|
+
*
|
|
14
|
+
* The lockout is why this matters more here than elsewhere. This account
|
|
15
|
+
* counts failed sign-ins and will lock or force a CAPTCHA that blocks
|
|
16
|
+
* server-side sign-in entirely, so a `credential_rejected` hint that invites
|
|
17
|
+
* retrying is actively harmful. {@link classifyMhlbError} carries that
|
|
18
|
+
* warning instead.
|
|
19
|
+
*/
|
|
20
|
+
const NOT_CONFIGURED = 'credentials are not configured';
|
|
21
|
+
export function classifyMhlbError(err) {
|
|
22
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23
|
+
if (msg.includes(NOT_CONFIGURED))
|
|
24
|
+
return { kind: 'no_credential' };
|
|
25
|
+
// The host is unreachable, or MYHOTLUNCHBOX_BASE_URL points somewhere wrong.
|
|
26
|
+
// Nothing here says the credential is bad — do not send anyone to change it.
|
|
27
|
+
if (msg.includes('Could not reach My Hot Lunchbox')) {
|
|
28
|
+
return {
|
|
29
|
+
kind: 'unreachable',
|
|
30
|
+
hint: 'Could not reach My Hot Lunchbox at all, so the credential was never tested. ' +
|
|
31
|
+
'Check network connectivity, or MYHOTLUNCHBOX_BASE_URL if the app has moved.',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (msg.includes('rejected the sign-in')) {
|
|
35
|
+
return {
|
|
36
|
+
kind: 'credential_rejected',
|
|
37
|
+
hint: 'My Hot Lunchbox rejected the sign-in. Check MYHOTLUNCHBOX_USERNAME / MYHOTLUNCHBOX_PASSWORD against ' +
|
|
38
|
+
'https://ordernow.myhotlunchbox.com — but do NOT re-run this with guesses: repeated failures can lock ' +
|
|
39
|
+
'the account or force a CAPTCHA that blocks server-side sign-in entirely.',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
export function registerHealthcheckTools(server, client,
|
|
45
|
+
/** Seam: injectable so tests need no process env. */
|
|
46
|
+
readConfig = () => loadConfig()) {
|
|
47
|
+
registerCredentialHealthcheckTool({
|
|
48
|
+
server,
|
|
49
|
+
prefix: 'mhlb',
|
|
50
|
+
hostLabel: 'ordernow.myhotlunchbox.com',
|
|
51
|
+
probePath: '/api/auth/userinfo',
|
|
52
|
+
resolveCredential: async () => {
|
|
53
|
+
const { username, password, baseUrl } = readConfig();
|
|
54
|
+
// Both halves or nothing: a username with no password cannot sign in,
|
|
55
|
+
// and sending it to the API would spend a failed attempt against an
|
|
56
|
+
// account that locks.
|
|
57
|
+
const source = username && password ? 'MYHOTLUNCHBOX_USERNAME+MYHOTLUNCHBOX_PASSWORD' : null;
|
|
58
|
+
return { source, detail: { base_url: baseUrl } };
|
|
59
|
+
},
|
|
60
|
+
// The same authenticated read `mhlb_whoami` makes: cheap, and it changes
|
|
61
|
+
// nothing — no order is placed, no balance moved.
|
|
62
|
+
probeFn: () => client.get('/auth/userinfo'),
|
|
63
|
+
classifyThrown: classifyMhlbError,
|
|
64
|
+
});
|
|
65
|
+
}
|
package/dist/tools/orders.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { toolAnnotations, PositiveInt, IsoDate, schemaConfirm } from '@chrischall/mcp-utils';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import { UNVERIFIED, minifiedResult, preview } from './_shared.js';
|
|
4
4
|
/**
|
|
5
5
|
* Ordering is read-modify-write throughout: `GET /event/createOrder` and
|
|
6
6
|
* `GET /event/editOrder` return a populated model (menu items, sizes, add-ons,
|
|
@@ -54,7 +54,7 @@ export function registerOrderTools(server, client) {
|
|
|
54
54
|
period: z.string().optional().describe('Ordering period value from mhlb_get_cart_tabs.'),
|
|
55
55
|
studentId: PositiveInt.optional().describe('Limit to one student.'),
|
|
56
56
|
},
|
|
57
|
-
}, async ({ orderStatus, period, studentId }) =>
|
|
57
|
+
}, async ({ orderStatus, period, studentId }) => minifiedResult(await client.get('/event/shoppingCart', {
|
|
58
58
|
orderStatus,
|
|
59
59
|
selectedPeriod: period,
|
|
60
60
|
selectedStudentId: studentId,
|
|
@@ -68,7 +68,7 @@ export function registerOrderTools(server, client) {
|
|
|
68
68
|
period: z.string().optional().describe('Ordering period to scope the tabs to.'),
|
|
69
69
|
studentId: PositiveInt.optional().describe('Limit to one student.'),
|
|
70
70
|
},
|
|
71
|
-
}, async ({ tabName, period, studentId }) =>
|
|
71
|
+
}, async ({ tabName, period, studentId }) => minifiedResult(await client.get('/event/ShoppingCartBaseData', {
|
|
72
72
|
shopingCartTabsName: tabName,
|
|
73
73
|
selectedPeriod: period,
|
|
74
74
|
selectedStudentId: studentId,
|
|
@@ -81,7 +81,7 @@ export function registerOrderTools(server, client) {
|
|
|
81
81
|
studentId: PositiveInt.describe('Student id from mhlb_list_students.'),
|
|
82
82
|
date: IsoDate.describe('The lunch date (YYYY-MM-DD).'),
|
|
83
83
|
},
|
|
84
|
-
}, async ({ studentId, date }) =>
|
|
84
|
+
}, async ({ studentId, date }) => minifiedResult(await client.get('/event/orderBaseData', { studentId, eventDate: date })));
|
|
85
85
|
server.registerTool('mhlb_get_order_form', {
|
|
86
86
|
description: 'Get the blank order model for a student on a specific lunch event — the exact structure that ' +
|
|
87
87
|
'mhlb_create_order expects back, pre-populated with the available items.',
|
|
@@ -90,7 +90,7 @@ export function registerOrderTools(server, client) {
|
|
|
90
90
|
eventId: PositiveInt.describe('Lunch event id, from mhlb_get_menu or mhlb_get_calendar.'),
|
|
91
91
|
studentId: PositiveInt.describe('Student id from mhlb_list_students.'),
|
|
92
92
|
},
|
|
93
|
-
}, async ({ eventId, studentId }) =>
|
|
93
|
+
}, async ({ eventId, studentId }) => minifiedResult(await client.get('/event/createOrder', { eventId, studentId })));
|
|
94
94
|
server.registerTool('mhlb_get_order', {
|
|
95
95
|
description: 'Get an existing order in editable form — the model mhlb_update_order expects back.',
|
|
96
96
|
annotations: toolAnnotations({ title: 'Get order', openWorld: true }),
|
|
@@ -99,7 +99,7 @@ export function registerOrderTools(server, client) {
|
|
|
99
99
|
eventId: PositiveInt.optional().describe('Lunch event id.'),
|
|
100
100
|
studentId: PositiveInt.optional().describe('Student id.'),
|
|
101
101
|
},
|
|
102
|
-
}, async ({ orderId, eventId, studentId }) =>
|
|
102
|
+
}, async ({ orderId, eventId, studentId }) => minifiedResult(await client.get('/event/editOrder', { orderId, eventId, studentId })));
|
|
103
103
|
server.registerTool('mhlb_create_order', {
|
|
104
104
|
description: 'Place a lunch order into the cart. Call mhlb_get_order_form first and send that model back with ' +
|
|
105
105
|
'quantities set. This adds to the cart — it does not pay; use mhlb_checkout for that.' + UNVERIFIED,
|
|
@@ -111,7 +111,7 @@ export function registerOrderTools(server, client) {
|
|
|
111
111
|
'This adds the lunch to the cart. Payment is a separate step (mhlb_checkout).',
|
|
112
112
|
]);
|
|
113
113
|
}
|
|
114
|
-
return
|
|
114
|
+
return minifiedResult(await client.write('/event/createOrder', order));
|
|
115
115
|
});
|
|
116
116
|
server.registerTool('mhlb_update_order', {
|
|
117
117
|
description: 'Change an existing lunch order. Call mhlb_get_order first and send that model back with your edits — ' +
|
|
@@ -124,7 +124,7 @@ export function registerOrderTools(server, client) {
|
|
|
124
124
|
'This is a whole-order replace: items missing from `order` are removed, not preserved.',
|
|
125
125
|
]);
|
|
126
126
|
}
|
|
127
|
-
return
|
|
127
|
+
return minifiedResult(await client.write('/event/editOrder', order));
|
|
128
128
|
});
|
|
129
129
|
server.registerTool('mhlb_delete_order', {
|
|
130
130
|
description: 'Cancel a lunch order. If it was already paid for, the refund behaviour is whatever My Hot Lunchbox ' +
|
|
@@ -139,6 +139,6 @@ export function registerOrderTools(server, client) {
|
|
|
139
139
|
'isRepeated: true removes the whole recurring series, not just this date.',
|
|
140
140
|
]);
|
|
141
141
|
}
|
|
142
|
-
return
|
|
142
|
+
return minifiedResult(await client.write('/event/deleteOrder', body));
|
|
143
143
|
});
|
|
144
144
|
}
|
package/dist/tools/reports.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
3
|
import { McpToolError, readEnvVar, toolAnnotations, PositiveInt, IsoDate } from '@chrischall/mcp-utils';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import {
|
|
5
|
+
import { minifiedResult } from './_shared.js';
|
|
6
6
|
/**
|
|
7
7
|
* The `/parentReports/print*` endpoints are declared `responseType: 'blob'` in
|
|
8
8
|
* the site's own client and stream a wkhtmltopdf-generated PDF. They generate a
|
|
@@ -73,7 +73,7 @@ function deliver(bytes, contentType, filename, inline) {
|
|
|
73
73
|
if (bytes.byteLength > MAX_INLINE_BYTES) {
|
|
74
74
|
throw new McpToolError(`Report is ${bytes.byteLength} bytes — too large to return inline (limit ${MAX_INLINE_BYTES}).`, { hint: 'Call again with inline: false to write it to a file instead.' });
|
|
75
75
|
}
|
|
76
|
-
return
|
|
76
|
+
return minifiedResult({
|
|
77
77
|
filename,
|
|
78
78
|
contentType,
|
|
79
79
|
bytes: bytes.byteLength,
|
|
@@ -96,7 +96,7 @@ function deliver(bytes, contentType, filename, inline) {
|
|
|
96
96
|
cause,
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
|
-
return
|
|
99
|
+
return minifiedResult({ path, contentType, bytes: bytes.byteLength });
|
|
100
100
|
}
|
|
101
101
|
/** The midpoint date the calendar report uses to title the PDF. */
|
|
102
102
|
export function midpoint(start, end) {
|
package/dist/tools/students.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { toolAnnotations, PositiveInt, schemaConfirm } from '@chrischall/mcp-utils';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import { UNVERIFIED, minifiedResult, preview } from './_shared.js';
|
|
4
4
|
/**
|
|
5
5
|
* The student form is a read-modify-write pair: `GET /parent/editChild` returns
|
|
6
6
|
* the populated model, `POST /parent/editChild` takes it back. Rather than
|
|
@@ -16,19 +16,19 @@ export function registerStudentTools(server, client) {
|
|
|
16
16
|
'calendar and ordering tool.',
|
|
17
17
|
annotations: toolAnnotations({ title: 'List students', openWorld: true }),
|
|
18
18
|
inputSchema: {},
|
|
19
|
-
}, async () =>
|
|
19
|
+
}, async () => minifiedResult(await client.get('/parent/childrenInfo')));
|
|
20
20
|
server.registerTool('mhlb_get_student_form', {
|
|
21
21
|
description: 'Get the editable profile for one student — school, grade, teacher, delivery location, allergies and ' +
|
|
22
22
|
'the dropdown options for each. Returns the exact model that mhlb_update_student expects back.',
|
|
23
23
|
annotations: toolAnnotations({ title: 'Get student form', openWorld: true }),
|
|
24
24
|
inputSchema: { studentId: PositiveInt.describe('Student id from mhlb_list_students.') },
|
|
25
|
-
}, async ({ studentId }) =>
|
|
25
|
+
}, async ({ studentId }) => minifiedResult(await client.get('/parent/editChild', { childId: studentId })));
|
|
26
26
|
server.registerTool('mhlb_new_student_form', {
|
|
27
27
|
description: 'Get a blank student profile plus the school/grade/teacher dropdown options, ready to fill in and ' +
|
|
28
28
|
'pass to mhlb_create_student.',
|
|
29
29
|
annotations: toolAnnotations({ title: 'New student form', openWorld: true }),
|
|
30
30
|
inputSchema: {},
|
|
31
|
-
}, async () =>
|
|
31
|
+
}, async () => minifiedResult(await client.get('/parent/createChild')));
|
|
32
32
|
server.registerTool('mhlb_create_student', {
|
|
33
33
|
description: 'Add a student to the account. Call mhlb_new_student_form first and send that model back with the ' +
|
|
34
34
|
'fields filled in.' + UNVERIFIED,
|
|
@@ -37,7 +37,7 @@ export function registerStudentTools(server, client) {
|
|
|
37
37
|
}, async ({ student, confirm }) => {
|
|
38
38
|
if (!confirm)
|
|
39
39
|
return preview('Create student', { method: 'POST', path: '/parent/createChild', body: student });
|
|
40
|
-
return
|
|
40
|
+
return minifiedResult(await client.write('/parent/createChild', student));
|
|
41
41
|
});
|
|
42
42
|
server.registerTool('mhlb_update_student', {
|
|
43
43
|
description: 'Update a student profile. Call mhlb_get_student_form first and send that model back with your edits — ' +
|
|
@@ -50,7 +50,7 @@ export function registerStudentTools(server, client) {
|
|
|
50
50
|
'This is a whole-record replace: any field missing from `student` is cleared, not preserved.',
|
|
51
51
|
]);
|
|
52
52
|
}
|
|
53
|
-
return
|
|
53
|
+
return minifiedResult(await client.write('/parent/editChild', student));
|
|
54
54
|
});
|
|
55
55
|
server.registerTool('mhlb_delete_student', {
|
|
56
56
|
description: 'Remove a student from the account. Irreversible from this API — their order history goes with them.' +
|
|
@@ -64,6 +64,6 @@ export function registerStudentTools(server, client) {
|
|
|
64
64
|
if (!confirm) {
|
|
65
65
|
return preview('Delete student', { method: 'POST', path: '/parent/deleteChild', query: { id: studentId } }, ['Deleting a student is not reversible through this API.']);
|
|
66
66
|
}
|
|
67
|
-
return
|
|
67
|
+
return minifiedResult(await client.write('/parent/deleteChild', undefined, { id: studentId }));
|
|
68
68
|
});
|
|
69
69
|
}
|
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.5.0'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myhotlunchbox-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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>",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"capture:writes": "node scripts/capture-writes.mjs"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@chrischall/mcp-utils": "^0.
|
|
48
|
+
"@chrischall/mcp-utils": "^0.23.0",
|
|
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.5.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "myhotlunchbox-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.5.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|