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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +32 -12
- package/dist/auth.js +17 -5
- package/dist/bundle.js +483 -98
- package/dist/token-cache.js +63 -0
- package/dist/tools/billing.js +12 -10
- package/dist/tools/checkout.js +85 -48
- package/dist/tools/orders.js +39 -9
- package/dist/version.js +1 -1
- package/mint.yaml +33 -10
- package/package.json +6 -4
- package/server.json +16 -2
- package/skills/myhotlunchbox/references/endpoints.md +29 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for My Hot Lunchbox — order school lunches, manage students, and track payments via natural language",
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.3.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "My Hot Lunchbox",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "MCP server for My Hot Lunchbox — school lunch calendar, ordering, and payments. Signs in server-side with the parent account credentials.",
|
|
18
|
-
"version": "0.
|
|
18
|
+
"version": "0.3.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myhotlunchbox-mcp",
|
|
3
3
|
"displayName": "My Hot Lunchbox",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "MCP server for My Hot Lunchbox — order school lunches, manage students, and track payments via natural language",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
// The FUNCTION form, so the cache is consulted before the password
|
|
138
|
+
// grant runs at all — the eager object form skips persistence, which
|
|
139
|
+
// would mean a cache that is written and never read.
|
|
140
|
+
initial: async () => {
|
|
141
|
+
const body = await this.passwordLogin();
|
|
142
|
+
return {
|
|
143
|
+
accessToken: this.assertAccessToken(body),
|
|
144
|
+
refreshToken: body.refresh_token,
|
|
145
|
+
expiresAt: MhlbAuth.expiryOf(body),
|
|
146
|
+
};
|
|
141
147
|
},
|
|
148
|
+
persistence: createTokenCache() ?? undefined,
|
|
149
|
+
onPersistError: reportCacheWriteFailure,
|
|
150
|
+
// `refresh` below already recovers from a revoked refresh token by
|
|
151
|
+
// falling back to a full login, so the library's own re-mint-on-revoked
|
|
152
|
+
// recovery would only add a third attempt after both have failed.
|
|
153
|
+
isRefreshRevoked: () => false,
|
|
142
154
|
refresh: async (refreshToken) => {
|
|
143
155
|
let next;
|
|
144
156
|
try {
|