@checkbox-site/mcp 0.1.0 → 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/README.md +3 -3
- package/dist/api.d.ts +7 -0
- package/dist/api.js +11 -1
- package/dist/format.d.ts +32 -1
- package/dist/format.js +261 -20
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/permissions.js +2 -2
- package/dist/resources.js +11 -10
- package/dist/server.d.ts +28 -3
- package/dist/server.js +104 -11
- package/dist/stdio.js +2 -1
- package/dist/todo.d.ts +25 -0
- package/dist/todo.js +35 -0
- package/dist/tools.d.ts +196 -1
- package/dist/tools.js +1084 -184
- package/dist/unknown-args.d.ts +22 -0
- package/dist/unknown-args.js +119 -0
- package/dist/version-check.d.ts +22 -0
- package/dist/version-check.js +50 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ your agent can set up a project with one human click, check checkout code before
|
|
|
7
7
|
done and run synthetic test subscriptions — against ROSCA, California's Automatic Renewal Law, New York GBL § 527-a
|
|
8
8
|
and the other state automatic-renewal laws.
|
|
9
9
|
|
|
10
|
-
Docs: https://checkbox.site/docs/mcp · Agent index: https://checkbox.site/llms.txt · Free
|
|
10
|
+
Docs: https://checkbox.site/docs/mcp · Agent index: https://checkbox.site/llms.txt · Free. Every feature. No credit card. Fair-use limits.
|
|
11
11
|
|
|
12
12
|
## Connect
|
|
13
13
|
|
|
@@ -45,7 +45,7 @@ Package: `@checkbox-site/mcp` on npm (Node 20+; binary `checkbox-mcp`). Options:
|
|
|
45
45
|
|
|
46
46
|
## Tools
|
|
47
47
|
|
|
48
|
-
Start with `setup_checkbox`; then `get_compliance_plan` → apply a task → `verify_task`, until the plan
|
|
48
|
+
Start with `setup_checkbox`; then `get_compliance_plan` → apply a task → `verify_task`, until the checks pass (plan status `compliant`).
|
|
49
49
|
Preflight tools (`check_disclosure`, `check_stripe_checkout_session`, `check_email`, `check_page_html`,
|
|
50
50
|
`get_notice_schedule`) work on code before it is deployed. The full list, with the API operation behind each tool, is
|
|
51
51
|
at https://checkbox.site/docs/mcp#tools.
|
|
@@ -56,7 +56,7 @@ the next tool to call.
|
|
|
56
56
|
|
|
57
57
|
## Not legal advice
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
Rules drafted from the statute text and updated as new statutes come into effect. Automated checks, not legal advice. Every rule cites its source, so you or your lawyer can check it.
|
|
60
60
|
|
|
61
61
|
## License
|
|
62
62
|
|
package/dist/api.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface NextAction {
|
|
|
20
20
|
endpoint?: string | null;
|
|
21
21
|
url?: string | null;
|
|
22
22
|
task?: string | null;
|
|
23
|
+
/** The MCP tool the API maps operation_id to (`call_api` when there is none). */
|
|
24
|
+
mcp_tool?: string | null;
|
|
23
25
|
}
|
|
24
26
|
/** A non-2xx /v1 response (the error envelope) or a network failure (status 0). */
|
|
25
27
|
export declare class CheckboxApiError extends Error {
|
|
@@ -39,6 +41,11 @@ export interface CheckboxApi {
|
|
|
39
41
|
readonly hasKey: boolean;
|
|
40
42
|
/** Base URL of the API (for links in tool output). */
|
|
41
43
|
readonly apiBase: string;
|
|
44
|
+
/**
|
|
45
|
+
* Round 21 (pilot 20 item 5): the oldest MCP server version the API said it expects (response header
|
|
46
|
+
* checkbox-mcp-minimum-version), once a response carried it. Absent on in-process clients (same version as the API).
|
|
47
|
+
*/
|
|
48
|
+
minimumMcpVersion?(): string | undefined;
|
|
42
49
|
}
|
|
43
50
|
/** Builds the query string for a request (`expand` arrays are not needed by the tools). */
|
|
44
51
|
export declare function withQuery(path: string, query?: ApiRequestOptions['query']): string;
|
package/dist/api.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// - The local stdio server (`checkbox-mcp`) implements it over HTTPS (createHttpApi, or an @checkbox-site/node client
|
|
7
7
|
// through fromSdkClient).
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { MinimumVersionTracker } from "./version-check.js";
|
|
9
10
|
/** API version the MCP tools are written against (sent as Checkbox-Version). */
|
|
10
11
|
export const API_VERSION = '2026-10-30';
|
|
11
12
|
export const DEFAULT_API_BASE = 'https://app.checkbox.site';
|
|
@@ -57,9 +58,11 @@ export function createHttpApi(opts) {
|
|
|
57
58
|
const f = opts.fetch ?? fetch;
|
|
58
59
|
const base = (opts.apiBase ?? DEFAULT_API_BASE).replace(/\/+$/, '');
|
|
59
60
|
const maxRetries = opts.maxRetries ?? 2;
|
|
61
|
+
const versions = new MinimumVersionTracker();
|
|
60
62
|
return {
|
|
61
63
|
hasKey: Boolean(opts.apiKey),
|
|
62
64
|
apiBase: base,
|
|
65
|
+
minimumMcpVersion: () => versions.minimum,
|
|
63
66
|
async request(method, path, ro = {}) {
|
|
64
67
|
const url = base + withQuery(path.startsWith('/') ? path : `/${path}`, ro.query);
|
|
65
68
|
const headers = {
|
|
@@ -95,6 +98,7 @@ export function createHttpApi(opts) {
|
|
|
95
98
|
await sleep(Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 10) * 1000 : 500 * 2 ** attempt);
|
|
96
99
|
continue;
|
|
97
100
|
}
|
|
101
|
+
versions.observe(res.headers);
|
|
98
102
|
const text = await res.text();
|
|
99
103
|
let json;
|
|
100
104
|
try {
|
|
@@ -122,13 +126,19 @@ export function fromSdkClient(client, info) {
|
|
|
122
126
|
: null;
|
|
123
127
|
if (!call)
|
|
124
128
|
return null;
|
|
129
|
+
const versions = new MinimumVersionTracker();
|
|
125
130
|
return {
|
|
126
131
|
...info,
|
|
132
|
+
minimumMcpVersion: () => versions.minimum,
|
|
127
133
|
async request(method, path, ro = {}) {
|
|
128
134
|
try {
|
|
129
|
-
|
|
135
|
+
const out = await call(method, path, ro);
|
|
136
|
+
// The Node SDK puts the response headers on a non-enumerable `lastResponse`.
|
|
137
|
+
versions.observe(out?.lastResponse?.headers);
|
|
138
|
+
return out;
|
|
130
139
|
}
|
|
131
140
|
catch (err) {
|
|
141
|
+
versions.observe(err?.headers);
|
|
132
142
|
throw toCheckboxApiError(err);
|
|
133
143
|
}
|
|
134
144
|
},
|
package/dist/format.d.ts
CHANGED
|
@@ -10,8 +10,30 @@ export declare function mcpToolFor(operationId: string | null | undefined): stri
|
|
|
10
10
|
* reads it as an instruction next to the "Next:" line. Full values stay in the fenced JSON.
|
|
11
11
|
*/
|
|
12
12
|
export declare function untrusted(value: unknown, max?: number): string;
|
|
13
|
+
/**
|
|
14
|
+
* Whether the `checkbox` CLI (@checkbox-site/cli) is on npm. While this is false, MCP output never shows `command`
|
|
15
|
+
* fields or a "CLI equivalent". It is published.
|
|
16
|
+
*/
|
|
17
|
+
export declare const CLI_PUBLISHED = true;
|
|
18
|
+
/** Docs for `checkbox ci` / the GitHub Action (checkbox.json + .github/workflows/checkbox.yml). */
|
|
19
|
+
export declare const CI_DOCS_URL = "https://checkbox.site/docs/guides/ci";
|
|
20
|
+
/**
|
|
21
|
+
* After a check passes, one line offering to keep it passing in CI. The Action runs the published CLI, so this stays
|
|
22
|
+
* off until CLI_PUBLISHED is true (`published` is for tests).
|
|
23
|
+
*/
|
|
24
|
+
export declare function ciHint(passing: boolean, published?: boolean): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Audit P2: text the API writes for SDK users names operations (verifyTask, updateSite, …) and REST paths. Over MCP
|
|
27
|
+
* those become the tool to call: a dedicated tool name, else `call_api` with the endpoint. CLI hints are dropped while
|
|
28
|
+
* the CLI is unpublished.
|
|
29
|
+
*/
|
|
30
|
+
export declare function mcpText(text: string): string;
|
|
31
|
+
/** Rewrites agent-facing strings in an API object for MCP (mcpText), and drops `command` while the CLI is unpublished. */
|
|
32
|
+
export declare function forMcp(value: unknown, depth?: number): unknown;
|
|
13
33
|
/** One line telling the model what to call next, in MCP terms. */
|
|
14
|
-
export declare function describeNextAction(next: NextAction | null | undefined
|
|
34
|
+
export declare function describeNextAction(next: NextAction | null | undefined, ctx?: {
|
|
35
|
+
data?: unknown;
|
|
36
|
+
}): string | null;
|
|
15
37
|
/** Pulls the top-level next_action off an API object. */
|
|
16
38
|
export declare function nextActionOf(obj: unknown): NextAction | null;
|
|
17
39
|
export interface ToolOutput {
|
|
@@ -26,6 +48,15 @@ export interface ToolOutput {
|
|
|
26
48
|
/** Mark the result as an error the model should act on (still a normal result). */
|
|
27
49
|
isError?: boolean;
|
|
28
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Round-3 pilot (nextjs, sveltekit): output was cut at 60 KB in the middle of the JSON, which agents then failed to
|
|
53
|
+
* parse. Output is always valid JSON: an oversized list is paged (the first items that fit, plus `_page` naming the
|
|
54
|
+
* ids of the rest to fetch one by one), never cut mid-value.
|
|
55
|
+
*/
|
|
56
|
+
export declare function fitJson(data: unknown, max?: number): {
|
|
57
|
+
json: string;
|
|
58
|
+
page?: string;
|
|
59
|
+
};
|
|
29
60
|
export declare function renderOutput(out: ToolOutput): CallToolResult;
|
|
30
61
|
/** An API error as a tool result: stable code, message, param and the API's own next_action. */
|
|
31
62
|
export declare function renderError(err: unknown): CallToolResult;
|
package/dist/format.js
CHANGED
|
@@ -23,6 +23,8 @@ export const OPERATION_TOOLS = {
|
|
|
23
23
|
checkEmail: 'check_email',
|
|
24
24
|
checkPage: 'check_page_html',
|
|
25
25
|
checkNoticeSchedule: 'get_notice_schedule',
|
|
26
|
+
listSiteSuggestions: 'suggest_improvements',
|
|
27
|
+
suggestImprovements: 'suggest_improvements',
|
|
26
28
|
listSites: 'list_sites',
|
|
27
29
|
createSite: 'create_site',
|
|
28
30
|
updateSite: 'update_site',
|
|
@@ -32,6 +34,7 @@ export const OPERATION_TOOLS = {
|
|
|
32
34
|
listFindings: 'list_findings',
|
|
33
35
|
retrieveFinding: 'get_finding',
|
|
34
36
|
retrieveFindingFix: 'get_finding',
|
|
37
|
+
publishShopifyTerms: 'publish_shopify_terms',
|
|
35
38
|
createScan: 'run_scan',
|
|
36
39
|
retrieveScan: 'get_scan',
|
|
37
40
|
listLawUpdates: 'list_law_updates',
|
|
@@ -72,6 +75,11 @@ const OPERATION_ENDPOINTS = {
|
|
|
72
75
|
listSiteFixes: 'GET /v1/sites/{site}/fixes',
|
|
73
76
|
listVerifications: 'GET /v1/sites/{site}/verifications',
|
|
74
77
|
cancelTestRun: 'POST /v1/test_runs/{test_run}/cancel',
|
|
78
|
+
createReceiptToken: 'POST /v1/receipt_tokens',
|
|
79
|
+
createReceipt: 'POST /v1/receipts',
|
|
80
|
+
retrieveReceiptSettings: 'GET /v1/receipt_settings',
|
|
81
|
+
updateReceiptSettings: 'POST /v1/receipt_settings',
|
|
82
|
+
retrieveDueNotice: 'GET /v1/notices/{notice}',
|
|
75
83
|
};
|
|
76
84
|
/**
|
|
77
85
|
* Quotes text Checkbox did not write (page HTML, email bodies, policy text, merchant-supplied strings) for the text part
|
|
@@ -86,23 +94,119 @@ export function untrusted(value, max = 80) {
|
|
|
86
94
|
s = s.replace(/</g, '‹').replace(/>/g, '›');
|
|
87
95
|
return `<untrusted_text>${JSON.stringify(s)}</untrusted_text>`;
|
|
88
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether the `checkbox` CLI (@checkbox-site/cli) is on npm. While this is false, MCP output never shows `command`
|
|
99
|
+
* fields or a "CLI equivalent". It is published.
|
|
100
|
+
*/
|
|
101
|
+
export const CLI_PUBLISHED = true;
|
|
102
|
+
/** Docs for `checkbox ci` / the GitHub Action (checkbox.json + .github/workflows/checkbox.yml). */
|
|
103
|
+
export const CI_DOCS_URL = 'https://checkbox.site/docs/guides/ci';
|
|
104
|
+
/**
|
|
105
|
+
* After a check passes, one line offering to keep it passing in CI. The Action runs the published CLI, so this stays
|
|
106
|
+
* off until CLI_PUBLISHED is true (`published` is for tests).
|
|
107
|
+
*/
|
|
108
|
+
export function ciHint(passing, published = CLI_PUBLISHED) {
|
|
109
|
+
if (!published || !passing)
|
|
110
|
+
return null;
|
|
111
|
+
return `Optional, ask the human once: keep these checks passing on every pull request. With a yes, write checkbox.json listing the files you just checked (Stripe params via "source" or "module", disclosure / email template files, the plans) and .github/workflows/checkbox.yml (uses: checkbox-site/checkbox@v1; offline, no key), then run \`npx @checkbox-site/cli ci\` to confirm. \`npx @checkbox-site/cli ci init --yes\` drafts both. ${CI_DOCS_URL}`;
|
|
112
|
+
}
|
|
113
|
+
/** REST paths of the preflight checks → their MCP tools (for text written for API users). */
|
|
114
|
+
const CHECK_PATH_TOOLS = {
|
|
115
|
+
disclosure: 'check_disclosure',
|
|
116
|
+
stripe_checkout_session: 'check_stripe_checkout_session',
|
|
117
|
+
stripe_payment_link: 'call_api (POST /v1/checks/stripe_payment_link)',
|
|
118
|
+
email: 'check_email',
|
|
119
|
+
page: 'check_page_html',
|
|
120
|
+
notice_schedule: 'get_notice_schedule',
|
|
121
|
+
};
|
|
122
|
+
/** CLI commands → the MCP tool that does the same (for text that still names a command). */
|
|
123
|
+
const CLI_TOOLS = { plan: 'get_compliance_plan', fix: 'get_task', answer: 'answer_task', verify: 'verify_site', init: 'get_setup_status', check: 'check_disclosure' };
|
|
124
|
+
const OP_RE = new RegExp(`\\b(${[...Object.keys(OPERATION_TOOLS), ...Object.keys(OPERATION_ENDPOINTS)].sort((a, b) => b.length - a.length).join('|')})\\b`, 'g');
|
|
125
|
+
/**
|
|
126
|
+
* Audit P2: text the API writes for SDK users names operations (verifyTask, updateSite, …) and REST paths. Over MCP
|
|
127
|
+
* those become the tool to call: a dedicated tool name, else `call_api` with the endpoint. CLI hints are dropped while
|
|
128
|
+
* the CLI is unpublished.
|
|
129
|
+
*/
|
|
130
|
+
export function mcpText(text) {
|
|
131
|
+
let s = text.replace(/(?:POST )?\/v1\/checks\/(disclosure|stripe_checkout_session|stripe_payment_link|email|page|notice_schedule)\b/g, (_m, k) => CHECK_PATH_TOOLS[k]);
|
|
132
|
+
s = s.replace(OP_RE, (op) => {
|
|
133
|
+
// Round 16 (pilot 15, sveltekit): "verifySetupSession" became plain get_setup_status, which only reads the checklist,
|
|
134
|
+
// so the agent looped on "Snippet installed: pending". The re-check is get_setup_status with verify=true.
|
|
135
|
+
if (op === 'verifySetupSession')
|
|
136
|
+
return 'get_setup_status (verify=true)';
|
|
137
|
+
const tool = OPERATION_TOOLS[op];
|
|
138
|
+
if (tool)
|
|
139
|
+
return tool;
|
|
140
|
+
const endpoint = OPERATION_ENDPOINTS[op];
|
|
141
|
+
return endpoint ? `call_api (${endpoint})` : op;
|
|
142
|
+
});
|
|
143
|
+
// Agent audit round 2: "call_api (POST /v1/sites/{site}/notice_settings) (POST /v1/sites/site_…/notice_settings)":
|
|
144
|
+
// an operation followed by its own concrete endpoint is said once, with the concrete path.
|
|
145
|
+
s = s.replace(/call_api \(((?:GET|POST|DELETE) )[^()]*\) \(\1([^()]+)\)/g, (_m, verb, path) => `call_api (${verb}${path})`);
|
|
146
|
+
if (!CLI_PUBLISHED) {
|
|
147
|
+
s = s
|
|
148
|
+
.replace(/\s*\((?:or )?(?:run )?`(?:npx [^`]*)?checkbox [^`]*`[^)]*\)/g, '')
|
|
149
|
+
.replace(/,? or (?:run )?`checkbox [^`]*`/g, '')
|
|
150
|
+
.replace(/ \(`checkbox [^`]*` (?:writes|does) [^)]*\)/g, '')
|
|
151
|
+
.replace(/(?:run )?`checkbox ([a-z-]+)[^`]*`/g, (_m, cmd) => `\`${CLI_TOOLS[cmd] ?? 'get_compliance_plan'}\``);
|
|
152
|
+
}
|
|
153
|
+
return s;
|
|
154
|
+
}
|
|
155
|
+
const TEXT_KEYS = new Set(['message', 'description', 'summary', 'fix', 'note', 'how', 'detail', 'headline']);
|
|
156
|
+
/** Round 21 (pilot 20 item 16): lists of agent-facing sentences the API writes with operation names (REST words). */
|
|
157
|
+
const TEXT_LIST_KEYS = new Set(['plan_terms_conflicts', 'notes', 'steps']);
|
|
158
|
+
/** Rewrites agent-facing strings in an API object for MCP (mcpText), and drops `command` while the CLI is unpublished. */
|
|
159
|
+
export function forMcp(value, depth = 0) {
|
|
160
|
+
if (depth > 12 || value === null || typeof value !== 'object')
|
|
161
|
+
return value;
|
|
162
|
+
if (Array.isArray(value))
|
|
163
|
+
return value.map((v) => forMcp(v, depth + 1));
|
|
164
|
+
const out = {};
|
|
165
|
+
for (const [k, v] of Object.entries(value)) {
|
|
166
|
+
if (k === 'command' && !CLI_PUBLISHED && (v === null || typeof v === 'string'))
|
|
167
|
+
continue;
|
|
168
|
+
out[k] =
|
|
169
|
+
typeof v === 'string' && TEXT_KEYS.has(k)
|
|
170
|
+
? mcpText(v)
|
|
171
|
+
: Array.isArray(v) && TEXT_LIST_KEYS.has(k)
|
|
172
|
+
? v.map((x) => (typeof x === 'string' ? mcpText(x) : forMcp(x, depth + 1)))
|
|
173
|
+
: forMcp(v, depth + 1);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
/** The argument hint for the tool a next step names (the right id for that tool). */
|
|
178
|
+
function argHint(tool, next, ctx) {
|
|
179
|
+
const d = (ctx.data ?? {});
|
|
180
|
+
if (tool === 'get_verification')
|
|
181
|
+
return d.object === 'verification' && typeof d.id === 'string' ? ` (verification: ${d.id})` : '';
|
|
182
|
+
if (tool === 'get_setup_status' && next.operation_id === 'verifySetupSession')
|
|
183
|
+
return ' (verify: true)';
|
|
184
|
+
const task = next.task ?? /\/v1\/tasks\/(task_[A-Za-z0-9]+)/.exec(next.endpoint ?? '')?.[1];
|
|
185
|
+
return task && ['get_task', 'verify_task', 'answer_task'].includes(tool) ? ` (task: ${task})` : '';
|
|
186
|
+
}
|
|
89
187
|
/** One line telling the model what to call next, in MCP terms. */
|
|
90
|
-
export function describeNextAction(next) {
|
|
188
|
+
export function describeNextAction(next, ctx = {}) {
|
|
91
189
|
if (!next || typeof next !== 'object' || !next.type)
|
|
92
190
|
return null;
|
|
93
|
-
const text = (next.message ?? next.description ?? '').trim();
|
|
191
|
+
const text = mcpText((next.message ?? next.description ?? '').trim());
|
|
94
192
|
if (next.type === 'done' || next.type === 'none')
|
|
95
193
|
return `Next: nothing left to do${text ? ` — ${text}` : '.'}`;
|
|
96
194
|
const op = next.operation_id ?? undefined;
|
|
97
|
-
|
|
195
|
+
// Prefer the API's own mapping (next_action.mcp_tool); `call_api` means "no dedicated tool".
|
|
196
|
+
const named = next.mcp_tool && next.mcp_tool !== 'call_api' ? next.mcp_tool : undefined;
|
|
197
|
+
const tool = named ?? (op ? OPERATION_TOOLS[op] : undefined);
|
|
98
198
|
const parts = [`Next (${next.type}): ${text || 'see next_action.'}`];
|
|
99
199
|
if (tool)
|
|
100
|
-
parts.push(`→ MCP tool \`${tool}\`${
|
|
200
|
+
parts.push(`→ MCP tool \`${tool}\`${argHint(tool, next, ctx)}.`);
|
|
101
201
|
else if (op) {
|
|
102
|
-
|
|
103
|
-
|
|
202
|
+
let endpoint = next.endpoint ?? OPERATION_ENDPOINTS[op];
|
|
203
|
+
const site = ctx.data ?? {};
|
|
204
|
+
const siteId = typeof site.site === 'string' && site.site.startsWith('site_') ? site.site : typeof site.id === 'string' && site.id.startsWith('site_') ? site.id : undefined;
|
|
205
|
+
if (endpoint && siteId)
|
|
206
|
+
endpoint = endpoint.replace('{site}', siteId);
|
|
207
|
+
parts.push(endpoint ? `→ MCP tool \`call_api\` with ${endpoint}.` : `→ MCP tool \`call_api\` (operation ${op}; paths in /v1/openapi.json).`);
|
|
104
208
|
}
|
|
105
|
-
if (next.command)
|
|
209
|
+
if (next.command && CLI_PUBLISHED)
|
|
106
210
|
parts.push(`CLI equivalent: \`${next.command}\`.`);
|
|
107
211
|
if (next.url && !tool)
|
|
108
212
|
parts.push(`Docs: ${next.url}`);
|
|
@@ -114,20 +218,131 @@ export function nextActionOf(obj) {
|
|
|
114
218
|
return n && typeof n === 'object' ? n : null;
|
|
115
219
|
}
|
|
116
220
|
const MAX_JSON_CHARS = 60_000;
|
|
221
|
+
/** The largest array in `data` (top level, or one level down), with its path. */
|
|
222
|
+
function largestArray(data) {
|
|
223
|
+
if (!data || typeof data !== 'object')
|
|
224
|
+
return null;
|
|
225
|
+
let best = null;
|
|
226
|
+
const consider = (path, v) => {
|
|
227
|
+
if (!Array.isArray(v) || v.length < 2)
|
|
228
|
+
return;
|
|
229
|
+
const size = JSON.stringify(v).length;
|
|
230
|
+
if (!best || size > best.size)
|
|
231
|
+
best = { path, items: v, size };
|
|
232
|
+
};
|
|
233
|
+
for (const [k, v] of Object.entries(data)) {
|
|
234
|
+
consider([k], v);
|
|
235
|
+
if (v && typeof v === 'object' && !Array.isArray(v))
|
|
236
|
+
for (const [k2, v2] of Object.entries(v))
|
|
237
|
+
consider([k, k2], v2);
|
|
238
|
+
}
|
|
239
|
+
return best;
|
|
240
|
+
}
|
|
241
|
+
function withItems(data, path, items) {
|
|
242
|
+
const root = { ...data };
|
|
243
|
+
if (path.length === 1)
|
|
244
|
+
root[path[0]] = items;
|
|
245
|
+
else
|
|
246
|
+
root[path[0]] = { ...root[path[0]], [path[1]]: items };
|
|
247
|
+
return root;
|
|
248
|
+
}
|
|
249
|
+
/** A list this short is never paged down to fewer items: its items' nested lists are shortened instead. */
|
|
250
|
+
const SMALL_LIST = 5;
|
|
251
|
+
/**
|
|
252
|
+
* Every item of `arr` kept; in each item, its largest nested list (top level or one level down) is cut to the first `k`
|
|
253
|
+
* entries, with `_truncated` saying how many it had. `k` is the largest that fits. Null when even k = 0 does not fit.
|
|
254
|
+
*/
|
|
255
|
+
function fitNestedLists(data, arr, max) {
|
|
256
|
+
const nested = arr.items.map((item) => (item && typeof item === 'object' && !Array.isArray(item) ? largestArray(item) : null));
|
|
257
|
+
const longest = Math.max(0, ...nested.map((n) => n?.items.length ?? 0));
|
|
258
|
+
if (!longest)
|
|
259
|
+
return null;
|
|
260
|
+
const render = (k) => {
|
|
261
|
+
const items = arr.items.map((item, i) => {
|
|
262
|
+
const n = nested[i];
|
|
263
|
+
if (!n || n.items.length <= k)
|
|
264
|
+
return item;
|
|
265
|
+
const cut = withItems(item, n.path, n.items.slice(0, k));
|
|
266
|
+
return { ...cut, _truncated: { list: n.path.join('.'), returned: k, total: n.items.length } };
|
|
267
|
+
});
|
|
268
|
+
return JSON.stringify(withItems(data, arr.path, items), null, 2);
|
|
269
|
+
};
|
|
270
|
+
if (render(0).length > max)
|
|
271
|
+
return null;
|
|
272
|
+
let lo = 0;
|
|
273
|
+
let hi = longest;
|
|
274
|
+
while (lo < hi) {
|
|
275
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
276
|
+
if (render(mid).length <= max)
|
|
277
|
+
lo = mid;
|
|
278
|
+
else
|
|
279
|
+
hi = mid - 1;
|
|
280
|
+
}
|
|
281
|
+
const lists = [...new Set(nested.filter((n) => !!n && n.items.length > lo).map((n) => n.path.join('.')))];
|
|
282
|
+
return {
|
|
283
|
+
json: render(lo),
|
|
284
|
+
page: `Output shortened: all ${arr.items.length} items of "${arr.path.join('.')}" shown, each with at most ${lo} entries of its "${lists.join('", "')}" list (see _truncated in each item). The JSON is complete and valid; retrieve one item by id to see more of it, or narrow the request.`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Round-3 pilot (nextjs, sveltekit): output was cut at 60 KB in the middle of the JSON, which agents then failed to
|
|
289
|
+
* parse. Output is always valid JSON: an oversized list is paged (the first items that fit, plus `_page` naming the
|
|
290
|
+
* ids of the rest to fetch one by one), never cut mid-value.
|
|
291
|
+
*/
|
|
292
|
+
export function fitJson(data, max = MAX_JSON_CHARS) {
|
|
293
|
+
const pretty = JSON.stringify(data, null, 2);
|
|
294
|
+
if (pretty.length <= max)
|
|
295
|
+
return { json: pretty };
|
|
296
|
+
const arr = largestArray(data);
|
|
297
|
+
if (arr) {
|
|
298
|
+
let lo = 0;
|
|
299
|
+
let hi = arr.items.length;
|
|
300
|
+
const render = (n) => {
|
|
301
|
+
const rest = arr.items.slice(n);
|
|
302
|
+
const ids = rest.map((x) => (x && typeof x === 'object' ? x.id : undefined)).filter((x) => typeof x === 'string');
|
|
303
|
+
return JSON.stringify(withItems({ ...data, _page: { list: arr.path.join('.'), returned: n, total: arr.items.length, remaining_ids: ids.slice(0, 200) } }, arr.path, arr.items.slice(0, n)), null, 2);
|
|
304
|
+
};
|
|
305
|
+
while (lo < hi) {
|
|
306
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
307
|
+
if (render(mid).length <= max)
|
|
308
|
+
lo = mid;
|
|
309
|
+
else
|
|
310
|
+
hi = mid - 1;
|
|
311
|
+
}
|
|
312
|
+
// MCP connector test: list_law_updates (limit 3) printed "0 of 2 items shown": each law update carried hundreds of
|
|
313
|
+
// changed requirements, so not even one item fitted. When paging would drop items of a short list (or all of them),
|
|
314
|
+
// every item is shown instead, with its own longest nested list cut to what fits.
|
|
315
|
+
if (lo < arr.items.length && (lo === 0 || arr.items.length <= SMALL_LIST)) {
|
|
316
|
+
const inner = fitNestedLists(data, arr, max);
|
|
317
|
+
if (inner)
|
|
318
|
+
return inner;
|
|
319
|
+
}
|
|
320
|
+
const json = render(lo);
|
|
321
|
+
if (json.length <= max) {
|
|
322
|
+
return { json, page: `Output paged: ${lo} of ${arr.items.length} items of "${arr.path.join('.')}" shown. The JSON is complete and valid; fetch the others by id (_page.remaining_ids), e.g. get_task for plan tasks, or narrow the request (a smaller limit).` };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const compact = JSON.stringify(data);
|
|
326
|
+
if (compact.length <= max)
|
|
327
|
+
return { json: compact, page: 'Output printed compactly (no indentation) to fit.' };
|
|
328
|
+
return {
|
|
329
|
+
json: JSON.stringify({ _truncated: true, size: compact.length, keys: data && typeof data === 'object' ? Object.keys(data) : [] }, null, 2),
|
|
330
|
+
page: `Output too large (${compact.length} characters): narrow the request (a smaller limit, or one item at a time).`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
117
333
|
export function renderOutput(out) {
|
|
118
|
-
const lines = [out.headline.trim()];
|
|
334
|
+
const lines = [mcpText(out.headline.trim())];
|
|
119
335
|
for (const n of out.notes ?? [])
|
|
120
336
|
if (n)
|
|
121
|
-
lines.push(`- ${n}`);
|
|
122
|
-
const next = describeNextAction(out.next === undefined ? nextActionOf(out.data) : out.next);
|
|
337
|
+
lines.push(`- ${mcpText(n)}`);
|
|
338
|
+
const next = describeNextAction(out.next === undefined ? nextActionOf(out.data) : out.next, { data: out.data });
|
|
123
339
|
if (next)
|
|
124
340
|
lines.push(next);
|
|
125
341
|
if (out.data !== undefined) {
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
lines.push('', '```json', json, '```');
|
|
342
|
+
const fitted = fitJson(forMcp(out.data));
|
|
343
|
+
if (fitted.page)
|
|
344
|
+
lines.push(`- ${fitted.page}`);
|
|
345
|
+
lines.push('', '```json', fitted.json, '```');
|
|
131
346
|
}
|
|
132
347
|
return { content: [{ type: 'text', text: lines.join('\n') }], ...(out.isError ? { isError: true } : {}) };
|
|
133
348
|
}
|
|
@@ -145,27 +360,53 @@ export function renderError(err) {
|
|
|
145
360
|
data.doc_url = err.docUrl;
|
|
146
361
|
if (err.nextAction)
|
|
147
362
|
data.next_action = err.nextAction;
|
|
148
|
-
const hint = ERROR_HINTS[err.code];
|
|
363
|
+
const hint = err.code === 'resource_missing' ? missingHint(err.message) : ERROR_HINTS[err.code];
|
|
149
364
|
return renderOutput({
|
|
150
365
|
headline: `Checkbox API error ${err.code}${err.param ? ` (param: ${err.param})` : ''}: ${err.message}`,
|
|
151
366
|
notes: hint ? [hint] : [],
|
|
152
367
|
data: { error: data },
|
|
153
|
-
next: err.nextAction ?? null,
|
|
368
|
+
next: err.nextAction ?? (err.code === 'resource_missing' ? missingNext(err.message) : null),
|
|
154
369
|
isError: true,
|
|
155
370
|
});
|
|
156
371
|
}
|
|
157
372
|
const message = err instanceof Error ? err.message : String(err);
|
|
158
373
|
return renderOutput({ headline: `Tool failed: ${message}`, isError: true });
|
|
159
374
|
}
|
|
375
|
+
/** Audit P2: where to find a valid id of the kind that was missing (by its prefix in the message). */
|
|
376
|
+
function missingKind(message) {
|
|
377
|
+
if (/\btask_/.test(message))
|
|
378
|
+
return 'task';
|
|
379
|
+
if (/\bver(?:ification)?_/.test(message))
|
|
380
|
+
return 'verification';
|
|
381
|
+
if (/\bfinding_/.test(message))
|
|
382
|
+
return 'finding';
|
|
383
|
+
if (/\bsite_/.test(message))
|
|
384
|
+
return 'site';
|
|
385
|
+
return 'other';
|
|
386
|
+
}
|
|
387
|
+
function missingHint(message) {
|
|
388
|
+
const where = { task: 'get_compliance_plan lists the current task ids', verification: 'get_task shows a task\'s last_verification id', finding: 'list_findings lists finding ids', site: 'list_sites lists site ids', other: 'get_compliance_plan and list_sites list the current ids' }[missingKind(message)];
|
|
389
|
+
return `The id does not exist in this mode (test keys only see test-mode objects): ${where}.`;
|
|
390
|
+
}
|
|
391
|
+
function missingNext(message) {
|
|
392
|
+
const k = missingKind(message);
|
|
393
|
+
if (k === 'site')
|
|
394
|
+
return { type: 'find_id', message: 'Call list_sites for the site ids in this mode.', operation_id: 'listSites' };
|
|
395
|
+
if (k === 'finding')
|
|
396
|
+
return { type: 'find_id', message: 'Call list_findings for the finding ids.', operation_id: 'listFindings' };
|
|
397
|
+
return { type: 'find_id', message: 'Call get_compliance_plan for the current task ids (each task carries its last_verification id).', operation_id: 'retrieveCompliancePlan' };
|
|
398
|
+
}
|
|
160
399
|
const ERROR_HINTS = {
|
|
161
|
-
insufficient_scope: '
|
|
400
|
+
insufficient_scope: 'This connection lacks that permission (a read-only connection or a restricted key). Ask the human to reconnect Checkbox with write access (or change the key\'s permissions in the Checkbox dashboard → Developers); never ask them to paste a key. Read-only tools still work.',
|
|
162
401
|
insufficient_role: 'The key acts as a viewer; writes need a member or admin key.',
|
|
163
402
|
api_key_missing: 'No Checkbox API key is configured: call setup_checkbox (one browser approval by the human) to get one.',
|
|
164
|
-
api_key_invalid: 'The configured Checkbox API key is unknown, revoked or expired:
|
|
403
|
+
api_key_invalid: 'The configured Checkbox API key is unknown, revoked or expired: reconnect with setup_checkbox (one browser approval by the human), or ask the human to fix the key in the Checkbox dashboard → Developers. Never ask them to paste a key into the chat.',
|
|
165
404
|
account_not_activated: 'Live mode is locked until a site domain is verified. Use a test key (ck_test_…) for setup and checks, or verify the domain first.',
|
|
166
405
|
parameter_invalid: 'Fix the named parameter and call the tool again.',
|
|
167
406
|
parameter_missing: 'Add the named parameter and call the tool again.',
|
|
168
407
|
parameter_unknown: 'Remove the named parameter and call the tool again.',
|
|
169
|
-
resource_missing: 'The id does not exist in this mode (test keys only see test-mode objects). Use list_sites / list_findings to find valid ids.',
|
|
170
408
|
rate_limit_exceeded: 'Wait the number of seconds in the message, then retry.',
|
|
409
|
+
shopify_not_connected: 'Only the merchant can connect the store: tell them the step in next_action (install the Checkbox app in the Shopify admin, then "Connect to Checkbox"). Do not retry until they say it is done.',
|
|
410
|
+
terms_incomplete: 'Checkbox never guesses prices or cancellation terms: send the missing plan facts (next_action), then call the tool again.',
|
|
411
|
+
platform_error: 'The merchant\'s platform refused the change; nothing was applied. Retry in a few minutes.',
|
|
171
412
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export { NO_KEY, keyAllows, keyContextFromPrefix, type KeyContext, type KeyRole,
|
|
|
4
4
|
export { CATALOG_URI, QUICKSTART_URI, makeMyCheckoutCompliantPrompt, quickstartMarkdown, renderCatalog, type CoverageDoc } from './resources.ts';
|
|
5
5
|
export { DEFAULT_MCP_URL, SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, createCheckboxMcpServer, type CheckboxMcp, type CheckboxMcpOptions } from './server.ts';
|
|
6
6
|
export { TOOLS, TOOL_OPERATIONS, ToolError, toolVisible, type IssuedKeys, type ToolContext, type ToolDef } from './tools.ts';
|
|
7
|
+
export { closestArgument, unknownArgumentsMessage } from './unknown-args.ts';
|
|
7
8
|
export { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
package/dist/index.js
CHANGED
|
@@ -6,5 +6,6 @@ export { NO_KEY, keyAllows, keyContextFromPrefix } from "./permissions.js";
|
|
|
6
6
|
export { CATALOG_URI, QUICKSTART_URI, makeMyCheckoutCompliantPrompt, quickstartMarkdown, renderCatalog } from "./resources.js";
|
|
7
7
|
export { DEFAULT_MCP_URL, SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, createCheckboxMcpServer } from "./server.js";
|
|
8
8
|
export { TOOLS, TOOL_OPERATIONS, ToolError, toolVisible } from "./tools.js";
|
|
9
|
+
export { closestArgument, unknownArgumentsMessage } from "./unknown-args.js";
|
|
9
10
|
// Re-exported so hosts use the same SDK copy as the server.
|
|
10
11
|
export { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
package/dist/permissions.js
CHANGED
|
@@ -23,8 +23,8 @@ export function keyAllows(key, scope) {
|
|
|
23
23
|
export function keyContextFromPrefix(apiKey, readOnly = false) {
|
|
24
24
|
if (!apiKey)
|
|
25
25
|
return { ...NO_KEY, readOnly };
|
|
26
|
-
const mode = /^(ck|rk)_test_/.test(apiKey) ? 'test' : /^(ck|rk|sk)_live_/.test(apiKey) ? 'live' : 'unknown';
|
|
27
|
-
const kind = apiKey.startsWith('rk_') ? 'restricted' : 'secret';
|
|
26
|
+
const mode = /^(ckr|ck|rk)_test_/.test(apiKey) ? 'test' : /^(ckr|ck|rk|sk)_live_/.test(apiKey) ? 'live' : 'unknown';
|
|
27
|
+
const kind = (apiKey.startsWith('rk_') || apiKey.startsWith('ckr_')) ? 'restricted' : 'secret';
|
|
28
28
|
// Restricted keys with unknown scopes: list everything and let the API answer insufficient_scope.
|
|
29
29
|
return { authenticated: true, mode, kind: kind === 'restricted' ? undefined : kind, readOnly };
|
|
30
30
|
}
|
package/dist/resources.js
CHANGED
|
@@ -38,9 +38,10 @@ export async function loadCatalog(api) {
|
|
|
38
38
|
export function quickstartMarkdown(opts) {
|
|
39
39
|
return `# Checkbox for AI agents — quickstart
|
|
40
40
|
|
|
41
|
-
Checkbox
|
|
42
|
-
|
|
43
|
-
cancellation flow, stores signed evidence, and tells you — the agent — exactly
|
|
41
|
+
Checkbox checks a subscription business against automatic-renewal laws (federal law, 27 states, DC and New York City:
|
|
42
|
+
FTC ROSCA, California ARL, New York and the other state laws) and keeps checking it: it watches the checkout, the
|
|
43
|
+
acknowledgment / reminder emails and the cancellation flow, stores signed evidence, and tells you — the agent — exactly
|
|
44
|
+
what to change. Free. Every feature. No credit card. Fair-use limits.
|
|
44
45
|
|
|
45
46
|
## The loop (agent-to-agent)
|
|
46
47
|
|
|
@@ -49,7 +50,7 @@ cancellation flow, stores signed evidence, and tells you — the agent — exact
|
|
|
49
50
|
credential (device login) it returns ONE link for the human ("Open … and click Approve"); call it again with the
|
|
50
51
|
device_code to receive test-mode keys. With an API key it starts a setup session. Pass what you know:
|
|
51
52
|
site_url, checkout_url, terms_url / refund_url / cancellation_url, billing_platform, plans, project.layout_file.
|
|
52
|
-
(The \`checkbox init\` CLI does the same and patches the layout file
|
|
53
|
+
(The \`checkbox init\` CLI, \`npx @checkbox-site/cli init\`, does the same and patches the layout file.)
|
|
53
54
|
2. Follow the setup **next_action** (install the snippet in the root layout, store a Stripe TEST key for test runs,
|
|
54
55
|
register checkout params). After each change: **update_setup**, then **get_setup_status** with verify=true.
|
|
55
56
|
3. **get_compliance_plan** — ordered tasks: requirement + citation, where (file / selector / Stripe object), the exact
|
|
@@ -59,7 +60,7 @@ cancellation flow, stores signed evidence, and tells you — the agent — exact
|
|
|
59
60
|
5. Tasks with status **manual** hold a question for the HUMAN: ask them, then **answer_task**. Never answer for them.
|
|
60
61
|
6. **start_test_run** — a synthetic subscription in Stripe test mode (card 4242, Checkbox inbox, test clock through
|
|
61
62
|
trial end and renewal, online cancellation) observes every lifecycle check; **get_test_run** for results.
|
|
62
|
-
7. Repeat 3-4 until the plan status
|
|
63
|
+
7. Repeat 3-4 until the checks pass (plan status value \`compliant\`, label "Checks pass"), then **complete_setup** (emails the human a summary).
|
|
63
64
|
|
|
64
65
|
Every tool result ends with "Next: …" naming the tool to call. Errors carry a stable \`code\` and often a next_action.
|
|
65
66
|
|
|
@@ -67,10 +68,10 @@ Every tool result ends with "Next: …" naming the tool to call. Errors carry a
|
|
|
67
68
|
|
|
68
69
|
- **check_disclosure** — disclosure text next to the pay button → verdicts + engine-verified replacement.
|
|
69
70
|
- **check_stripe_checkout_session** — the params you pass to stripe.checkout.sessions.create → corrected_params.
|
|
70
|
-
- **check_email** — a lifecycle email → verdicts +
|
|
71
|
+
- **check_email** — a lifecycle email → verdicts + a template that passes the checks.
|
|
71
72
|
- **check_page_html** — the rendered checkout page HTML → verdicts.
|
|
72
73
|
- **get_notice_schedule** — when each required notice must be sent for a plan.
|
|
73
|
-
- **get_compliant_disclosure** / **get_elements_snippet** — locked
|
|
74
|
+
- **get_compliant_disclosure** / **get_elements_snippet** — locked wording that passes the checks, or the drop-in
|
|
74
75
|
\`<checkbox-disclosure>\` element that also records consent as evidence.
|
|
75
76
|
|
|
76
77
|
## Keys and modes
|
|
@@ -98,15 +99,15 @@ Automated compliance checks, not legal advice.
|
|
|
98
99
|
/** The make_my_checkout_compliant prompt: inspect the code → preflight → patch → verify. */
|
|
99
100
|
export function makeMyCheckoutCompliantPrompt(args) {
|
|
100
101
|
const target = args.site_url ? ` for ${args.site_url}` : '';
|
|
101
|
-
return `Make this project's subscription checkout
|
|
102
|
+
return `Make this project's subscription checkout pass the automatic-renewal checks${target}, using the Checkbox MCP tools. Work autonomously; only stop to ask me (the human) when a tool says a human must decide or approve.
|
|
102
103
|
|
|
103
104
|
1. Inspect the code first. Find: the framework${args.framework ? ` (expected: ${args.framework})` : ''}, the root layout file, where the subscribe / pay button is rendered, where subscriptions are created (stripe.checkout.sessions.create, payment links, or ${args.billing_platform ?? 'the billing platform'} calls), the plans and prices (amount, interval, trial), the Terms / refund / cancellation policy pages, and which emails are sent to subscribers (acknowledgment, trial / renewal reminders, cancellation confirmation).
|
|
104
105
|
2. Preflight before changing anything:
|
|
105
106
|
- check_stripe_checkout_session with the exact params the code passes to Stripe (plus merchant terms when it uses a Price id);
|
|
106
107
|
- check_disclosure with the text next to the pay button (or check_page_html with the rendered checkout HTML);
|
|
107
108
|
- check_email for each subscriber email template, and get_notice_schedule for when reminders must go out.
|
|
108
|
-
3. Patch the code with the returned fixes: corrected_params for Stripe, the replacement disclosure + an unchecked, required consent checkbox directly above the pay button (or get_elements_snippet for the drop-in Checkbox Elements component),
|
|
109
|
+
3. Patch the code with the returned fixes: corrected_params for Stripe, the replacement disclosure + an unchecked, required consent checkbox directly above the pay button (or get_elements_snippet for the drop-in Checkbox Elements component), email templates that pass the checks and send timing, and an online cancellation path. Keep the existing code style; do not invent prices or terms — use the real ones from the code.
|
|
109
110
|
4. Re-run the same checks on the patched code until they pass (styling items may stay needs_review until a rendered scan).
|
|
110
|
-
5. If Checkbox is not set up for this project yet, call setup_checkbox (one approval link for me), then get_compliance_plan and work the tasks: apply → verify_task → next, until the plan
|
|
111
|
+
5. If Checkbox is not set up for this project yet, call setup_checkbox (one approval link for me), then get_compliance_plan and work the tasks: apply → verify_task → next, until the checks pass (plan status compliant). Ask me any "manual" task questions verbatim; never answer them yourself.
|
|
111
112
|
6. Finish with a short summary: what changed (files), which laws/citations each change satisfies, and anything still open.`;
|
|
112
113
|
}
|