@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 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.6";
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.6';
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 (pass wait: 30 to get the answer as soon as the run moves on). 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.',
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
- wait: z.number().int().min(0).max(30).optional().describe('Long-poll: wait up to this many seconds for the run to move on before answering.'),
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(30, Math.floor(args.wait)) : undefined;
1244
- const run = await api.request('GET', `/v1/test_runs/${enc(id)}`, wait ? { query: { wait } } : undefined);
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","trial_days":14,"cancel_url":"https://acme.example/account/cancel","merchant_name":"Acme","consent":{"checkbox":true,"prechecked":false}}.',
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 (it is semibold). needs_review on styling means a rendered scan must confirm.',
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('Where and how the disclosure is shown (defaults: above the button, same page, 14px, high contrast).'),
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 the page has one. Omit when there is none.'),
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 (minimum = legal floor only; balanced = + recommended suggestions, the default; maximum = + extras; only required items ever affect compliance).',
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
- 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}]` : ''}`),
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.6",
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.6",
80
+ "@checkbox-site/node": "^0.2.8",
81
81
  "@modelcontextprotocol/sdk": "^1.32.2",
82
82
  "zod": "^4.6.7"
83
83
  }