@chrischall/pickuppatrol-mcp 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/dist/auth.d.ts +38 -0
- package/dist/auth.js +97 -15
- package/dist/bundle.js +798 -193
- package/dist/client.d.ts +2 -0
- package/dist/client.js +18 -4
- package/dist/tools/account.d.ts +9 -0
- package/dist/tools/account.js +43 -17
- package/dist/tools/defaults.js +13 -13
- package/dist/tools/plans.js +6 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
- package/server.json +2 -2
- package/dist/tools/_confirm.d.ts +0 -37
- package/dist/tools/_confirm.js +0 -32
package/dist/client.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export declare class PickUpPatrolClient {
|
|
|
17
17
|
private readonly auth;
|
|
18
18
|
private readonly fetchImpl;
|
|
19
19
|
constructor(opts?: ClientOptions);
|
|
20
|
+
/** Where the credentials came from (`'env'`), or `null` — for the healthcheck. */
|
|
21
|
+
credentialSource(): string | null;
|
|
20
22
|
/** Request a DTO by name. GET args go on the query string, others in the body. */
|
|
21
23
|
call<T>(method: 'GET' | 'POST' | 'PUT' | 'PATCH', dto: string, args?: Record<string, unknown>): Promise<T>;
|
|
22
24
|
private headers;
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname, join } from 'path';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
|
-
import { loadDotenvSafely, McpToolError, buildQueryString } from '@chrischall/mcp-utils';
|
|
4
|
-
import { PickUpPatrolAuth, BASE_URL, BASE_PATH, REQUEST_TIMEOUT_MS, describeResponseStatus, } from './auth.js';
|
|
3
|
+
import { loadDotenvSafely, McpToolError, buildQueryString, EdgeBlockedError, detectEdgeBlock, } from '@chrischall/mcp-utils';
|
|
4
|
+
import { PickUpPatrolAuth, BASE_URL, BASE_PATH, REQUEST_TIMEOUT_MS, SERVICE, 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
|
|
@@ -29,6 +29,10 @@ export class PickUpPatrolClient {
|
|
|
29
29
|
this.auth = opts.auth ?? new PickUpPatrolAuth(opts);
|
|
30
30
|
this.fetchImpl = opts.fetchImpl ?? ((url, init) => fetch(url, init));
|
|
31
31
|
}
|
|
32
|
+
/** Where the credentials came from (`'env'`), or `null` — for the healthcheck. */
|
|
33
|
+
credentialSource() {
|
|
34
|
+
return this.auth.credentialSource;
|
|
35
|
+
}
|
|
32
36
|
/** Request a DTO by name. GET args go on the query string, others in the body. */
|
|
33
37
|
async call(method, dto, args) {
|
|
34
38
|
const query = method === 'GET' && args ? buildQueryString(args) : '';
|
|
@@ -39,7 +43,7 @@ export class PickUpPatrolClient {
|
|
|
39
43
|
body: method === 'GET' || args === undefined ? undefined : JSON.stringify(args),
|
|
40
44
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
41
45
|
}));
|
|
42
|
-
return this.parse(res, dto);
|
|
46
|
+
return this.parse(res, method, dto);
|
|
43
47
|
}
|
|
44
48
|
headers(session) {
|
|
45
49
|
const headers = {
|
|
@@ -52,9 +56,19 @@ export class PickUpPatrolClient {
|
|
|
52
56
|
headers['Cookie'] = session.cookieHeader;
|
|
53
57
|
return headers;
|
|
54
58
|
}
|
|
55
|
-
async parse(res, dto) {
|
|
59
|
+
async parse(res, method, dto) {
|
|
56
60
|
const text = await res.text();
|
|
57
61
|
if (!res.ok) {
|
|
62
|
+
// A CDN/WAF refusal page never reached PickUp Patrol: say so, rather than
|
|
63
|
+
// blaming the credentials (chrischall/mcp-host#1015).
|
|
64
|
+
const edge = detectEdgeBlock({ body: text, status: res.status, headers: res.headers });
|
|
65
|
+
if (edge !== null) {
|
|
66
|
+
throw new EdgeBlockedError(res.status, edge.vendor, {
|
|
67
|
+
service: SERVICE,
|
|
68
|
+
method,
|
|
69
|
+
path: `${BASE_PATH}/${dto}`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
58
72
|
// ServiceStack returns its error envelope as JSON, but a gateway or an
|
|
59
73
|
// auth redirect can return HTML — so the parse is best-effort and the
|
|
60
74
|
// status is always part of the message.
|
package/dist/tools/account.d.ts
CHANGED
|
@@ -6,3 +6,12 @@ export declare function summarizeStudent(student: Student): Record<string, unkno
|
|
|
6
6
|
/** Order the weekly defaults Sunday→Saturday and label each day. */
|
|
7
7
|
export declare function summarizeDefaultPlans(plans: DefaultPlan[] | null | undefined): unknown[];
|
|
8
8
|
export declare function registerAccountTools(server: McpServer, client: PickUpPatrolClient): void;
|
|
9
|
+
/**
|
|
10
|
+
* Name the PickUp Patrol failures the shared status ladder cannot see: its
|
|
11
|
+
* sign-in rejection and two-factor errors carry no HTTP status. Anything else
|
|
12
|
+
* (an edge block, a timeout, a 5xx) falls through to the shared ladder.
|
|
13
|
+
*/
|
|
14
|
+
export declare function classifyPupError(err: unknown): {
|
|
15
|
+
kind: string;
|
|
16
|
+
hint?: string;
|
|
17
|
+
} | undefined;
|
package/dist/tools/account.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { minifiedResult } from '@chrischall/mcp-utils';
|
|
3
|
+
import { runCredentialHealthcheck } from '@chrischall/mcp-utils/healthcheck';
|
|
4
|
+
import { SignInRejectedError, UnusableSessionError } from '../auth.js';
|
|
3
5
|
import { dayIdToName } from '../dates.js';
|
|
4
6
|
import { VERSION } from '../version.js';
|
|
5
7
|
/** Project a student down to the fields a parent actually asks about. */
|
|
@@ -75,23 +77,47 @@ export function registerAccountTools(server, client) {
|
|
|
75
77
|
return minifiedResult(raw === true ? student : summarizeStudent(student));
|
|
76
78
|
});
|
|
77
79
|
server.registerTool('pup_healthcheck', {
|
|
78
|
-
description:
|
|
80
|
+
description: "Verify the configured credentials sign in and the PickUp Patrol API answers. Reports the server version, the students the account can see, and — when it fails — an error.kind saying which hop broke: no_credential (nothing configured), credential_rejected (PickUp Patrol refused the username/password), verification_pending (a two-factor account), edge_blocked (a CDN/WAF refused the request before PickUp Patrol saw it, so the credentials were never judged), timeout, transport or http. Read-only; never returns the credentials.",
|
|
79
81
|
annotations: { readOnlyHint: true },
|
|
80
82
|
}, async () => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
83
|
+
let session;
|
|
84
|
+
// A healthcheck reports rather than throws: the whole point is to say
|
|
85
|
+
// what is wrong, and an exception here reads to the host as the tool
|
|
86
|
+
// itself being broken. The shared ladder supplies the `kind`
|
|
87
|
+
// (chrischall/mcp-host#1015); the account facts ride beside it.
|
|
88
|
+
const shared = await runCredentialHealthcheck({
|
|
89
|
+
server,
|
|
90
|
+
prefix: 'pup',
|
|
91
|
+
hostLabel: 'app.pickuppatrol.net',
|
|
92
|
+
probePath: '/api/json/reply/GetSession',
|
|
93
|
+
resolveCredential: async () => ({ source: client.credentialSource() }),
|
|
94
|
+
probeFn: async () => {
|
|
95
|
+
session = await client.getSession();
|
|
96
|
+
},
|
|
97
|
+
classifyThrown: classifyPupError,
|
|
98
|
+
});
|
|
99
|
+
const result = JSON.parse(shared.content[0].text);
|
|
100
|
+
return minifiedResult({
|
|
101
|
+
...result,
|
|
102
|
+
version: VERSION,
|
|
103
|
+
...(session !== undefined
|
|
104
|
+
? {
|
|
105
|
+
signedInAs: session.Email ?? session.PrimaryEmail ?? session.DisplayName ?? null,
|
|
106
|
+
studentCount: session.Children?.length ?? 0,
|
|
107
|
+
}
|
|
108
|
+
: {}),
|
|
109
|
+
});
|
|
96
110
|
});
|
|
97
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Name the PickUp Patrol failures the shared status ladder cannot see: its
|
|
114
|
+
* sign-in rejection and two-factor errors carry no HTTP status. Anything else
|
|
115
|
+
* (an edge block, a timeout, a 5xx) falls through to the shared ladder.
|
|
116
|
+
*/
|
|
117
|
+
export function classifyPupError(err) {
|
|
118
|
+
if (err instanceof SignInRejectedError)
|
|
119
|
+
return { kind: 'credential_rejected', hint: err.hint };
|
|
120
|
+
if (err instanceof UnusableSessionError)
|
|
121
|
+
return { kind: 'verification_pending', hint: err.hint };
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
package/dist/tools/defaults.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { McpToolError, minifiedResult } from '@chrischall/mcp-utils';
|
|
2
|
+
import { CONFIRM_FLOW_SENTENCE, confirmTokenParam, confirmWrite, McpToolError, minifiedResult, } from '@chrischall/mcp-utils';
|
|
3
|
+
import { BASE_PATH } from '../auth.js';
|
|
3
4
|
import { applyDefaultPlans, clearDefaultPlans } from '../plans.js';
|
|
4
5
|
import { dayIdToName, nameToDayId } from '../dates.js';
|
|
5
6
|
import { summarizeDefaultPlans } from './account.js';
|
|
6
7
|
import { proofsMatch, resolveTransportation } from './plans.js';
|
|
7
|
-
import { CONFIRM_FLOW, confirmTokenParam, confirmWrite } from './_confirm.js';
|
|
8
8
|
/**
|
|
9
9
|
* Accept weekdays as names ("Monday") or ids (1 = Sunday … 7 = Saturday).
|
|
10
10
|
* A name that is not a weekday is rejected rather than coerced — silently
|
|
@@ -54,7 +54,7 @@ export function registerDefaultPlanTools(server, client) {
|
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
56
|
server.registerTool('pup_set_default_plans', {
|
|
57
|
-
description: `Change a student's weekly default dismissal plan for one or more weekdays, or clear every default. This is how the child leaves school on any date without a specific plan. ${
|
|
57
|
+
description: `Change a student's weekly default dismissal plan for one or more weekdays, or clear every default. This is how the child leaves school on any date without a specific plan. ${CONFIRM_FLOW_SENTENCE} Read pup_list_transportations first.`,
|
|
58
58
|
inputSchema: z.object({
|
|
59
59
|
student_id: z.number().int().describe('Student id, from pup_list_students'),
|
|
60
60
|
days: z
|
|
@@ -91,12 +91,12 @@ export function registerDefaultPlanTools(server, client) {
|
|
|
91
91
|
tool: 'pup_set_default_plans',
|
|
92
92
|
action: 'default_plans.clear',
|
|
93
93
|
summary: `Clear every weekday default for ${student.FirstName ?? 'the student'}`,
|
|
94
|
-
|
|
95
|
-
dto: 'Student',
|
|
94
|
+
account: undefined,
|
|
96
95
|
target: String(student_id),
|
|
97
96
|
revision: student.ModifiedDate,
|
|
98
|
-
payload,
|
|
97
|
+
request: { method: 'PUT', path: `${BASE_PATH}/Student`, body: payload },
|
|
99
98
|
willSend: { StudentId: student.StudentId, DefaultPlans: [] },
|
|
99
|
+
preview: { dto: 'Student' },
|
|
100
100
|
confirmToken,
|
|
101
101
|
});
|
|
102
102
|
if (gate)
|
|
@@ -133,16 +133,16 @@ export function registerDefaultPlanTools(server, client) {
|
|
|
133
133
|
tool: 'pup_set_default_plans',
|
|
134
134
|
action: 'default_plans.set',
|
|
135
135
|
summary: action,
|
|
136
|
-
|
|
137
|
-
dto: 'Student',
|
|
136
|
+
account: undefined,
|
|
138
137
|
target: String(student_id),
|
|
139
138
|
revision: student.ModifiedDate,
|
|
140
|
-
payload,
|
|
139
|
+
request: { method: 'PUT', path: `${BASE_PATH}/Student`, body: payload },
|
|
141
140
|
willSend: {
|
|
142
141
|
StudentId: student.StudentId,
|
|
143
142
|
DefaultPlans: payload.DefaultPlans,
|
|
144
143
|
note: 'The whole student record is sent back with only DefaultPlans changed.',
|
|
145
144
|
},
|
|
145
|
+
preview: { dto: 'Student' },
|
|
146
146
|
confirmToken,
|
|
147
147
|
});
|
|
148
148
|
if (gate)
|
|
@@ -185,7 +185,7 @@ export function registerDefaultPlanTools(server, client) {
|
|
|
185
185
|
});
|
|
186
186
|
}));
|
|
187
187
|
server.registerTool('pup_mark_defaults_reviewed', {
|
|
188
|
-
description: `Mark a student's default plans as reviewed, clearing the school's 'needs review' prompt. ${
|
|
188
|
+
description: `Mark a student's default plans as reviewed, clearing the school's 'needs review' prompt. ${CONFIRM_FLOW_SENTENCE}`,
|
|
189
189
|
inputSchema: z.object({
|
|
190
190
|
student_id: z.number().int().describe('Student id, from pup_list_students'),
|
|
191
191
|
reviewed: z.boolean().optional().describe('Defaults to true'),
|
|
@@ -197,10 +197,10 @@ export function registerDefaultPlanTools(server, client) {
|
|
|
197
197
|
tool: 'pup_mark_defaults_reviewed',
|
|
198
198
|
action: 'default_plans.mark_reviewed',
|
|
199
199
|
summary: `Mark student ${student_id}'s defaults as ${value ? 'reviewed' : 'not reviewed'}`,
|
|
200
|
-
|
|
201
|
-
dto: 'SetDefaultsReviewed',
|
|
200
|
+
account: undefined,
|
|
202
201
|
target: String(student_id),
|
|
203
|
-
|
|
202
|
+
request: { method: 'PUT', path: `${BASE_PATH}/SetDefaultsReviewed`, body: { StudentId: student_id, Reviewed: value } },
|
|
203
|
+
preview: { dto: 'SetDefaultsReviewed' },
|
|
204
204
|
confirmToken,
|
|
205
205
|
});
|
|
206
206
|
if (gate)
|
package/dist/tools/plans.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { McpToolError, minifiedResult } from '@chrischall/mcp-utils';
|
|
2
|
+
import { CONFIRM_FLOW_SENTENCE, confirmTokenParam, confirmWrite, McpToolError, minifiedResult, } from '@chrischall/mcp-utils';
|
|
3
|
+
import { BASE_PATH } from '../auth.js';
|
|
3
4
|
import { buildPlanUpdates } from '../plans.js';
|
|
4
5
|
import { weekdayOf } from '../dates.js';
|
|
5
|
-
import { CONFIRM_FLOW, confirmTokenParam, confirmWrite } from './_confirm.js';
|
|
6
6
|
/**
|
|
7
7
|
* Reduce a time of day to `HH:MM:SS` so a read-back can be compared with what
|
|
8
8
|
* was sent. Accepts `H:MM`, `HH:MM:SS`, an ISO date-time, and the XSD duration
|
|
@@ -90,7 +90,7 @@ export function registerPlanTools(server, client) {
|
|
|
90
90
|
}),
|
|
91
91
|
}, async ({ student_id, date }) => minifiedResult(await client.getPlanEdit(date, student_id)));
|
|
92
92
|
server.registerTool('pup_set_plan', {
|
|
93
|
-
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. ${
|
|
93
|
+
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. ${CONFIRM_FLOW_SENTENCE} The preview shows the exact payload. Read pup_list_transportations first — options differ in whether they require a note, a car number or an early-dismissal time.`,
|
|
94
94
|
inputSchema: z.object({
|
|
95
95
|
student_id: z.number().int().describe('Student id, from pup_list_students'),
|
|
96
96
|
dates: z
|
|
@@ -139,10 +139,10 @@ export function registerPlanTools(server, client) {
|
|
|
139
139
|
tool: 'pup_set_plan',
|
|
140
140
|
action: 'plans.set',
|
|
141
141
|
summary: action,
|
|
142
|
-
|
|
143
|
-
dto: 'UpdatePlans',
|
|
142
|
+
account: undefined,
|
|
144
143
|
target: String(student_id),
|
|
145
|
-
|
|
144
|
+
request: { method: 'PUT', path: `${BASE_PATH}/UpdatePlans`, body: { Plans: plans } },
|
|
145
|
+
preview: { dto: 'UpdatePlans' },
|
|
146
146
|
confirmToken,
|
|
147
147
|
});
|
|
148
148
|
if (gate)
|
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.1.
|
|
3
|
+
"version": "1.1.4",
|
|
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,13 +57,13 @@
|
|
|
57
57
|
"test:watch": "vitest"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@chrischall/mcp-utils": "^2.
|
|
61
|
-
"@modelcontextprotocol/server": "^2.
|
|
60
|
+
"@chrischall/mcp-utils": "^2.14.0",
|
|
61
|
+
"@modelcontextprotocol/server": "^2.3.0",
|
|
62
62
|
"dotenv": "^18.0.2",
|
|
63
63
|
"zod": "^4.6.5"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@modelcontextprotocol/client": "^2.
|
|
66
|
+
"@modelcontextprotocol/client": "^2.3.0",
|
|
67
67
|
"@types/node": "^26.1.1",
|
|
68
68
|
"@vitest/coverage-v8": "^5.0.0",
|
|
69
69
|
"esbuild": "^0.28.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.1.
|
|
9
|
+
"version": "1.1.4",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/pickuppatrol-mcp",
|
|
14
|
-
"version": "1.1.
|
|
14
|
+
"version": "1.1.4",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|
package/dist/tools/_confirm.d.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { CallToolResult, InputRequiredResult, ServerContext } from '@modelcontextprotocol/server';
|
|
2
|
-
import { confirmTokenParam } from '@chrischall/mcp-utils';
|
|
3
|
-
export { confirmTokenParam };
|
|
4
|
-
/** What every gated tool's description says about the confirmation step. */
|
|
5
|
-
export declare const CONFIRM_FLOW = "Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).";
|
|
6
|
-
export interface ConfirmWriteOptions {
|
|
7
|
-
/** The tool name the token is bound to. */
|
|
8
|
-
tool: string;
|
|
9
|
-
/** `<service>.<verb>` identifier for the operation. */
|
|
10
|
-
action: string;
|
|
11
|
-
/** Human-readable sentence describing what will change. */
|
|
12
|
-
summary: string;
|
|
13
|
-
method: string;
|
|
14
|
-
dto: string;
|
|
15
|
-
/** The primary id acted on. */
|
|
16
|
-
target: string;
|
|
17
|
-
/** A version of the target that rotates on edit, when the API has one. */
|
|
18
|
-
revision?: string | null | undefined;
|
|
19
|
-
/** EXACTLY what the write will send — hashed into the token. */
|
|
20
|
-
payload: unknown;
|
|
21
|
-
/** What the preview shows as `willSend`; defaults to `payload`. */
|
|
22
|
-
willSend?: unknown;
|
|
23
|
-
/** The phase-2 token from the tool's input. */
|
|
24
|
-
confirmToken: string | undefined;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Confirm-gate for a mutating tool. A client that can show a prompt gets one;
|
|
28
|
-
* one that cannot gets the two-step token flow (MCP_CONFIRM_MODE): phase 1
|
|
29
|
-
* makes **no** write and returns the preview plus a token, phase 2 proceeds
|
|
30
|
-
* only if the freshly rebuilt payload still matches what was previewed.
|
|
31
|
-
* `undefined` means proceed; anything else is the result to return.
|
|
32
|
-
*
|
|
33
|
-
* The gate matters more here than in most of the fleet: these writes change
|
|
34
|
-
* how a child leaves school. A hallucinated call must not silently put a
|
|
35
|
-
* student on a different bus.
|
|
36
|
-
*/
|
|
37
|
-
export declare function confirmWrite(ctx: ServerContext, options: ConfirmWriteOptions): Promise<InputRequiredResult | CallToolResult | undefined>;
|
package/dist/tools/_confirm.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { confirmationFromEnv, confirmTokenParam, requireConfirmationWithFallback, } from '@chrischall/mcp-utils';
|
|
2
|
-
export { confirmTokenParam };
|
|
3
|
-
/** What every gated tool's description says about the confirmation step. */
|
|
4
|
-
export const CONFIRM_FLOW = 'Asks the user to confirm first: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).';
|
|
5
|
-
/**
|
|
6
|
-
* Confirm-gate for a mutating tool. A client that can show a prompt gets one;
|
|
7
|
-
* one that cannot gets the two-step token flow (MCP_CONFIRM_MODE): phase 1
|
|
8
|
-
* makes **no** write and returns the preview plus a token, phase 2 proceeds
|
|
9
|
-
* only if the freshly rebuilt payload still matches what was previewed.
|
|
10
|
-
* `undefined` means proceed; anything else is the result to return.
|
|
11
|
-
*
|
|
12
|
-
* The gate matters more here than in most of the fleet: these writes change
|
|
13
|
-
* how a child leaves school. A hallucinated call must not silently put a
|
|
14
|
-
* student on a different bus.
|
|
15
|
-
*/
|
|
16
|
-
export function confirmWrite(ctx, options) {
|
|
17
|
-
const { tool, action, summary, method, dto, target, revision, payload, willSend, confirmToken } = options;
|
|
18
|
-
const preview = { action: summary, method, dto, willSend: willSend ?? payload };
|
|
19
|
-
return requireConfirmationWithFallback(ctx, confirmationFromEnv({
|
|
20
|
-
action,
|
|
21
|
-
message: `Review and confirm this change: ${summary}`,
|
|
22
|
-
details: preview,
|
|
23
|
-
tool,
|
|
24
|
-
confirmToken,
|
|
25
|
-
subject: () => ({
|
|
26
|
-
target,
|
|
27
|
-
...(revision ? { revision } : {}),
|
|
28
|
-
payload,
|
|
29
|
-
preview,
|
|
30
|
-
}),
|
|
31
|
-
}));
|
|
32
|
-
}
|