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.
@@ -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.3.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.3.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.3.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
- 34 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.
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/index.js
31348
- function textResult(data) {
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
- var jsonResult = textResult;
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.3.0";
32251
+ var VERSION = "0.5.0";
32244
32252
 
32245
32253
  // src/tools/_shared.ts
32246
32254
  function preview(action, request, notes) {
32247
- return jsonResult({
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 () => jsonResult(await client2.get("/auth/userinfo"))
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 jsonResult({ reset: true, hadActiveSession: wasAuthenticated });
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 () => jsonResult(await client2.get("/parent/childrenInfo"))
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 }) => jsonResult(await client2.get("/parent/editChild", { childId: 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 () => jsonResult(await client2.get("/parent/createChild"))
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 jsonResult(await client2.write("/parent/createChild", student));
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 jsonResult(await client2.write("/parent/editChild", student));
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 jsonResult(await client2.write("/parent/deleteChild", void 0, { id: studentId }));
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 }) => jsonResult(await client2.write("/calendar/studentSchoolData", { start: startDate, end: 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 }) => jsonResult(await client2.get("/calendar/studentOrderItems", { 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 }) => jsonResult(
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 }) => jsonResult(
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 }) => jsonResult(await client2.get("/event/orderBaseData", { studentId, eventDate: 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 }) => jsonResult(await client2.get("/event/createOrder", { 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 }) => jsonResult(await client2.get("/event/editOrder", { 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 jsonResult(await client2.write("/event/createOrder", order));
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 jsonResult(await client2.write("/event/editOrder", order));
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 jsonResult(await client2.write("/event/deleteOrder", body));
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 }) => jsonResult(await client2.get("/event/transactionsList", { selectedPeriod: period, selectedStudentId: 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 }) => jsonResult(await client2.get("/event/transactionDetails", { id: 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 }) => jsonResult(await client2.get("/event/upcomingSubscriptions", { 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 () => jsonResult(await client2.get("/event/subscription"))
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 jsonResult(
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 jsonResult(await client2.write("/event/unsubcribeOrder", body));
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 () => jsonResult(await client2.get("/parent/giftCardDataTables"))
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 jsonResult(await client2.write("/parent/applyGiftCard", void 0, { giftCardCode: code }));
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 () => jsonResult(await client2.get("/parent/coupon"))
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 jsonResult(await client2.write("/parent/applyCoupon", void 0, { couponCode: code }));
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 jsonResult(await client2.write("/parent/removeCoupon"));
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 jsonResult(await client2.write("/payment/initCheckout", body));
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 jsonResult({ result, expectedTotal, idempotencyKey: key });
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 jsonResult({
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 jsonResult({ path, contentType, bytes: bytes.byteLength });
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
  });
@@ -1,4 +1,4 @@
1
- import { jsonResult, textResult } from '@chrischall/mcp-utils';
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 jsonResult({
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
- export { jsonResult, textResult };
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
@@ -1,5 +1,5 @@
1
1
  import { toolAnnotations } from '@chrischall/mcp-utils';
2
- import { jsonResult } from './_shared.js';
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 () => jsonResult(await client.get('/auth/userinfo')));
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 jsonResult({ reset: true, hadActiveSession: wasAuthenticated });
19
+ return minifiedResult({ reset: true, hadActiveSession: wasAuthenticated });
20
20
  });
21
21
  }
@@ -1,6 +1,6 @@
1
1
  import { toolAnnotations, PositiveInt, NonEmptyString, schemaConfirm } from '@chrischall/mcp-utils';
2
2
  import { z } from 'zod';
3
- import { jsonResult, preview, UNVERIFIED } from './_shared.js';
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 }) => jsonResult(await client.get('/event/transactionsList', { selectedPeriod: period, selectedStudentId: 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 }) => jsonResult(await client.get('/event/transactionDetails', { id: 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 }) => jsonResult(await client.get('/event/upcomingSubscriptions', { 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 () => jsonResult(await client.get('/event/subscription')));
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 jsonResult(await client.write('/parent/changeSubscriptionStatus', undefined, { isEnableSubscription: enabled }));
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 jsonResult(await client.write('/event/unsubcribeOrder', body));
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 () => jsonResult(await client.get('/parent/giftCardDataTables')));
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 jsonResult(await client.write('/parent/applyGiftCard', undefined, { giftCardCode: code }));
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 () => jsonResult(await client.get('/parent/coupon')));
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 jsonResult(await client.write('/parent/applyCoupon', undefined, { couponCode: code }));
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 jsonResult(await client.write('/parent/removeCoupon'));
99
+ return minifiedResult(await client.write('/parent/removeCoupon'));
100
100
  });
101
101
  }
@@ -1,5 +1,5 @@
1
1
  import { toolAnnotations, PositiveInt, IsoDate } from '@chrischall/mcp-utils';
2
- import { jsonResult } from './_shared.js';
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 }) => jsonResult(await client.write('/calendar/studentSchoolData', { start: startDate, end: 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 }) => jsonResult(await client.get('/calendar/studentOrderItems', { studentId, date })));
24
+ }, async ({ studentId, date }) => minifiedResult(await client.get('/calendar/studentOrderItems', { studentId, date })));
25
25
  }
@@ -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 { jsonResult, preview, UNVERIFIED } from './_shared.js';
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 jsonResult(await client.write('/payment/initCheckout', body));
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 jsonResult({ result, expectedTotal, idempotencyKey: key });
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
+ }
@@ -1,6 +1,6 @@
1
1
  import { toolAnnotations, PositiveInt, IsoDate, schemaConfirm } from '@chrischall/mcp-utils';
2
2
  import { z } from 'zod';
3
- import { jsonResult, preview, UNVERIFIED } from './_shared.js';
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 }) => jsonResult(await client.get('/event/shoppingCart', {
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 }) => jsonResult(await client.get('/event/ShoppingCartBaseData', {
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 }) => jsonResult(await client.get('/event/orderBaseData', { studentId, eventDate: 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 }) => jsonResult(await client.get('/event/createOrder', { 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 }) => jsonResult(await client.get('/event/editOrder', { 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 jsonResult(await client.write('/event/createOrder', order));
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 jsonResult(await client.write('/event/editOrder', order));
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 jsonResult(await client.write('/event/deleteOrder', body));
142
+ return minifiedResult(await client.write('/event/deleteOrder', body));
143
143
  });
144
144
  }
@@ -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 { jsonResult } from './_shared.js';
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 jsonResult({
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 jsonResult({ path, contentType, bytes: bytes.byteLength });
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) {
@@ -1,6 +1,6 @@
1
1
  import { toolAnnotations, PositiveInt, schemaConfirm } from '@chrischall/mcp-utils';
2
2
  import { z } from 'zod';
3
- import { jsonResult, preview, UNVERIFIED } from './_shared.js';
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 () => jsonResult(await client.get('/parent/childrenInfo')));
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 }) => jsonResult(await client.get('/parent/editChild', { childId: 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 () => jsonResult(await client.get('/parent/createChild')));
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 jsonResult(await client.write('/parent/createChild', student));
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 jsonResult(await client.write('/parent/editChild', student));
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 jsonResult(await client.write('/parent/deleteChild', undefined, { id: studentId }));
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.3.0'; // x-release-please-version
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.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.17.1",
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.3.0",
9
+ "version": "0.5.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "myhotlunchbox-mcp",
14
- "version": "0.3.0",
14
+ "version": "0.5.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },