@chrischall/pickuppatrol-mcp 1.0.0 → 1.0.2
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/dist/auth.d.ts +12 -0
- package/dist/auth.js +57 -15
- package/dist/bundle.js +228 -39
- package/dist/client.js +4 -3
- package/dist/tools/defaults.js +12 -2
- package/dist/tools/plans.d.ts +24 -3
- package/dist/tools/plans.js +44 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "PickUp Patrol school-dismissal tools for Claude Code",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.2"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"displayName": "PickUp Patrol",
|
|
15
15
|
"source": "./",
|
|
16
16
|
"description": "Read and change your children's school dismissal plans in PickUp Patrol — defaults, day-by-day changes and school cutoff times — via MCP",
|
|
17
|
-
"version": "1.0.
|
|
17
|
+
"version": "1.0.2",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Chris Chall"
|
|
20
20
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pickuppatrol",
|
|
3
3
|
"displayName": "PickUp Patrol",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"description": "Read and change your children's school dismissal plans in PickUp Patrol — defaults, day-by-day changes and school cutoff times — via MCP",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Chall"
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ResponseStatus } from './types.js';
|
|
2
2
|
export declare const BASE_URL = "https://app.pickuppatrol.net";
|
|
3
3
|
export declare const BASE_PATH = "/api/json/reply";
|
|
4
|
+
/** Upper bound on any one request to PickUp Patrol, sign-in included. */
|
|
5
|
+
export declare const REQUEST_TIMEOUT_MS = 30000;
|
|
4
6
|
/** Minimal `fetch` seam so tests never open a socket. */
|
|
5
7
|
export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
6
8
|
/**
|
|
@@ -23,6 +25,8 @@ export interface AuthOptions {
|
|
|
23
25
|
username?: string;
|
|
24
26
|
password?: string;
|
|
25
27
|
fetchImpl?: FetchLike;
|
|
28
|
+
/** Sign-in timeout; defaults to `REQUEST_TIMEOUT_MS`. A test seam. */
|
|
29
|
+
timeoutMs?: number;
|
|
26
30
|
}
|
|
27
31
|
/**
|
|
28
32
|
* Pull the most useful message out of a ServiceStack error envelope. Field
|
|
@@ -39,6 +43,7 @@ export declare class PickUpPatrolAuth {
|
|
|
39
43
|
private readonly password;
|
|
40
44
|
private readonly configError;
|
|
41
45
|
private readonly fetchImpl;
|
|
46
|
+
private readonly timeoutMs;
|
|
42
47
|
private session;
|
|
43
48
|
private inFlight;
|
|
44
49
|
private permanentError;
|
|
@@ -57,6 +62,13 @@ export declare class PickUpPatrolAuth {
|
|
|
57
62
|
* once and the call replayed exactly once — never more, so a server that
|
|
58
63
|
* answers 401 unconditionally cannot turn into a login loop against the
|
|
59
64
|
* account.
|
|
65
|
+
*
|
|
66
|
+
* If the session minted for the replay is rejected too, the sign-in is
|
|
67
|
+
* "succeeding" without producing a usable session — in practice a
|
|
68
|
+
* two-factor account. The login-time check cannot see that (Azure's
|
|
69
|
+
* ARRAffinity cookie means the jar is never empty), so this is where it is
|
|
70
|
+
* caught, and it is cached as permanent: otherwise every later call would
|
|
71
|
+
* spend two more sign-ins against the account.
|
|
60
72
|
*/
|
|
61
73
|
withAuth(call: (session: PupSession) => Promise<Response>): Promise<Response>;
|
|
62
74
|
private login;
|
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readEnvVar, McpToolError, CookieJar } from '@chrischall/mcp-utils';
|
|
2
2
|
export const BASE_URL = 'https://app.pickuppatrol.net';
|
|
3
3
|
export const BASE_PATH = '/api/json/reply';
|
|
4
|
+
/** Upper bound on any one request to PickUp Patrol, sign-in included. */
|
|
5
|
+
export const REQUEST_TIMEOUT_MS = 30_000;
|
|
4
6
|
/**
|
|
5
7
|
* Pull the most useful message out of a ServiceStack error envelope. Field
|
|
6
8
|
* errors are more specific than the top-level message, so they win.
|
|
@@ -21,6 +23,7 @@ export class PickUpPatrolAuth {
|
|
|
21
23
|
password;
|
|
22
24
|
configError;
|
|
23
25
|
fetchImpl;
|
|
26
|
+
timeoutMs;
|
|
24
27
|
session = null;
|
|
25
28
|
inFlight = null;
|
|
26
29
|
permanentError = null;
|
|
@@ -43,6 +46,7 @@ export class PickUpPatrolAuth {
|
|
|
43
46
|
this.configError = null;
|
|
44
47
|
}
|
|
45
48
|
this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetch(url, init));
|
|
49
|
+
this.timeoutMs = opts.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
46
50
|
}
|
|
47
51
|
/** True once a login has succeeded — used by the healthcheck tool. */
|
|
48
52
|
get isAuthenticated() {
|
|
@@ -80,27 +84,64 @@ export class PickUpPatrolAuth {
|
|
|
80
84
|
* once and the call replayed exactly once — never more, so a server that
|
|
81
85
|
* answers 401 unconditionally cannot turn into a login loop against the
|
|
82
86
|
* account.
|
|
87
|
+
*
|
|
88
|
+
* If the session minted for the replay is rejected too, the sign-in is
|
|
89
|
+
* "succeeding" without producing a usable session — in practice a
|
|
90
|
+
* two-factor account. The login-time check cannot see that (Azure's
|
|
91
|
+
* ARRAffinity cookie means the jar is never empty), so this is where it is
|
|
92
|
+
* caught, and it is cached as permanent: otherwise every later call would
|
|
93
|
+
* spend two more sign-ins against the account.
|
|
83
94
|
*/
|
|
84
95
|
async withAuth(call) {
|
|
85
96
|
const first = await call(await this.ensure());
|
|
86
97
|
if (first.status !== 401)
|
|
87
98
|
return first;
|
|
88
99
|
this.invalidate();
|
|
89
|
-
|
|
100
|
+
const replay = await call(await this.ensure());
|
|
101
|
+
if (replay.status !== 401)
|
|
102
|
+
return replay;
|
|
103
|
+
this.invalidate();
|
|
104
|
+
this.permanentError = new McpToolError('PickUp Patrol accepted the sign-in but rejected the session it just issued', {
|
|
105
|
+
hint: 'This usually means the account has two-factor authentication enabled, which this server does not yet complete. Sign in at https://app.pickuppatrol.net/ to check, then restart the server.',
|
|
106
|
+
});
|
|
107
|
+
throw this.permanentError;
|
|
90
108
|
}
|
|
91
109
|
async login() {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
// Bounded like every other request. `ensure()` shares this promise with
|
|
111
|
+
// every concurrent and later caller until it settles, so an unanswered
|
|
112
|
+
// sign-in would otherwise stall every tool — the healthcheck included —
|
|
113
|
+
// for as long as undici's own ~300s default. The signal also covers the
|
|
114
|
+
// body read below.
|
|
115
|
+
const signal = AbortSignal.timeout(this.timeoutMs);
|
|
116
|
+
let res;
|
|
117
|
+
let body;
|
|
118
|
+
try {
|
|
119
|
+
res = await this.fetchImpl(`${BASE_URL}${BASE_PATH}/Authenticate`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
provider: 'credentials',
|
|
124
|
+
UserName: this.username,
|
|
125
|
+
Password: this.password,
|
|
126
|
+
RememberMe: true,
|
|
127
|
+
}),
|
|
128
|
+
redirect: 'manual',
|
|
129
|
+
signal,
|
|
130
|
+
});
|
|
131
|
+
body = (await res.json().catch((err) => {
|
|
132
|
+
if (signal.aborted)
|
|
133
|
+
throw err;
|
|
134
|
+
return null;
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
// Transient: a timeout says nothing about the credentials, so it is
|
|
139
|
+
// never cached as permanentError and the next call signs in afresh.
|
|
140
|
+
if (signal.aborted) {
|
|
141
|
+
throw new McpToolError(`PickUp Patrol did not answer the sign-in within ${this.timeoutMs / 1000}s`, { hint: 'The service may be slow or down. Try again shortly.' });
|
|
142
|
+
}
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
104
145
|
if (!res.ok) {
|
|
105
146
|
const detail = describeResponseStatus(body?.ResponseStatus);
|
|
106
147
|
const code = body?.ResponseStatus?.ErrorCode ?? '';
|
|
@@ -119,8 +160,9 @@ export class PickUpPatrolAuth {
|
|
|
119
160
|
throw error;
|
|
120
161
|
}
|
|
121
162
|
// Two-factor accounts return a session that is not yet usable; the SPA
|
|
122
|
-
// routes them to /two-factor.
|
|
123
|
-
//
|
|
163
|
+
// routes them to /two-factor. A login with no token and no cookie at all
|
|
164
|
+
// is caught here; the live deployment always sets ARRAffinity, though, so
|
|
165
|
+
// the usual two-factor signal is the rejected fresh session in withAuth().
|
|
124
166
|
const cookieHeader = collectCookieHeader(res);
|
|
125
167
|
const bearerToken = body?.BearerToken ?? null;
|
|
126
168
|
if (!bearerToken && !cookieHeader) {
|
package/dist/bundle.js
CHANGED
|
@@ -23302,6 +23302,20 @@ var require_v4 = __commonJS({
|
|
|
23302
23302
|
}
|
|
23303
23303
|
});
|
|
23304
23304
|
|
|
23305
|
+
// node_modules/@chrischall/mcp-utils/dist/cancel/index.js
|
|
23306
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
23307
|
+
var storage = new AsyncLocalStorage();
|
|
23308
|
+
function withCallSignal(signal, fn, request) {
|
|
23309
|
+
return signal ? storage.run({ signal, ...request ? { request } : {} }, fn) : fn();
|
|
23310
|
+
}
|
|
23311
|
+
|
|
23312
|
+
// node_modules/@chrischall/mcp-utils/dist/caller/index.js
|
|
23313
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
23314
|
+
var storage2 = new AsyncLocalStorage2();
|
|
23315
|
+
function withCallerCapabilities(capabilities, fn) {
|
|
23316
|
+
return capabilities ? storage2.run(capabilities, fn) : fn();
|
|
23317
|
+
}
|
|
23318
|
+
|
|
23305
23319
|
// node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
23306
23320
|
var __create2 = Object.create;
|
|
23307
23321
|
var __defProp2 = Object.defineProperty;
|
|
@@ -37977,13 +37991,28 @@ var API_KEY_RE = new RegExp([
|
|
|
37977
37991
|
"whsec_[A-Za-z0-9]{16,}\\b"
|
|
37978
37992
|
// webhook signing secret (Stripe-style)
|
|
37979
37993
|
].map((p) => `\\b${p}`).join("|"), "g");
|
|
37980
|
-
var QUERY_SECRET_RE = /([?&](?:
|
|
37994
|
+
var QUERY_SECRET_RE = /([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
|
|
37995
|
+
var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
|
|
37996
|
+
var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
|
|
37997
|
+
var HEADER_SECRET_RE = /(\b(?:x[-_][a-z0-9_-]*?(?:api[-_]?key|token|secret|auth)[a-z0-9_-]*|api[-_]?key)\s*:\s*)[^\s,;"'<>`]+/gi;
|
|
37981
37998
|
var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
|
|
37982
|
-
var JSON_SECRET_KEYS =
|
|
37983
|
-
|
|
37984
|
-
|
|
37999
|
+
var JSON_SECRET_KEYS = [
|
|
38000
|
+
"(?:access|refresh|id|auth|session|bearer|csrf|xsrf)[_-]?token",
|
|
38001
|
+
"client[_-]?secret",
|
|
38002
|
+
"api[_-]?secret",
|
|
38003
|
+
"(?:x[_-])?api[_-]?key",
|
|
38004
|
+
"private[_-]?key",
|
|
38005
|
+
"x[_-][a-z0-9_-]*?(?:api[_-]?key|token|secret|auth)[a-z0-9_-]*",
|
|
38006
|
+
"(?:proxy-)?authorization",
|
|
38007
|
+
"password",
|
|
38008
|
+
"passwd",
|
|
38009
|
+
"secret",
|
|
38010
|
+
"token"
|
|
38011
|
+
].join("|");
|
|
38012
|
+
var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")(?:[^"\\\\]|\\\\.)*(")`, "gi");
|
|
38013
|
+
var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')(?:[^'\\\\]|\\\\.)*(')`, "gi");
|
|
37985
38014
|
function redactSecrets(text) {
|
|
37986
|
-
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
38015
|
+
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(HEADER_SECRET_RE, "$1[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(OAUTH_CODE_RE, "$1[REDACTED]").replace(FORM_PASSWORD_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
|
|
37987
38016
|
}
|
|
37988
38017
|
function messageOf(err) {
|
|
37989
38018
|
if (err instanceof Error)
|
|
@@ -57690,17 +57719,73 @@ Hint: ${err.hint}`);
|
|
|
57690
57719
|
}
|
|
57691
57720
|
throw err;
|
|
57692
57721
|
}
|
|
57722
|
+
function mcpRequestFrom(args) {
|
|
57723
|
+
const ctx = args.at(-1);
|
|
57724
|
+
if (typeof ctx !== "object" || ctx === null)
|
|
57725
|
+
return void 0;
|
|
57726
|
+
const req = ctx.mcpReq;
|
|
57727
|
+
if (typeof req !== "object" || req === null)
|
|
57728
|
+
return void 0;
|
|
57729
|
+
return typeof req.notify === "function" ? req : void 0;
|
|
57730
|
+
}
|
|
57731
|
+
function callSignalFrom(args) {
|
|
57732
|
+
const ctx = args.at(-1);
|
|
57733
|
+
if (typeof ctx !== "object" || ctx === null)
|
|
57734
|
+
return void 0;
|
|
57735
|
+
const req = ctx.mcpReq;
|
|
57736
|
+
if (typeof req !== "object" || req === null)
|
|
57737
|
+
return void 0;
|
|
57738
|
+
const signal = req.signal;
|
|
57739
|
+
return signal instanceof AbortSignal ? signal : void 0;
|
|
57740
|
+
}
|
|
57741
|
+
function declaredCapabilitiesFrom(server) {
|
|
57742
|
+
const inner = server.server;
|
|
57743
|
+
if (typeof inner !== "object" || inner === null)
|
|
57744
|
+
return void 0;
|
|
57745
|
+
const read = inner.getClientCapabilities;
|
|
57746
|
+
if (typeof read !== "function")
|
|
57747
|
+
return void 0;
|
|
57748
|
+
try {
|
|
57749
|
+
const declared = read.call(inner);
|
|
57750
|
+
return typeof declared === "object" && declared !== null ? declared : void 0;
|
|
57751
|
+
} catch {
|
|
57752
|
+
return void 0;
|
|
57753
|
+
}
|
|
57754
|
+
}
|
|
57693
57755
|
function surfaceToolHints(server) {
|
|
57694
57756
|
const register2 = server.registerTool.bind(server);
|
|
57695
|
-
server.registerTool = (name, config2, cb) => register2(name, config2, (...args) =>
|
|
57696
|
-
|
|
57697
|
-
|
|
57698
|
-
|
|
57699
|
-
|
|
57700
|
-
|
|
57701
|
-
|
|
57702
|
-
|
|
57703
|
-
|
|
57757
|
+
server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => (
|
|
57758
|
+
// THE CALLER'S CANCELLATION, made ambient for the whole handler
|
|
57759
|
+
// (`cancel/index.ts`). The SDK delivers it and the fleet ignored it:
|
|
57760
|
+
// measured against @modelcontextprotocol/server 2.0.0, a cancelled
|
|
57761
|
+
// call aborts `ctx.mcpReq.signal` with the caller's reason and the
|
|
57762
|
+
// handler runs to completion regardless — so the HTTP request stays
|
|
57763
|
+
// in flight, the child keeps burning metered CPU, and the upstream
|
|
57764
|
+
// keeps being hit for somebody who has gone. claude.ai sent 101
|
|
57765
|
+
// cancellations in the week to 2026-09-20.
|
|
57766
|
+
//
|
|
57767
|
+
// Here because this is the one wrapper every tool already passes
|
|
57768
|
+
// through: threading the signal by hand would mean editing several
|
|
57769
|
+
// hundred handlers and missing exactly the ones nobody edits.
|
|
57770
|
+
withCallSignal(callSignalFrom(args), () => (
|
|
57771
|
+
// WHAT THE CALLER CAN DO, made ambient for the same reason and in
|
|
57772
|
+
// the same place (`caller/index.ts`). Only the 2025-era half needs
|
|
57773
|
+
// the wrapper — a 2026-07-28 request answers for itself off its own
|
|
57774
|
+
// envelope — but a guarded tool that cannot tell whether the caller
|
|
57775
|
+
// can be shown a prompt returns one the SDK then refuses to deliver,
|
|
57776
|
+
// and that refusal reaches the caller as an unexplained protocol
|
|
57777
|
+
// error it had no chance to catch.
|
|
57778
|
+
withCallerCapabilities(declaredCapabilitiesFrom(server), () => {
|
|
57779
|
+
let result;
|
|
57780
|
+
try {
|
|
57781
|
+
result = cb(...args);
|
|
57782
|
+
} catch (err) {
|
|
57783
|
+
return hintResultOrRethrow(err);
|
|
57784
|
+
}
|
|
57785
|
+
return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
|
|
57786
|
+
})
|
|
57787
|
+
), mcpRequestFrom(args))
|
|
57788
|
+
));
|
|
57704
57789
|
}
|
|
57705
57790
|
async function createMcpServer(opts) {
|
|
57706
57791
|
const server = new McpServer({ name: opts.name, version: opts.version }, { supportedProtocolVersions: [...SERVER_PROTOCOL_VERSIONS] });
|
|
@@ -57715,13 +57800,47 @@ async function createMcpServer(opts) {
|
|
|
57715
57800
|
}
|
|
57716
57801
|
return server;
|
|
57717
57802
|
}
|
|
57803
|
+
var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
57804
|
+
var DEFAULT_REPEAT_SIGNAL_GRACE_MS = 500;
|
|
57718
57805
|
function withGracefulShutdown(target, opts = {}) {
|
|
57719
57806
|
const shouldExit = opts.exit ?? true;
|
|
57807
|
+
if (opts.timeoutMs !== void 0 && !(Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0)) {
|
|
57808
|
+
throw new RangeError("withGracefulShutdown: timeoutMs must be a finite number greater than 0.");
|
|
57809
|
+
}
|
|
57810
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
57811
|
+
if (opts.repeatSignalGraceMs !== void 0 && !(Number.isFinite(opts.repeatSignalGraceMs) && opts.repeatSignalGraceMs >= 0)) {
|
|
57812
|
+
throw new RangeError("withGracefulShutdown: repeatSignalGraceMs must be a finite number >= 0.");
|
|
57813
|
+
}
|
|
57814
|
+
const repeatGraceMs = opts.repeatSignalGraceMs ?? DEFAULT_REPEAT_SIGNAL_GRACE_MS;
|
|
57720
57815
|
let shuttingDown = false;
|
|
57816
|
+
let firstSignalAt = 0;
|
|
57817
|
+
let exited = false;
|
|
57818
|
+
const exitOnce = () => {
|
|
57819
|
+
if (exited)
|
|
57820
|
+
return;
|
|
57821
|
+
exited = true;
|
|
57822
|
+
process.exit(0);
|
|
57823
|
+
};
|
|
57721
57824
|
const handler = (signal) => {
|
|
57722
|
-
if (shuttingDown)
|
|
57825
|
+
if (shuttingDown) {
|
|
57826
|
+
if (Date.now() - firstSignalAt < repeatGraceMs)
|
|
57827
|
+
return;
|
|
57828
|
+
if (shouldExit) {
|
|
57829
|
+
console.error(`[mcp-utils] second ${signal} during shutdown \u2014 exiting now`);
|
|
57830
|
+
exitOnce();
|
|
57831
|
+
}
|
|
57723
57832
|
return;
|
|
57833
|
+
}
|
|
57724
57834
|
shuttingDown = true;
|
|
57835
|
+
firstSignalAt = Date.now();
|
|
57836
|
+
let timer;
|
|
57837
|
+
if (shouldExit) {
|
|
57838
|
+
timer = setTimeout(() => {
|
|
57839
|
+
console.error(`[mcp-utils] graceful shutdown on ${signal} did not finish in ${timeoutMs}ms \u2014 exiting`);
|
|
57840
|
+
exitOnce();
|
|
57841
|
+
}, timeoutMs);
|
|
57842
|
+
timer.unref?.();
|
|
57843
|
+
}
|
|
57725
57844
|
void (async () => {
|
|
57726
57845
|
try {
|
|
57727
57846
|
if (opts.onSignal)
|
|
@@ -57730,8 +57849,10 @@ function withGracefulShutdown(target, opts = {}) {
|
|
|
57730
57849
|
} catch (err) {
|
|
57731
57850
|
console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
|
|
57732
57851
|
} finally {
|
|
57852
|
+
if (timer !== void 0)
|
|
57853
|
+
clearTimeout(timer);
|
|
57733
57854
|
if (shouldExit)
|
|
57734
|
-
|
|
57855
|
+
exitOnce();
|
|
57735
57856
|
}
|
|
57736
57857
|
})();
|
|
57737
57858
|
};
|
|
@@ -57902,6 +58023,7 @@ import { fileURLToPath } from "url";
|
|
|
57902
58023
|
// src/auth.ts
|
|
57903
58024
|
var BASE_URL = "https://app.pickuppatrol.net";
|
|
57904
58025
|
var BASE_PATH = "/api/json/reply";
|
|
58026
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
57905
58027
|
function describeResponseStatus(status) {
|
|
57906
58028
|
if (!status) return null;
|
|
57907
58029
|
const fieldError = status.Errors?.find((e) => e?.Message);
|
|
@@ -57912,6 +58034,7 @@ var PickUpPatrolAuth = class {
|
|
|
57912
58034
|
password;
|
|
57913
58035
|
configError;
|
|
57914
58036
|
fetchImpl;
|
|
58037
|
+
timeoutMs;
|
|
57915
58038
|
session = null;
|
|
57916
58039
|
inFlight = null;
|
|
57917
58040
|
permanentError = null;
|
|
@@ -57933,6 +58056,7 @@ var PickUpPatrolAuth = class {
|
|
|
57933
58056
|
this.configError = null;
|
|
57934
58057
|
}
|
|
57935
58058
|
this.fetchImpl = opts.fetchImpl ?? ((url4, init) => fetch(url4, init));
|
|
58059
|
+
this.timeoutMs = opts.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
57936
58060
|
}
|
|
57937
58061
|
/** True once a login has succeeded — used by the healthcheck tool. */
|
|
57938
58062
|
get isAuthenticated() {
|
|
@@ -57964,26 +58088,59 @@ var PickUpPatrolAuth = class {
|
|
|
57964
58088
|
* once and the call replayed exactly once — never more, so a server that
|
|
57965
58089
|
* answers 401 unconditionally cannot turn into a login loop against the
|
|
57966
58090
|
* account.
|
|
58091
|
+
*
|
|
58092
|
+
* If the session minted for the replay is rejected too, the sign-in is
|
|
58093
|
+
* "succeeding" without producing a usable session — in practice a
|
|
58094
|
+
* two-factor account. The login-time check cannot see that (Azure's
|
|
58095
|
+
* ARRAffinity cookie means the jar is never empty), so this is where it is
|
|
58096
|
+
* caught, and it is cached as permanent: otherwise every later call would
|
|
58097
|
+
* spend two more sign-ins against the account.
|
|
57967
58098
|
*/
|
|
57968
58099
|
async withAuth(call) {
|
|
57969
58100
|
const first = await call(await this.ensure());
|
|
57970
58101
|
if (first.status !== 401) return first;
|
|
57971
58102
|
this.invalidate();
|
|
57972
|
-
|
|
58103
|
+
const replay = await call(await this.ensure());
|
|
58104
|
+
if (replay.status !== 401) return replay;
|
|
58105
|
+
this.invalidate();
|
|
58106
|
+
this.permanentError = new McpToolError(
|
|
58107
|
+
"PickUp Patrol accepted the sign-in but rejected the session it just issued",
|
|
58108
|
+
{
|
|
58109
|
+
hint: "This usually means the account has two-factor authentication enabled, which this server does not yet complete. Sign in at https://app.pickuppatrol.net/ to check, then restart the server."
|
|
58110
|
+
}
|
|
58111
|
+
);
|
|
58112
|
+
throw this.permanentError;
|
|
57973
58113
|
}
|
|
57974
58114
|
async login() {
|
|
57975
|
-
const
|
|
57976
|
-
|
|
57977
|
-
|
|
57978
|
-
|
|
57979
|
-
|
|
57980
|
-
|
|
57981
|
-
|
|
57982
|
-
|
|
57983
|
-
|
|
57984
|
-
|
|
57985
|
-
|
|
57986
|
-
|
|
58115
|
+
const signal = AbortSignal.timeout(this.timeoutMs);
|
|
58116
|
+
let res;
|
|
58117
|
+
let body;
|
|
58118
|
+
try {
|
|
58119
|
+
res = await this.fetchImpl(`${BASE_URL}${BASE_PATH}/Authenticate`, {
|
|
58120
|
+
method: "POST",
|
|
58121
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
58122
|
+
body: JSON.stringify({
|
|
58123
|
+
provider: "credentials",
|
|
58124
|
+
UserName: this.username,
|
|
58125
|
+
Password: this.password,
|
|
58126
|
+
RememberMe: true
|
|
58127
|
+
}),
|
|
58128
|
+
redirect: "manual",
|
|
58129
|
+
signal
|
|
58130
|
+
});
|
|
58131
|
+
body = await res.json().catch((err) => {
|
|
58132
|
+
if (signal.aborted) throw err;
|
|
58133
|
+
return null;
|
|
58134
|
+
});
|
|
58135
|
+
} catch (err) {
|
|
58136
|
+
if (signal.aborted) {
|
|
58137
|
+
throw new McpToolError(
|
|
58138
|
+
`PickUp Patrol did not answer the sign-in within ${this.timeoutMs / 1e3}s`,
|
|
58139
|
+
{ hint: "The service may be slow or down. Try again shortly." }
|
|
58140
|
+
);
|
|
58141
|
+
}
|
|
58142
|
+
throw err;
|
|
58143
|
+
}
|
|
57987
58144
|
if (!res.ok) {
|
|
57988
58145
|
const detail = describeResponseStatus(body?.ResponseStatus);
|
|
57989
58146
|
const code = body?.ResponseStatus?.ErrorCode ?? "";
|
|
@@ -58024,7 +58181,6 @@ try {
|
|
|
58024
58181
|
await loadDotenvSafely({ path: join(dir, "..", ".env"), override: false });
|
|
58025
58182
|
} catch {
|
|
58026
58183
|
}
|
|
58027
|
-
var REQUEST_TIMEOUT_MS = 3e4;
|
|
58028
58184
|
var PickUpPatrolClient = class {
|
|
58029
58185
|
auth;
|
|
58030
58186
|
fetchImpl;
|
|
@@ -58069,7 +58225,9 @@ var PickUpPatrolClient = class {
|
|
|
58069
58225
|
throw new McpToolError(
|
|
58070
58226
|
`PickUp Patrol ${dto} failed (HTTP ${res.status})${detail ? `: ${detail}` : ""}`,
|
|
58071
58227
|
{
|
|
58072
|
-
|
|
58228
|
+
// A 401 never gets here: withAuth() re-signs-in once and turns a
|
|
58229
|
+
// second 401 into a permanent unusable-session error.
|
|
58230
|
+
hint: res.status === 403 ? "The session was rejected. Check PICKUPPATROL_USERNAME and PICKUPPATROL_PASSWORD." : void 0
|
|
58073
58231
|
}
|
|
58074
58232
|
);
|
|
58075
58233
|
}
|
|
@@ -58158,7 +58316,7 @@ var PickUpPatrolClient = class {
|
|
|
58158
58316
|
var client = new PickUpPatrolClient();
|
|
58159
58317
|
|
|
58160
58318
|
// src/version.ts
|
|
58161
|
-
var VERSION = "1.0.
|
|
58319
|
+
var VERSION = "1.0.2";
|
|
58162
58320
|
|
|
58163
58321
|
// src/dates.ts
|
|
58164
58322
|
var WEEKDAY_NAMES = [
|
|
@@ -58517,8 +58675,27 @@ function previewUnlessConfirmed(confirm, action, method, dto, body) {
|
|
|
58517
58675
|
}
|
|
58518
58676
|
|
|
58519
58677
|
// src/tools/plans.ts
|
|
58520
|
-
function
|
|
58521
|
-
|
|
58678
|
+
function normalizeTimeOfDay(time3) {
|
|
58679
|
+
const trimmed = time3.trim();
|
|
58680
|
+
const pad = (n) => (n ?? "0").padStart(2, "0");
|
|
58681
|
+
const duration3 = /^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(?:\.\d+)?S)?$/.exec(trimmed);
|
|
58682
|
+
if (duration3 && trimmed !== "PT") return `${pad(duration3[1])}:${pad(duration3[2])}:${pad(duration3[3])}`;
|
|
58683
|
+
const clock = /(?:^|T)(\d{1,2}):(\d{2})(?::(\d{2}))?/.exec(trimmed);
|
|
58684
|
+
if (clock) return `${pad(clock[1])}:${pad(clock[2])}:${pad(clock[3])}`;
|
|
58685
|
+
return trimmed;
|
|
58686
|
+
}
|
|
58687
|
+
function proofsMatch(actual, expected) {
|
|
58688
|
+
if (actual.transportationId !== expected.transportationId) return false;
|
|
58689
|
+
if ((actual.note ?? "").trim() !== (expected.note ?? "").trim()) return false;
|
|
58690
|
+
if (expected.earlyDismissalTime !== void 0) {
|
|
58691
|
+
const want = expected.earlyDismissalTime === null ? "" : normalizeTimeOfDay(expected.earlyDismissalTime);
|
|
58692
|
+
const got = actual.earlyDismissalTime ? normalizeTimeOfDay(actual.earlyDismissalTime) : "";
|
|
58693
|
+
if (want !== got) return false;
|
|
58694
|
+
}
|
|
58695
|
+
if (expected.carNumber !== void 0) {
|
|
58696
|
+
if ((actual.carNumber ?? "").trim() !== (expected.carNumber ?? "").trim()) return false;
|
|
58697
|
+
}
|
|
58698
|
+
return true;
|
|
58522
58699
|
}
|
|
58523
58700
|
function expectedPlanState(requested) {
|
|
58524
58701
|
return requested.transportationId === null ? { transportationId: null, note: null } : requested;
|
|
@@ -58591,14 +58768,18 @@ function registerPlanTools(server, client2) {
|
|
|
58591
58768
|
await client2.updatePlans(plans);
|
|
58592
58769
|
const expected = expectedPlanState({
|
|
58593
58770
|
transportationId: transportation_id,
|
|
58594
|
-
note: plans[0]?.Note ?? null
|
|
58771
|
+
note: plans[0]?.Note ?? null,
|
|
58772
|
+
earlyDismissalTime: plans[0]?.EarlyDismissalTime,
|
|
58773
|
+
carNumber: plans[0]?.CarNumber
|
|
58595
58774
|
});
|
|
58596
58775
|
const verification = await Promise.all(
|
|
58597
58776
|
dates.map(async (date5) => {
|
|
58598
58777
|
const after = await client2.getPlanEdit(date5, student_id);
|
|
58599
58778
|
const actual = {
|
|
58600
58779
|
transportationId: after.TransportationId ?? null,
|
|
58601
|
-
note: after.Note ?? null
|
|
58780
|
+
note: after.Note ?? null,
|
|
58781
|
+
earlyDismissalTime: after.EarlyDismissalTime ?? null,
|
|
58782
|
+
carNumber: after.CarNumber ?? null
|
|
58602
58783
|
};
|
|
58603
58784
|
return {
|
|
58604
58785
|
date: date5,
|
|
@@ -58737,13 +58918,21 @@ function registerDefaultPlanTools(server, client2) {
|
|
|
58737
58918
|
if (gate) return gate;
|
|
58738
58919
|
await client2.updateStudent(payload);
|
|
58739
58920
|
const after = await client2.getStudent(student_id);
|
|
58740
|
-
const
|
|
58921
|
+
const sent = payload.DefaultPlans?.find((p) => p.DayId === dayIds[0]);
|
|
58741
58922
|
const unchanged = [...new Set(dayIds)].filter((dayId) => {
|
|
58742
58923
|
const plan = (after.DefaultPlans ?? []).find((p) => p.DayId === dayId);
|
|
58743
58924
|
if (plan === void 0) return true;
|
|
58744
58925
|
return !proofsMatch(
|
|
58745
|
-
{
|
|
58746
|
-
|
|
58926
|
+
{
|
|
58927
|
+
transportationId: plan.TransportationId ?? null,
|
|
58928
|
+
note: plan.Note ?? null,
|
|
58929
|
+
earlyDismissalTime: plan.EarlyDismissalTime ?? null
|
|
58930
|
+
},
|
|
58931
|
+
{
|
|
58932
|
+
transportationId: transportation.TransportationId,
|
|
58933
|
+
note: sent?.Note ?? null,
|
|
58934
|
+
earlyDismissalTime: sent?.EarlyDismissalTime ?? void 0
|
|
58935
|
+
}
|
|
58747
58936
|
);
|
|
58748
58937
|
});
|
|
58749
58938
|
return minifiedResult({
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname, join } from 'path';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { loadDotenvSafely, McpToolError, buildQueryString } from '@chrischall/mcp-utils';
|
|
4
|
-
import { PickUpPatrolAuth, BASE_URL, BASE_PATH, describeResponseStatus } from './auth.js';
|
|
4
|
+
import { PickUpPatrolAuth, BASE_URL, BASE_PATH, REQUEST_TIMEOUT_MS, describeResponseStatus, } from './auth.js';
|
|
5
5
|
// Load .env for local dev; silently skip when dotenv is unavailable (the mcpb
|
|
6
6
|
// bundle externalises it). The try/catch guards a runtime where
|
|
7
7
|
// `import.meta.url` is undefined and `fileURLToPath` would throw at module
|
|
@@ -13,7 +13,6 @@ try {
|
|
|
13
13
|
catch {
|
|
14
14
|
/* non-Node runtime: no .env to load */
|
|
15
15
|
}
|
|
16
|
-
const REQUEST_TIMEOUT_MS = 30_000;
|
|
17
16
|
/**
|
|
18
17
|
* Thin typed client over PickUp Patrol's ServiceStack API.
|
|
19
18
|
*
|
|
@@ -68,7 +67,9 @@ export class PickUpPatrolClient {
|
|
|
68
67
|
detail = null;
|
|
69
68
|
}
|
|
70
69
|
throw new McpToolError(`PickUp Patrol ${dto} failed (HTTP ${res.status})${detail ? `: ${detail}` : ''}`, {
|
|
71
|
-
|
|
70
|
+
// A 401 never gets here: withAuth() re-signs-in once and turns a
|
|
71
|
+
// second 401 into a permanent unusable-session error.
|
|
72
|
+
hint: res.status === 403
|
|
72
73
|
? 'The session was rejected. Check PICKUPPATROL_USERNAME and PICKUPPATROL_PASSWORD.'
|
|
73
74
|
: undefined,
|
|
74
75
|
});
|
package/dist/tools/defaults.js
CHANGED
|
@@ -131,12 +131,22 @@ export function registerDefaultPlanTools(server, client) {
|
|
|
131
131
|
// the comparison entirely: it advances by itself, which would make every
|
|
132
132
|
// write look successful.
|
|
133
133
|
const after = await client.getStudent(student_id);
|
|
134
|
-
|
|
134
|
+
// The early-dismissal time is part of the proof when one was sent: a
|
|
135
|
+
// time-only change keeps the option and note identical.
|
|
136
|
+
const sent = payload.DefaultPlans?.find((p) => p.DayId === dayIds[0]);
|
|
135
137
|
const unchanged = [...new Set(dayIds)].filter((dayId) => {
|
|
136
138
|
const plan = (after.DefaultPlans ?? []).find((p) => p.DayId === dayId);
|
|
137
139
|
if (plan === undefined)
|
|
138
140
|
return true;
|
|
139
|
-
return !proofsMatch({
|
|
141
|
+
return !proofsMatch({
|
|
142
|
+
transportationId: plan.TransportationId ?? null,
|
|
143
|
+
note: plan.Note ?? null,
|
|
144
|
+
earlyDismissalTime: plan.EarlyDismissalTime ?? null,
|
|
145
|
+
}, {
|
|
146
|
+
transportationId: transportation.TransportationId,
|
|
147
|
+
note: sent?.Note ?? null,
|
|
148
|
+
earlyDismissalTime: sent?.EarlyDismissalTime ?? undefined,
|
|
149
|
+
});
|
|
140
150
|
});
|
|
141
151
|
return minifiedResult({
|
|
142
152
|
action,
|
package/dist/tools/plans.d.ts
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import type { PickUpPatrolClient } from '../client.js';
|
|
3
3
|
import type { Transportation } from '../types.js';
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* The fields whose change proves a plan write actually landed.
|
|
6
|
+
*
|
|
7
|
+
* `earlyDismissalTime` and `carNumber` are part of the proof whenever the
|
|
8
|
+
* write sent them: moving an early dismissal from 14:30 to 13:00 keeps the
|
|
9
|
+
* option and note identical, so without the time a silently dropped write
|
|
10
|
+
* would read back as verified. Left `undefined` on the expected side, they
|
|
11
|
+
* are not compared — the write did not carry them.
|
|
12
|
+
*/
|
|
5
13
|
export interface PlanProof {
|
|
6
14
|
transportationId: number | null;
|
|
7
15
|
note: string | null;
|
|
16
|
+
earlyDismissalTime?: string | null | undefined;
|
|
17
|
+
carNumber?: string | null | undefined;
|
|
8
18
|
}
|
|
9
|
-
/**
|
|
10
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Reduce a time of day to `HH:MM:SS` so a read-back can be compared with what
|
|
21
|
+
* was sent. Accepts `H:MM`, `HH:MM:SS`, an ISO date-time, and the XSD duration
|
|
22
|
+
* ServiceStack uses for a `TimeSpan` (`PT13H30M`). Anything unrecognised is
|
|
23
|
+
* returned trimmed, so it can only ever compare unequal — a false "unchanged"
|
|
24
|
+
* is recoverable, a false "verified" is not.
|
|
25
|
+
*/
|
|
26
|
+
export declare function normalizeTimeOfDay(time: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Compare a read-back proof with the expected one, ignoring whitespace the
|
|
29
|
+
* service may normalise off a note or car number.
|
|
30
|
+
*/
|
|
31
|
+
export declare function proofsMatch(actual: PlanProof, expected: PlanProof): boolean;
|
|
11
32
|
/**
|
|
12
33
|
* What `GetPlanEdit` should report once a write has landed.
|
|
13
34
|
*
|
package/dist/tools/plans.js
CHANGED
|
@@ -3,9 +3,44 @@ import { McpToolError, minifiedResult } from '@chrischall/mcp-utils';
|
|
|
3
3
|
import { buildPlanUpdates } from '../plans.js';
|
|
4
4
|
import { weekdayOf } from '../dates.js';
|
|
5
5
|
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Reduce a time of day to `HH:MM:SS` so a read-back can be compared with what
|
|
8
|
+
* was sent. Accepts `H:MM`, `HH:MM:SS`, an ISO date-time, and the XSD duration
|
|
9
|
+
* ServiceStack uses for a `TimeSpan` (`PT13H30M`). Anything unrecognised is
|
|
10
|
+
* returned trimmed, so it can only ever compare unequal — a false "unchanged"
|
|
11
|
+
* is recoverable, a false "verified" is not.
|
|
12
|
+
*/
|
|
13
|
+
export function normalizeTimeOfDay(time) {
|
|
14
|
+
const trimmed = time.trim();
|
|
15
|
+
const pad = (n) => (n ?? '0').padStart(2, '0');
|
|
16
|
+
const duration = /^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(?:\.\d+)?S)?$/.exec(trimmed);
|
|
17
|
+
if (duration && trimmed !== 'PT')
|
|
18
|
+
return `${pad(duration[1])}:${pad(duration[2])}:${pad(duration[3])}`;
|
|
19
|
+
const clock = /(?:^|T)(\d{1,2}):(\d{2})(?::(\d{2}))?/.exec(trimmed);
|
|
20
|
+
if (clock)
|
|
21
|
+
return `${pad(clock[1])}:${pad(clock[2])}:${pad(clock[3])}`;
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compare a read-back proof with the expected one, ignoring whitespace the
|
|
26
|
+
* service may normalise off a note or car number.
|
|
27
|
+
*/
|
|
28
|
+
export function proofsMatch(actual, expected) {
|
|
29
|
+
if (actual.transportationId !== expected.transportationId)
|
|
30
|
+
return false;
|
|
31
|
+
if ((actual.note ?? '').trim() !== (expected.note ?? '').trim())
|
|
32
|
+
return false;
|
|
33
|
+
if (expected.earlyDismissalTime !== undefined) {
|
|
34
|
+
const want = expected.earlyDismissalTime === null ? '' : normalizeTimeOfDay(expected.earlyDismissalTime);
|
|
35
|
+
const got = actual.earlyDismissalTime ? normalizeTimeOfDay(actual.earlyDismissalTime) : '';
|
|
36
|
+
if (want !== got)
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (expected.carNumber !== undefined) {
|
|
40
|
+
if ((actual.carNumber ?? '').trim() !== (expected.carNumber ?? '').trim())
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
9
44
|
}
|
|
10
45
|
/**
|
|
11
46
|
* What `GetPlanEdit` should report once a write has landed.
|
|
@@ -104,7 +139,8 @@ export function registerPlanTools(server, client) {
|
|
|
104
139
|
return gate;
|
|
105
140
|
await client.updatePlans(plans);
|
|
106
141
|
// A 2xx is not proof the change persisted — re-read each date and
|
|
107
|
-
// compare the
|
|
142
|
+
// compare the fields that prove it (option, note, and the time / car
|
|
143
|
+
// number when sent). ModifiedDate is deliberately not
|
|
108
144
|
// compared: it advances on its own, which would make every write look
|
|
109
145
|
// successful.
|
|
110
146
|
// One expectation for the whole call: every date in a single UpdatePlans
|
|
@@ -112,12 +148,16 @@ export function registerPlanTools(server, client) {
|
|
|
112
148
|
const expected = expectedPlanState({
|
|
113
149
|
transportationId: transportation_id,
|
|
114
150
|
note: plans[0]?.Note ?? null,
|
|
151
|
+
earlyDismissalTime: plans[0]?.EarlyDismissalTime,
|
|
152
|
+
carNumber: plans[0]?.CarNumber,
|
|
115
153
|
});
|
|
116
154
|
const verification = await Promise.all(dates.map(async (date) => {
|
|
117
155
|
const after = await client.getPlanEdit(date, student_id);
|
|
118
156
|
const actual = {
|
|
119
157
|
transportationId: after.TransportationId ?? null,
|
|
120
158
|
note: after.Note ?? null,
|
|
159
|
+
earlyDismissalTime: after.EarlyDismissalTime ?? null,
|
|
160
|
+
carNumber: after.CarNumber ?? null,
|
|
121
161
|
};
|
|
122
162
|
return {
|
|
123
163
|
date,
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/pickuppatrol-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"mcpName": "io.github.chrischall/pickuppatrol-mcp",
|
|
6
6
|
"description": "PickUp Patrol MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
"test:watch": "vitest"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@chrischall/mcp-utils": "^
|
|
60
|
+
"@chrischall/mcp-utils": "^2.4.0",
|
|
61
61
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
62
62
|
"dotenv": "^17.4.0",
|
|
63
|
-
"zod": "^4.6.
|
|
63
|
+
"zod": "^4.6.5"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@modelcontextprotocol/client": "^2.0.0",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/pickuppatrol-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/pickuppatrol-mcp",
|
|
14
|
-
"version": "1.0.
|
|
14
|
+
"version": "1.0.2",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|