@chrischall/pickuppatrol-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/dist/types.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Wire types for the PickUp Patrol ServiceStack API.
3
+ *
4
+ * Every field here was read off the shipped SPA's own request/response DTO
5
+ * classes or observed on a live response — see `docs/PICKUPPATROL-API.md` for
6
+ * the capture. Responses are typed loosely on purpose: the API is
7
+ * undocumented, so an unexpected extra field must not break a read.
8
+ */
9
+ export {};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Single source of truth for the server's self-reported version.
3
+ *
4
+ * The literal on the VERSION line below is rewritten by release-please (it is
5
+ * registered in `release-please-config.json` under `extra-files`). Everything
6
+ * that needs a version imports this constant rather than repeating the
7
+ * annotation, so there is exactly one line to keep in sync and
8
+ * `versionSyncTest` has exactly one line to check.
9
+ */
10
+ export declare const VERSION = "0.0.0";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Single source of truth for the server's self-reported version.
3
+ *
4
+ * The literal on the VERSION line below is rewritten by release-please (it is
5
+ * registered in `release-please-config.json` under `extra-files`). Everything
6
+ * that needs a version imports this constant rather than repeating the
7
+ * annotation, so there is exactly one line to keep in sync and
8
+ * `versionSyncTest` has exactly one line to check.
9
+ */
10
+ export const VERSION = '0.0.0'; // x-release-please-version
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@chrischall/pickuppatrol-mcp",
3
+ "version": "0.0.0",
4
+ "license": "MIT",
5
+ "mcpName": "io.github.chrischall/pickuppatrol-mcp",
6
+ "description": "PickUp Patrol MCP server for Claude — developed and maintained by AI (Claude Code)",
7
+ "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/chrischall/pickuppatrol-mcp.git"
11
+ },
12
+ "keywords": [
13
+ "mcp",
14
+ "model-context-protocol",
15
+ "claude",
16
+ "ai",
17
+ "pickuppatrol",
18
+ "school",
19
+ "dismissal",
20
+ "carpool",
21
+ "parents"
22
+ ],
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=22.0.0"
26
+ },
27
+ "bin": {
28
+ "pickuppatrol-mcp": "dist/index.js"
29
+ },
30
+ "main": "dist/lib.js",
31
+ "types": "dist/lib.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/lib.d.ts",
35
+ "import": "./dist/lib.js"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "files": [
43
+ "dist",
44
+ ".claude-plugin",
45
+ "skills",
46
+ ".mcp.json",
47
+ "server.json"
48
+ ],
49
+ "scripts": {
50
+ "build": "tsc && npm run bundle",
51
+ "bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
52
+ "dev": "node dist/index.js",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "test:coverage": "vitest run --coverage",
56
+ "test:watch": "vitest"
57
+ },
58
+ "dependencies": {
59
+ "@chrischall/mcp-utils": "^0.14.0",
60
+ "@modelcontextprotocol/sdk": "^1.29.0",
61
+ "dotenv": "^17.4.0",
62
+ "zod": "^4.4.3"
63
+ },
64
+ "devDependencies": {
65
+ "@types/node": "^26.1.1",
66
+ "@vitest/coverage-v8": "^4.1.10",
67
+ "esbuild": "^0.28.0",
68
+ "typescript": "^7.0.2",
69
+ "vitest": "^4.1.7"
70
+ }
71
+ }
package/server.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.chrischall/pickuppatrol-mcp",
4
+ "description": "Read and change your children's school dismissal plans in PickUp Patrol",
5
+ "repository": {
6
+ "url": "https://github.com/chrischall/pickuppatrol-mcp",
7
+ "source": "github"
8
+ },
9
+ "version": "0.0.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "@chrischall/pickuppatrol-mcp",
14
+ "version": "0.0.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ }
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,107 @@
1
+ ---
2
+ name: pickuppatrol-api
3
+ description: "Read and change your children's school dismissal plans on PickUp Patrol (app.pickuppatrol.net) from a shell with curl — students, weekly defaults, day-by-day changes, school cutoff times."
4
+ ---
5
+
6
+ # PickUp Patrol from a shell
7
+
8
+ `app.pickuppatrol.net` is a ServiceStack JSON API behind an Ionic/Vue SPA. It is
9
+ reachable **server-side with plain `curl`** — no bot wall, no captcha, no
10
+ browser bridge. Use this when you want PickUp Patrol data in a script or on a
11
+ machine without the `pickuppatrol-mcp` server.
12
+
13
+ Full captured shapes: `references/api.md`.
14
+
15
+ ## Sign in once per shell
16
+
17
+ ```bash
18
+ export PUP_USER='you@example.com'
19
+ read -rs PUP_PASS && export PUP_PASS # never put the password in a command line
20
+ export PUP=https://app.pickuppatrol.net/api/json/reply
21
+
22
+ pup_login() {
23
+ local jar; jar=$(mktemp -t pupjar)
24
+ curl -sS -c "$jar" -X POST "$PUP/Authenticate" \
25
+ -H 'Content-Type: application/json' \
26
+ -d "$(jq -nc --arg u "$PUP_USER" --arg p "$PUP_PASS" \
27
+ '{provider:"credentials",UserName:$u,Password:$p,RememberMe:true}')" \
28
+ -o /tmp/pup_auth.json -w '%{http_code}' >/tmp/pup_code
29
+ [ "$(cat /tmp/pup_code)" = 200 ] || { jq -r '.ResponseStatus.Message' /tmp/pup_auth.json >&2; return 1; }
30
+ export PUP_JAR="$jar"
31
+ PUP_TOKEN=$(jq -r '.BearerToken // empty' /tmp/pup_auth.json); export PUP_TOKEN
32
+ }
33
+
34
+ # Every call goes through this: it sends the session cookies, and the bearer
35
+ # token as well when the deployment issued one.
36
+ pup() {
37
+ local method=$1 dto=$2; shift 2
38
+ local -a auth=(-b "$PUP_JAR")
39
+ [ -n "$PUP_TOKEN" ] && auth+=(-H "Authorization: Bearer $PUP_TOKEN")
40
+ curl -sS -X "$method" "$PUP/$dto" -H 'Content-Type: application/json' "${auth[@]}" "$@"
41
+ }
42
+ ```
43
+
44
+ **Never re-run a rejected login.** PickUp Patrol answers bad credentials with
45
+ `ResponseStatus.ErrorCode` starting `LOGIN-ERROR`, counts the attempt against
46
+ the account, and a lockout clears only through their support desk. One failure
47
+ = stop and fix the credentials.
48
+
49
+ ## Reads
50
+
51
+ ```bash
52
+ pup GET GetSession | jq '{name:.DisplayName, email:.Email, kids:[.Children[].StudentId]}'
53
+ pup GET GetChildren | jq -r '.[] | "\(.StudentId) \(.FirstName) — \(.SchoolName)"'
54
+
55
+ # Weekly default plan for one student. DayId is 1-based, SUNDAY = 1.
56
+ pup GET GetChildren | jq '.[] | select(.StudentId==1050046) | .DefaultPlans
57
+ | sort_by(.DayId) | map({day:.WeekDayName, option:.TransportationName, note:.Note})'
58
+
59
+ # The dismissal options a school offers, with the rules each imposes.
60
+ pup GET "GetTransportations?SchoolId=1703" | jq -r '.[] | select(.IsActive)
61
+ | "\(.TransportationId)\t\(.Name)\tnote=\(.IsNoteRequired)\tcar=\(.UseCarNumbers)\tearly=\(.IsEarlyDismissal)"'
62
+
63
+ pup GET "GetPlanEdit?PlanDate=2026-08-17&StudentId=1050046" | jq '{TransportationName,Note,IsLocked}'
64
+ pup GET "GetInvalidPlanDates?SchoolId=1703" | jq 'length' # non-school days
65
+ ```
66
+
67
+ ## Writes
68
+
69
+ Writes change how a child actually leaves school. Read
70
+ `GetTransportations` first and obey the option's rules, or the request is
71
+ rejected (or worse, silently ignored):
72
+
73
+ - `IsNoteRequired` → `Note` must be non-empty
74
+ - `UseCarNumbers` → send `CarNumber`; otherwise omit the key
75
+ - `IsEarlyDismissal` → send `EarlyDismissalTime` as `HH:MM:SS`; otherwise omit it
76
+ - `IsLimited` → only usable if the id is in the student's `LimitedIds`
77
+
78
+ **Change specific dates** — `Plans` is an array, one entry per date:
79
+
80
+ ```bash
81
+ pup PUT UpdatePlans -d '{"Plans":[{
82
+ "StudentId":1050046,"SchoolId":1703,"PlanDate":"2026-08-17",
83
+ "TransportationId":41246,"TransportationName":"PickUp","Note":"Chris Hall"}]}'
84
+ ```
85
+
86
+ **Clear a date back to the weekly default** — a null transportation:
87
+
88
+ ```bash
89
+ pup PUT UpdatePlans -d '{"Plans":[{
90
+ "StudentId":1050046,"SchoolId":1703,"PlanDate":"2026-08-17",
91
+ "TransportationId":null,"TransportationName":"Default plan","Note":null}]}'
92
+ ```
93
+
94
+ **Change the weekly defaults** — there is no default-plans endpoint. Read the
95
+ whole student, edit `DefaultPlans`, PUT the whole record back
96
+ (`references/api.md` has the read-modify-write recipe).
97
+
98
+ ### Always re-read to verify
99
+
100
+ A 2xx is not proof. The school silently ignores a change past its cutoff time.
101
+
102
+ ```bash
103
+ pup GET "GetPlanEdit?PlanDate=2026-08-17&StudentId=1050046" | jq '.TransportationId'
104
+ ```
105
+
106
+ Compare `TransportationId` — **not** `ModifiedDate`, which advances on its own
107
+ and would make every write look successful.
@@ -0,0 +1,168 @@
1
+ # PickUp Patrol API reference
2
+
3
+ Every shape here was read off the shipped SPA's own request DTO classes or
4
+ observed on a live signed-in session (2026-08-16). No credential, cookie or
5
+ token value is recorded.
6
+
7
+ - Base: `https://app.pickuppatrol.net/api/json/reply/<DtoName>`
8
+ - GET args on the query string; POST/PUT/PATCH args in a JSON body
9
+ - Auth: session cookies from `Authenticate`, plus `Authorization: Bearer <jwt>`
10
+ when the deployment issues one
11
+ - Errors: ServiceStack `{"ResponseStatus":{"ErrorCode","Message","Errors":[…]}}`;
12
+ an unauthenticated call to any authorised DTO is a bare `401`
13
+
14
+ Assumes the `pup` / `pup_login` shell functions from `SKILL.md`.
15
+
16
+ ## Authentication
17
+
18
+ ```bash
19
+ curl -sS -c jar -X POST "$PUP/Authenticate" -H 'Content-Type: application/json' \
20
+ -d '{"provider":"credentials","UserName":"…","Password":"…","RememberMe":true}'
21
+ ```
22
+
23
+ Response: `{UserId, SessionId, UserName, DisplayName, BearerToken, RefreshToken,
24
+ Roles, Permissions, ResponseStatus}`.
25
+
26
+ Two-factor accounts (`User.OtpTypeId` set) need `POST /CreateOtp {OtpTypeId}`
27
+ then `PUT /VerifyOtp {Otp, RememberMe}`. Token refresh is
28
+ `POST /GetAccessToken {refreshToken}` or `GET /RequestNewtoken?OldToken=…`.
29
+
30
+ ## Read DTOs
31
+
32
+ | DTO | Args |
33
+ |---|---|
34
+ | `GetSession` | — |
35
+ | `GetSessionInfo` | — |
36
+ | `GetChildren` | — |
37
+ | `GetStudent` | `StudentId`, `MergeTimeWithNote?` |
38
+ | `GetDefaultPlansReviewNeeded` | — |
39
+ | `GetParentPlans` | `StartDate`, `EndDate` |
40
+ | `GetPlanEdit` | `PlanDate`, `StudentId` |
41
+ | `GetBoldedDates` | `StartDate`, `EndDate` (dates differing from the default) |
42
+ | `GetInvalidPlanDates` | `SchoolId` (non-school days) |
43
+ | `GetTransportations` | `SchoolId` |
44
+ | `GetCarNumbers` | `SchoolId` |
45
+ | `GetSchool` | `SchoolId` |
46
+ | `GetSchoolNotifyTimes` | `SchoolId` |
47
+ | `GetSchoolSettings` | `SchoolId` |
48
+ | `GetMiddayPlan` | `StudentId`, `PlanDate`, `TypeId?` |
49
+ | `GetParentHealthScreenAudits` | `ScreenDate` |
50
+ | `GetTermsOfUse` | — |
51
+
52
+ ### Student
53
+
54
+ ```
55
+ StudentId SchoolId SchoolName FirstName LastName IsActive SASId TeacherId
56
+ AllowPlans CreateDate CreatedBy ModifiedDate ModifiedBy DefaultsModifiedDate
57
+ DefaultPlanModifiedBy DefaultsReviewedDate DefaultsReviewedBy DefaultPlans
58
+ SafetyFlag DefaultCarNumber LimitedIds
59
+ ```
60
+
61
+ `DefaultPlans[]`:
62
+
63
+ ```json
64
+ {"StudentId":1050046,"DayId":2,"TransportationId":41246,"Note":"Chris Hall",
65
+ "TransportationName":"PickUp","WeekDayName":"Monday","EarlyDismissalTime":null,
66
+ "CarNumber":null,"UseCarNumbers":false}
67
+ ```
68
+
69
+ `DayId` is 1-based with **Sunday = 1** — verified from a live record where
70
+ `DayId: 2` carried `WeekDayName: "Monday"`.
71
+
72
+ ### Transportation
73
+
74
+ ```
75
+ TransportationId SchoolId Name NoteHint IsNoteRequired UseCarNumbers
76
+ IsNotePrivate IsActive Sequence IsEarlyDismissal IsLimited CutoffTime
77
+ CutoffTime{Sunday…Saturday} AllowParentCheck
78
+ ```
79
+
80
+ ### GetPlanEdit
81
+
82
+ ```
83
+ PlanDate StudentId FirstName LastName SchoolId TransportationId Note IsLocked
84
+ TransportationName SchoolName BusRouteUrl ValidationErrors EarlyDismissalTime
85
+ CarNumber LimitedIds IsNotePrivate
86
+ ```
87
+
88
+ ## Write DTOs
89
+
90
+ | DTO | Verb | Body |
91
+ |---|---|---|
92
+ | `UpdatePlans` | PUT | `{Plans:[…]}` |
93
+ | `Student` | PUT | the whole student record |
94
+ | `SetDefaultsReviewed` | PUT | `{StudentId, Reviewed}` |
95
+ | `UpdateMiddayPlanCheck` | PUT | `{StudentId, PlanDate, TypeId, Action}` |
96
+ | `MiddayPlan` | PUT | full midday-plan record |
97
+ | `UpdateSendPlanConfirmationEmail` | PUT | `{Send}` |
98
+ | `UpdateUserLanguage` | PATCH | `{Language}` |
99
+ | `AcceptTerms` | PUT | — |
100
+ | `ChangePassword` | PUT | `{CurrentPassword, NewPassword, ConfirmPassword}` |
101
+
102
+ ### UpdatePlans element
103
+
104
+ ```json
105
+ {"StudentId":1050046,"SchoolId":1703,"PlanDate":"2026-08-17",
106
+ "TransportationId":41246,"TransportationName":"PickUp","Note":"Chris Hall",
107
+ "EarlyDismissalTime":"13:00:00","CarNumber":"12"}
108
+ ```
109
+
110
+ Omit `EarlyDismissalTime` unless the option has `IsEarlyDismissal`, and
111
+ `CarNumber` unless it has `UseCarNumbers` — the web app leaves both keys off
112
+ otherwise.
113
+
114
+ Build several dates at once:
115
+
116
+ ```bash
117
+ jq -nc --argjson dates '["2026-08-17","2026-08-18"]' '
118
+ {Plans: ($dates | map({
119
+ StudentId:1050046, SchoolId:1703, PlanDate:.,
120
+ TransportationId:41246, TransportationName:"PickUp", Note:"Chris Hall"}))}' \
121
+ | xargs -0 -I{} sh -c 'pup PUT UpdatePlans -d "$1"' _ {}
122
+ ```
123
+
124
+ ### Weekly defaults: read-modify-write the whole student
125
+
126
+ There is no default-plans endpoint. Read the student, replace only
127
+ `DefaultPlans`, PUT the whole record to the `Student` DTO.
128
+
129
+ ```bash
130
+ STUDENT=1050046
131
+ pup GET "GetStudent?StudentId=$STUDENT" > /tmp/pup_student.json
132
+
133
+ # Set Monday (DayId 2) and Tuesday (3) to Bus (41245).
134
+ jq '.DefaultPlans = (
135
+ (.DefaultPlans // []) as $p
136
+ | [2,3] as $days
137
+ | ($p | map(select(.DayId as $d | $days | index($d) | not)))
138
+ + ($days | map({DayId:., TransportationId:41245, TransportationName:"Bus", Note:null}))
139
+ | sort_by(.DayId))' /tmp/pup_student.json > /tmp/pup_student_new.json
140
+
141
+ pup PUT Student -d @/tmp/pup_student_new.json
142
+
143
+ # Verify: compare TransportationId per DayId, never DefaultsModifiedDate.
144
+ pup GET "GetStudent?StudentId=$STUDENT" \
145
+ | jq '.DefaultPlans | sort_by(.DayId) | map({DayId, TransportationId})'
146
+ ```
147
+
148
+ Clear every default with `jq '.DefaultPlans = []'`.
149
+
150
+ Send every other field back verbatim — the whole record round-trips, so a
151
+ dropped field is a field cleared.
152
+
153
+ ## Gates
154
+
155
+ - `GetSchoolSettings(...).General.AllowDefaultPlans` — school allows default plans
156
+ - `Student.AllowPlans` — this student's plans may be changed
157
+ - `GetSchoolNotifyTimes` — a weekday with no `NotifyTime<Day>` is not a school day
158
+ - `GetPlanEdit(...).IsLocked` — the cutoff for that date has passed
159
+
160
+ ## Verifying a write
161
+
162
+ Re-read and compare the field that proves the change:
163
+
164
+ - one-off plan → `GetPlanEdit(PlanDate, StudentId).TransportationId`
165
+ - weekly defaults → `GetStudent(StudentId).DefaultPlans[].TransportationId`
166
+
167
+ Exclude `ModifiedDate` and `DefaultsModifiedDate`: they advance on their own, so
168
+ including them makes every write look successful.