myhotlunchbox-mcp 0.0.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 +35 -0
- package/.claude-plugin/plugin.json +22 -0
- package/.mcp.json +14 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/auth.js +192 -0
- package/dist/bundle.js +32435 -0
- package/dist/client.js +104 -0
- package/dist/config.js +18 -0
- package/dist/index.js +31 -0
- package/dist/tools/_shared.js +26 -0
- package/dist/tools/account.js +21 -0
- package/dist/tools/billing.js +97 -0
- package/dist/tools/calendar.js +50 -0
- package/dist/tools/checkout.js +87 -0
- package/dist/tools/orders.js +114 -0
- package/dist/tools/reports.js +44 -0
- package/dist/tools/students.js +69 -0
- package/dist/version.js +2 -0
- package/package.json +57 -0
- package/server.json +43 -0
- package/skills/myhotlunchbox/SKILL.md +109 -0
- package/skills/myhotlunchbox/references/endpoints.md +123 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "myhotlunchbox-mcp",
|
|
4
|
+
"owner": {
|
|
5
|
+
"name": "Chris Hall",
|
|
6
|
+
"email": "chris.c.hall@gmail.com"
|
|
7
|
+
},
|
|
8
|
+
"metadata": {
|
|
9
|
+
"description": "MCP server for My Hot Lunchbox — order school lunches, manage students, and track payments via natural language",
|
|
10
|
+
"version": "0.0.0"
|
|
11
|
+
},
|
|
12
|
+
"plugins": [
|
|
13
|
+
{
|
|
14
|
+
"name": "myhotlunchbox-mcp",
|
|
15
|
+
"displayName": "My Hot Lunchbox",
|
|
16
|
+
"source": "./",
|
|
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.0.0",
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Chris Hall"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/chrischall/myhotlunchbox-mcp",
|
|
23
|
+
"repository": "https://github.com/chrischall/myhotlunchbox-mcp",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"myhotlunchbox",
|
|
27
|
+
"school-lunch",
|
|
28
|
+
"lunch-ordering",
|
|
29
|
+
"mcp",
|
|
30
|
+
"parent"
|
|
31
|
+
],
|
|
32
|
+
"category": "productivity"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "myhotlunchbox-mcp",
|
|
3
|
+
"displayName": "My Hot Lunchbox",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"description": "MCP server for My Hot Lunchbox — order school lunches, manage students, and track payments via natural language",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Chris Hall",
|
|
8
|
+
"email": "chris.c.hall@gmail.com"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/chrischall/myhotlunchbox-mcp",
|
|
11
|
+
"repository": "https://github.com/chrischall/myhotlunchbox-mcp",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"myhotlunchbox",
|
|
15
|
+
"school-lunch",
|
|
16
|
+
"lunch-ordering",
|
|
17
|
+
"mcp",
|
|
18
|
+
"parent"
|
|
19
|
+
],
|
|
20
|
+
"skills": "./skills/",
|
|
21
|
+
"mcp": "./.mcp.json"
|
|
22
|
+
}
|
package/.mcp.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"myhotlunchbox": {
|
|
4
|
+
"command": "node",
|
|
5
|
+
"args": [
|
|
6
|
+
"${CLAUDE_PLUGIN_ROOT}/dist/index.js"
|
|
7
|
+
],
|
|
8
|
+
"env": {
|
|
9
|
+
"MYHOTLUNCHBOX_USERNAME": "${MYHOTLUNCHBOX_USERNAME}",
|
|
10
|
+
"MYHOTLUNCHBOX_PASSWORD": "${MYHOTLUNCHBOX_PASSWORD}"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Hall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# myhotlunchbox-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [My Hot Lunchbox](https://www.myhotlunchbox.com) — read the
|
|
4
|
+
school lunch calendar, manage students, place and change orders, and track
|
|
5
|
+
deliveries and payments on a parent account.
|
|
6
|
+
|
|
7
|
+
> Developed and maintained by AI (Claude Code). Use at your own discretion.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx myhotlunchbox-mcp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or as a Claude Code plugin:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
/plugin marketplace add chrischall/myhotlunchbox-mcp
|
|
19
|
+
/plugin install myhotlunchbox-mcp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Configure
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
MYHOTLUNCHBOX_USERNAME=you@example.com
|
|
26
|
+
MYHOTLUNCHBOX_PASSWORD=…
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That is the whole setup. The server performs a real server-side sign-in against
|
|
30
|
+
`ordernow.myhotlunchbox.com` (OAuth2 password grant) and renews the session with
|
|
31
|
+
the refresh token it receives — **no browser extension, no signed-in tab, no
|
|
32
|
+
captured cookie**. Nothing is written to disk.
|
|
33
|
+
|
|
34
|
+
`MYHOTLUNCHBOX_BASE_URL` overrides the app origin if it ever moves.
|
|
35
|
+
|
|
36
|
+
The server boots without credentials so a host's install-time `tools/list` probe
|
|
37
|
+
still works; the configuration error surfaces on the first tool call.
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
38 tools, all prefixed `mhlb_`.
|
|
42
|
+
|
|
43
|
+
**Account** — `mhlb_whoami`, `mhlb_session_reset`
|
|
44
|
+
|
|
45
|
+
**Students** — `mhlb_list_students`, `mhlb_get_student_form`,
|
|
46
|
+
`mhlb_new_student_form`, `mhlb_create_student`, `mhlb_update_student`,
|
|
47
|
+
`mhlb_delete_student`
|
|
48
|
+
|
|
49
|
+
**Calendar & deliveries** — `mhlb_get_calendar`, `mhlb_get_day`,
|
|
50
|
+
`mhlb_next_delivery`, `mhlb_list_upcoming_deliveries`,
|
|
51
|
+
`mhlb_list_past_deliveries`, `mhlb_list_vendors`
|
|
52
|
+
|
|
53
|
+
**Ordering** — `mhlb_get_cart`, `mhlb_get_cart_tabs`, `mhlb_get_menu`,
|
|
54
|
+
`mhlb_get_order_form`, `mhlb_get_order`, `mhlb_create_order`,
|
|
55
|
+
`mhlb_update_order`, `mhlb_delete_order`
|
|
56
|
+
|
|
57
|
+
**Billing** — `mhlb_list_transactions`, `mhlb_get_transaction`,
|
|
58
|
+
`mhlb_list_subscriptions`, `mhlb_get_subscription_settings`,
|
|
59
|
+
`mhlb_set_subscription_enabled`, `mhlb_unsubscribe_order`,
|
|
60
|
+
`mhlb_list_gift_cards`, `mhlb_apply_gift_card`, `mhlb_get_coupon`,
|
|
61
|
+
`mhlb_apply_coupon`, `mhlb_remove_coupon`
|
|
62
|
+
|
|
63
|
+
**Checkout** — `mhlb_init_checkout`, `mhlb_checkout`
|
|
64
|
+
|
|
65
|
+
**Reports** — `mhlb_print_orders`, `mhlb_print_calendar`,
|
|
66
|
+
`mhlb_print_transactions`
|
|
67
|
+
|
|
68
|
+
## Writes are confirm-gated
|
|
69
|
+
|
|
70
|
+
Every mutating tool takes `confirm`. Without `confirm: true` it makes **no**
|
|
71
|
+
network call and returns a dry-run preview of exactly what it would send.
|
|
72
|
+
|
|
73
|
+
`mhlb_checkout` charges a real payment method, so it takes one extra safeguard:
|
|
74
|
+
an `expectedTotal` that must match the total in the payment payload. Price the
|
|
75
|
+
cart with `mhlb_init_checkout`, read the total it reports, and pass that figure.
|
|
76
|
+
A stale cart fails closed instead of paying a different amount.
|
|
77
|
+
|
|
78
|
+
### Unverified writes
|
|
79
|
+
|
|
80
|
+
The write endpoints' paths, verbs and query parameters were extracted from the
|
|
81
|
+
site's own compiled API client and are reliable. Their **request bodies have not
|
|
82
|
+
been exercised against a live account** — every such tool says so in its
|
|
83
|
+
description. Inspect the dry-run preview before confirming, and re-read the
|
|
84
|
+
resource afterwards: a `200` is not proof a write persisted.
|
|
85
|
+
|
|
86
|
+
## Ordering is read-modify-write
|
|
87
|
+
|
|
88
|
+
There is no "add item X" call. Fetch the model, edit it, send it back whole:
|
|
89
|
+
|
|
90
|
+
1. `mhlb_get_menu` — what is orderable for a student on a date
|
|
91
|
+
2. `mhlb_get_order_form` — the order model to fill in
|
|
92
|
+
3. `mhlb_create_order` — send it back (with `confirm: true`)
|
|
93
|
+
4. `mhlb_init_checkout` → `mhlb_checkout` — price, then pay
|
|
94
|
+
|
|
95
|
+
Fields omitted from the payload are **cleared**, not preserved.
|
|
96
|
+
|
|
97
|
+
## Shell skill
|
|
98
|
+
|
|
99
|
+
`skills/myhotlunchbox` covers the same account from a shell with `curl` — no MCP
|
|
100
|
+
process needed. Useful in scripts, or on a machine where this server is not
|
|
101
|
+
installed.
|
|
102
|
+
|
|
103
|
+
## Notes
|
|
104
|
+
|
|
105
|
+
- Only the parent role is wired. The same API also serves school-admin and
|
|
106
|
+
vendor roles; those endpoints return `403`, which the client reports as a role
|
|
107
|
+
mismatch rather than a broken session.
|
|
108
|
+
- `docs/MYHOTLUNCHBOX-API.md` records how the API was mapped and exactly what is
|
|
109
|
+
verified. `docs/api-surface.txt` is the full 359-endpoint extraction.
|
|
110
|
+
|
|
111
|
+
## Licence
|
|
112
|
+
|
|
113
|
+
MIT
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { McpToolError, truncateErrorMessage } from '@chrischall/mcp-utils';
|
|
2
|
+
import { TokenManager } from '@chrischall/mcp-utils/session';
|
|
3
|
+
import { API_PREFIX, OAUTH_SCOPE } from './config.js';
|
|
4
|
+
/** Fallback lifetime when the server omits `expires_in`. */
|
|
5
|
+
const DEFAULT_TOKEN_LIFETIME_S = 3600;
|
|
6
|
+
/**
|
|
7
|
+
* Strip the caller's own secrets out of an upstream body before it is ever
|
|
8
|
+
* rendered to a user.
|
|
9
|
+
*
|
|
10
|
+
* `truncateErrorMessage`/`redactSecrets` match secret *shapes* (Bearer, JWT,
|
|
11
|
+
* `sk-`, …). A password has no shape, so it would survive them untouched — and
|
|
12
|
+
* this is the one error path that has just sent one. Splice out the literal
|
|
13
|
+
* values as well. The empty-needle guard matters: `split('')` would splice the
|
|
14
|
+
* placeholder between every character.
|
|
15
|
+
*/
|
|
16
|
+
export function scrubCredentials(text, secrets) {
|
|
17
|
+
let out = text;
|
|
18
|
+
for (const secret of secrets) {
|
|
19
|
+
if (!secret)
|
|
20
|
+
continue;
|
|
21
|
+
out = out.split(secret).join('[redacted]');
|
|
22
|
+
}
|
|
23
|
+
return truncateErrorMessage(out);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Owns the OAuth2 session against `/api/auth/login`.
|
|
27
|
+
*
|
|
28
|
+
* The web app uses a password grant with `offline_access`, so a real
|
|
29
|
+
* server-side login is possible — no browser bridge, no captured cookie. The
|
|
30
|
+
* refresh token renews the session; if the refresh grant itself fails (the
|
|
31
|
+
* refresh token expired or was revoked) we fall back to a full password login
|
|
32
|
+
* rather than dropping the caller into a re-auth loop.
|
|
33
|
+
*/
|
|
34
|
+
export class MhlbAuth {
|
|
35
|
+
config;
|
|
36
|
+
fetchImpl;
|
|
37
|
+
manager = null;
|
|
38
|
+
loginInFlight = null;
|
|
39
|
+
constructor(config, fetchImpl = fetch) {
|
|
40
|
+
this.config = config;
|
|
41
|
+
this.fetchImpl = fetchImpl;
|
|
42
|
+
}
|
|
43
|
+
/** Credentials are checked lazily so the server still boots without them. */
|
|
44
|
+
requireCredentials() {
|
|
45
|
+
const { username, password } = this.config;
|
|
46
|
+
if (!username || !password) {
|
|
47
|
+
throw new McpToolError('My Hot Lunchbox credentials are not configured.', {
|
|
48
|
+
hint: 'Set MYHOTLUNCHBOX_USERNAME and MYHOTLUNCHBOX_PASSWORD (the email and password you use at ' +
|
|
49
|
+
'https://ordernow.myhotlunchbox.com) in your .env or MCP host config, then retry.',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return { username, password };
|
|
53
|
+
}
|
|
54
|
+
tokenUrl() {
|
|
55
|
+
return `${this.config.baseUrl}${API_PREFIX}/auth/login`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* POST a grant to the token endpoint. The web app sends
|
|
59
|
+
* `application/x-www-form-urlencoded` (it builds the body with `qs.stringify`).
|
|
60
|
+
*/
|
|
61
|
+
async postGrant(form) {
|
|
62
|
+
const { password } = this.config;
|
|
63
|
+
let res;
|
|
64
|
+
try {
|
|
65
|
+
res = await this.fetchImpl(this.tokenUrl(), {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
69
|
+
accept: 'application/json',
|
|
70
|
+
},
|
|
71
|
+
body: new URLSearchParams(form).toString(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (cause) {
|
|
75
|
+
throw new McpToolError(`Could not reach My Hot Lunchbox at ${this.config.baseUrl}.`, { hint: 'Check network connectivity, or override MYHOTLUNCHBOX_BASE_URL if the app has moved.', cause });
|
|
76
|
+
}
|
|
77
|
+
const raw = await res.text();
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
let parsed = null;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(raw);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* non-JSON error body — fall through to the raw text */
|
|
85
|
+
}
|
|
86
|
+
const detail = parsed?.error_description ?? parsed?.error ?? raw;
|
|
87
|
+
// Never auto-retry a rejected credential: these servers count attempts.
|
|
88
|
+
throw new McpToolError(`My Hot Lunchbox rejected the sign-in (HTTP ${res.status}): ${scrubCredentials(detail, [password, form.password, form.refresh_token])}`, {
|
|
89
|
+
hint: parsed?.error === 'invalid_grant'
|
|
90
|
+
? 'Check MYHOTLUNCHBOX_USERNAME / MYHOTLUNCHBOX_PASSWORD. Do not retry with guesses — repeated failures can lock the account or force a CAPTCHA that blocks server-side sign-in entirely.'
|
|
91
|
+
: 'Sign in once at https://ordernow.myhotlunchbox.com to confirm the account is active, then retry.',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(raw);
|
|
96
|
+
}
|
|
97
|
+
catch (cause) {
|
|
98
|
+
throw new McpToolError('My Hot Lunchbox returned a non-JSON token response.', {
|
|
99
|
+
hint: 'The sign-in endpoint may have changed. Re-capture the login request from the web app.',
|
|
100
|
+
cause,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
static expiryOf(body) {
|
|
105
|
+
return Date.now() + (body.expires_in ?? DEFAULT_TOKEN_LIFETIME_S) * 1000;
|
|
106
|
+
}
|
|
107
|
+
assertAccessToken(body) {
|
|
108
|
+
if (!body.access_token) {
|
|
109
|
+
throw new McpToolError('My Hot Lunchbox returned no access token.', {
|
|
110
|
+
hint: 'The sign-in succeeded but carried no `access_token`. Re-capture the login request from the web app.',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return body.access_token;
|
|
114
|
+
}
|
|
115
|
+
/** Full password grant. */
|
|
116
|
+
async passwordLogin() {
|
|
117
|
+
const { username, password } = this.requireCredentials();
|
|
118
|
+
return this.postGrant({
|
|
119
|
+
grant_type: 'password',
|
|
120
|
+
username,
|
|
121
|
+
password,
|
|
122
|
+
scope: OAUTH_SCOPE,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Build (once) the TokenManager that fronts every authenticated call.
|
|
127
|
+
* Single-flight: concurrent first-callers share one login.
|
|
128
|
+
*/
|
|
129
|
+
async ensureManager() {
|
|
130
|
+
if (this.manager)
|
|
131
|
+
return this.manager;
|
|
132
|
+
if (this.loginInFlight)
|
|
133
|
+
return this.loginInFlight;
|
|
134
|
+
this.loginInFlight = (async () => {
|
|
135
|
+
const body = await this.passwordLogin();
|
|
136
|
+
const manager = new TokenManager({
|
|
137
|
+
initial: {
|
|
138
|
+
accessToken: this.assertAccessToken(body),
|
|
139
|
+
refreshToken: body.refresh_token,
|
|
140
|
+
expiresAt: MhlbAuth.expiryOf(body),
|
|
141
|
+
},
|
|
142
|
+
refresh: async (refreshToken) => {
|
|
143
|
+
let next;
|
|
144
|
+
try {
|
|
145
|
+
next = await this.postGrant({ grant_type: 'refresh_token', refresh_token: refreshToken });
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// The refresh token expired or was revoked. We still hold the
|
|
149
|
+
// password, so recover with a full login instead of surfacing a
|
|
150
|
+
// re-auth error the caller cannot act on.
|
|
151
|
+
next = await this.passwordLogin();
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
accessToken: this.assertAccessToken(next),
|
|
155
|
+
refreshToken: next.refresh_token,
|
|
156
|
+
expiresAt: MhlbAuth.expiryOf(next),
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
this.manager = manager;
|
|
161
|
+
return manager;
|
|
162
|
+
})();
|
|
163
|
+
try {
|
|
164
|
+
return await this.loginInFlight;
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
this.loginInFlight = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Run an authenticated request. Delegates to {@link TokenManager.withAuth},
|
|
172
|
+
* which refreshes proactively inside the skew window and replays exactly once
|
|
173
|
+
* on a `401`.
|
|
174
|
+
*/
|
|
175
|
+
async withAuth(call) {
|
|
176
|
+
const manager = await this.ensureManager();
|
|
177
|
+
return manager.withAuth(call);
|
|
178
|
+
}
|
|
179
|
+
/** Drop the cached session. Exposed for the `_signout` tool and for tests. */
|
|
180
|
+
reset() {
|
|
181
|
+
this.manager = null;
|
|
182
|
+
this.loginInFlight = null;
|
|
183
|
+
}
|
|
184
|
+
/** Raw fetch through the injected implementation (no auth attached). */
|
|
185
|
+
fetch(url, init) {
|
|
186
|
+
return this.fetchImpl(url, init);
|
|
187
|
+
}
|
|
188
|
+
/** Whether a session has been established in this process. */
|
|
189
|
+
get isAuthenticated() {
|
|
190
|
+
return this.manager !== null;
|
|
191
|
+
}
|
|
192
|
+
}
|