@checkbox-site/mcp 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/key-file.d.ts +13 -0
- package/dist/key-file.js +41 -8
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/stdio.d.ts +9 -0
- package/dist/stdio.js +44 -15
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +54 -10
- package/package.json +15 -11
package/dist/key-file.d.ts
CHANGED
|
@@ -17,5 +17,18 @@ export declare const ENV_FILE = ".env.local";
|
|
|
17
17
|
export declare const FALLBACK_FILE = ".checkbox/keys.env";
|
|
18
18
|
/** Writes the issued keys to the project's git-ignored env file. Never throws. */
|
|
19
19
|
export declare function saveKeysToProject(root: string, keys: KeysToSave): SavedKeys;
|
|
20
|
+
/**
|
|
21
|
+
* The value of NAME in a dotenv-style text: `NAME=value`, `export NAME=value`, quoted or not, with an optional trailing
|
|
22
|
+
* comment; the last assignment wins, as in dotenv loaders.
|
|
23
|
+
*
|
|
24
|
+
* Round 29 (pilot 28 item 10, security-reviewer): the reader only accepted [A-Za-z0-9_], but keys are base64url
|
|
25
|
+
* (packages/db/src/repos/api-v1.ts newKeyString), so about half contain "-". Those keys were saved and then never read
|
|
26
|
+
* back: each restart of checkbox-mcp started without a key and asked for a new device login.
|
|
27
|
+
*/
|
|
28
|
+
export declare function readEnvValue(text: string, name: string): string | undefined;
|
|
29
|
+
/** Every key an earlier device login saved in this folder (.env.local, then .checkbox/keys.env), per variable. */
|
|
30
|
+
export declare function readSavedKeys(root: string): Partial<KeysToSave> & {
|
|
31
|
+
file?: string;
|
|
32
|
+
};
|
|
20
33
|
/** The CHECKBOX_SECRET_KEY saved by an earlier device login in this folder (.env.local, then .checkbox/keys.env). */
|
|
21
34
|
export declare function readSavedKey(root: string): string | undefined;
|
package/dist/key-file.js
CHANGED
|
@@ -42,9 +42,10 @@ function upsertLines(existing, entries, header) {
|
|
|
42
42
|
let text = existing ?? '';
|
|
43
43
|
const missing = [];
|
|
44
44
|
for (const [k, v] of entries) {
|
|
45
|
-
const re = new RegExp(
|
|
45
|
+
const re = new RegExp(`^[ \\t]*(?:export[ \\t]+)?${k}[ \\t]*=.*$`, 'gm');
|
|
46
|
+
// A function replacement: a value is never read as a "$&" pattern. Every earlier assignment gets the new value.
|
|
46
47
|
if (re.test(text))
|
|
47
|
-
text = text.replace(re, `${k}=${v}`);
|
|
48
|
+
text = text.replace(re, () => `${k}=${v}`);
|
|
48
49
|
else
|
|
49
50
|
missing.push([k, v]);
|
|
50
51
|
}
|
|
@@ -106,13 +107,45 @@ export function saveKeysToProject(root, keys) {
|
|
|
106
107
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
|
-
/**
|
|
110
|
-
export
|
|
110
|
+
/**
|
|
111
|
+
* The value of NAME in a dotenv-style text: `NAME=value`, `export NAME=value`, quoted or not, with an optional trailing
|
|
112
|
+
* comment; the last assignment wins, as in dotenv loaders.
|
|
113
|
+
*
|
|
114
|
+
* Round 29 (pilot 28 item 10, security-reviewer): the reader only accepted [A-Za-z0-9_], but keys are base64url
|
|
115
|
+
* (packages/db/src/repos/api-v1.ts newKeyString), so about half contain "-". Those keys were saved and then never read
|
|
116
|
+
* back: each restart of checkbox-mcp started without a key and asked for a new device login.
|
|
117
|
+
*/
|
|
118
|
+
export function readEnvValue(text, name) {
|
|
119
|
+
const re = new RegExp(`^[ \\t]*(?:export[ \\t]+)?${name}[ \\t]*=[ \\t]*(?:"([^"\\r\\n]*)"|'([^'\\r\\n]*)'|([^\\s#]*))[ \\t]*(?:#.*)?$`, 'gm');
|
|
120
|
+
let found;
|
|
121
|
+
for (let m = re.exec(text); m; m = re.exec(text))
|
|
122
|
+
found = (m[1] ?? m[2] ?? m[3] ?? '').trim();
|
|
123
|
+
return found || undefined;
|
|
124
|
+
}
|
|
125
|
+
/** Every key an earlier device login saved in this folder (.env.local, then .checkbox/keys.env), per variable. */
|
|
126
|
+
export function readSavedKeys(root) {
|
|
111
127
|
for (const file of [ENV_FILE, FALLBACK_FILE]) {
|
|
112
128
|
const text = read(join(root, file));
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
129
|
+
if (!text)
|
|
130
|
+
continue;
|
|
131
|
+
const secret = readEnvValue(text, 'CHECKBOX_SECRET_KEY');
|
|
132
|
+
if (!secret)
|
|
133
|
+
continue;
|
|
134
|
+
const out = { secret_key: secret, file };
|
|
135
|
+
const pk = readEnvValue(text, 'CHECKBOX_PUBLISHABLE_KEY');
|
|
136
|
+
const live = readEnvValue(text, 'CHECKBOX_LIVE_SECRET_KEY');
|
|
137
|
+
const livePk = readEnvValue(text, 'CHECKBOX_LIVE_PUBLISHABLE_KEY');
|
|
138
|
+
if (pk)
|
|
139
|
+
out.publishable_key = pk;
|
|
140
|
+
if (live)
|
|
141
|
+
out.live_secret_key = live;
|
|
142
|
+
if (livePk)
|
|
143
|
+
out.live_publishable_key = livePk;
|
|
144
|
+
return out;
|
|
116
145
|
}
|
|
117
|
-
return
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
/** The CHECKBOX_SECRET_KEY saved by an earlier device login in this folder (.env.local, then .checkbox/keys.env). */
|
|
149
|
+
export function readSavedKey(root) {
|
|
150
|
+
return readSavedKeys(root).secret_key;
|
|
118
151
|
}
|
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.4";
|
|
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.4';
|
|
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/stdio.d.ts
CHANGED
|
@@ -22,6 +22,15 @@ export declare function buildApi(cfg: {
|
|
|
22
22
|
}, f?: typeof fetch): Promise<CheckboxApi>;
|
|
23
23
|
/** Confirms the key and learns its mode / permissions (retrieveAccount). Falls back to the key prefix. */
|
|
24
24
|
export declare function describeKey(api: CheckboxApi, apiKey: string | undefined, readOnly: boolean): Promise<KeyContext>;
|
|
25
|
+
/**
|
|
26
|
+
* The key this server starts with: the configured one (flag / env), else the one an earlier device login saved in this
|
|
27
|
+
* project. Round 29 (pilot 28 item 18): a rejected configured key falls back to the project's saved key before the
|
|
28
|
+
* server starts without a key (which sends the agent to device login).
|
|
29
|
+
*/
|
|
30
|
+
export declare function startupKey(cfg: ResolvedConfig, cwd: string, log: (line: string) => void, f?: typeof fetch): Promise<{
|
|
31
|
+
api: CheckboxApi;
|
|
32
|
+
key: KeyContext;
|
|
33
|
+
}>;
|
|
25
34
|
/** Round 27 (pilot 26 item 22): the stderr note when --read-only is on but the key itself can still write. */
|
|
26
35
|
export declare function readOnlyNote(readOnly: boolean, key: KeyContext): string | null;
|
|
27
36
|
export declare function runStdio(opts?: StdioOptions): Promise<void>;
|
package/dist/stdio.js
CHANGED
|
@@ -62,6 +62,49 @@ export async function describeKey(api, apiKey, readOnly) {
|
|
|
62
62
|
return base; // restricted key without account:read, or the API is unreachable right now
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* The key this server starts with: the configured one (flag / env), else the one an earlier device login saved in this
|
|
67
|
+
* project. Round 29 (pilot 28 item 18): a rejected configured key falls back to the project's saved key before the
|
|
68
|
+
* server starts without a key (which sends the agent to device login).
|
|
69
|
+
*/
|
|
70
|
+
export async function startupKey(cfg, cwd, log, f) {
|
|
71
|
+
// Round 25 (pilot 24 item 12): a key an earlier device login saved in this folder (never shown in the transcript).
|
|
72
|
+
if (!cfg.apiKey) {
|
|
73
|
+
const saved = readSavedKey(cwd);
|
|
74
|
+
if (saved)
|
|
75
|
+
cfg.apiKey = saved;
|
|
76
|
+
}
|
|
77
|
+
let api = await buildApi(cfg, f);
|
|
78
|
+
let key;
|
|
79
|
+
try {
|
|
80
|
+
key = await describeKey(api, cfg.apiKey, cfg.readOnly);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
// Round 29 (pilot 28 item 18): a rejected configured key falls back to the key saved in this project before any
|
|
84
|
+
// device login.
|
|
85
|
+
const saved = readSavedKey(cwd);
|
|
86
|
+
let adopted;
|
|
87
|
+
if (saved && saved !== cfg.apiKey) {
|
|
88
|
+
try {
|
|
89
|
+
const savedApi = await buildApi({ ...cfg, apiKey: saved }, f);
|
|
90
|
+
adopted = await describeKey(savedApi, saved, cfg.readOnly);
|
|
91
|
+
api = savedApi;
|
|
92
|
+
cfg.apiKey = saved;
|
|
93
|
+
log(`checkbox-mcp: the configured API key was rejected (${err.message}); using the key saved in this project instead.`);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
adopted = undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (adopted)
|
|
100
|
+
key = adopted;
|
|
101
|
+
else {
|
|
102
|
+
log(`checkbox-mcp: the API key was rejected (${err.message}). Starting without a key: use setup_checkbox to get a new one.`);
|
|
103
|
+
key = keyContextFromPrefix(undefined, cfg.readOnly);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { api, key };
|
|
107
|
+
}
|
|
65
108
|
/** Round 27 (pilot 26 item 22): the stderr note when --read-only is on but the key itself can still write. */
|
|
66
109
|
export function readOnlyNote(readOnly, key) {
|
|
67
110
|
if (!readOnly || !key.authenticated || key.kind !== 'secret' || key.role === 'viewer')
|
|
@@ -84,21 +127,7 @@ export async function runStdio(opts = {}) {
|
|
|
84
127
|
}
|
|
85
128
|
const cfg = resolveConfig(env, argv);
|
|
86
129
|
const cwd = opts.cwd ?? process.cwd();
|
|
87
|
-
|
|
88
|
-
if (!cfg.apiKey) {
|
|
89
|
-
const saved = readSavedKey(cwd);
|
|
90
|
-
if (saved)
|
|
91
|
-
cfg.apiKey = saved;
|
|
92
|
-
}
|
|
93
|
-
const api = await buildApi(cfg, opts.fetch);
|
|
94
|
-
let key;
|
|
95
|
-
try {
|
|
96
|
-
key = await describeKey(api, cfg.apiKey, cfg.readOnly);
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
log(`checkbox-mcp: the API key was rejected (${err.message}). Starting without a key: use setup_checkbox to get a new one.`);
|
|
100
|
-
key = keyContextFromPrefix(undefined, cfg.readOnly);
|
|
101
|
-
}
|
|
130
|
+
const { api, key } = await startupKey(cfg, cwd, log, opts.fetch);
|
|
102
131
|
// Round 27 (pilot 26 item 22, security-reviewer): --read-only only hides tools in this server. Say so when the key can
|
|
103
132
|
// still write, so nobody mistakes it for a limit on the key.
|
|
104
133
|
const note = readOnlyNote(cfg.readOnly, key);
|
package/dist/tools.d.ts
CHANGED
|
@@ -242,6 +242,14 @@ export declare function shopifyLines(sh: SiteShopifyLite | null | undefined, sit
|
|
|
242
242
|
* each with its full summary). Each update keeps its first changes in short form, with the total.
|
|
243
243
|
*/
|
|
244
244
|
export declare function lawUpdateItem(u: Record<string, unknown>): Record<string, unknown>;
|
|
245
|
+
/**
|
|
246
|
+
* Round 29 (pilot 28 item 25): next_actions name an operation_id, but call_api only took method + path, so agents
|
|
247
|
+
* looked the path up in openapi.json. call_api now also takes { operation_id, path_params } and fills in both.
|
|
248
|
+
*/
|
|
249
|
+
export declare function resolveCallApiTarget(args: Record<string, unknown>): {
|
|
250
|
+
method: 'GET' | 'POST' | 'DELETE';
|
|
251
|
+
path: string;
|
|
252
|
+
};
|
|
245
253
|
/** Every tool, in the order clients list them. */
|
|
246
254
|
/**
|
|
247
255
|
* Round 12 (pilot 11, nextjs + agent users): setup_checkbox dropped unknown fields silently; the homepage prompt says
|
package/dist/tools.js
CHANGED
|
@@ -7,6 +7,7 @@ import { planTodo, todoLine } from "./todo.js";
|
|
|
7
7
|
import { CheckboxApiError } from "./api.js";
|
|
8
8
|
import { ciHint, nextActionOf, untrusted } from "./format.js";
|
|
9
9
|
import { keyAllows } from "./permissions.js";
|
|
10
|
+
import { OPERATIONS } from '@checkbox-site/node';
|
|
10
11
|
/** A failure the tool explains itself (not an API error): rendered as an error result with a next step. */
|
|
11
12
|
export class ToolError extends Error {
|
|
12
13
|
next;
|
|
@@ -728,7 +729,7 @@ const getSetupStatus = {
|
|
|
728
729
|
const pl = sitePlansNote(res.setup_session?.site_plans);
|
|
729
730
|
if (pl)
|
|
730
731
|
notes.push(pl);
|
|
731
|
-
return { headline: `Re-checked setup session ${id}.`, ...(notes.length ? { notes } : {}), data: withoutLegacyScore(res), next: nextActionOf(res) ?? nextActionOf(res.session) };
|
|
732
|
+
return { headline: `Re-checked setup session ${id}.`, ...(notes.length ? { notes } : {}), data: withoutLegacyScore(res), next: nextActionOf(res) ?? nextActionOf(res.setup_session) ?? nextActionOf(res.session) };
|
|
732
733
|
}
|
|
733
734
|
const session = await api.request('GET', `/v1/setup_sessions/${enc(id)}`);
|
|
734
735
|
const open = (session.steps ?? []).filter((s) => s.status && !['done', 'skipped', 'not_applicable'].includes(s.status));
|
|
@@ -753,7 +754,7 @@ const updateSetup = {
|
|
|
753
754
|
input: {
|
|
754
755
|
setup_session: z.string().max(100).optional().describe('Setup session id (setup_…). Default: the newest one.'),
|
|
755
756
|
...setupFactsShape,
|
|
756
|
-
snippet_installed_in: z.string().max(500).optional().describe('File you added the Checkbox snippet tag to, e.g. "app/layout.tsx".'),
|
|
757
|
+
snippet_installed_in: z.union([z.string().max(500), z.array(z.string().max(300)).max(20)]).optional().describe('File(s) you added the Checkbox snippet tag to, e.g. "app/layout.tsx".'),
|
|
757
758
|
env_written: z.string().max(500).optional().describe('File you wrote the keys to, e.g. ".env.local".'),
|
|
758
759
|
live_site: z.string().max(100).optional().describe('Live-mode site id (site_…) if you created one with a live key.'),
|
|
759
760
|
refetch_policies: z.boolean().optional().describe('Fetch the policy pages again even if their URLs did not change (after editing them).'),
|
|
@@ -763,6 +764,15 @@ const updateSetup = {
|
|
|
763
764
|
async run(args, { api }) {
|
|
764
765
|
const id = await resolveSetupSession(api, args.setup_session);
|
|
765
766
|
const body = pick(args, [...Object.keys(setupFactsShape), 'snippet_installed_in', 'env_written', 'live_site', 'refetch_policies']);
|
|
767
|
+
// Round 29 (pilot 28 item 25, sveltekit-cursor): a snippet in several files was refused as an array; the API keeps one
|
|
768
|
+
// string, so a list is sent as "a, b".
|
|
769
|
+
if (Array.isArray(body.snippet_installed_in)) {
|
|
770
|
+
const files = body.snippet_installed_in.map((f) => f.trim()).filter(Boolean);
|
|
771
|
+
if (files.length)
|
|
772
|
+
body.snippet_installed_in = files.join(', ').slice(0, 500);
|
|
773
|
+
else
|
|
774
|
+
delete body.snippet_installed_in;
|
|
775
|
+
}
|
|
766
776
|
const session = await api.request('POST', `/v1/setup_sessions/${enc(id)}`, { body });
|
|
767
777
|
// Round 20 (pilot 19 item 6): a plans change re-evaluates the site; say where the new plan's tasks show up.
|
|
768
778
|
const notes = Array.isArray(args.plans) && args.plans.length
|
|
@@ -963,7 +973,7 @@ const verifyTask = {
|
|
|
963
973
|
.describe('Email tasks: the new template (passes when it matches one plan fully).'),
|
|
964
974
|
plan: z.string().min(1).max(200).optional().describe('Email tasks: only this plan (name).'),
|
|
965
975
|
price_id: z.string().min(1).max(200).optional().describe('Email tasks: only this plan (Price id).'),
|
|
966
|
-
recheck: z.boolean().optional().describe('
|
|
976
|
+
recheck: z.boolean().optional().describe('Not needed: a done task is always re-checked now; one closed with nothing observed passing is not_checked.'),
|
|
967
977
|
wait,
|
|
968
978
|
},
|
|
969
979
|
scopes: ['compliance_plans:write'],
|
|
@@ -1107,8 +1117,9 @@ const startTestRun = {
|
|
|
1107
1117
|
description: [
|
|
1108
1118
|
'Observes the WHOLE subscription lifecycle, not just the checkout: in the billing platform\'s TEST mode Checkbox subscribes a test customer (card 4242…) whose email is a Checkbox inbox, waits for your acknowledgment email, fast-forwards a Stripe test clock through every legal notice window before trial end and renewal (collecting reminder emails), then cancels online and waits for the cancellation confirmation. Nothing is charged.',
|
|
1109
1119
|
'Needs a Stripe TEST-mode restricted key (rk_test_…) stored for the site: store it yourself with store_stripe_test_key (one key for checks, fixes and test runs; live keys stay with the human in the dashboard). Never ask the human to paste a key into the chat. If missing, the error\'s next_action says so. For non-Stripe sites use type "generic".',
|
|
1110
|
-
'Usually 2
|
|
1120
|
+
'Usually 1-2 minutes (real_checkout: a few): poll get_test_run (its next_action says when, and minutes_left estimates the rest). Each step reports observations, check results and agent-readable fixes; apply them and run again.',
|
|
1111
1121
|
'mode "real_checkout" is the PROOF run: the shopper buys through the merchant\'s own checkout on its test deployment (stripe.checkout_url or stripe.payment_link), so the merchant\'s webhook handlers and email code fire; give stripe.test_account + stripe.cancel_url when customers cancel in the app. A step is "passed" only with evidence; "warning" means needs review.',
|
|
1122
|
+
'Cancel: stripe.cancel_via "stripe_portal" (default) or "merchant_flow" (cancel_url + cancel_steps; behind a login, test_account { login_url, email, password }).',
|
|
1112
1123
|
].join('\n'),
|
|
1113
1124
|
input: {
|
|
1114
1125
|
site: siteId.optional(),
|
|
@@ -1247,7 +1258,7 @@ const checkStripeCheckoutSession = {
|
|
|
1247
1258
|
name: 'check_stripe_checkout_session',
|
|
1248
1259
|
title: 'Check Stripe Checkout Session params (returns corrected params)',
|
|
1249
1260
|
description: [
|
|
1250
|
-
'Before shipping Stripe Checkout code: pass EXACTLY the object your code passes to stripe.checkout.sessions.create (mode "subscription"). Checkbox evaluates it against automatic-renewal laws and returns correction.corrected_params — the full params with the fix applied, ready to paste — plus an RFC 6902 JSON patch and before/after verification per check.',
|
|
1261
|
+
'Before shipping Stripe Checkout code: pass, as params, EXACTLY the object your code passes to stripe.checkout.sessions.create (mode "subscription"). Checkbox evaluates it against automatic-renewal laws and returns correction.corrected_params — the full params with the fix applied, ready to paste — plus an RFC 6902 JSON patch and before/after verification per check.',
|
|
1251
1262
|
'With a Price id (price_…) in line_items, pass site (its plans are used) or merchant { price, interval, trial_days, merchant_name, cancel_url }.',
|
|
1252
1263
|
'Note: Stripe styles custom_text itself, so "clear and conspicuous" stays needs_review here; the plan shows it as a note, never a fix.',
|
|
1253
1264
|
].join('\n'),
|
|
@@ -2231,26 +2242,59 @@ const getNoticeTemplates = {
|
|
|
2231
2242
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
2232
2243
|
// Escape hatch
|
|
2233
2244
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
2245
|
+
/**
|
|
2246
|
+
* Round 29 (pilot 28 item 25): next_actions name an operation_id, but call_api only took method + path, so agents
|
|
2247
|
+
* looked the path up in openapi.json. call_api now also takes { operation_id, path_params } and fills in both.
|
|
2248
|
+
*/
|
|
2249
|
+
export function resolveCallApiTarget(args) {
|
|
2250
|
+
const opId = typeof args.operation_id === 'string' ? args.operation_id.trim() : '';
|
|
2251
|
+
if (!opId) {
|
|
2252
|
+
if (typeof args.path !== 'string' || !args.method)
|
|
2253
|
+
throw new ToolError('Give method and path, or operation_id (with path_params for the ids in its path).');
|
|
2254
|
+
return { method: args.method, path: args.path };
|
|
2255
|
+
}
|
|
2256
|
+
const ops = OPERATIONS;
|
|
2257
|
+
const op = Object.prototype.hasOwnProperty.call(ops, opId) ? ops[opId] : undefined;
|
|
2258
|
+
if (!op) {
|
|
2259
|
+
const lower = opId.toLowerCase();
|
|
2260
|
+
const close = Object.keys(ops).filter((k) => k.toLowerCase().includes(lower) || lower.includes(k.toLowerCase())).slice(0, 5);
|
|
2261
|
+
throw new ToolError(`Unknown operation_id "${opId}".${close.length ? ` Did you mean ${close.join(', ')}?` : ''} Operation ids are in https://app.checkbox.site/v1/openapi.json.`);
|
|
2262
|
+
}
|
|
2263
|
+
if (args.method && args.method !== op.httpMethod)
|
|
2264
|
+
throw new ToolError(`${opId} is ${op.httpMethod} ${op.path}, not ${String(args.method)}: leave method out.`);
|
|
2265
|
+
const given = (args.path_params && typeof args.path_params === 'object' ? args.path_params : {});
|
|
2266
|
+
const missing = op.pathParams.filter((p) => typeof given[p] !== 'string' || !given[p].trim());
|
|
2267
|
+
if (missing.length)
|
|
2268
|
+
throw new ToolError(`${opId} (${op.httpMethod} ${op.path}) needs path_params ${missing.map((m) => `"${m}"`).join(', ')}, e.g. {"path_params":{${op.pathParams.map((p) => `"${p}":"…"`).join(',')}}}.`);
|
|
2269
|
+
const extra = Object.keys(given).filter((k) => !op.pathParams.includes(k));
|
|
2270
|
+
if (extra.length)
|
|
2271
|
+
throw new ToolError(`${opId} (${op.httpMethod} ${op.path}) has no path parameter ${extra.join(', ')}${op.pathParams.length ? `; it takes ${op.pathParams.join(', ')}` : ''}. Put other fields in body or query.`);
|
|
2272
|
+
const path = op.path.replace(/\{([A-Za-z0-9_]+)\}/g, (_m, name) => encodeURIComponent(String(given[name]).trim()));
|
|
2273
|
+
if (typeof args.path === 'string' && args.path && args.path !== path)
|
|
2274
|
+
throw new ToolError(`operation_id ${opId} gives the path ${path}, but path is ${args.path}: give one of them.`);
|
|
2275
|
+
return { method: op.httpMethod, path };
|
|
2276
|
+
}
|
|
2234
2277
|
const callApi = {
|
|
2235
2278
|
name: 'call_api',
|
|
2236
2279
|
title: 'Call any Checkbox API endpoint',
|
|
2237
2280
|
description: [
|
|
2238
2281
|
'For a next_action whose operation has no dedicated tool (e.g. createStripeCheckoutConfig, updateTask, updateNoticeSettings, listConsents). Calls the Checkbox /v1 API with this server\'s connection; paths and bodies are in https://app.checkbox.site/v1/openapi.json.',
|
|
2239
|
-
'Examples: {"method":"GET","path":"/v1/account"} ; {"method":"POST","path":"/v1/sites/site_123/domain_verification/check"}.',
|
|
2282
|
+
'Examples: {"method":"GET","path":"/v1/account"} ; {"method":"POST","path":"/v1/sites/site_123/domain_verification/check"} ; {"operation_id":"createStripeCheckoutConfig","path_params":{"site":"site_123"},"body":{…}}.',
|
|
2240
2283
|
'Never send API keys or Stripe keys through it, except a Stripe TEST restricted key via store_stripe_test_key (not call_api). Live keys are added by the human in the Checkbox dashboard, never pasted into the chat.',
|
|
2241
2284
|
'GET is read-only. POST/DELETE change data: only do what the user or a next_action asked for. DELETE archives or removes objects — confirm with the user first.',
|
|
2242
2285
|
].join('\n'),
|
|
2243
2286
|
input: {
|
|
2244
|
-
method: z.enum(['GET', 'POST', 'DELETE']).describe('HTTP method.'),
|
|
2245
|
-
path: z.string().max(500).describe('Path starting with /v1/, e.g. "/v1/sites/site_123/domain_verification/check".'),
|
|
2287
|
+
method: z.enum(['GET', 'POST', 'DELETE']).optional().describe('HTTP method (not needed with operation_id).'),
|
|
2288
|
+
path: z.string().max(500).optional().describe('Path starting with /v1/, e.g. "/v1/sites/site_123/domain_verification/check". Give this or operation_id.'),
|
|
2289
|
+
operation_id: z.string().max(100).optional().describe('Instead of method + path: a next_action\'s operation_id.'),
|
|
2290
|
+
path_params: z.record(z.string(), z.string().max(200)).optional().describe('With operation_id: ids in its path, e.g. {"site":"site_123"}.'),
|
|
2246
2291
|
query: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe('Query parameters.'),
|
|
2247
2292
|
body: z.record(z.string(), z.unknown()).optional().describe('JSON body (POST).'),
|
|
2248
2293
|
},
|
|
2249
2294
|
scopes: [],
|
|
2250
2295
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, title: 'Call API' },
|
|
2251
2296
|
async run(args, { api, key }) {
|
|
2252
|
-
const method = args
|
|
2253
|
-
const path = String(args.path);
|
|
2297
|
+
const { method, path } = resolveCallApiTarget(args);
|
|
2254
2298
|
if (!/^\/v1\/[A-Za-z0-9_\-./{}]*$/.test(path) || path.includes('..') || /[{}]/.test(path)) {
|
|
2255
2299
|
throw new ToolError('path must be a concrete /v1/… path (fill in ids; no {placeholders}, query strings or "..").');
|
|
2256
2300
|
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkbox-site/mcp",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Checkbox MCP server:
|
|
3
|
+
"version": "0.2.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
|
+
"checkbox",
|
|
7
|
+
"mcp",
|
|
8
|
+
"mcp-server",
|
|
9
|
+
"model-context-protocol",
|
|
10
|
+
"ai-agent",
|
|
11
|
+
"stripe",
|
|
12
|
+
"stripe-checkout",
|
|
6
13
|
"subscription",
|
|
7
14
|
"subscription-compliance",
|
|
8
15
|
"auto-renewal",
|
|
9
|
-
"autorenewal",
|
|
10
16
|
"automatic-renewal",
|
|
11
17
|
"compliance",
|
|
12
18
|
"rosca",
|
|
13
19
|
"click-to-cancel",
|
|
20
|
+
"ci",
|
|
21
|
+
"subscription compliance",
|
|
22
|
+
"auto renewal",
|
|
23
|
+
"autorenewal",
|
|
14
24
|
"california-arl",
|
|
15
25
|
"auto-renewal-law",
|
|
16
26
|
"auto-renewal-compliance",
|
|
@@ -19,14 +29,8 @@
|
|
|
19
29
|
"stripe-subscriptions",
|
|
20
30
|
"ftc",
|
|
21
31
|
"negative-option",
|
|
22
|
-
"stripe",
|
|
23
|
-
"stripe-checkout",
|
|
24
32
|
"checkout",
|
|
25
|
-
"cancellation"
|
|
26
|
-
"mcp",
|
|
27
|
-
"model-context-protocol",
|
|
28
|
-
"mcp-server",
|
|
29
|
-
"ai-agent"
|
|
33
|
+
"cancellation"
|
|
30
34
|
],
|
|
31
35
|
"mcpName": "site.checkbox/checkbox",
|
|
32
36
|
"license": "MIT",
|
|
@@ -73,7 +77,7 @@
|
|
|
73
77
|
"prepack": "npm run build"
|
|
74
78
|
},
|
|
75
79
|
"dependencies": {
|
|
76
|
-
"@checkbox-site/node": "^0.2.
|
|
80
|
+
"@checkbox-site/node": "^0.2.4",
|
|
77
81
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
78
82
|
"zod": "^4.6.7"
|
|
79
83
|
}
|