@checkbox-site/mcp 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Checkbox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,63 @@
1
- # Temporary Holding Version
1
+ # Checkbox MCP server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <!-- mcp-name: site.checkbox/checkbox -->
4
+
5
+ Subscription-law compliance for AI coding agents. Connect Claude, Cursor, VS Code or any MCP client to Checkbox and
6
+ your agent can set up a project with one human click, check checkout code before it ships, work the compliance plan to
7
+ done and run synthetic test subscriptions — against ROSCA, California's Automatic Renewal Law, New York GBL § 527-a
8
+ and the other state automatic-renewal laws.
9
+
10
+ Docs: https://checkbox.site/docs/mcp · Agent index: https://checkbox.site/llms.txt · Free to start.
11
+
12
+ ## Connect
13
+
14
+ **Remote (recommended)** — Streamable HTTP at `https://app.checkbox.site/mcp`, nothing to install, no API key to copy:
15
+ the server implements MCP OAuth, so your client opens a Checkbox sign-in and you click **Approve** once.
16
+
17
+ - claude.ai / Claude Desktop: Settings → Connectors → **Add custom connector** → `https://app.checkbox.site/mcp` →
18
+ Connect → Approve.
19
+ - Claude Code: `claude mcp add --transport http checkbox https://app.checkbox.site/mcp`, then `/mcp` → Authenticate.
20
+ - Cursor (`.cursor/mcp.json`): `{ "mcpServers": { "checkbox": { "url": "https://app.checkbox.site/mcp" } } }`, then
21
+ Connect.
22
+
23
+ API keys still work everywhere (CI, headless agents): add `--header "Authorization: Bearer $CHECKBOX_SECRET_KEY"`
24
+ (Claude Code) or `"headers": { "Authorization": "Bearer ${env:CHECKBOX_SECRET_KEY}" }` (Cursor). Agents without a
25
+ browser: `https://app.checkbox.site/mcp?login=device` offers `setup_checkbox`, which returns an approval link and then
26
+ test-mode keys.
27
+
28
+ **Local (stdio)** — for clients without remote MCP support:
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "checkbox": {
34
+ "command": "npx",
35
+ "args": ["-y", "@checkbox-site/mcp"],
36
+ "env": { "CHECKBOX_API_KEY": "ck_test_..." }
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ Package: `@checkbox-site/mcp` on npm (Node 20+; binary `checkbox-mcp`). Options:
43
+ `--read-only`, `--api-key`, `--api-base`; environment `CHECKBOX_API_KEY` (falls back to `CHECKBOX_SECRET_KEY`),
44
+ `CHECKBOX_API_BASE`, `CHECKBOX_MCP_READ_ONLY=true`.
45
+
46
+ ## Tools
47
+
48
+ Start with `setup_checkbox`; then `get_compliance_plan` → apply a task → `verify_task`, until the plan is compliant.
49
+ Preflight tools (`check_disclosure`, `check_stripe_checkout_session`, `check_email`, `check_page_html`,
50
+ `get_notice_schedule`) work on code before it is deployed. The full list, with the API operation behind each tool, is
51
+ at https://checkbox.site/docs/mcp#tools.
52
+
53
+ Every tool is a thin call to the public API (`https://app.checkbox.site/v1`), so keys, scopes, test/live mode and rate
54
+ limits are the API's. Write tools say so and carry `readOnlyHint: false`. Results are written for models and end with
55
+ the next tool to call.
56
+
57
+ ## Not legal advice
58
+
59
+ Checkbox output is an automated assessment against a legal catalog pending attorney review; it is not legal advice.
60
+
61
+ ## License
62
+
63
+ MIT
package/dist/api.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ /** API version the MCP tools are written against (sent as Checkbox-Version). */
2
+ export declare const API_VERSION = "2026-10-30";
3
+ export declare const DEFAULT_API_BASE = "https://app.checkbox.site";
4
+ export type HttpMethod = 'GET' | 'POST' | 'DELETE';
5
+ export interface ApiRequestOptions {
6
+ query?: Record<string, string | number | boolean | undefined>;
7
+ body?: unknown;
8
+ /** POST only; a fresh key is generated when omitted, so a transport retry never applies a call twice. */
9
+ idempotencyKey?: string;
10
+ /** Send without the API key (public routes: device login). */
11
+ anonymous?: boolean;
12
+ }
13
+ /** `next_action` as the API returns it (errors, setup sessions, plans, tasks, checks). */
14
+ export interface NextAction {
15
+ type: string;
16
+ message?: string;
17
+ description?: string;
18
+ operation_id?: string | null;
19
+ command?: string | null;
20
+ endpoint?: string | null;
21
+ url?: string | null;
22
+ task?: string | null;
23
+ }
24
+ /** A non-2xx /v1 response (the error envelope) or a network failure (status 0). */
25
+ export declare class CheckboxApiError extends Error {
26
+ readonly status: number;
27
+ readonly type: string;
28
+ readonly code: string;
29
+ readonly param?: string | undefined;
30
+ readonly requestId?: string | undefined;
31
+ readonly nextAction?: NextAction | undefined;
32
+ readonly docUrl?: string | undefined;
33
+ constructor(message: string, status: number, type: string, code: string, param?: string | undefined, requestId?: string | undefined, nextAction?: NextAction | undefined, docUrl?: string | undefined);
34
+ }
35
+ export interface CheckboxApi {
36
+ /** Calls `method path` (path starts with /v1/). Resolves with the parsed JSON body; rejects with CheckboxApiError. */
37
+ request<T = unknown>(method: HttpMethod, path: string, opts?: ApiRequestOptions): Promise<T>;
38
+ /** Whether an API key is configured (false: only the device-login setup tools work). */
39
+ readonly hasKey: boolean;
40
+ /** Base URL of the API (for links in tool output). */
41
+ readonly apiBase: string;
42
+ }
43
+ /** Builds the query string for a request (`expand` arrays are not needed by the tools). */
44
+ export declare function withQuery(path: string, query?: ApiRequestOptions['query']): string;
45
+ /** Turns a /v1 response (status + parsed body) into a value or a CheckboxApiError. */
46
+ export declare function interpretResponse<T>(status: number, json: unknown, requestId?: string, rawText?: string): T;
47
+ export interface HttpApiOptions {
48
+ apiKey?: string;
49
+ apiBase?: string;
50
+ userAgent?: string;
51
+ fetch?: typeof fetch;
52
+ /** Retries on network errors, 429 and 5xx (default 2). POSTs are safe to retry: they carry an Idempotency-Key. */
53
+ maxRetries?: number;
54
+ timeoutMs?: number;
55
+ }
56
+ /** /v1 over HTTPS with fetch. Used by the stdio server when no SDK client is supplied. */
57
+ export declare function createHttpApi(opts: HttpApiOptions): CheckboxApi;
58
+ /**
59
+ * Adapts a Node SDK client (@checkbox-site/node) to CheckboxApi. Any client with a Stripe-style
60
+ * `rawRequest(method, path, params?, options?)` or a `request(method, path, { query, body, idempotencyKey })` works;
61
+ * errors with `status`/`code`/`type` fields are mapped to CheckboxApiError.
62
+ */
63
+ export declare function fromSdkClient(client: unknown, info: {
64
+ hasKey: boolean;
65
+ apiBase: string;
66
+ }): CheckboxApi | null;
package/dist/api.js ADDED
@@ -0,0 +1,146 @@
1
+ // The one thing the MCP tools need from the outside world: a way to call the Checkbox /v1 API.
2
+ //
3
+ // - The remote server (apps/api, https://app.checkbox.site/mcp) implements it in-process with Fastify's inject, so a
4
+ // tool call goes through exactly the same /v1 auth, scopes, test/live mode, idempotency and rate limits as an HTTP
5
+ // call with the same key.
6
+ // - The local stdio server (`checkbox-mcp`) implements it over HTTPS (createHttpApi, or an @checkbox-site/node client
7
+ // through fromSdkClient).
8
+ import { randomUUID } from 'node:crypto';
9
+ /** API version the MCP tools are written against (sent as Checkbox-Version). */
10
+ export const API_VERSION = '2026-10-30';
11
+ export const DEFAULT_API_BASE = 'https://app.checkbox.site';
12
+ /** A non-2xx /v1 response (the error envelope) or a network failure (status 0). */
13
+ export class CheckboxApiError extends Error {
14
+ status;
15
+ type;
16
+ code;
17
+ param;
18
+ requestId;
19
+ nextAction;
20
+ docUrl;
21
+ constructor(message, status, type, code, param, requestId, nextAction, docUrl) {
22
+ super(message);
23
+ this.status = status;
24
+ this.type = type;
25
+ this.code = code;
26
+ this.param = param;
27
+ this.requestId = requestId;
28
+ this.nextAction = nextAction;
29
+ this.docUrl = docUrl;
30
+ this.name = 'CheckboxApiError';
31
+ }
32
+ }
33
+ /** Builds the query string for a request (`expand` arrays are not needed by the tools). */
34
+ export function withQuery(path, query) {
35
+ const entries = Object.entries(query ?? {}).filter(([, v]) => v !== undefined && v !== '');
36
+ if (!entries.length)
37
+ return path;
38
+ const qs = new URLSearchParams(entries.map(([k, v]) => [k, String(v)]));
39
+ return `${path}${path.includes('?') ? '&' : '?'}${qs.toString()}`;
40
+ }
41
+ /** Turns a /v1 response (status + parsed body) into a value or a CheckboxApiError. */
42
+ export function interpretResponse(status, json, requestId, rawText) {
43
+ if (status >= 200 && status < 300)
44
+ return json;
45
+ const e = json?.error;
46
+ if (e && typeof e === 'object') {
47
+ return throwError(new CheckboxApiError(typeof e.message === 'string' ? e.message : `HTTP ${status}`, status, typeof e.type === 'string' ? e.type : 'api_error', typeof e.code === 'string' ? e.code : 'unknown_error', typeof e.param === 'string' ? e.param : undefined, typeof e.request_id === 'string' ? e.request_id : requestId, e.next_action && typeof e.next_action === 'object' ? e.next_action : undefined, typeof e.doc_url === 'string' ? e.doc_url : undefined));
48
+ }
49
+ return throwError(new CheckboxApiError(`HTTP ${status}: ${(rawText ?? '').slice(0, 200) || 'empty response'}`, status, 'api_error', 'unexpected_response', undefined, requestId));
50
+ }
51
+ function throwError(err) {
52
+ throw err;
53
+ }
54
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
55
+ /** /v1 over HTTPS with fetch. Used by the stdio server when no SDK client is supplied. */
56
+ export function createHttpApi(opts) {
57
+ const f = opts.fetch ?? fetch;
58
+ const base = (opts.apiBase ?? DEFAULT_API_BASE).replace(/\/+$/, '');
59
+ const maxRetries = opts.maxRetries ?? 2;
60
+ return {
61
+ hasKey: Boolean(opts.apiKey),
62
+ apiBase: base,
63
+ async request(method, path, ro = {}) {
64
+ const url = base + withQuery(path.startsWith('/') ? path : `/${path}`, ro.query);
65
+ const headers = {
66
+ accept: 'application/json',
67
+ 'checkbox-version': API_VERSION,
68
+ 'user-agent': opts.userAgent ?? 'checkbox-mcp',
69
+ };
70
+ if (opts.apiKey && !ro.anonymous)
71
+ headers.authorization = `Bearer ${opts.apiKey}`;
72
+ if (ro.body !== undefined)
73
+ headers['content-type'] = 'application/json';
74
+ if (method === 'POST')
75
+ headers['idempotency-key'] = ro.idempotencyKey ?? `mcp_${randomUUID()}`;
76
+ for (let attempt = 0;; attempt++) {
77
+ let res;
78
+ try {
79
+ res = await f(url, {
80
+ method,
81
+ headers,
82
+ ...(ro.body !== undefined ? { body: JSON.stringify(ro.body) } : {}),
83
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 90_000),
84
+ });
85
+ }
86
+ catch (err) {
87
+ if (attempt < maxRetries) {
88
+ await sleep(300 * 2 ** attempt);
89
+ continue;
90
+ }
91
+ throw new CheckboxApiError(`Could not reach ${base}: ${err instanceof Error ? err.message : String(err)}`, 0, 'api_connection_error', 'network_error');
92
+ }
93
+ if ((res.status === 429 || res.status >= 500) && attempt < maxRetries) {
94
+ const retryAfter = Number(res.headers.get('retry-after'));
95
+ await sleep(Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 10) * 1000 : 500 * 2 ** attempt);
96
+ continue;
97
+ }
98
+ const text = await res.text();
99
+ let json;
100
+ try {
101
+ json = text ? JSON.parse(text) : undefined;
102
+ }
103
+ catch {
104
+ json = undefined;
105
+ }
106
+ return interpretResponse(res.status, json, res.headers.get('request-id') ?? undefined, text);
107
+ }
108
+ },
109
+ };
110
+ }
111
+ /**
112
+ * Adapts a Node SDK client (@checkbox-site/node) to CheckboxApi. Any client with a Stripe-style
113
+ * `rawRequest(method, path, params?, options?)` or a `request(method, path, { query, body, idempotencyKey })` works;
114
+ * errors with `status`/`code`/`type` fields are mapped to CheckboxApiError.
115
+ */
116
+ export function fromSdkClient(client, info) {
117
+ const c = client;
118
+ const call = c?.request
119
+ ? (m, p, o) => c.request(m, p, { query: o.query, body: o.body, idempotencyKey: o.idempotencyKey ?? (m === 'POST' ? `mcp_${randomUUID()}` : undefined) })
120
+ : c?.rawRequest
121
+ ? (m, p, o) => c.rawRequest(m, withQuery(p, o.query), o.body, { idempotencyKey: o.idempotencyKey ?? (m === 'POST' ? `mcp_${randomUUID()}` : undefined) })
122
+ : null;
123
+ if (!call)
124
+ return null;
125
+ return {
126
+ ...info,
127
+ async request(method, path, ro = {}) {
128
+ try {
129
+ return (await call(method, path, ro));
130
+ }
131
+ catch (err) {
132
+ throw toCheckboxApiError(err);
133
+ }
134
+ },
135
+ };
136
+ }
137
+ function toCheckboxApiError(err) {
138
+ if (err instanceof CheckboxApiError)
139
+ return err;
140
+ const e = (err ?? {});
141
+ const raw = (e.raw ?? e.error ?? {});
142
+ const pick = (k) => (typeof e[k] === 'string' ? e[k] : typeof raw[k] === 'string' ? raw[k] : undefined);
143
+ const status = typeof e.status === 'number' ? e.status : typeof e.statusCode === 'number' ? e.statusCode : 0;
144
+ const next = (e.nextAction ?? e.next_action ?? raw.next_action);
145
+ return new CheckboxApiError(err instanceof Error ? err.message : pick('message') ?? 'Request failed', status, pick('type') ?? (status ? 'api_error' : 'api_connection_error'), pick('code') ?? (status ? 'unknown_error' : 'network_error'), pick('param'), pick('requestId') ?? pick('request_id'), next && typeof next === 'object' ? next : undefined, pick('docUrl') ?? pick('doc_url'));
146
+ }
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // The `checkbox-mcp` executable (package.json "bin").
3
+ import { runStdio } from "./stdio.js";
4
+ runStdio().catch((err) => {
5
+ process.stderr.write(`checkbox-mcp: ${err instanceof Error ? err.message : String(err)}\n`);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,40 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import { type NextAction } from './api.ts';
3
+ /** API operationId → the MCP tool that performs it. Operations without a tool are reached with call_api. */
4
+ export declare const OPERATION_TOOLS: Record<string, string>;
5
+ /** The MCP tool that performs an operation: its dedicated tool, else `call_api`. */
6
+ export declare function mcpToolFor(operationId: string | null | undefined): string | null;
7
+ /**
8
+ * Quotes text Checkbox did not write (page HTML, email bodies, policy text, merchant-supplied strings) for the text part
9
+ * of a tool result: JSON-encoded on one line, truncated, inside explicit <untrusted_text> delimiters, so a model never
10
+ * reads it as an instruction next to the "Next:" line. Full values stay in the fenced JSON.
11
+ */
12
+ export declare function untrusted(value: unknown, max?: number): string;
13
+ /** One line telling the model what to call next, in MCP terms. */
14
+ export declare function describeNextAction(next: NextAction | null | undefined): string | null;
15
+ /** Pulls the top-level next_action off an API object. */
16
+ export declare function nextActionOf(obj: unknown): NextAction | null;
17
+ export interface ToolOutput {
18
+ /** One or two sentences: what happened / where things stand. */
19
+ headline: string;
20
+ /** Extra lines (bullets) between the headline and the JSON. */
21
+ notes?: string[];
22
+ /** The API object (or a composite). Printed as JSON. */
23
+ data?: unknown;
24
+ /** Overrides data.next_action for the "Next:" line. */
25
+ next?: NextAction | null;
26
+ /** Mark the result as an error the model should act on (still a normal result). */
27
+ isError?: boolean;
28
+ }
29
+ /**
30
+ * Round-3 pilot (nextjs, sveltekit): output was cut at 60 KB in the middle of the JSON, which agents then failed to
31
+ * parse. Output is always valid JSON: an oversized list is paged (the first items that fit, plus `_page` naming the
32
+ * ids of the rest to fetch one by one), never cut mid-value.
33
+ */
34
+ export declare function fitJson(data: unknown, max?: number): {
35
+ json: string;
36
+ page?: string;
37
+ };
38
+ export declare function renderOutput(out: ToolOutput): CallToolResult;
39
+ /** An API error as a tool result: stable code, message, param and the API's own next_action. */
40
+ export declare function renderError(err: unknown): CallToolResult;
package/dist/format.js ADDED
@@ -0,0 +1,238 @@
1
+ import { CheckboxApiError } from "./api.js";
2
+ /** API operationId → the MCP tool that performs it. Operations without a tool are reached with call_api. */
3
+ export const OPERATION_TOOLS = {
4
+ createDeviceAuthorization: 'setup_checkbox',
5
+ createDeviceToken: 'setup_checkbox',
6
+ createSetupSession: 'setup_checkbox',
7
+ retrieveSetupSession: 'get_setup_status',
8
+ listSetupSessions: 'get_setup_status',
9
+ verifySetupSession: 'get_setup_status',
10
+ updateSetupSession: 'update_setup',
11
+ completeSetupSession: 'complete_setup',
12
+ retrieveCompliancePlan: 'get_compliance_plan',
13
+ retrieveTask: 'get_task',
14
+ verifyTask: 'verify_task',
15
+ answerTask: 'answer_task',
16
+ verifySite: 'verify_site',
17
+ retrieveVerification: 'get_verification',
18
+ createTestRun: 'start_test_run',
19
+ retrieveTestRun: 'get_test_run',
20
+ listTestRuns: 'get_test_run',
21
+ checkDisclosure: 'check_disclosure',
22
+ checkStripeCheckoutSession: 'check_stripe_checkout_session',
23
+ checkEmail: 'check_email',
24
+ checkPage: 'check_page_html',
25
+ checkNoticeSchedule: 'get_notice_schedule',
26
+ listSiteSuggestions: 'suggest_improvements',
27
+ suggestImprovements: 'suggest_improvements',
28
+ listSites: 'list_sites',
29
+ createSite: 'create_site',
30
+ updateSite: 'update_site',
31
+ retrieveSite: 'get_site_status',
32
+ retrieveLatestEvaluation: 'get_site_status',
33
+ createEvaluation: 'verify_site',
34
+ listFindings: 'list_findings',
35
+ retrieveFinding: 'get_finding',
36
+ retrieveFindingFix: 'get_finding',
37
+ createScan: 'run_scan',
38
+ retrieveScan: 'get_scan',
39
+ listLawUpdates: 'list_law_updates',
40
+ createFeedback: 'send_feedback',
41
+ createDisclosure: 'get_elements_snippet',
42
+ retrieveReceipt: 'get_receipt',
43
+ verifyReceipt: 'get_receipt',
44
+ listReceipts: 'list_receipts',
45
+ listDueNotices: 'list_due_notices',
46
+ markNoticeSent: 'mark_notice_sent',
47
+ markNoticeMissed: 'mark_notice_sent',
48
+ listNoticeTemplates: 'get_notice_templates',
49
+ checkNoticeTemplate: 'get_notice_templates',
50
+ };
51
+ /** The MCP tool that performs an operation: its dedicated tool, else `call_api`. */
52
+ export function mcpToolFor(operationId) {
53
+ if (!operationId)
54
+ return null;
55
+ return OPERATION_TOOLS[operationId] ?? 'call_api';
56
+ }
57
+ /** Arguments for the generic call_api tool when an operation has no dedicated tool. */
58
+ const OPERATION_ENDPOINTS = {
59
+ createTestCredential: 'POST /v1/sites/{site}/test_credentials',
60
+ connectBillingPlatform: 'POST /v1/sites/{site}/integrations',
61
+ createStripeCheckoutConfig: 'POST /v1/sites/{site}/stripe_checkout_configs',
62
+ createDomainVerification: 'POST /v1/sites/{site}/domain_verification',
63
+ checkDomainVerification: 'POST /v1/sites/{site}/domain_verification/check',
64
+ retrieveDomainVerification: 'GET /v1/sites/{site}/domain_verification',
65
+ updateTask: 'POST /v1/tasks/{task}',
66
+ updateFinding: 'POST /v1/findings/{finding}',
67
+ listConsents: 'GET /v1/consents',
68
+ updateNoticeSettings: 'POST /v1/sites/{site}/notice_settings',
69
+ retrieveNoticeSettings: 'GET /v1/sites/{site}/notice_settings',
70
+ createWebhookEndpoint: 'POST /v1/webhook_endpoints',
71
+ createCertificate: 'POST /v1/sites/{site}/certificates',
72
+ retrieveAccount: 'GET /v1/account',
73
+ checkStripePaymentLink: 'POST /v1/checks/stripe_payment_link',
74
+ listSiteFixes: 'GET /v1/sites/{site}/fixes',
75
+ listVerifications: 'GET /v1/sites/{site}/verifications',
76
+ cancelTestRun: 'POST /v1/test_runs/{test_run}/cancel',
77
+ };
78
+ /**
79
+ * Quotes text Checkbox did not write (page HTML, email bodies, policy text, merchant-supplied strings) for the text part
80
+ * of a tool result: JSON-encoded on one line, truncated, inside explicit <untrusted_text> delimiters, so a model never
81
+ * reads it as an instruction next to the "Next:" line. Full values stay in the fenced JSON.
82
+ */
83
+ export function untrusted(value, max = 80) {
84
+ let s = String(value ?? '').replace(/\s+/g, ' ').trim();
85
+ if (s.length > max)
86
+ s = `${s.slice(0, max)}…`;
87
+ // No markup that could close the delimiter or open a fake one.
88
+ s = s.replace(/</g, '‹').replace(/>/g, '›');
89
+ return `<untrusted_text>${JSON.stringify(s)}</untrusted_text>`;
90
+ }
91
+ /** One line telling the model what to call next, in MCP terms. */
92
+ export function describeNextAction(next) {
93
+ if (!next || typeof next !== 'object' || !next.type)
94
+ return null;
95
+ const text = (next.message ?? next.description ?? '').trim();
96
+ if (next.type === 'done' || next.type === 'none')
97
+ return `Next: nothing left to do${text ? ` — ${text}` : '.'}`;
98
+ const op = next.operation_id ?? undefined;
99
+ const tool = op ? OPERATION_TOOLS[op] : undefined;
100
+ const parts = [`Next (${next.type}): ${text || 'see next_action.'}`];
101
+ if (tool)
102
+ parts.push(`→ MCP tool \`${tool}\`${next.task ? ` (task: ${next.task})` : ''}.`);
103
+ else if (op) {
104
+ const endpoint = next.endpoint ?? OPERATION_ENDPOINTS[op];
105
+ parts.push(endpoint ? `→ MCP tool \`call_api\` with ${endpoint} (operation ${op}).` : `→ operation ${op} (use \`call_api\`; paths in /v1/openapi.json).`);
106
+ }
107
+ if (next.command)
108
+ parts.push(`CLI equivalent: \`${next.command}\`.`);
109
+ if (next.url && !tool)
110
+ parts.push(`Docs: ${next.url}`);
111
+ return parts.join(' ');
112
+ }
113
+ /** Pulls the top-level next_action off an API object. */
114
+ export function nextActionOf(obj) {
115
+ const n = obj?.next_action;
116
+ return n && typeof n === 'object' ? n : null;
117
+ }
118
+ const MAX_JSON_CHARS = 60_000;
119
+ /** The largest array in `data` (top level, or one level down), with its path. */
120
+ function largestArray(data) {
121
+ if (!data || typeof data !== 'object')
122
+ return null;
123
+ let best = null;
124
+ const consider = (path, v) => {
125
+ if (!Array.isArray(v) || v.length < 2)
126
+ return;
127
+ const size = JSON.stringify(v).length;
128
+ if (!best || size > best.size)
129
+ best = { path, items: v, size };
130
+ };
131
+ for (const [k, v] of Object.entries(data)) {
132
+ consider([k], v);
133
+ if (v && typeof v === 'object' && !Array.isArray(v))
134
+ for (const [k2, v2] of Object.entries(v))
135
+ consider([k, k2], v2);
136
+ }
137
+ return best;
138
+ }
139
+ function withItems(data, path, items) {
140
+ const root = { ...data };
141
+ if (path.length === 1)
142
+ root[path[0]] = items;
143
+ else
144
+ root[path[0]] = { ...root[path[0]], [path[1]]: items };
145
+ return root;
146
+ }
147
+ /**
148
+ * Round-3 pilot (nextjs, sveltekit): output was cut at 60 KB in the middle of the JSON, which agents then failed to
149
+ * parse. Output is always valid JSON: an oversized list is paged (the first items that fit, plus `_page` naming the
150
+ * ids of the rest to fetch one by one), never cut mid-value.
151
+ */
152
+ export function fitJson(data, max = MAX_JSON_CHARS) {
153
+ const pretty = JSON.stringify(data, null, 2);
154
+ if (pretty.length <= max)
155
+ return { json: pretty };
156
+ const arr = largestArray(data);
157
+ if (arr) {
158
+ let lo = 0;
159
+ let hi = arr.items.length;
160
+ const render = (n) => {
161
+ const rest = arr.items.slice(n);
162
+ const ids = rest.map((x) => (x && typeof x === 'object' ? x.id : undefined)).filter((x) => typeof x === 'string');
163
+ 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);
164
+ };
165
+ while (lo < hi) {
166
+ const mid = Math.ceil((lo + hi) / 2);
167
+ if (render(mid).length <= max)
168
+ lo = mid;
169
+ else
170
+ hi = mid - 1;
171
+ }
172
+ const json = render(lo);
173
+ if (json.length <= max) {
174
+ 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).` };
175
+ }
176
+ }
177
+ const compact = JSON.stringify(data);
178
+ if (compact.length <= max)
179
+ return { json: compact, page: 'Output printed compactly (no indentation) to fit.' };
180
+ return {
181
+ json: JSON.stringify({ _truncated: true, size: compact.length, keys: data && typeof data === 'object' ? Object.keys(data) : [] }, null, 2),
182
+ page: `Output too large (${compact.length} characters): narrow the request (a smaller limit, or one item at a time).`,
183
+ };
184
+ }
185
+ export function renderOutput(out) {
186
+ const lines = [out.headline.trim()];
187
+ for (const n of out.notes ?? [])
188
+ if (n)
189
+ lines.push(`- ${n}`);
190
+ const next = describeNextAction(out.next === undefined ? nextActionOf(out.data) : out.next);
191
+ if (next)
192
+ lines.push(next);
193
+ if (out.data !== undefined) {
194
+ const fitted = fitJson(out.data);
195
+ if (fitted.page)
196
+ lines.push(`- ${fitted.page}`);
197
+ lines.push('', '```json', fitted.json, '```');
198
+ }
199
+ return { content: [{ type: 'text', text: lines.join('\n') }], ...(out.isError ? { isError: true } : {}) };
200
+ }
201
+ /** An API error as a tool result: stable code, message, param and the API's own next_action. */
202
+ export function renderError(err) {
203
+ if (err instanceof CheckboxApiError) {
204
+ const data = { type: err.type, code: err.code, message: err.message };
205
+ if (err.param)
206
+ data.param = err.param;
207
+ if (err.status)
208
+ data.http_status = err.status;
209
+ if (err.requestId)
210
+ data.request_id = err.requestId;
211
+ if (err.docUrl)
212
+ data.doc_url = err.docUrl;
213
+ if (err.nextAction)
214
+ data.next_action = err.nextAction;
215
+ const hint = ERROR_HINTS[err.code];
216
+ return renderOutput({
217
+ headline: `Checkbox API error ${err.code}${err.param ? ` (param: ${err.param})` : ''}: ${err.message}`,
218
+ notes: hint ? [hint] : [],
219
+ data: { error: data },
220
+ next: err.nextAction ?? null,
221
+ isError: true,
222
+ });
223
+ }
224
+ const message = err instanceof Error ? err.message : String(err);
225
+ return renderOutput({ headline: `Tool failed: ${message}`, isError: true });
226
+ }
227
+ const ERROR_HINTS = {
228
+ insufficient_scope: 'The API key this MCP server uses is a restricted key without that permission. Ask the human for a key with the permission (Dashboard → Developers → API keys) or use a read-only tool.',
229
+ insufficient_role: 'The key acts as a viewer; writes need a member or admin key.',
230
+ api_key_missing: 'No Checkbox API key is configured: call setup_checkbox (one browser approval by the human) to get one.',
231
+ api_key_invalid: 'The configured Checkbox API key is unknown, revoked or expired: call setup_checkbox to get a new one, or ask the human for a valid key.',
232
+ 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.',
233
+ parameter_invalid: 'Fix the named parameter and call the tool again.',
234
+ parameter_missing: 'Add the named parameter and call the tool again.',
235
+ parameter_unknown: 'Remove the named parameter and call the tool again.',
236
+ 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.',
237
+ rate_limit_exceeded: 'Wait the number of seconds in the message, then retry.',
238
+ };
@@ -0,0 +1,7 @@
1
+ export { API_VERSION, CheckboxApiError, DEFAULT_API_BASE, createHttpApi, fromSdkClient, interpretResponse, withQuery, type ApiRequestOptions, type CheckboxApi, type HttpMethod, type NextAction, } from './api.ts';
2
+ export { OPERATION_TOOLS, describeNextAction, renderError, renderOutput, type ToolOutput } from './format.ts';
3
+ export { NO_KEY, keyAllows, keyContextFromPrefix, type KeyContext, type KeyRole, type ToolScope } from './permissions.ts';
4
+ export { CATALOG_URI, QUICKSTART_URI, makeMyCheckoutCompliantPrompt, quickstartMarkdown, renderCatalog, type CoverageDoc } from './resources.ts';
5
+ export { DEFAULT_MCP_URL, SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, createCheckboxMcpServer, type CheckboxMcp, type CheckboxMcpOptions } from './server.ts';
6
+ export { TOOLS, TOOL_OPERATIONS, ToolError, toolVisible, type IssuedKeys, type ToolContext, type ToolDef } from './tools.ts';
7
+ export { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ // @checkbox-site/mcp — the Checkbox MCP server (tools, resources, prompts) shared by the remote endpoint
2
+ // (https://app.checkbox.site/mcp, apps/api/src/mcp) and the local stdio binary (dist/bin.js).
3
+ export { API_VERSION, CheckboxApiError, DEFAULT_API_BASE, createHttpApi, fromSdkClient, interpretResponse, withQuery, } from "./api.js";
4
+ export { OPERATION_TOOLS, describeNextAction, renderError, renderOutput } from "./format.js";
5
+ export { NO_KEY, keyAllows, keyContextFromPrefix } from "./permissions.js";
6
+ export { CATALOG_URI, QUICKSTART_URI, makeMyCheckoutCompliantPrompt, quickstartMarkdown, renderCatalog } from "./resources.js";
7
+ export { DEFAULT_MCP_URL, SERVER_INSTRUCTIONS, SERVER_NAME, SERVER_VERSION, createCheckboxMcpServer } from "./server.js";
8
+ export { TOOLS, TOOL_OPERATIONS, ToolError, toolVisible } from "./tools.js";
9
+ // Re-exported so hosts use the same SDK copy as the server.
10
+ export { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -0,0 +1,30 @@
1
+ export type Access = 'read' | 'write';
2
+ /** `<resource>:<access>` as in the /v1 restricted-key scopes (apps/api/src/v1/scopes.ts). */
3
+ export type ToolScope = `${string}:${Access}`;
4
+ export type KeyRole = 'admin' | 'member' | 'viewer';
5
+ /** What is known about the key the server acts with. */
6
+ export interface KeyContext {
7
+ /** No key at all: only the setup (device login) tool and the docs work. */
8
+ authenticated: boolean;
9
+ /** test / live, or unknown (stdio before the first call). */
10
+ mode: 'test' | 'live' | 'unknown';
11
+ kind?: 'secret' | 'restricted';
12
+ role?: KeyRole;
13
+ /** Restricted keys only: `{ resource: 'read' | 'write' }`. */
14
+ scopes?: Record<string, string> | null;
15
+ /** Hide every write tool regardless of the key (`?read_only=true`, `--read-only`). */
16
+ readOnly?: boolean;
17
+ /**
18
+ * How the remote connection authenticated: an API key in the Authorization header, or an OAuth access token from
19
+ * the one-click connector flow (claude.ai / Claude Desktop / Claude Code / Cursor). With OAuth there is no key to
20
+ * copy or save: setup_checkbox registers the site and runs the first checks straight away.
21
+ */
22
+ auth?: 'api_key' | 'oauth';
23
+ /** OAuth only: the connected app's name as approved by the human (e.g. "Claude"). */
24
+ appName?: string;
25
+ }
26
+ export declare const NO_KEY: KeyContext;
27
+ /** Whether the key may use one scope (write implies read; viewer keys are read-only). */
28
+ export declare function keyAllows(key: KeyContext, scope: ToolScope): boolean;
29
+ /** Infers the mode and kind from the key prefix (the server confirms with retrieveAccount when it can). */
30
+ export declare function keyContextFromPrefix(apiKey: string | undefined, readOnly?: boolean): KeyContext;