myhotlunchbox-mcp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.2.0"
10
+ "version": "0.3.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "My Hot Lunchbox",
16
16
  "source": "./",
17
17
  "description": "MCP server for My Hot Lunchbox — school lunch calendar, ordering, and payments. Signs in server-side with the parent account credentials.",
18
- "version": "0.2.0",
18
+ "version": "0.3.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "myhotlunchbox-mcp",
3
3
  "displayName": "My Hot Lunchbox",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "MCP server for My Hot Lunchbox — order school lunches, manage students, and track payments via natural language",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/README.md CHANGED
@@ -71,18 +71,38 @@ directory); existing files are never overwritten.
71
71
  Every mutating tool takes `confirm`. Without `confirm: true` it makes **no**
72
72
  network call and returns a dry-run preview of exactly what it would send.
73
73
 
74
- `mhlb_checkout` charges a real payment method, so it takes one extra safeguard:
75
- an `expectedTotal` that must match the total in the payment payload. Price the
76
- cart with `mhlb_init_checkout`, read the total it reports, and pass that figure.
77
- A stale cart fails closed instead of paying a different amount.
78
-
79
- ### Unverified writes
80
-
81
- The write endpoints' paths, verbs and query parameters were extracted from the
82
- site's own compiled API client and are reliable. Their **request bodies have not
83
- been exercised against a live account** — every such tool says so in its
84
- description. Inspect the dry-run preview before confirming, and re-read the
85
- resource afterwards: a `200` is not proof a write persisted.
74
+ `mhlb_checkout` charges a real payment method. The server prices the charge from
75
+ `orderIds`, so nothing client-side can bind the amount — there is no total in the
76
+ request to check against. `expectedTotal` is therefore **attribution, not a
77
+ guard**: you state what you expected, and it is recorded in the dry run and in
78
+ the result so an unexpected charge is traceable to the call that made it. What
79
+ the tool does refuse outright is paying a non-zero total with no `orderIds`.
80
+
81
+ ### Writes: shapes captured, acceptance unverified
82
+
83
+ `npm run capture:writes` runs every mutating tool against a local proxy that
84
+ forwards reads to the real service but answers writes itself, so the payloads
85
+ are built from genuine server models and nothing happens upstream. It also
86
+ proves all 13 refuse to send anything without `confirm: true`.
87
+
88
+ What that established, and corrected: `mhlb_delete_order` and
89
+ `mhlb_unsubscribe_order` take `{orderId, eventDate, studentId, isRepeated,
90
+ isSubscribed}` — not the order model — and checkout takes
91
+ `{orderIds, checkoutType, couponCode, giftCardCode, schoolDonations}`.
92
+
93
+ **What is still unverified is whether the server accepts these bodies.** Shape
94
+ is not acceptance; only a real write shows that, and none has been made. Inspect
95
+ the dry-run preview before confirming, and re-read afterwards — a `200` is not
96
+ proof a write persisted.
97
+
98
+ Two limits on `mhlb_checkout` specifically:
99
+
100
+ - It can only pay with a card **already saved** on the account. Paying with a
101
+ new card needs a Stripe token minted by Stripe.js in a browser, which no
102
+ server-side client can produce.
103
+ - It generates an idempotency key and returns it. If a checkout fails
104
+ ambiguously, retry with that same `idempotencyKey` rather than a fresh call —
105
+ that is what stops a retry becoming a second charge.
86
106
 
87
107
  ## Ordering is read-modify-write
88
108
 
package/dist/auth.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { McpToolError, truncateErrorMessage } from '@chrischall/mcp-utils';
2
2
  import { TokenManager } from '@chrischall/mcp-utils/session';
3
3
  import { API_PREFIX, OAUTH_SCOPE } from './config.js';
4
+ import { createTokenCache, reportCacheWriteFailure } from './token-cache.js';
4
5
  /** Fallback lifetime when the server omits `expires_in`. */
5
6
  const DEFAULT_TOKEN_LIFETIME_S = 3600;
6
7
  /**
@@ -132,13 +133,24 @@ export class MhlbAuth {
132
133
  if (this.loginInFlight)
133
134
  return this.loginInFlight;
134
135
  this.loginInFlight = (async () => {
135
- const body = await this.passwordLogin();
136
136
  const manager = new TokenManager({
137
- initial: {
138
- accessToken: this.assertAccessToken(body),
139
- refreshToken: body.refresh_token,
140
- expiresAt: MhlbAuth.expiryOf(body),
137
+ // The FUNCTION form, so the cache is consulted before the password
138
+ // grant runs at all — the eager object form skips persistence, which
139
+ // would mean a cache that is written and never read.
140
+ initial: async () => {
141
+ const body = await this.passwordLogin();
142
+ return {
143
+ accessToken: this.assertAccessToken(body),
144
+ refreshToken: body.refresh_token,
145
+ expiresAt: MhlbAuth.expiryOf(body),
146
+ };
141
147
  },
148
+ persistence: createTokenCache() ?? undefined,
149
+ onPersistError: reportCacheWriteFailure,
150
+ // `refresh` below already recovers from a revoked refresh token by
151
+ // falling back to a full login, so the library's own re-mint-on-revoked
152
+ // recovery would only add a third attempt after both have failed.
153
+ isRefreshRevoked: () => false,
142
154
  refresh: async (refreshToken) => {
143
155
  let next;
144
156
  try {