@checkbox-site/mcp 0.2.8 → 0.2.9

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/format.d.ts CHANGED
@@ -47,13 +47,18 @@ export interface ToolOutput {
47
47
  next?: NextAction | null;
48
48
  /** Mark the result as an error the model should act on (still a normal result). */
49
49
  isError?: boolean;
50
+ /**
51
+ * Round 36 (pilot 35 item 18: setup and plan responses ran to 20-44 KB): print the JSON without indentation. The tools
52
+ * with a `verbose` argument use it by default.
53
+ */
54
+ compactJson?: boolean;
50
55
  }
51
56
  /**
52
57
  * Round-3 pilot (nextjs, sveltekit): output was cut at 60 KB in the middle of the JSON, which agents then failed to
53
58
  * parse. Output is always valid JSON: an oversized list is paged (the first items that fit, plus `_page` naming the
54
59
  * ids of the rest to fetch one by one), never cut mid-value.
55
60
  */
56
- export declare function fitJson(data: unknown, max?: number): {
61
+ export declare function fitJson(data: unknown, max?: number, flatFirst?: boolean): {
57
62
  json: string;
58
63
  page?: string;
59
64
  };
package/dist/format.js CHANGED
@@ -296,7 +296,12 @@ function fitNestedLists(data, arr, max) {
296
296
  * parse. Output is always valid JSON: an oversized list is paged (the first items that fit, plus `_page` naming the
297
297
  * ids of the rest to fetch one by one), never cut mid-value.
298
298
  */
299
- export function fitJson(data, max = MAX_JSON_CHARS) {
299
+ export function fitJson(data, max = MAX_JSON_CHARS, flatFirst = false) {
300
+ if (flatFirst) {
301
+ const flat = JSON.stringify(data);
302
+ if (flat.length <= max)
303
+ return { json: flat };
304
+ }
300
305
  const pretty = JSON.stringify(data, null, 2);
301
306
  if (pretty.length <= max)
302
307
  return { json: pretty };
@@ -349,7 +354,7 @@ export function renderOutput(out) {
349
354
  // Round 23 (pilot 22 item 27, agent-discovery: a result with no "Next:" line): every result names what comes next.
350
355
  lines.push(next ?? (out.isError ? FALLBACK_NEXT_ERROR : FALLBACK_NEXT));
351
356
  if (out.data !== undefined) {
352
- const fitted = fitJson(forMcp(out.data));
357
+ const fitted = fitJson(forMcp(out.data), MAX_JSON_CHARS, out.compactJson === true);
353
358
  if (fitted.page)
354
359
  lines.push(`- ${fitted.page}`);
355
360
  lines.push('', '```json', fitted.json, '```');
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";
8
+ export declare const SERVER_VERSION = "0.2.9";
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.8';
12
+ export const SERVER_VERSION = '0.2.9';
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.d.ts CHANGED
@@ -139,6 +139,20 @@ export declare function sitePlansNote(plans: unknown): string | null;
139
139
  * and complete_setup carry them in full), and once the Stripe key is stored its permission checklist is dropped. Pure.
140
140
  */
141
141
  export declare function compactSetupDoc<T>(doc: T): T;
142
+ /**
143
+ * Round 36 (pilot 35 item 18, new-app / dev-approver: setup responses of 20-40 KB): the default setup response. On top of
144
+ * compactSetupDoc, the latest check's top findings become a count (get_compliance_plan and list_findings have them) and
145
+ * finished checklist steps keep only key, title and status. Every open step, id, status and next_action stays. Pure.
146
+ */
147
+ export declare function compactSetupResponse<T>(doc: T): T;
148
+ /** A task's next_action without what an MCP agent does not use: the CLI command, and the endpoint when a tool performs it. Pure. */
149
+ export declare function slimNextAction(na: unknown, taskId?: string): unknown;
150
+ /**
151
+ * Round 36 (pilot 35 item 18: get_compliance_plan answered with 28-44 KB): the default plan. Open tasks as slimPlan,
152
+ * with `why` shortened and next_action without the CLI command; closed tasks as id, key, title, status, tier and why
153
+ * they closed; go_live as its summary (the notes list each step). verbose=true returns the API's full plan. Pure.
154
+ */
155
+ export declare function compactPlan(plan: PlanLite): PlanLite;
142
156
  /**
143
157
  * Pilot round 9 (nextjs): setup_checkbox still showed `insurable: false` and `score: 26.8` next to the status label.
144
158
  * The API keeps both (deprecated) for backward compatibility; agents see only the one status.
package/dist/tools.js CHANGED
@@ -87,7 +87,6 @@ const listLimit = z.number().int().min(1).max(100).optional().describe('How many
87
87
  const startingAfter = z.string().max(100).optional().describe('Pagination cursor: the last id of the previous page (when has_more was true).');
88
88
  /** Plan fields agents reach for, mapped to the setup field (plans[] amounts are minor units). */
89
89
  const PLAN_FIELD_HINTS = Object.freeze({
90
- price: 'amount (minor units: 800 = $8.00)',
91
90
  price_cents: 'amount (minor units: 800 = $8.00)',
92
91
  amount_cents: 'amount',
93
92
  unit_amount: 'amount',
@@ -99,15 +98,15 @@ const PLAN_FIELD_HINTS = Object.freeze({
99
98
  cancel_url: 'account_url (a site field, not a plan field)',
100
99
  });
101
100
  const setupFactsShape = {
102
- site_url: z.string().max(2048).optional().describe('Public URL of the site (production, or a preview deployment), e.g. "https://acme.example".'),
103
- checkout_url: z.string().max(2048).optional().describe('Page where customers subscribe (pricing or checkout page).'),
101
+ site_url: z.string().max(2048).optional().describe('Public URL of the site (production or a preview), e.g. "https://acme.example".'),
102
+ checkout_url: z.string().max(2048).optional().describe('Pricing or checkout page.'),
104
103
  account_url: z.string().max(2048).nullable().optional().describe('Account page where subscribers manage / cancel (null clears a URL).'),
105
104
  terms_url: z.string().max(2048).nullable().optional().describe('Terms of service URL. Checkbox fetches and checks it.'),
106
105
  privacy_url: z.string().max(2048).nullable().optional().describe('Privacy policy URL.'),
107
106
  refund_url: z.string().max(2048).nullable().optional().describe('Refund policy URL.'),
108
107
  cancellation_url: z.string().max(2048).nullable().optional().describe('Cancellation policy URL, if separate from the Terms.'),
109
108
  support_email: z.string().max(320).optional().describe('Support address for subscribers.'),
110
- merchant_name: z.string().max(100).optional().describe('The brand subscribers know (names the seller in generated disclosures), e.g. from the <title> or package.json (also accepted as "brand"). Default: the host name, which the plan then asks you to replace.'),
109
+ merchant_name: z.string().max(100).optional().describe('The brand subscribers know (names the seller in generated disclosures), e.g. from the <title> or package.json Default: the host name, which the plan asks you to replace.'),
111
110
  billing_platform: z
112
111
  .enum(['stripe', 'shopify', 'recharge', 'chargebee', 'recurly', 'paddle', 'lemon_squeezy', 'woocommerce', 'custom'])
113
112
  .optional()
@@ -126,10 +125,13 @@ const setupFactsShape = {
126
125
  intro_amount_off: z.number().int().positive().optional(),
127
126
  intro_periods: z.number().int().min(1).max(36).optional(),
128
127
  price_id: z.string().max(200).optional().describe('Stripe Price id.'),
128
+ // Round 36 (pilot 35 item 21, codex / dev-approver: the first call sent plans[].price in dollars and was refused).
129
+ // The API takes it as amount in MAJOR units and refuses a price that disagrees with amount.
130
+ price: z.number().positive().optional().describe('Or major units (19 = $19).'),
129
131
  }, 'plans[]', PLAN_FIELD_HINTS))
130
132
  .max(50)
131
133
  .optional()
132
- .describe('Plans and prices found in the code, e.g. [{"name":"Pro","amount":1900,"currency":"usd","interval":"month","trial_days":14}]; a promo adds intro_percent_off and intro_periods. They decide which laws apply and what generated copy states.'),
134
+ .describe('Plans and prices found in the code, e.g. [{"name":"Pro","amount":1900,"currency":"usd","interval":"month","trial_days":14}]; a promo adds intro_percent_off and intro_periods. They decide which laws apply.'),
133
135
  project: z
134
136
  .object({
135
137
  name: z.string().max(200).optional().describe('package.json name.'),
@@ -422,7 +424,7 @@ export function sitePlansNote(plans) {
422
424
  * (site_url) and the first checks run in the same call (homepage + snippet, policies fetched and checked, evaluation,
423
425
  * first scan), exactly like the end of the device-login path.
424
426
  */
425
- async function setupOverOAuth(api, session, ctx) {
427
+ async function setupOverOAuth(api, session, ctx, verbose = false) {
426
428
  const notes = [
427
429
  `Connected to Checkbox through ${ctx.key.appName ?? ctx.clientName ?? 'this app'} with the human's approval (${ctx.key.mode} mode${ctx.key.kind === 'restricted' ? ', read-only' : ''}). No API key is needed: do not ask the human for one.`,
428
430
  ];
@@ -457,11 +459,13 @@ async function setupOverOAuth(api, session, ctx) {
457
459
  return {
458
460
  headline: `Setup session ${session.id} started${session.business?.site_url ? ` for ${untrusted(session.business.site_url, 100)}` : ''}${session.site ? ` (test-mode site ${session.site})` : ''}.`,
459
461
  notes,
462
+ compactJson: !verbose,
460
463
  ...(next ? { next } : {}),
461
464
  data: (() => {
462
465
  const doc = { ...session, ...(fresh ?? {}) };
463
466
  const keys = labelledPublicKeys(doc.install?.snippet_key, doc.publishable_key);
464
- return compactSetupDoc(withoutLegacyScore({ ...doc, ...(keys.length ? { public_keys: keys } : {}), ...(firstCheck ? { first_check: firstCheck } : {}) }));
467
+ const out = withoutLegacyScore({ ...doc, ...(keys.length ? { public_keys: keys } : {}), ...(firstCheck ? { first_check: firstCheck } : {}) });
468
+ return verbose ? out : compactSetupResponse(out);
465
469
  })(),
466
470
  };
467
471
  }
@@ -476,7 +480,7 @@ export function compactSetupDoc(doc) {
476
480
  const d = { ...doc };
477
481
  const g = d.go_live;
478
482
  if (g && typeof g === 'object' && Array.isArray(g.steps))
479
- d.go_live = { summary: g.summary ?? null, steps: g.steps.length, full: 'get_compliance_plan (go_live) or complete_setup' };
483
+ d.go_live = { summary: g.summary ?? null, steps: g.steps.length, full: 'the go-live notes of get_compliance_plan, complete_setup, or verbose=true' };
480
484
  const st = d.stripe;
481
485
  if (st && st.test_credential_stored === true && st.restricted_key && Array.isArray(st.restricted_key.permissions)) {
482
486
  const { permissions: _p, ...rk } = st.restricted_key;
@@ -484,6 +488,67 @@ export function compactSetupDoc(doc) {
484
488
  }
485
489
  return d;
486
490
  }
491
+ /** `verbose` on the setup and plan tools (round 36, pilot 35 item 18). */
492
+ const verboseArg = z.boolean().optional().describe('true: the full JSON object.');
493
+ const CLOSED_STEPS = new Set(['done', 'skipped', 'not_applicable']);
494
+ /**
495
+ * Round 36 (pilot 35 item 18, new-app / dev-approver: setup responses of 20-40 KB): the default setup response. On top of
496
+ * compactSetupDoc, the latest check's top findings become a count (get_compliance_plan and list_findings have them) and
497
+ * finished checklist steps keep only key, title and status. Every open step, id, status and next_action stays. Pure.
498
+ */
499
+ export function compactSetupResponse(doc) {
500
+ if (!doc || typeof doc !== 'object')
501
+ return doc;
502
+ const d = compactSetupDoc({ ...doc });
503
+ const c = d.compliance;
504
+ if (c && typeof c === 'object' && Array.isArray(c.top_findings)) {
505
+ const { top_findings: top, ...rest } = c;
506
+ d.compliance = { ...rest, top_findings_count: top.length };
507
+ }
508
+ if (Array.isArray(d.steps)) {
509
+ d.steps = d.steps.map((st) => {
510
+ const x = st;
511
+ return x && typeof x === 'object' && typeof x.status === 'string' && CLOSED_STEPS.has(x.status) ? { key: x.key, title: x.title, status: x.status } : st;
512
+ });
513
+ }
514
+ for (const k of ['setup_session', 'session'])
515
+ if (d[k] && typeof d[k] === 'object')
516
+ d[k] = compactSetupResponse(d[k]);
517
+ return d;
518
+ }
519
+ /** A task's next_action without what an MCP agent does not use: the CLI command, and the endpoint when a tool performs it. Pure. */
520
+ export function slimNextAction(na, taskId) {
521
+ if (!na || typeof na !== 'object' || Array.isArray(na))
522
+ return na;
523
+ const { command: _command, ...rest } = na;
524
+ if (taskId && rest.task === taskId)
525
+ delete rest.task;
526
+ if (typeof rest.mcp_tool === 'string' && rest.mcp_tool !== 'call_api')
527
+ delete rest.endpoint;
528
+ return rest;
529
+ }
530
+ /**
531
+ * Round 36 (pilot 35 item 18: get_compliance_plan answered with 28-44 KB): the default plan. Open tasks as slimPlan,
532
+ * with `why` shortened and next_action without the CLI command; closed tasks as id, key, title, status, tier and why
533
+ * they closed; go_live as its summary (the notes list each step). verbose=true returns the API's full plan. Pure.
534
+ */
535
+ export function compactPlan(plan) {
536
+ const slim = slimPlan(plan);
537
+ const full = new Map((plan.tasks ?? []).map((t) => [t.id, t]));
538
+ const tasks = (slim.tasks ?? []).map((t) => {
539
+ const x = t;
540
+ if (t.status === 'done' || t.status === 'wont_fix') {
541
+ const f = full.get(t.id) ?? {};
542
+ return { id: t.id, key: x.key, title: t.title, status: t.status, tier: t.tier, compliance_status: x.compliance_status, closed_reason: f.closed_reason ?? null };
543
+ }
544
+ return { ...x, why: clipWords(x.why, 200), next_action: slimNextAction(x.next_action, t.id) };
545
+ });
546
+ const out = { ...slim, tasks };
547
+ const g = out.go_live;
548
+ if (g && typeof g === 'object' && Array.isArray(g.steps))
549
+ out.go_live = { summary: g.summary ?? null, steps: g.steps.length, full: 'the notes above, or verbose=true' };
550
+ return out;
551
+ }
487
552
  /**
488
553
  * Pilot round 9 (nextjs): setup_checkbox still showed `insurable: false` and `score: 26.8` next to the status label.
489
554
  * The API keeps both (deprecated) for backward compatibility; agents see only the one status.
@@ -544,6 +609,7 @@ const setupCheckbox = {
544
609
  device_code: z.string().max(200).optional().describe('From an earlier setup_checkbox call: poll that browser approval instead of starting a new one.'),
545
610
  live: z.boolean().optional().describe('Also request live-mode keys (live mode activates after domain verification). Default false: test mode only.'),
546
611
  agent: z.string().max(100).optional().describe('Which AI agent is doing this, shown to the human on the approval page, e.g. "Claude Code".'),
612
+ verbose: verboseArg,
547
613
  },
548
614
  scopes: ['setup_sessions:write'],
549
615
  public: true,
@@ -630,6 +696,7 @@ const setupCheckbox = {
630
696
  return {
631
697
  headline: `Approved. Checkbox account ready${site ? ` with test-mode site ${site}` : ''}; setup session ${res.setup_session ?? '(none)'}.`,
632
698
  notes,
699
+ compactJson: !args.verbose,
633
700
  ...(next ? { next } : {}),
634
701
  data: {
635
702
  ...(saved ? withoutSecrets(res) : res),
@@ -677,7 +744,7 @@ const setupCheckbox = {
677
744
  body.client = { name: 'checkbox-mcp', version: ctx.serverVersion, ...(args.agent || ctx.key.appName || ctx.clientName ? { agent: String(args.agent ?? ctx.key.appName ?? ctx.clientName).slice(0, 100) } : {}) };
678
745
  const session = await api.request('POST', '/v1/setup_sessions', { body });
679
746
  if (ctx.key.auth === 'oauth')
680
- return setupOverOAuth(api, session, ctx);
747
+ return setupOverOAuth(api, session, ctx, args.verbose === true);
681
748
  const notes = session.review_url ? [`Human review page: ${session.review_url}`] : [];
682
749
  if (session.next_action?.type === 'verify_domain')
683
750
  notes.push(firstCheckNote(session.next_action));
@@ -687,9 +754,11 @@ const setupCheckbox = {
687
754
  return {
688
755
  headline: `Setup session ${session.id} started${session.business?.site_url ? ` for ${untrusted(session.business.site_url, 100)}` : ''}${session.site ? ` (test-mode site ${session.site})` : ''}.`,
689
756
  notes,
757
+ compactJson: !args.verbose,
690
758
  data: (() => {
691
759
  const keys = labelledPublicKeys(session.install?.snippet_key, session.publishable_key);
692
- return compactSetupDoc(keys.length ? { ...withoutLegacyScore(session), public_keys: keys } : withoutLegacyScore(session));
760
+ const out = keys.length ? { ...withoutLegacyScore(session), public_keys: keys } : withoutLegacyScore(session);
761
+ return args.verbose ? out : compactSetupResponse(out);
693
762
  })(),
694
763
  };
695
764
  },
@@ -750,7 +819,7 @@ const getSetupStatus = {
750
819
  const pl = sitePlansNote(res.setup_session?.site_plans);
751
820
  if (pl)
752
821
  notes.push(pl);
753
- return { headline: `Re-checked setup session ${id}.`, ...(notes.length ? { notes } : {}), data: withoutLegacyScore(res), next: nextActionOf(res) ?? nextActionOf(res.setup_session) ?? nextActionOf(res.session) };
822
+ return { headline: `Re-checked setup session ${id}.`, ...(notes.length ? { notes } : {}), compactJson: true, data: compactSetupResponse(withoutLegacyScore(res)), next: nextActionOf(res) ?? nextActionOf(res.setup_session) ?? nextActionOf(res.session) };
754
823
  }
755
824
  const session = await api.request('GET', `/v1/setup_sessions/${enc(id)}`);
756
825
  const open = (session.steps ?? []).filter((s) => s.status && !['done', 'skipped', 'not_applicable'].includes(s.status));
@@ -760,7 +829,8 @@ const getSetupStatus = {
760
829
  headline: `Setup session ${session.id}: ${session.status}; ${open.length} checklist step(s) open.`,
761
830
  // The policies step says what an unverified preview read (or that the pages wait for domain verification).
762
831
  notes: [...open.slice(0, 8).map((s) => `${s.status}: ${s.title ?? s.key}${s.key === 'policies' && s.summary ? ` (${s.summary.slice(0, 240)})` : ''}`), ...(ro ? [ro] : []), ...(plansLine ? [plansLine] : []), ...(session.next_action?.type === 'done' ? goLiveNotes(session.go_live) : [])],
763
- data: withoutLegacyScore(session),
832
+ compactJson: true,
833
+ data: compactSetupResponse(withoutLegacyScore(session)),
764
834
  };
765
835
  },
766
836
  };
@@ -799,7 +869,7 @@ const updateSetup = {
799
869
  const notes = Array.isArray(args.plans) && args.plans.length
800
870
  ? ['Plans saved: Checkbox re-evaluates the site with them now, so get_compliance_plan lists each plan\'s tasks (e.g. the trial reminder of a plan with a trial). If one seems missing, call verify_site to re-check at once.']
801
871
  : [];
802
- return { headline: `Updated setup session ${session.id}.`, notes, data: withoutLegacyScore(session) };
872
+ return { headline: `Updated setup session ${session.id}.`, notes, compactJson: true, data: compactSetupResponse(withoutLegacyScore(session)) };
803
873
  },
804
874
  };
805
875
  /** Round 13 (pilot 12, codex: "sent" while nothing new arrived): the summary email's real delivery status, with when. */
@@ -899,12 +969,12 @@ const getCompliancePlan = {
899
969
  title: 'Get the compliance plan (ordered tasks)',
900
970
  description: [
901
971
  'The main tool after setup. Returns the ordered tasks that make the site pass the automatic-renewal checks, recomputed from the latest observations.',
902
- 'Each task has: requirement + legal citation, why (what Checkbox observed), where (file / component / URL / CSS selector / Stripe object), change (exact unified-diff template, copy text or Stripe params), and verify (how to check).',
972
+ 'Each task here is a summary (id, status, why, where, change type, next_action); get_task has the full change (diff, copy or Stripe params) and its legal citation.',
903
973
  'Plan status: action_required (apply open tasks), verifying (waiting for confirming observations), compliant (done: the checks pass — "pending_live_check" tasks count as passing: they complete on their own or with a proof run), blocked (only human questions — "Your call" — left).',
904
- 'Tasks are tagged tier required / recommended / extra. Only required tasks matter for compliance; recommended and extra tasks are optional suggestions (mention them once, do not push them).',
974
+ 'Only tier "required" tasks count; "recommended" and "extra" tasks are optional suggestions (mention them once, do not push them).',
905
975
  'Loop: apply the first open required task in the code → verify_task → get_compliance_plan → … until the checks pass (status compliant). Tasks with status "manual" carry a question with a recommended answer: ask the HUMAN, then answer_task. Never invent answers.',
906
976
  ].join('\n'),
907
- input: { site: siteId.optional() },
977
+ input: { site: siteId.optional(), verbose: verboseArg },
908
978
  scopes: ['compliance_plans:read'],
909
979
  annotations: { ...ro, title: 'Compliance plan' },
910
980
  async run(args, { api }) {
@@ -945,15 +1015,17 @@ const getCompliancePlan = {
945
1015
  ...(plan.status === 'compliant' ? await reviewHint(api, site, 'plan_compliant') : []),
946
1016
  ],
947
1017
  // Normal-round pilot (sveltekit): the full plan (53K characters) was cut mid-JSON by the client. The plan here is a
948
- // compact summary of every task; get_task returns one task in full.
949
- data: slimPlan(shown),
1018
+ // compact summary of every task; get_task returns one task in full. Round 36 (pilot 35 item 18): compact JSON by
1019
+ // default, the API's full plan with verbose=true.
1020
+ compactJson: !args.verbose,
1021
+ data: args.verbose ? shown : compactPlan(shown),
950
1022
  };
951
1023
  },
952
1024
  };
953
1025
  const getTask = {
954
1026
  name: 'get_task',
955
1027
  title: 'Get one compliance task',
956
- description: 'One task of the compliance plan with the full change to apply (unified-diff template with machine-applicable edits, copy, or Stripe params), where to apply it, the legal requirement, its last verification and next_action. Use it when the plan JSON was truncated or before applying a task.',
1028
+ description: 'One plan task in full: the change to apply (unified-diff template with machine-applicable edits, copy or Stripe params), where, the legal requirement, its last verification and next_action. Call it before applying a task.',
957
1029
  input: { task: z.string().max(100).describe('Task id (task_…) from get_compliance_plan.') },
958
1030
  scopes: ['compliance_plans:read'],
959
1031
  annotations: { ...ro, title: 'Task' },
@@ -1263,7 +1335,7 @@ const getTestRun = {
1263
1335
  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(', ') : '';
1264
1336
  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;
1265
1337
  return {
1266
- headline: `Test run ${run.id}: ${run.status_label ?? statusLine(run.status)}${run.phase && run.status !== 'succeeded' && run.status !== 'failed' ? ` (phase ${run.phase})` : ''}.`,
1338
+ headline: `Test run ${run.id}: ${run.status_label ?? statusLine(run.status)}${run.phase && run.status !== 'succeeded' && run.status !== 'failed' ? ` (phase ${run.phase})` : ''}.${run.plan_status_label ? ` Plan after this run: ${run.plan_status_label}.` : ''}`,
1267
1339
  notes: [...(waiting ? [waiting] : []), ...(run.steps ?? []).map((s) => `${s.status ?? '?'}: ${s.title ?? s.key}${s.code ? ` (${s.code})` : ''}`)].slice(0, 13),
1268
1340
  data: run,
1269
1341
  ...(next ? { next } : {}),
@@ -1393,12 +1465,12 @@ const checkPageHtml = {
1393
1465
  title: 'Check a checkout / pricing page (HTML or URL)',
1394
1466
  description: [
1395
1467
  'Checks a whole checkout or pricing page: finds the purchase button, the renewal disclosure, consent checkboxes and terms/cancel links, and evaluates them. Send html = the RENDERED page (after JavaScript; for React/Next pages, render it or paste the server output) — this is how to check a page before deploying.',
1396
- 'Or send url to fetch a live public page (live mode on an activated account only; test keys must send html).',
1468
+ 'Or send url to fetch a public page (live keys; test keys: your registered site\'s origin).',
1397
1469
  'Static HTML cannot measure styling, so placement/styling passes come back as needs_review until a scan confirms; failures are real. When it fails, replacement holds verified drop-in markup.',
1398
1470
  ].join('\n'),
1399
1471
  input: {
1400
1472
  html: z.string().max(1_000_000).optional().describe('Rendered checkout page HTML. Give html or url.'),
1401
- url: z.string().max(2048).optional().describe('Live public page URL to fetch instead (live, activated accounts).'),
1473
+ url: z.string().max(2048).optional().describe('Public page URL to fetch instead.'),
1402
1474
  page_kind: z.enum(['checkout', 'pricing']).optional().describe('Default checkout.'),
1403
1475
  terms: planTerms.optional().describe('The plan sold on the page. merchant_name / cancel_url alone are fine (no price compared).'),
1404
1476
  plans: z.array(strictObject(planTermsShape, 'plans[]')).max(20).optional().describe('Multi-plan pricing page: EVERY plan it sells (price, interval, trial_days each). Each plan card is judged against its own plan; plan_replacements gives each plan its own markup for its own button.'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkbox-site/mcp",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
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.8",
80
+ "@checkbox-site/node": "^0.2.9",
81
81
  "@modelcontextprotocol/sdk": "^1.32.2",
82
82
  "zod": "^4.6.7"
83
83
  }