@checkbox-site/mcp 0.2.6 → 0.2.8
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/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/tools.js +38 -14
- package/package.json +2 -2
package/dist/server.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { KeyContext } from './permissions.ts';
|
|
|
5
5
|
import { type IssuedKeys, type ToolContext, type ToolDef } from './tools.ts';
|
|
6
6
|
export declare const SERVER_NAME = "checkbox";
|
|
7
7
|
/** Kept equal to packages/mcp/package.json "version" (@checkbox-site/mcp); mcp.test.ts checks it. */
|
|
8
|
-
export declare const SERVER_VERSION = "0.2.
|
|
8
|
+
export declare const SERVER_VERSION = "0.2.8";
|
|
9
9
|
export declare const DEFAULT_MCP_URL = "https://app.checkbox.site/mcp";
|
|
10
10
|
/** The coverage phrase used across Checkbox (the API docs tests check the count against the rules catalog). */
|
|
11
11
|
export declare const COVERAGE_PHRASE = "federal law, 27 states, DC and New York City";
|
package/dist/server.js
CHANGED
|
@@ -9,7 +9,7 @@ import { likelyMisspelling, unknownArgumentsMessage, unknownKeysParams } from ".
|
|
|
9
9
|
import { outdatedNotice } from "./version-check.js";
|
|
10
10
|
export const SERVER_NAME = 'checkbox';
|
|
11
11
|
/** Kept equal to packages/mcp/package.json "version" (@checkbox-site/mcp); mcp.test.ts checks it. */
|
|
12
|
-
export const SERVER_VERSION = '0.2.
|
|
12
|
+
export const SERVER_VERSION = '0.2.8';
|
|
13
13
|
export const DEFAULT_MCP_URL = 'https://app.checkbox.site/mcp';
|
|
14
14
|
/** The coverage phrase used across Checkbox (the API docs tests check the count against the rules catalog). */
|
|
15
15
|
export const COVERAGE_PHRASE = 'federal law, 27 states, DC and New York City';
|
package/dist/tools.js
CHANGED
|
@@ -1220,14 +1220,24 @@ const startTestRun = {
|
|
|
1220
1220
|
};
|
|
1221
1221
|
},
|
|
1222
1222
|
};
|
|
1223
|
+
const TEST_RUN_MAX_WAIT = 55;
|
|
1224
|
+
const TEST_RUN_DONE = new Set(['succeeded', 'incomplete', 'failed', 'canceled']);
|
|
1223
1225
|
const getTestRun = {
|
|
1224
1226
|
name: 'get_test_run',
|
|
1225
1227
|
title: 'Get a test run (steps, observations, fixes)',
|
|
1226
|
-
description: 'A synthetic test subscription with every step (checkout, acknowledgment email, reminders at each legal window, cancellation, confirmation): what was observed, check results, evidence ids and fixes. While running, next_action.type is "wait" and waiting_for says what it waits for
|
|
1228
|
+
description: 'A synthetic test subscription with every step (checkout, acknowledgment email, reminders at each legal window, cancellation, confirmation): what was observed, check results, evidence ids and fixes. While running, next_action.type is "wait" and waiting_for says what it waits for: pass wait: 55 to get the answer as soon as the run finishes (one or two calls per run). When finished, apply the fixes (usually via get_compliance_plan) and start another run to verify. Without test_run, returns the newest run of the site.',
|
|
1227
1229
|
input: {
|
|
1228
1230
|
test_run: z.string().max(100).optional().describe('Test run id (run_…). Default: the newest run of the site.'),
|
|
1229
1231
|
site: siteId.optional(),
|
|
1230
|
-
|
|
1232
|
+
// Round 34 (pilot 33 item 22, core-codex: 25 polls with wait=30 for runs of 1-2 minutes, because each poll answered
|
|
1233
|
+
// on every step): wait up to 55 s (under common 60 s client tool timeouts), answering when the run finishes.
|
|
1234
|
+
wait: z
|
|
1235
|
+
.number()
|
|
1236
|
+
.int()
|
|
1237
|
+
.min(0)
|
|
1238
|
+
.transform((n) => Math.min(TEST_RUN_MAX_WAIT, n))
|
|
1239
|
+
.optional()
|
|
1240
|
+
.describe(`Seconds to wait for the run to finish before answering (0-${TEST_RUN_MAX_WAIT}; larger values wait ${TEST_RUN_MAX_WAIT}). Answers at once when it finishes.`),
|
|
1231
1241
|
},
|
|
1232
1242
|
scopes: ['test_runs:read'],
|
|
1233
1243
|
annotations: { ...ro, title: 'Test run' },
|
|
@@ -1240,8 +1250,15 @@ const getTestRun = {
|
|
|
1240
1250
|
throw new ToolError(`Site ${site} has no test run yet.`, { type: 'start_test_run', message: 'Call start_test_run.', operation_id: 'createTestRun' });
|
|
1241
1251
|
id = list.data[0].id;
|
|
1242
1252
|
}
|
|
1243
|
-
const wait = typeof args.wait === 'number' && args.wait > 0 ? Math.min(
|
|
1244
|
-
const
|
|
1253
|
+
const wait = typeof args.wait === 'number' && args.wait > 0 ? Math.min(TEST_RUN_MAX_WAIT, Math.floor(args.wait)) : 0;
|
|
1254
|
+
const deadline = Date.now() + wait * 1000;
|
|
1255
|
+
// The API long-polls up to 30 s per call and answers on every step; keep asking until the run finishes or time is up.
|
|
1256
|
+
let run = await api.request('GET', `/v1/test_runs/${enc(id)}`, wait ? { query: { wait: Math.min(30, wait) } } : undefined);
|
|
1257
|
+
for (let left = Math.floor((deadline - Date.now()) / 1000); wait && !TEST_RUN_DONE.has(run.status) && left >= 1; left = Math.floor((deadline - Date.now()) / 1000)) {
|
|
1258
|
+
run = await api.request('GET', `/v1/test_runs/${enc(id)}`, { query: { wait: Math.min(30, left) } });
|
|
1259
|
+
}
|
|
1260
|
+
const running = !TEST_RUN_DONE.has(run.status);
|
|
1261
|
+
const next = running && run.next_action?.type === 'wait' ? { ...run.next_action, message: `${(run.next_action.message ?? '').replace(/\s*Call retrieveTestRun with \?wait=30[^.]*\.?/, '').trim()} Call get_test_run again with wait: ${TEST_RUN_MAX_WAIT}: it answers as soon as the run finishes.`.trim() } : undefined;
|
|
1245
1262
|
const w = run.waiting_for;
|
|
1246
1263
|
const progress = w ? [w.step && w.steps_total ? `step ${w.step} of ${w.steps_total}` : '', w.minutes_left ? `about ${w.minutes_left} min left` : ''].filter(Boolean).join(', ') : '';
|
|
1247
1264
|
const waiting = w ? `Waiting for: ${w.what}${w.until ? ` (until ${new Date(w.until * 1000).toISOString().slice(0, 16).replace('T', ' ')} UTC at the latest)` : ''}${progress ? ` [${progress}]` : ''}` : null;
|
|
@@ -1249,6 +1266,7 @@ const getTestRun = {
|
|
|
1249
1266
|
headline: `Test run ${run.id}: ${run.status_label ?? statusLine(run.status)}${run.phase && run.status !== 'succeeded' && run.status !== 'failed' ? ` (phase ${run.phase})` : ''}.`,
|
|
1250
1267
|
notes: [...(waiting ? [waiting] : []), ...(run.steps ?? []).map((s) => `${s.status ?? '?'}: ${s.title ?? s.key}${s.code ? ` (${s.code})` : ''}`)].slice(0, 13),
|
|
1251
1268
|
data: run,
|
|
1269
|
+
...(next ? { next } : {}),
|
|
1252
1270
|
};
|
|
1253
1271
|
},
|
|
1254
1272
|
};
|
|
@@ -1260,8 +1278,8 @@ const checkDisclosure = {
|
|
|
1260
1278
|
title: 'Check checkout disclosure text',
|
|
1261
1279
|
description: [
|
|
1262
1280
|
'Before shipping a checkout: checks the automatic-renewal disclosure shown next to the purchase button against every applicable law (ROSCA, CA, NY, …) and returns per-check verdicts with citations plus a REPLACEMENT verified by the rules engine (disclosure_text, consent_label, button_text, drop-in html and jsx).',
|
|
1263
|
-
'Pass the text exactly as rendered plus the plan terms. Example: {"text":"Renews monthly at $19.99 until you cancel.","price":19.99,"interval":"month","
|
|
1264
|
-
'If verdict is fail, put replacement.jsx (React) or .html above the button, then call again with the new text, consent and placement.bold: true
|
|
1281
|
+
'Pass the text exactly as rendered plus the plan terms. Example: {"text":"Renews monthly at $19.99 until you cancel.","price":19.99,"interval":"month","cancel_url":"https://acme.example/account/cancel","consent":{"checkbox":true,"prechecked":false}}.',
|
|
1282
|
+
'If verdict is fail, put replacement.jsx (React) or .html above the button, then call again with the new text, consent and placement.bold: true. needs_review on styling means a rendered scan must confirm.',
|
|
1265
1283
|
].join('\n'),
|
|
1266
1284
|
input: {
|
|
1267
1285
|
text: z.string().min(1).max(5000).describe('The disclosure shown next to the purchase button, exactly as rendered.'),
|
|
@@ -1281,7 +1299,7 @@ const checkDisclosure = {
|
|
|
1281
1299
|
body_font_size_px: z.number().min(4).max(96).optional(),
|
|
1282
1300
|
}, 'placement')
|
|
1283
1301
|
.optional()
|
|
1284
|
-
.describe('
|
|
1302
|
+
.describe('Defaults: above the button, same page, 14px, high contrast.'),
|
|
1285
1303
|
consent: strictObject({
|
|
1286
1304
|
checkbox: z.boolean(),
|
|
1287
1305
|
prechecked: z.boolean().optional(),
|
|
@@ -1289,7 +1307,8 @@ const checkDisclosure = {
|
|
|
1289
1307
|
required: z.boolean().optional(),
|
|
1290
1308
|
}, 'consent')
|
|
1291
1309
|
.optional()
|
|
1292
|
-
.describe('The consent checkbox, if
|
|
1310
|
+
.describe('The consent checkbox, if any.'),
|
|
1311
|
+
checkout_type: z.enum(['own_page', 'stripe_checkout', 'payment_link']).optional().describe('Stripe-hosted: no box here'),
|
|
1293
1312
|
jurisdictions: jurisdictions.optional(),
|
|
1294
1313
|
as_of: asOf.optional(),
|
|
1295
1314
|
},
|
|
@@ -1679,7 +1698,7 @@ const updateSite = {
|
|
|
1679
1698
|
name: 'update_site',
|
|
1680
1699
|
title: 'Update a site\'s settings (write)',
|
|
1681
1700
|
description: [
|
|
1682
|
-
'WRITE: updates a site — the fields plan tasks of change.type "config" ask for (their change.content is this tool\'s input): checkout_url, checkout_steps, test_account { login_url, email, password } (stored encrypted; the response echoes it without the password), name (the merchant name in generated legal copy), platform, jurisdictions, monitoring_interval_minutes, protection_level
|
|
1701
|
+
'WRITE: updates a site — the fields plan tasks of change.type "config" ask for (their change.content is this tool\'s input): checkout_url, checkout_steps, checkout_pages, test_account { login_url, email, password } (stored encrypted; the response echoes it without the password), name (the merchant name in generated legal copy), platform, jurisdictions, monitoring_interval_minutes, protection_level.',
|
|
1683
1702
|
'The response is the site; check checkout.url / checkout.test_account to confirm. Then verify_task the task that asked for it.',
|
|
1684
1703
|
].join('\n'),
|
|
1685
1704
|
input: {
|
|
@@ -1687,10 +1706,11 @@ const updateSite = {
|
|
|
1687
1706
|
name: z.string().max(200).optional().describe('Merchant / brand name (used in generated disclosures).'),
|
|
1688
1707
|
checkout_url: z.string().max(2048).optional().describe('Page with the Subscribe / Buy button.'),
|
|
1689
1708
|
checkout_steps: z.array(z.record(z.string(), z.unknown())).max(20).optional().describe('Steps to reach the purchase button (clicks / fills), as in the API.'),
|
|
1709
|
+
checkout_pages: z.array(z.record(z.string(), z.string())).max(4).optional().describe('[{url,plan}] per plan'),
|
|
1690
1710
|
test_account: strictObject({
|
|
1691
1711
|
login_url: z.string().max(2048),
|
|
1692
1712
|
email: z.string().max(320),
|
|
1693
|
-
password: z.string().max(200),
|
|
1713
|
+
password: z.string().max(200).optional(),
|
|
1694
1714
|
reactivate_url: z.string().max(2048).optional().describe('Test-only page that re-subscribes the test account.'),
|
|
1695
1715
|
}, 'test_account')
|
|
1696
1716
|
.optional()
|
|
@@ -1704,7 +1724,7 @@ const updateSite = {
|
|
|
1704
1724
|
annotations: { ...write(false, true), title: 'Update site' },
|
|
1705
1725
|
async run(args, { api }) {
|
|
1706
1726
|
const site = await resolveSite(api, args.site);
|
|
1707
|
-
const body = pick(args, ['name', 'checkout_url', 'checkout_steps', 'test_account', 'platform', 'jurisdictions', 'monitoring_interval_minutes', 'protection_level']);
|
|
1727
|
+
const body = pick(args, ['name', 'checkout_url', 'checkout_steps', 'checkout_pages', 'test_account', 'platform', 'jurisdictions', 'monitoring_interval_minutes', 'protection_level']);
|
|
1708
1728
|
if (!Object.keys(body).length)
|
|
1709
1729
|
throw new ToolError('Pass at least one field to change (e.g. checkout_url, test_account, name).');
|
|
1710
1730
|
const updated = await api.request('POST', `/v1/sites/${enc(site)}`, { body });
|
|
@@ -1832,9 +1852,13 @@ const listFindings = {
|
|
|
1832
1852
|
starting_after: args.starting_after,
|
|
1833
1853
|
},
|
|
1834
1854
|
});
|
|
1855
|
+
// Round 34 (pilot 33 item 11, core-new-app: status=fail listed 0 while the plan said "2 fixes needed"): status is
|
|
1856
|
+
// the engine's verdict; the fix count is the status label (a needs_review finding can need a fix).
|
|
1857
|
+
const failNote = args.status === 'fail' && !list.data.length ? ' status=fail is the engine verdict only: a needs_review finding can still be counted as a fix (its label says "Needs a fix"). The fixes to make are the tasks of get_compliance_plan.' : '';
|
|
1835
1858
|
return {
|
|
1836
|
-
headline: `${list.data.length}${list.has_more ? '+' : ''} finding(s)
|
|
1837
|
-
|
|
1859
|
+
headline: `${list.data.length}${list.has_more ? '+' : ''} finding(s).${failNote}`,
|
|
1860
|
+
// Round 35 (pilot 34 item 7): each finding with what the plan says about it (the plan decides what is left to do).
|
|
1861
|
+
notes: list.data.map((f) => `${f.severity.toUpperCase()} ${f.id} [${f.compliance?.label ?? f.status}] ${f.check_id}: ${f.title}${f.requirements?.[0] ? ` [${f.requirements[0].citation}]` : ''}${f.plan_task ? ` — plan: ${f.plan_task.label} (${f.plan_task.id})` : ''}`),
|
|
1838
1862
|
data: list,
|
|
1839
1863
|
};
|
|
1840
1864
|
},
|
|
@@ -1858,7 +1882,7 @@ const getFinding = {
|
|
|
1858
1882
|
? await optional(api.request('GET', `/v1/findings/${enc(id)}/fix`, { query: pick(args, ['target', 'cancel_url', 'plan']) }))
|
|
1859
1883
|
: { unavailable: 'This key has no fixes:read permission.' };
|
|
1860
1884
|
return {
|
|
1861
|
-
headline: `${finding.severity.toUpperCase()} ${finding.id} [${finding.compliance?.label ?? finding.status}] ${finding.check_id}: ${finding.title}`,
|
|
1885
|
+
headline: `${finding.severity.toUpperCase()} ${finding.id} [${finding.compliance?.label ?? finding.status}] ${finding.check_id}: ${finding.title}${finding.plan_task ? ` — plan: ${finding.plan_task.label} (${finding.plan_task.id})` : ''}`,
|
|
1862
1886
|
notes: [complianceBullet(finding.check_id, finding.compliance, finding.status, finding.title)].filter((x) => !!x),
|
|
1863
1887
|
data: { finding, fix },
|
|
1864
1888
|
next: findingNext(finding, nextActionOf(fix)),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkbox-site/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Checkbox MCP server: auto-renewal law and subscription compliance checks for AI agents (FTC ROSCA, California ARL, New York and other states) on Stripe Checkout params, pricing pages, disclosures and emails. Remote at https://app.checkbox.site/mcp, or local stdio.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"checkbox",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"prepack": "npm run build"
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
-
"@checkbox-site/node": "^0.2.
|
|
80
|
+
"@checkbox-site/node": "^0.2.8",
|
|
81
81
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
82
82
|
"zod": "^4.6.7"
|
|
83
83
|
}
|