@chrischall/pickuppatrol-mcp 0.1.1 → 0.2.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/dist/bundle.js +508 -215
- package/dist/tools/_confirm.js +2 -2
- package/dist/tools/account.js +6 -6
- package/dist/tools/defaults.js +6 -7
- package/dist/tools/plans.js +5 -6
- package/dist/tools/school.js +6 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/mint.yaml +22 -0
- package/package.json +6 -5
- package/server.json +2 -2
- package/dist/tools/_errors.d.ts +0 -14
- package/dist/tools/_errors.js +0 -26
package/dist/tools/_confirm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { minifiedResult, schemaConfirm } from '@chrischall/mcp-utils';
|
|
2
2
|
export { schemaConfirm };
|
|
3
3
|
/**
|
|
4
4
|
* Confirm-gate for a mutating tool (the fleet convention). Without
|
|
@@ -12,7 +12,7 @@ export { schemaConfirm };
|
|
|
12
12
|
export function previewUnlessConfirmed(confirm, action, method, dto, body) {
|
|
13
13
|
if (confirm === true)
|
|
14
14
|
return null;
|
|
15
|
-
return
|
|
15
|
+
return minifiedResult({
|
|
16
16
|
dryRun: true,
|
|
17
17
|
action,
|
|
18
18
|
method,
|
package/dist/tools/account.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { messageOf, minifiedResult } from '@chrischall/mcp-utils';
|
|
3
3
|
import { dayIdToName } from '../dates.js';
|
|
4
4
|
import { VERSION } from '../version.js';
|
|
5
5
|
/** Project a student down to the fields a parent actually asks about. */
|
|
@@ -36,7 +36,7 @@ export function registerAccountTools(server, client) {
|
|
|
36
36
|
annotations: { readOnlyHint: true },
|
|
37
37
|
}, async () => {
|
|
38
38
|
const session = await client.getSession();
|
|
39
|
-
return
|
|
39
|
+
return minifiedResult({
|
|
40
40
|
userId: session.UserId ?? null,
|
|
41
41
|
name: session.DisplayName ?? [session.FirstName, session.LastName].filter(Boolean).join(' '),
|
|
42
42
|
email: session.Email ?? session.PrimaryEmail ?? null,
|
|
@@ -55,7 +55,7 @@ export function registerAccountTools(server, client) {
|
|
|
55
55
|
client.getDefaultPlansReviewNeeded(),
|
|
56
56
|
]);
|
|
57
57
|
const needsReview = new Map(review.map((r) => [r.StudentId, r.NeedsReview]));
|
|
58
|
-
return
|
|
58
|
+
return minifiedResult(students.map((student) => ({
|
|
59
59
|
...summarizeStudent(student),
|
|
60
60
|
needsDefaultsReview: needsReview.get(student.StudentId) ?? false,
|
|
61
61
|
})));
|
|
@@ -72,7 +72,7 @@ export function registerAccountTools(server, client) {
|
|
|
72
72
|
},
|
|
73
73
|
}, async ({ student_id, raw }) => {
|
|
74
74
|
const student = await client.getStudent(student_id);
|
|
75
|
-
return
|
|
75
|
+
return minifiedResult(raw === true ? student : summarizeStudent(student));
|
|
76
76
|
});
|
|
77
77
|
server.registerTool('pup_healthcheck', {
|
|
78
78
|
description: 'Verify the configured credentials sign in and the PickUp Patrol API answers. Reports the server version and the students the account can see.',
|
|
@@ -80,7 +80,7 @@ export function registerAccountTools(server, client) {
|
|
|
80
80
|
}, async () => {
|
|
81
81
|
try {
|
|
82
82
|
const session = await client.getSession();
|
|
83
|
-
return
|
|
83
|
+
return minifiedResult({
|
|
84
84
|
ok: true,
|
|
85
85
|
version: VERSION,
|
|
86
86
|
signedInAs: session.Email ?? session.PrimaryEmail ?? session.DisplayName ?? null,
|
|
@@ -91,7 +91,7 @@ export function registerAccountTools(server, client) {
|
|
|
91
91
|
// A healthcheck reports rather than throws: the whole point is to say
|
|
92
92
|
// what is wrong, and an exception here reads to the host as the tool
|
|
93
93
|
// itself being broken.
|
|
94
|
-
return
|
|
94
|
+
return minifiedResult({ ok: false, version: VERSION, error: messageOf(err) });
|
|
95
95
|
}
|
|
96
96
|
});
|
|
97
97
|
}
|
package/dist/tools/defaults.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { McpToolError,
|
|
2
|
+
import { McpToolError, minifiedResult } from '@chrischall/mcp-utils';
|
|
3
3
|
import { applyDefaultPlans, clearDefaultPlans } from '../plans.js';
|
|
4
4
|
import { dayIdToName, nameToDayId } from '../dates.js';
|
|
5
5
|
import { summarizeDefaultPlans } from './account.js';
|
|
6
6
|
import { proofsMatch, resolveTransportation } from './plans.js';
|
|
7
7
|
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
8
|
-
import { withHints } from './_errors.js';
|
|
9
8
|
/**
|
|
10
9
|
* Accept weekdays as names ("Monday") or ids (1 = Sunday … 7 = Saturday).
|
|
11
10
|
* A name that is not a weekday is rejected rather than coerced — silently
|
|
@@ -43,7 +42,7 @@ export function registerDefaultPlanTools(server, client) {
|
|
|
43
42
|
client.getStudent(student_id),
|
|
44
43
|
client.getDefaultPlansReviewNeeded(),
|
|
45
44
|
]);
|
|
46
|
-
return
|
|
45
|
+
return minifiedResult({
|
|
47
46
|
studentId: student.StudentId,
|
|
48
47
|
name: [student.FirstName, student.LastName].filter(Boolean).join(' '),
|
|
49
48
|
schoolId: student.SchoolId,
|
|
@@ -78,7 +77,7 @@ export function registerDefaultPlanTools(server, client) {
|
|
|
78
77
|
.describe('Remove every weekday default instead of setting one (days is ignored)'),
|
|
79
78
|
confirm: schemaConfirm,
|
|
80
79
|
},
|
|
81
|
-
},
|
|
80
|
+
}, (async ({ student_id, days, transportation_id, note, early_dismissal_time, clear_all, confirm }) => {
|
|
82
81
|
// Read-modify-write: PickUp Patrol has no default-plans endpoint, so the
|
|
83
82
|
// whole student record round-trips. Reading it here (before the confirm
|
|
84
83
|
// gate) is what makes the dry-run show the real payload; it mutates
|
|
@@ -92,7 +91,7 @@ export function registerDefaultPlanTools(server, client) {
|
|
|
92
91
|
await client.updateStudent(payload);
|
|
93
92
|
const after = await client.getStudent(student_id);
|
|
94
93
|
const remaining = after.DefaultPlans ?? [];
|
|
95
|
-
return
|
|
94
|
+
return minifiedResult({
|
|
96
95
|
action: 'Cleared every weekday default',
|
|
97
96
|
defaultPlans: summarizeDefaultPlans(remaining),
|
|
98
97
|
verified: remaining.length === 0,
|
|
@@ -139,7 +138,7 @@ export function registerDefaultPlanTools(server, client) {
|
|
|
139
138
|
return true;
|
|
140
139
|
return !proofsMatch({ transportationId: plan.TransportationId ?? null, note: plan.Note ?? null }, { transportationId: transportation.TransportationId, note: sentNote });
|
|
141
140
|
});
|
|
142
|
-
return
|
|
141
|
+
return minifiedResult({
|
|
143
142
|
action,
|
|
144
143
|
defaultPlans: summarizeDefaultPlans(after.DefaultPlans),
|
|
145
144
|
verified: unchanged.length === 0,
|
|
@@ -166,7 +165,7 @@ export function registerDefaultPlanTools(server, client) {
|
|
|
166
165
|
await client.setDefaultsReviewed(student_id, value);
|
|
167
166
|
const review = await client.getDefaultPlansReviewNeeded();
|
|
168
167
|
const needsReview = review.find((r) => r.StudentId === student_id)?.NeedsReview ?? false;
|
|
169
|
-
return
|
|
168
|
+
return minifiedResult({
|
|
170
169
|
studentId: student_id,
|
|
171
170
|
needsDefaultsReview: needsReview,
|
|
172
171
|
verified: needsReview === !value,
|
package/dist/tools/plans.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { McpToolError,
|
|
2
|
+
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
|
-
import { withHints } from './_errors.js';
|
|
7
6
|
/** Compare two proofs, ignoring whitespace the service may normalise off a note. */
|
|
8
7
|
export function proofsMatch(a, b) {
|
|
9
8
|
return a.transportationId === b.transportationId && (a.note ?? '').trim() === (b.note ?? '').trim();
|
|
@@ -46,7 +45,7 @@ export function registerPlanTools(server, client) {
|
|
|
46
45
|
start_date: z.string().describe('YYYY-MM-DD'),
|
|
47
46
|
end_date: z.string().describe('YYYY-MM-DD'),
|
|
48
47
|
},
|
|
49
|
-
}, async ({ start_date, end_date }) =>
|
|
48
|
+
}, async ({ start_date, end_date }) => minifiedResult(await client.getParentPlans(start_date, end_date)));
|
|
50
49
|
server.registerTool('pup_get_plan', {
|
|
51
50
|
description: 'The dismissal plan for one student on one date — the option in force, any note, the early-dismissal time, and whether the date is locked because the cutoff has passed.',
|
|
52
51
|
annotations: { readOnlyHint: true },
|
|
@@ -54,7 +53,7 @@ export function registerPlanTools(server, client) {
|
|
|
54
53
|
student_id: z.number().int().describe('Student id, from pup_list_students'),
|
|
55
54
|
date: z.string().describe('YYYY-MM-DD'),
|
|
56
55
|
},
|
|
57
|
-
}, async ({ student_id, date }) =>
|
|
56
|
+
}, async ({ student_id, date }) => minifiedResult(await client.getPlanEdit(date, student_id)));
|
|
58
57
|
server.registerTool('pup_set_plan', {
|
|
59
58
|
description: "Change how a student is dismissed on one or more specific dates, or clear those dates back to the student's weekly default. This changes how a child actually leaves school, so it requires confirm: true; without it you get a dry-run of the exact payload. Read pup_list_transportations first — options differ in whether they require a note, a car number or an early-dismissal time.",
|
|
60
59
|
inputSchema: {
|
|
@@ -79,7 +78,7 @@ export function registerPlanTools(server, client) {
|
|
|
79
78
|
.describe('Car number, for options where usesCarNumbers is true'),
|
|
80
79
|
confirm: schemaConfirm,
|
|
81
80
|
},
|
|
82
|
-
},
|
|
81
|
+
}, (async ({ student_id, dates, transportation_id, note, early_dismissal_time, car_number, confirm }) => {
|
|
83
82
|
// The reads below resolve and validate the payload; they mutate nothing.
|
|
84
83
|
// Running them before the confirm gate is deliberate: it makes the
|
|
85
84
|
// dry-run show the exact bytes that would be sent, already checked
|
|
@@ -132,7 +131,7 @@ export function registerPlanTools(server, client) {
|
|
|
132
131
|
};
|
|
133
132
|
}));
|
|
134
133
|
const failed = verification.filter((v) => !v.verified);
|
|
135
|
-
return
|
|
134
|
+
return minifiedResult({
|
|
136
135
|
action,
|
|
137
136
|
applied: verification,
|
|
138
137
|
verified: failed.length === 0,
|
package/dist/tools/school.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { minifiedResult } from '@chrischall/mcp-utils';
|
|
3
3
|
/**
|
|
4
4
|
* Project a transportation to its identity plus the rules that decide what a
|
|
5
5
|
* plan using it must carry — an agent choosing an option needs those rules up
|
|
@@ -32,7 +32,7 @@ export function registerSchoolTools(server, client) {
|
|
|
32
32
|
}, async ({ school_id, include_inactive }) => {
|
|
33
33
|
const options = await client.getTransportations(school_id);
|
|
34
34
|
const visible = include_inactive === true ? options : options.filter((o) => o.IsActive !== false);
|
|
35
|
-
return
|
|
35
|
+
return minifiedResult(visible.map(summarizeTransportation));
|
|
36
36
|
});
|
|
37
37
|
server.registerTool('pup_get_school', {
|
|
38
38
|
description: 'A school profile together with its per-weekday notify times and plan cutoff times, and the settings that decide whether parents may set plans at all.',
|
|
@@ -46,7 +46,7 @@ export function registerSchoolTools(server, client) {
|
|
|
46
46
|
client.getSchoolNotifyTimes(school_id),
|
|
47
47
|
client.getSchoolSettings(school_id),
|
|
48
48
|
]);
|
|
49
|
-
return
|
|
49
|
+
return minifiedResult({ school, notifyTimes, settings });
|
|
50
50
|
});
|
|
51
51
|
server.registerTool('pup_list_non_school_days', {
|
|
52
52
|
description: 'Dates a plan cannot be set for at a school (holidays, closures, weekends), and optionally the dates in a range that already differ from the student defaults.',
|
|
@@ -62,10 +62,10 @@ export function registerSchoolTools(server, client) {
|
|
|
62
62
|
}, async ({ school_id, start_date, end_date }) => {
|
|
63
63
|
const invalidDates = await client.getInvalidPlanDates(school_id);
|
|
64
64
|
if (start_date === undefined || end_date === undefined) {
|
|
65
|
-
return
|
|
65
|
+
return minifiedResult({ invalidDates });
|
|
66
66
|
}
|
|
67
67
|
const changedDates = await client.getBoldedDates(start_date, end_date);
|
|
68
|
-
return
|
|
68
|
+
return minifiedResult({ invalidDates, changedDates });
|
|
69
69
|
});
|
|
70
70
|
server.registerTool('pup_list_car_numbers', {
|
|
71
71
|
description: 'The car numbers a school has issued to this account, for dismissal options where usesCarNumbers is true.',
|
|
@@ -73,5 +73,5 @@ export function registerSchoolTools(server, client) {
|
|
|
73
73
|
inputSchema: {
|
|
74
74
|
school_id: z.number().int().describe('School id, from pup_list_students'),
|
|
75
75
|
},
|
|
76
|
-
}, async ({ school_id }) =>
|
|
76
|
+
}, async ({ school_id }) => minifiedResult(await client.getCarNumbers(school_id)));
|
|
77
77
|
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/mint.yaml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
name: PickUp Patrol
|
|
3
|
+
slug: pickuppatrol
|
|
4
|
+
summary: >-
|
|
5
|
+
Read and change your children's school dismissal plans in PickUp Patrol —
|
|
6
|
+
defaults, day-by-day changes and school cutoff times
|
|
7
|
+
env:
|
|
8
|
+
- name: PICKUPPATROL_USERNAME
|
|
9
|
+
required: false
|
|
10
|
+
help: >-
|
|
11
|
+
The email address you sign in to app.pickuppatrol.net with.
|
|
12
|
+
- name: PICKUPPATROL_PASSWORD
|
|
13
|
+
secret: true
|
|
14
|
+
required: false
|
|
15
|
+
help: >-
|
|
16
|
+
Your PickUp Patrol password. Stored by the host, never logged by this
|
|
17
|
+
server.
|
|
18
|
+
egress:
|
|
19
|
+
allow:
|
|
20
|
+
# Only hosts the SERVER process actually fetches. Hosts that appear
|
|
21
|
+
# solely in a URL this server BUILDS and returns are excluded.
|
|
22
|
+
- app.pickuppatrol.net
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/pickuppatrol-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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)",
|
|
@@ -44,19 +44,20 @@
|
|
|
44
44
|
".claude-plugin",
|
|
45
45
|
"skills",
|
|
46
46
|
".mcp.json",
|
|
47
|
-
"server.json"
|
|
47
|
+
"server.json",
|
|
48
|
+
"mint.yaml"
|
|
48
49
|
],
|
|
49
50
|
"scripts": {
|
|
50
51
|
"build": "tsc && npm run bundle",
|
|
51
52
|
"bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --outfile=dist/bundle.js",
|
|
52
53
|
"dev": "node dist/index.js",
|
|
53
54
|
"typecheck": "tsc --noEmit",
|
|
54
|
-
"test": "vitest run",
|
|
55
|
-
"test:coverage": "vitest run --coverage",
|
|
55
|
+
"test": "npm run typecheck && vitest run",
|
|
56
|
+
"test:coverage": "npm run typecheck && vitest run --coverage",
|
|
56
57
|
"test:watch": "vitest"
|
|
57
58
|
},
|
|
58
59
|
"dependencies": {
|
|
59
|
-
"@chrischall/mcp-utils": "^0.
|
|
60
|
+
"@chrischall/mcp-utils": "^0.23.0",
|
|
60
61
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
61
62
|
"dotenv": "^17.4.0",
|
|
62
63
|
"zod": "^4.4.3"
|
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": "0.
|
|
9
|
+
"version": "0.2.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/pickuppatrol-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.2.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|
package/dist/tools/_errors.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Surface an `McpToolError`'s `hint` to the caller.
|
|
4
|
-
*
|
|
5
|
-
* The MCP tool boundary turns a thrown error into a result carrying only
|
|
6
|
-
* `message`, so a `hint` — which is where the actionable half lives here ("the
|
|
7
|
-
* available options are …", "the school describes the note as …") — is
|
|
8
|
-
* otherwise dropped on the floor. Wrapping the handler folds it into the text
|
|
9
|
-
* the caller actually sees.
|
|
10
|
-
*
|
|
11
|
-
* Only `McpToolError` is handled: an unexpected error keeps propagating, so a
|
|
12
|
-
* genuine bug still reads as one instead of being flattened into advice.
|
|
13
|
-
*/
|
|
14
|
-
export declare function withHints<A>(handler: (args: A) => Promise<CallToolResult>): (args: A) => Promise<CallToolResult>;
|
package/dist/tools/_errors.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { McpToolError, errorResult } from '@chrischall/mcp-utils';
|
|
2
|
-
/**
|
|
3
|
-
* Surface an `McpToolError`'s `hint` to the caller.
|
|
4
|
-
*
|
|
5
|
-
* The MCP tool boundary turns a thrown error into a result carrying only
|
|
6
|
-
* `message`, so a `hint` — which is where the actionable half lives here ("the
|
|
7
|
-
* available options are …", "the school describes the note as …") — is
|
|
8
|
-
* otherwise dropped on the floor. Wrapping the handler folds it into the text
|
|
9
|
-
* the caller actually sees.
|
|
10
|
-
*
|
|
11
|
-
* Only `McpToolError` is handled: an unexpected error keeps propagating, so a
|
|
12
|
-
* genuine bug still reads as one instead of being flattened into advice.
|
|
13
|
-
*/
|
|
14
|
-
export function withHints(handler) {
|
|
15
|
-
return async (args) => {
|
|
16
|
-
try {
|
|
17
|
-
return await handler(args);
|
|
18
|
-
}
|
|
19
|
-
catch (err) {
|
|
20
|
-
if (err instanceof McpToolError && err.hint) {
|
|
21
|
-
return errorResult(`${err.message}\n\nHint: ${err.hint}`);
|
|
22
|
-
}
|
|
23
|
-
throw err;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
}
|