@usethrottle/mcp 0.1.1 → 0.2.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/dist/lib.cjs ADDED
@@ -0,0 +1,3328 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/lib.ts
21
+ var lib_exports = {};
22
+ __export(lib_exports, {
23
+ ThrottleClient: () => ThrottleClient,
24
+ VERSION: () => VERSION,
25
+ buildInstructions: () => buildInstructions,
26
+ createServer: () => createServer,
27
+ loadContractIndex: () => loadContractIndex,
28
+ loadDocsIndex: () => loadDocsIndex,
29
+ registerableActions: () => registerableActions
30
+ });
31
+ module.exports = __toCommonJS(lib_exports);
32
+
33
+ // src/server.ts
34
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
35
+ var import_types = require("@modelcontextprotocol/sdk/types.js");
36
+
37
+ // ../core-scopes/src/scopes.ts
38
+ function hasScope(granted, required) {
39
+ if (granted.includes("*")) return true;
40
+ if (granted.includes(required)) return true;
41
+ const sep = required.lastIndexOf(":");
42
+ if (sep === -1) return false;
43
+ const resource = required.slice(0, sep);
44
+ const action = required.slice(sep + 1);
45
+ if (action === "read" && granted.includes(`${resource}:write`)) return true;
46
+ return false;
47
+ }
48
+ var REG = {};
49
+ function add(group, resource, actions, labels, opts = {}) {
50
+ const hasRead = actions.includes("read");
51
+ for (const action of actions) {
52
+ const id = `${resource}:${action}`;
53
+ const implies = action === "write" && hasRead ? [`${resource}:read`] : [];
54
+ REG[id] = {
55
+ id,
56
+ resource,
57
+ action,
58
+ group,
59
+ label: (action === "read" ? labels.read : labels.write) ?? `${action} ${resource}`,
60
+ sensitive: opts.sensitive ?? false,
61
+ staffOnly: opts.staffOnly ?? false,
62
+ // publishable defaults false; only the curated set below is true
63
+ publishableAllowed: opts.publishableAllowed ?? false,
64
+ // publishableOnly defaults false; only customer_accounts:write sets it
65
+ publishableOnly: opts.publishableOnly ?? false,
66
+ // extension default true for commerce scopes; Taxonomy B passes false
67
+ extensionAllowed: opts.extensionAllowed ?? true,
68
+ implies
69
+ };
70
+ }
71
+ }
72
+ add("Orders", "orders", ["read", "write"], { read: "View orders", write: "Manage orders" });
73
+ add("Quotes", "quotes", ["read", "write"], { read: "View quotes", write: "Manage quotes" });
74
+ add("Orders", "order_cancellations", ["write"], { write: "Cancel orders" });
75
+ add("Orders", "order_returns", ["read", "write"], { read: "View returns", write: "Process returns" });
76
+ add("Orders", "order_comps", ["write"], { write: "Comp orders (write off the balance)" }, { sensitive: true });
77
+ add("Orders", "order_holds", ["write"], { write: "Freeze and unfreeze orders" });
78
+ add(
79
+ "Imports",
80
+ "imports",
81
+ ["read", "write"],
82
+ { read: "View data imports", write: "Run and undo data imports" },
83
+ { extensionAllowed: false }
84
+ );
85
+ add("Payments", "payments", ["read", "write"], { read: "View payments", write: "Create payments" });
86
+ add("Payments", "payment_authorizations", ["write"], { write: "Authorize payments" });
87
+ add("Payments", "payment_captures", ["write"], { write: "Capture payments" });
88
+ add("Payments", "payment_refunds", ["read", "write"], { read: "View refunds", write: "Refund payments" }, { sensitive: true });
89
+ add("Payments", "payment_voids", ["write"], { write: "Void payments" }, { sensitive: true });
90
+ add("Payments", "payment_records", ["write"], { write: "Record payments received outside Throttle" }, { sensitive: true });
91
+ add("Payments", "payment_disputes", ["read", "write"], { read: "View disputes", write: "Handle disputes" }, { sensitive: true });
92
+ add("Payments", "payment_methods", ["read", "write"], { read: "View payment methods", write: "Tokenize payment methods" }, { sensitive: true });
93
+ add("Customers", "customers", ["read", "write"], { read: "View customers", write: "Manage customers" });
94
+ add("Customers", "customer_pii", ["read"], { read: "View customer PII" }, { sensitive: true });
95
+ add("Customers", "customer_addresses", ["read", "write"], { read: "View addresses", write: "Manage addresses" });
96
+ add("Customers", "customer_payment_methods", ["read", "write"], { read: "View stored cards", write: "Manage stored cards" }, { sensitive: true });
97
+ add(
98
+ "Customer accounts",
99
+ "customer_accounts",
100
+ ["write"],
101
+ { write: "Let shoppers register, sign in and manage their account" },
102
+ { publishableAllowed: true, publishableOnly: true, extensionAllowed: false }
103
+ );
104
+ add("Carts", "carts", ["read", "write"], { read: "View carts", write: "Manage carts" });
105
+ add("Carts", "cart_items", ["write"], { write: "Manage line items" });
106
+ add("Carts", "checkout_sessions", ["read", "write"], { read: "View checkout sessions", write: "Manage checkout sessions" });
107
+ add("Discounts", "discounts", ["read", "write"], { read: "View discounts", write: "Manage discounts" });
108
+ add("Subscriptions", "subscriptions", ["read", "write"], { read: "View subscriptions", write: "Manage subscriptions" });
109
+ add("Subscriptions", "subscription_pauses", ["write"], { write: "Pause/resume subscriptions" });
110
+ add("Subscriptions", "subscription_cancellations", ["write"], { write: "Cancel subscriptions" });
111
+ add("Subscriptions", "subscription_usage", ["read", "write"], { read: "View subscription usage", write: "Report subscription usage" }, { extensionAllowed: false });
112
+ add("Fulfillments", "fulfillments", ["read", "write"], { read: "View fulfillments", write: "Manage fulfillments" });
113
+ add("Fulfillments", "fulfillment_shipments", ["write"], { write: "Update shipments" });
114
+ add("Fulfillments", "fulfillment_digital", ["read", "write"], { read: "View digital delivery", write: "Manage digital delivery" });
115
+ add("Fulfillments", "fulfillment_access", ["write"], { write: "Manage access credentials" });
116
+ add("Fulfillments", "fulfillment_services", ["write"], { write: "Manage service fulfillment" });
117
+ add("Invoices / AR", "invoices", ["read"], { read: "View invoices & aging" });
118
+ add("Invoices / AR", "invoice_payments", ["write"], { write: "Mark invoices paid" }, { sensitive: true });
119
+ add("Invoices / AR", "invoice_dunning", ["write"], { write: "Send dunning notices" });
120
+ add("Shipping & Tax", "shipping_tax", ["read", "write"], { read: "View shipping/tax config", write: "Edit shipping/tax config" });
121
+ add("Shipping & Tax", "shipping_tax_publications", ["write"], { write: "Publish shipping/tax config" });
122
+ add("Shipping & Tax", "shipping_quotes", ["write"], { write: "Request shipping quotes" }, { publishableAllowed: true });
123
+ add("Shipping & Tax", "tax_calculations", ["write"], { write: "Calculate tax" }, { publishableAllowed: true });
124
+ add("Shipping & Tax", "shipping_tax_quote_tokens", ["read", "write"], { read: "View quote tokens", write: "Manage quote tokens" });
125
+ add("Shipping & Tax", "shipping_tax_snapshots", ["write"], { write: "Accept external snapshots" });
126
+ add(
127
+ "Scripts",
128
+ "storefront_scripts",
129
+ ["read", "write"],
130
+ {
131
+ read: "Load storefront scripts",
132
+ // `write` covers exactly one thing: reporting a script's own load outcome
133
+ // from the merchant's storefront, so the health panel stops being
134
+ // hosted-checkout-only. On checkout the session id in the URL is the
135
+ // capability that scopes that write; a storefront page has no session, so
136
+ // the pk it already holds is what scopes it instead. It writes nothing but
137
+ // load telemetry — authoring stays on `application_scripts:write`, which
138
+ // is not publishable-allowed.
139
+ write: "Report storefront script load outcomes"
140
+ },
141
+ { publishableAllowed: true }
142
+ );
143
+ add("Scripts", "application_scripts", ["read", "write"], {
144
+ read: "View merchant-authored scripts",
145
+ write: "Create, edit and disable merchant-authored scripts"
146
+ }, { extensionAllowed: false });
147
+ add("Scripts", "extension_scripts", ["read", "write"], {
148
+ read: "View an extension's buyer-facing scripts",
149
+ write: "Create, edit and delete an extension's buyer-facing scripts"
150
+ }, { extensionAllowed: false });
151
+ add("Connectors", "connectors", ["read", "write"], { read: "View connectors", write: "Manage connectors" });
152
+ add("Webhooks & Events", "webhooks", ["read", "write"], { read: "View webhooks", write: "Manage webhooks" });
153
+ add("Webhooks & Events", "webhook_deliveries", ["read", "write"], { read: "View deliveries", write: "Retry deliveries" });
154
+ add("Webhooks & Events", "events", ["read"], { read: "View events" });
155
+ add("Webhooks & Events", "audit_logs", ["read"], { read: "View audit log" });
156
+ add("Applications", "applications", ["read", "write"], { read: "View applications", write: "Manage applications" });
157
+ add("Workspace", "merchant", ["read", "write"], { read: "View workspace settings", write: "Edit workspace settings" });
158
+ add("Workspace", "embed_config", ["read", "write"], { read: "View embed config", write: "Edit embed config" });
159
+ add("Workspace", "branding", ["write"], { write: "Upload branding assets" });
160
+ var HUMAN = { extensionAllowed: false };
161
+ add("Team", "team_members", ["read", "write"], { read: "View members", write: "Manage members" }, HUMAN);
162
+ add("Team", "team_invites", ["read", "write"], { read: "View invites", write: "Manage invites" }, HUMAN);
163
+ add("Platform billing", "billing", ["read", "write"], { read: "View billing", write: "Manage subscription" }, HUMAN);
164
+ add("Platform billing", "billing_payment_methods", ["read", "write"], { read: "View billing cards", write: "Manage billing cards" }, HUMAN);
165
+ add("Email", "email_templates", ["read", "write"], { read: "View email templates", write: "Edit email templates" }, HUMAN);
166
+ add("Email", "email_settings", ["read", "write"], { read: "View email settings", write: "Edit email settings" }, HUMAN);
167
+ add("Go-live", "go_live", ["write"], { write: "Submit go-live" }, HUMAN);
168
+ add("Partner", "partner", ["read", "write"], { read: "View partner profile", write: "Edit partner profile" }, HUMAN);
169
+ add("Partner", "affiliates", ["read", "write"], { read: "View affiliates", write: "Manage affiliates" }, HUMAN);
170
+ add("Notifications", "notifications", ["write"], { write: "Send notifications" }, HUMAN);
171
+ add("Notifications", "preferences", ["read", "write"], { read: "View preferences", write: "Edit preferences" }, HUMAN);
172
+ add("Integrations", "integrations", ["read", "write"], { read: "View integrations", write: "Manage integrations" }, HUMAN);
173
+ add("Support", "support", ["read", "write"], { read: "View support tickets", write: "Manage support tickets" }, HUMAN);
174
+ add("Admin (staff)", "admin", ["read", "write"], { read: "View admin data", write: "Perform admin actions" }, { sensitive: true, staffOnly: true, extensionAllowed: false });
175
+ add(
176
+ "Extensions",
177
+ "extensions",
178
+ ["read", "write"],
179
+ { read: "View extensions & installs", write: "Author, version & publish extensions" },
180
+ HUMAN
181
+ );
182
+ REG["extensions:install"] = {
183
+ id: "extensions:install",
184
+ resource: "extensions",
185
+ action: "install",
186
+ group: "Extensions",
187
+ label: "Install, configure & manage extensions",
188
+ sensitive: false,
189
+ staffOnly: false,
190
+ publishableAllowed: false,
191
+ publishableOnly: false,
192
+ extensionAllowed: false,
193
+ implies: []
194
+ };
195
+ REG["*"] = {
196
+ id: "*",
197
+ resource: "*",
198
+ action: "write",
199
+ group: "Global",
200
+ label: "Full access",
201
+ sensitive: true,
202
+ staffOnly: false,
203
+ publishableAllowed: false,
204
+ publishableOnly: false,
205
+ extensionAllowed: false,
206
+ implies: []
207
+ };
208
+ var SCOPE_REGISTRY = REG;
209
+ var ALL_SCOPE_IDS = Object.keys(REG);
210
+ var SCOPE_GROUPS = (() => {
211
+ const out = {};
212
+ for (const def of Object.values(REG)) {
213
+ if (def.id === "*") continue;
214
+ (out[def.group] ??= []).push(def.id);
215
+ }
216
+ return out;
217
+ })();
218
+
219
+ // ../core-scopes/src/events.ts
220
+ var EVENT_SCOPE_OVERRIDES = {
221
+ "payment.refunded": "payment_refunds:read",
222
+ "payment.partially_refunded": "payment_refunds:read",
223
+ "payment.refund_failed": "payment_refunds:read",
224
+ // Money returned, so it belongs with the other refund events rather than
225
+ // behind the broad subscriptions:read the `subscription.` prefix would give.
226
+ "subscription.invoice_refunded": "payment_refunds:read",
227
+ "payment.disputed": "payment_disputes:read",
228
+ "payment.dispute_cleared": "payment_disputes:read",
229
+ "fulfillment.digital.delivered": "fulfillment_digital:read"
230
+ };
231
+ var PREFIX_RULES = [
232
+ ["checkout.session.", "carts:read"],
233
+ ["cart.", "carts:read"],
234
+ ["order.", "orders:read"],
235
+ ["quote.", "quotes:read"],
236
+ ["return.", "order_returns:read"],
237
+ ["payment.", "payments:read"],
238
+ ["customer.", "customers:read"],
239
+ ["subscription.", "subscriptions:read"],
240
+ ["fulfillment.", "fulfillments:read"],
241
+ ["discount.", "discounts:read"],
242
+ ["invoice.", "invoices:read"],
243
+ ["import.", "imports:read"],
244
+ ["shipping_tax.", "shipping_tax:read"],
245
+ ["workspace.", "billing:read"],
246
+ // `extensions:read` is a human scope (extensions must never watch other
247
+ // extensions), so only a workspace endpoint can SUBSCRIBE to this family.
248
+ // The uninstalled installation's own endpoint is addressed directly by the
249
+ // uninstall path and needs no subscription at all.
250
+ ["extension.", "extensions:read"],
251
+ // Subscribing to script outcomes needs script read — the payload names the
252
+ // script key and its hash, which is the merchant's script inventory.
253
+ ["script.", "application_scripts:read"]
254
+ ];
255
+ function eventReadScope(type) {
256
+ if (EVENT_SCOPE_OVERRIDES[type]) return EVENT_SCOPE_OVERRIDES[type];
257
+ for (const [prefix, scope] of PREFIX_RULES) {
258
+ if (type.startsWith(prefix)) return scope;
259
+ }
260
+ throw new Error(`Unknown event type: ${type}`);
261
+ }
262
+
263
+ // ../core-scopes/src/issuance.ts
264
+ var PUBLISHABLE_SCOPES = Object.values(SCOPE_REGISTRY).filter((d) => d.publishableAllowed).map((d) => d.id);
265
+ var CONNECTOR_GRANTABLE_SENSITIVE_SCOPES = ["customer_pii:read"];
266
+ function validateScopesForOAuth(scopes, opts = {}) {
267
+ const invalid = [];
268
+ for (const id of scopes) {
269
+ if (id === "*") {
270
+ invalid.push(id);
271
+ continue;
272
+ }
273
+ const def = SCOPE_REGISTRY[id];
274
+ if (!def || def.staffOnly) {
275
+ invalid.push(id);
276
+ continue;
277
+ }
278
+ if (def.sensitive && !(opts.connector && CONNECTOR_GRANTABLE_SENSITIVE_SCOPES.includes(id))) invalid.push(id);
279
+ }
280
+ return { ok: invalid.length === 0, invalid };
281
+ }
282
+
283
+ // ../core-scopes/src/shopper-pii.ts
284
+ var PERSON_KEYS = /* @__PURE__ */ new Set([
285
+ "email",
286
+ "customerEmail",
287
+ "billingEmail",
288
+ "buyerEmail",
289
+ "authorEmail",
290
+ "authorName",
291
+ "phone",
292
+ "firstName",
293
+ "lastName",
294
+ "addressLine1",
295
+ "addressLine2",
296
+ "billingAddressLines",
297
+ "processorResponse",
298
+ "shareToken",
299
+ "company",
300
+ "externalId",
301
+ "externalCustomerId",
302
+ // Free-form: contents cannot be inspected for a person, so the field goes.
303
+ "metadata",
304
+ "notes",
305
+ "internalNotes",
306
+ "buyerNote",
307
+ "buyerReference",
308
+ "changeSummary",
309
+ "declinedReason",
310
+ "compReason",
311
+ "holdReason",
312
+ "body",
313
+ "acceptance",
314
+ "acceptanceSelections",
315
+ "clientContext",
316
+ "ipAddress",
317
+ "userAgent",
318
+ "vatNumber",
319
+ "buyerVatNumber",
320
+ "taxExemptionNumber"
321
+ ]);
322
+ var PERSON_CONTAINERS = /* @__PURE__ */ new Set(["customer", "buyer"]);
323
+ function isPlainObject(value) {
324
+ if (value === null || typeof value !== "object") return false;
325
+ const proto = Object.getPrototypeOf(value);
326
+ return proto === Object.prototype || proto === null;
327
+ }
328
+ function walk(value, parentKey, depth) {
329
+ if (depth > 32) return value;
330
+ if (Array.isArray(value)) return value.map((item) => walk(item, parentKey, depth + 1));
331
+ if (!isPlainObject(value)) return value;
332
+ const out = {};
333
+ const isPerson = parentKey !== null && PERSON_CONTAINERS.has(parentKey);
334
+ for (const [key, child] of Object.entries(value)) {
335
+ if (PERSON_KEYS.has(key) || isPerson && key === "name") {
336
+ out[key] = child === void 0 ? child : null;
337
+ } else {
338
+ out[key] = walk(child, key, depth + 1);
339
+ }
340
+ }
341
+ return out;
342
+ }
343
+ function redactShopperPii(value) {
344
+ return walk(value, null, 0);
345
+ }
346
+
347
+ // ../action-catalog/src/actions.ts
348
+ var OAUTH_WITHHELD = "Over an OAuth connection (ChatGPT, Claude) these are withheld, because a buyer can type them: line-item and quote-item names, SKUs and reference ids; company; every address line except the country; the merchant's external ids; notes, messages, reasons and metadata. Describe items by quantity and amount, and say the rest is withheld rather than missing.";
349
+ var ACTIONS = [
350
+ // ---------------------------------------------------------------- identity
351
+ {
352
+ id: "whoami",
353
+ title: "Identify the connected credential",
354
+ description: "Report which workspace, application and environment this credential is pinned to, and what it is allowed to do. Call this first when you need to know whether you are talking to production or a sandbox.",
355
+ kind: "read",
356
+ scopes: [],
357
+ input: [],
358
+ requests: [{ method: "GET", path: "/api/v1/whoami" }],
359
+ channels: ["mcp", "zapier", "make", "n8n"]
360
+ },
361
+ // ------------------------------------------------------- local (no API call)
362
+ {
363
+ id: "search_docs",
364
+ title: "Search the Throttle developer docs",
365
+ description: "Full-text search over the Throttle developer documentation. Returns titles, canonical usethrottle.dev/docs URLs, and matching excerpts. Prefer this over recalling API details from memory.",
366
+ kind: "read",
367
+ scopes: [],
368
+ input: [
369
+ { name: "query", type: "string", required: true, description: "What to search for." },
370
+ { name: "limit", type: "number", required: false, description: "Maximum results." }
371
+ ],
372
+ requests: [],
373
+ channels: ["mcp"]
374
+ },
375
+ {
376
+ id: "describe_endpoint",
377
+ title: "Describe an API endpoint",
378
+ description: "Return the canonical request and response shape of a Throttle API endpoint from the published OpenAPI contract: path parameters, request body fields, required scope. Use before writing any integration code.",
379
+ kind: "read",
380
+ scopes: [],
381
+ input: [
382
+ {
383
+ name: "path",
384
+ type: "string",
385
+ required: true,
386
+ description: 'Endpoint path or fragment, e.g. "/api/v1/carts" or "carts".'
387
+ },
388
+ {
389
+ name: "method",
390
+ type: "string",
391
+ required: false,
392
+ description: "Narrow to one HTTP method.",
393
+ enum: ["GET", "POST", "PATCH", "PUT", "DELETE"]
394
+ }
395
+ ],
396
+ requests: [],
397
+ channels: ["mcp"]
398
+ },
399
+ {
400
+ id: "validate_payload",
401
+ title: "Validate a request body against the API contract",
402
+ description: "Check a request body against the published contract before sending it. Names every unsupported field and gives its canonical replacement \u2014 Throttle is strict camelCase and rejects retired aliases such as line_items, unit_price, customer_id and storeId. Full reference: https://usethrottle.dev/docs/developers/api-reference.",
403
+ kind: "read",
404
+ scopes: [],
405
+ input: [
406
+ { name: "method", type: "string", required: true, description: "HTTP method." },
407
+ { name: "path", type: "string", required: true, description: "Endpoint path." },
408
+ { name: "body", type: "object", required: true, description: "The request body to check." }
409
+ ],
410
+ requests: [],
411
+ channels: ["mcp"]
412
+ },
413
+ // ------------------------------------------------------------ generic reads
414
+ {
415
+ id: "api_get",
416
+ title: "Read any Throttle API endpoint",
417
+ description: "Escape hatch for reads the curated tools do not cover: performs a GET against any /api/v1 path and returns the response. Cannot write. Confirm the path with describe_endpoint first if unsure. Full reference: https://usethrottle.dev/docs/developers/api-reference.",
418
+ kind: "read",
419
+ scopes: [],
420
+ credentialScoped: true,
421
+ input: [
422
+ {
423
+ name: "path",
424
+ type: "string",
425
+ required: true,
426
+ description: "Path beginning with /api/v1, query string included."
427
+ },
428
+ {
429
+ name: "applicationId",
430
+ type: "string",
431
+ required: false,
432
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
433
+ }
434
+ ],
435
+ requests: [],
436
+ channels: ["mcp"],
437
+ guardrails: [
438
+ "GET only \u2014 reject any other method.",
439
+ "Reject paths outside /api/v1 so the tool cannot be pointed at another host or a non-versioned surface.",
440
+ "Server-side scope enforcement still applies; surface a 403 insufficient_scopes verbatim rather than retrying."
441
+ ]
442
+ },
443
+ // ------------------------------------------------------------------- orders
444
+ {
445
+ id: "search_orders",
446
+ title: "Search orders",
447
+ description: "List orders, newest first, with optional free-text and status filters.",
448
+ kind: "read",
449
+ scopes: ["orders:read"],
450
+ input: [
451
+ { name: "q", type: "string", required: false, description: "Free-text search." },
452
+ { name: "status", type: "string", required: false, description: "Order status filter." },
453
+ { name: "paymentStatus", type: "string", required: false, description: "Payment status filter." },
454
+ {
455
+ name: "createdAfter",
456
+ type: "string",
457
+ required: false,
458
+ description: "ISO 8601 lower bound on creation time, exclusive. For a single day, pass that day's midnight and the next day's midnight."
459
+ },
460
+ {
461
+ name: "createdBefore",
462
+ type: "string",
463
+ required: false,
464
+ description: "ISO 8601 upper bound on creation time, exclusive."
465
+ },
466
+ { name: "limit", type: "number", required: false, description: "Page size." },
467
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
468
+ {
469
+ name: "applicationId",
470
+ type: "string",
471
+ required: false,
472
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
473
+ }
474
+ ],
475
+ requests: [{ method: "GET", path: "/api/v1/orders" }],
476
+ channels: ["mcp", "zapier", "make", "n8n"],
477
+ guardrails: [
478
+ OAUTH_WITHHELD,
479
+ "Over an OAuth connection, q matches the order id, order number and status fields only."
480
+ ]
481
+ },
482
+ {
483
+ id: "get_order",
484
+ title: "Get one order with its returns and fulfillments",
485
+ description: "Full picture of a single order: the order itself plus its payments, returns and fulfillments in one response.",
486
+ kind: "read",
487
+ scopes: ["orders:read"],
488
+ sectionScopes: ["payments:read", "order_returns:read", "fulfillments:read"],
489
+ input: [
490
+ { name: "id", type: "string", required: true, description: "Order id." },
491
+ {
492
+ name: "applicationId",
493
+ type: "string",
494
+ required: false,
495
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
496
+ }
497
+ ],
498
+ requests: [
499
+ { method: "GET", path: "/api/v1/orders/:id" },
500
+ { method: "GET", path: "/api/v1/orders/:id/payments" },
501
+ { method: "GET", path: "/api/v1/orders/:id/returns" },
502
+ { method: "GET", path: "/api/v1/orders/:orderId/fulfillments" }
503
+ ],
504
+ channels: ["mcp"],
505
+ guardrails: [
506
+ "Sub-reads need their own scopes (payments:read, order_returns:read, fulfillments:read). Skip a section the credential cannot read and say so in the result rather than failing the whole call.",
507
+ OAUTH_WITHHELD
508
+ ]
509
+ },
510
+ // ---------------------------------------------------------------- customers
511
+ {
512
+ id: "search_customers",
513
+ title: "Search customers",
514
+ description: "Find customers by name, email or external id, or list them by creation date. Over an OAuth connection (ChatGPT, Claude) text search is not available \u2014 list by date instead \u2014 because every field it could match is withheld there.",
515
+ kind: "read",
516
+ scopes: ["customers:read"],
517
+ input: [
518
+ { name: "q", type: "string", required: false, description: "Free-text search." },
519
+ {
520
+ name: "createdAfter",
521
+ type: "string",
522
+ required: false,
523
+ description: "ISO 8601 lower bound on creation time, exclusive. For a single day, pass that day's midnight and the next day's midnight."
524
+ },
525
+ {
526
+ name: "createdBefore",
527
+ type: "string",
528
+ required: false,
529
+ description: "ISO 8601 upper bound on creation time, exclusive."
530
+ },
531
+ { name: "limit", type: "number", required: false, description: "Page size." },
532
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
533
+ {
534
+ name: "applicationId",
535
+ type: "string",
536
+ required: false,
537
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
538
+ }
539
+ ],
540
+ requests: [{ method: "GET", path: "/api/v1/customers" }],
541
+ channels: ["mcp", "zapier", "make", "n8n"],
542
+ guardrails: [
543
+ 'email, phone, firstName, lastName, name and metadata are withheld (null, or absent from the result) unless the credential holds customer_pii:read, which OAuth connections cannot hold. Over an OAuth connection company, externalId and externalCustomerId are withheld too, and q is refused. Do not describe a missing name/email as "no name on file"; say it is withheld.'
544
+ ]
545
+ },
546
+ {
547
+ id: "get_customer",
548
+ title: "Get one customer with subscriptions and addresses",
549
+ description: "Full picture of a single customer: the record plus their subscriptions and addresses in one response.",
550
+ kind: "read",
551
+ scopes: ["customers:read"],
552
+ sectionScopes: ["customer_addresses:read", "subscriptions:read"],
553
+ input: [
554
+ { name: "id", type: "string", required: true, description: "Customer id." },
555
+ {
556
+ name: "applicationId",
557
+ type: "string",
558
+ required: false,
559
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
560
+ }
561
+ ],
562
+ requests: [
563
+ { method: "GET", path: "/api/v1/customers/:id" },
564
+ { method: "GET", path: "/api/v1/customers/:id/addresses" },
565
+ { method: "GET", path: "/api/v1/subscriptions" }
566
+ ],
567
+ channels: ["mcp"],
568
+ guardrails: [
569
+ "Sub-reads need customer_addresses:read / subscriptions:read. Omit a section the credential cannot read instead of failing.",
570
+ "email, phone, firstName, lastName, name and metadata on the customer record are withheld (null, or absent from the result) unless the credential holds customer_pii:read, which OAuth connections cannot hold. Say PII is redacted rather than reporting the buyer as anonymous.",
571
+ OAUTH_WITHHELD
572
+ ]
573
+ },
574
+ // ------------------------------------------------------------- subscriptions
575
+ {
576
+ id: "list_subscriptions",
577
+ title: "List subscriptions",
578
+ description: "List subscriptions with optional customer and status filters.",
579
+ kind: "read",
580
+ scopes: ["subscriptions:read"],
581
+ input: [
582
+ { name: "customerId", type: "string", required: false, description: "Filter by customer." },
583
+ { name: "status", type: "string", required: false, description: "Filter by status." },
584
+ {
585
+ name: "createdAfter",
586
+ type: "string",
587
+ required: false,
588
+ description: "ISO 8601 lower bound on creation time, exclusive. For a single day, pass that day's midnight and the next day's midnight."
589
+ },
590
+ {
591
+ name: "createdBefore",
592
+ type: "string",
593
+ required: false,
594
+ description: "ISO 8601 upper bound on creation time, exclusive."
595
+ },
596
+ { name: "limit", type: "number", required: false, description: "Page size." },
597
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
598
+ {
599
+ name: "applicationId",
600
+ type: "string",
601
+ required: false,
602
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
603
+ }
604
+ ],
605
+ requests: [{ method: "GET", path: "/api/v1/subscriptions" }],
606
+ channels: ["mcp", "zapier", "make", "n8n"]
607
+ },
608
+ {
609
+ id: "get_subscription",
610
+ title: "Get one subscription",
611
+ description: "Read a single subscription including its current period and pending changes.",
612
+ kind: "read",
613
+ scopes: ["subscriptions:read"],
614
+ input: [
615
+ { name: "id", type: "string", required: true, description: "Subscription id." },
616
+ {
617
+ name: "applicationId",
618
+ type: "string",
619
+ required: false,
620
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
621
+ }
622
+ ],
623
+ requests: [{ method: "GET", path: "/api/v1/subscriptions/:id" }],
624
+ channels: ["mcp", "zapier", "make", "n8n"]
625
+ },
626
+ // ------------------------------------------------------------------- quotes
627
+ {
628
+ id: "search_quotes",
629
+ title: "Search quotes",
630
+ description: 'List quotes with optional status filter \u2014 useful for "which quotes expire soon".',
631
+ kind: "read",
632
+ scopes: ["quotes:read"],
633
+ input: [
634
+ { name: "status", type: "string", required: false, description: "Filter by quote status." },
635
+ {
636
+ name: "createdAfter",
637
+ type: "string",
638
+ required: false,
639
+ description: "ISO 8601 lower bound on creation time, exclusive. For a single day, pass that day's midnight and the next day's midnight."
640
+ },
641
+ {
642
+ name: "createdBefore",
643
+ type: "string",
644
+ required: false,
645
+ description: "ISO 8601 upper bound on creation time, exclusive."
646
+ },
647
+ { name: "limit", type: "number", required: false, description: "Page size." },
648
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
649
+ {
650
+ name: "applicationId",
651
+ type: "string",
652
+ required: false,
653
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
654
+ }
655
+ ],
656
+ requests: [{ method: "GET", path: "/api/v1/quotes" }],
657
+ channels: ["mcp", "zapier", "make", "n8n"]
658
+ },
659
+ {
660
+ id: "get_quote",
661
+ title: "Get one quote",
662
+ description: "Read a single quote: status, totals, and each revision with its line items and payment terms.",
663
+ kind: "read",
664
+ scopes: ["quotes:read"],
665
+ input: [
666
+ { name: "id", type: "string", required: true, description: "Quote id." },
667
+ {
668
+ name: "applicationId",
669
+ type: "string",
670
+ required: false,
671
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
672
+ }
673
+ ],
674
+ requests: [{ method: "GET", path: "/api/v1/quotes/:id" }],
675
+ channels: ["mcp", "zapier", "make", "n8n"],
676
+ guardrails: [OAUTH_WITHHELD]
677
+ },
678
+ // ----------------------------------------------------------------- invoices
679
+ {
680
+ id: "list_invoices",
681
+ title: "List invoices",
682
+ description: "List issued invoices with optional status filter.",
683
+ kind: "read",
684
+ scopes: ["invoices:read"],
685
+ input: [
686
+ { name: "status", type: "string", required: false, description: "Filter by invoice status." },
687
+ { name: "limit", type: "number", required: false, description: "Page size." },
688
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
689
+ {
690
+ name: "applicationId",
691
+ type: "string",
692
+ required: false,
693
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
694
+ }
695
+ ],
696
+ requests: [{ method: "GET", path: "/api/v1/invoices" }],
697
+ channels: ["mcp", "zapier", "make", "n8n"],
698
+ guardrails: [OAUTH_WITHHELD]
699
+ },
700
+ {
701
+ id: "receivables_aging",
702
+ title: "Receivables aging",
703
+ description: 'Outstanding receivables bucketed by age \u2014 answers "what is overdue and by how long".',
704
+ kind: "read",
705
+ scopes: ["invoices:read"],
706
+ input: [
707
+ {
708
+ name: "applicationId",
709
+ type: "string",
710
+ required: false,
711
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
712
+ }
713
+ ],
714
+ requests: [{ method: "GET", path: "/api/v1/invoices/aging" }],
715
+ channels: ["mcp", "zapier", "make", "n8n"]
716
+ },
717
+ // --------------------------------------------------------------- discounts
718
+ {
719
+ id: "search_discounts",
720
+ title: "Search discounts",
721
+ description: "List discount codes with optional filters \u2014 type, active state, and creation date range.",
722
+ kind: "read",
723
+ scopes: ["discounts:read"],
724
+ input: [
725
+ { name: "q", type: "string", required: false, description: "Free-text search." },
726
+ { name: "code", type: "string", required: false, description: "Exact code match." },
727
+ {
728
+ name: "type",
729
+ type: "string",
730
+ required: false,
731
+ description: "Filter by discount type.",
732
+ enum: ["percentage", "fixed_amount"]
733
+ },
734
+ { name: "isActive", type: "boolean", required: false, description: "Filter by active state." },
735
+ { name: "createdAfter", type: "string", required: false, description: "ISO 8601 date lower bound." },
736
+ { name: "createdBefore", type: "string", required: false, description: "ISO 8601 date upper bound." },
737
+ { name: "limit", type: "number", required: false, description: "Page size." },
738
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
739
+ {
740
+ name: "applicationId",
741
+ type: "string",
742
+ required: false,
743
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
744
+ }
745
+ ],
746
+ requests: [{ method: "GET", path: "/api/v1/discounts" }],
747
+ channels: ["mcp", "zapier", "make", "n8n"]
748
+ },
749
+ {
750
+ id: "get_discount",
751
+ title: "Get one discount",
752
+ description: "Read a single discount code: value, conditions, usage limits and redemption count.",
753
+ kind: "read",
754
+ scopes: ["discounts:read"],
755
+ input: [
756
+ { name: "id", type: "string", required: true, description: "Discount id." },
757
+ {
758
+ name: "applicationId",
759
+ type: "string",
760
+ required: false,
761
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
762
+ }
763
+ ],
764
+ requests: [{ method: "GET", path: "/api/v1/discounts/:id" }],
765
+ channels: ["mcp", "zapier", "make", "n8n"]
766
+ },
767
+ // ---------------------------------------------------------------- payments
768
+ {
769
+ id: "search_payments",
770
+ title: "Search payments",
771
+ description: "List payments with optional filters \u2014 method, status, order, amount range, or a Net-N overdue bucket. Today payments are only reachable nested inside get_order; this is the standalone entry point.",
772
+ kind: "read",
773
+ scopes: ["payments:read"],
774
+ input: [
775
+ { name: "method", type: "string", required: false, description: "Filter by payment method." },
776
+ { name: "status", type: "string", required: false, description: "Filter by payment status." },
777
+ { name: "orderId", type: "string", required: false, description: "Filter to one order." },
778
+ {
779
+ name: "aging",
780
+ type: "string",
781
+ required: false,
782
+ description: "Overdue bucket filter, Net-N payments only.",
783
+ enum: ["0_30", "31_60", "61_90", "90_plus"]
784
+ },
785
+ { name: "dueFrom", type: "string", required: false, description: "Due-date range start, YYYY-MM-DD." },
786
+ { name: "dueTo", type: "string", required: false, description: "Due-date range end, YYYY-MM-DD." },
787
+ { name: "amountMin", type: "number", required: false, description: "Minimum amount, minor units." },
788
+ { name: "amountMax", type: "number", required: false, description: "Maximum amount, minor units." },
789
+ { name: "limit", type: "number", required: false, description: "Page size." },
790
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
791
+ {
792
+ name: "applicationId",
793
+ type: "string",
794
+ required: false,
795
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
796
+ }
797
+ ],
798
+ requests: [{ method: "GET", path: "/api/v1/payments" }],
799
+ channels: ["mcp", "zapier", "make", "n8n"]
800
+ },
801
+ {
802
+ id: "get_payment",
803
+ title: "Get one payment with its transactions",
804
+ description: "Read a single payment plus its full transaction history (authorize/capture/refund/void attempts).",
805
+ kind: "read",
806
+ scopes: ["payments:read"],
807
+ input: [
808
+ { name: "id", type: "string", required: true, description: "Payment id." },
809
+ {
810
+ name: "applicationId",
811
+ type: "string",
812
+ required: false,
813
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
814
+ }
815
+ ],
816
+ requests: [
817
+ { method: "GET", path: "/api/v1/payments/:id" },
818
+ { method: "GET", path: "/api/v1/payments/:id/transactions" }
819
+ ],
820
+ channels: ["mcp"],
821
+ guardrails: [
822
+ "The transactions sub-read needs its own read of the same payment scope; report it as unavailable rather than failing the whole call if the underlying request errors."
823
+ ]
824
+ },
825
+ // ---------------------------------------------------------------- invoices
826
+ {
827
+ id: "get_invoice",
828
+ title: "Get one invoice",
829
+ description: "Read a single issued invoice: line items, totals, status and the billing snapshot at issue time.",
830
+ kind: "read",
831
+ scopes: ["invoices:read"],
832
+ input: [
833
+ { name: "id", type: "string", required: true, description: "Invoice id." },
834
+ {
835
+ name: "applicationId",
836
+ type: "string",
837
+ required: false,
838
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
839
+ }
840
+ ],
841
+ requests: [{ method: "GET", path: "/api/v1/invoices/:id" }],
842
+ channels: ["mcp", "zapier", "make", "n8n"],
843
+ guardrails: [OAUTH_WITHHELD]
844
+ },
845
+ // ------------------------------------------------------------------ carts
846
+ {
847
+ id: "list_abandoned_carts",
848
+ title: "List abandoned carts",
849
+ description: "Carts that were started but never finalized, with recovery status \u2014 the abandoned-cart recovery list.",
850
+ kind: "read",
851
+ scopes: ["carts:read"],
852
+ input: [
853
+ { name: "sinceDays", type: "number", required: false, description: "How many days back to look." },
854
+ { name: "limit", type: "number", required: false, description: "Page size." },
855
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
856
+ {
857
+ name: "applicationId",
858
+ type: "string",
859
+ required: false,
860
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
861
+ }
862
+ ],
863
+ requests: [
864
+ { method: "GET", path: "/api/v1/abandoned-carts" },
865
+ { method: "GET", path: "/api/v1/abandoned-carts/summary" }
866
+ ],
867
+ channels: ["mcp", "zapier", "make", "n8n"],
868
+ guardrails: ["Report the summary as unavailable rather than failing the whole call if that sub-read errors."]
869
+ },
870
+ // ---------------------------------------------------------------- debugging
871
+ {
872
+ id: "search_events",
873
+ title: "Search the event log",
874
+ description: "Search the immutable event log by type or entity. The first stop when asking what actually happened to an order, cart or subscription.",
875
+ kind: "read",
876
+ scopes: ["events:read"],
877
+ input: [
878
+ { name: "type", type: "string", required: false, description: "Event type, e.g. order.created." },
879
+ { name: "entityType", type: "string", required: false, description: "Entity type filter." },
880
+ { name: "entityId", type: "string", required: false, description: "Entity id filter." },
881
+ { name: "limit", type: "number", required: false, description: "Page size." },
882
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
883
+ {
884
+ name: "applicationId",
885
+ type: "string",
886
+ required: false,
887
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
888
+ }
889
+ ],
890
+ requests: [{ method: "GET", path: "/api/v1/events" }],
891
+ channels: ["mcp", "n8n"]
892
+ },
893
+ {
894
+ id: "list_webhook_deliveries",
895
+ title: "List webhook deliveries",
896
+ description: "Recent outbound webhook deliveries, filterable by endpoint, status and event type.",
897
+ kind: "read",
898
+ scopes: ["webhook_deliveries:read"],
899
+ input: [
900
+ { name: "endpointId", type: "string", required: false, description: "Filter by endpoint." },
901
+ {
902
+ name: "status",
903
+ type: "string",
904
+ required: false,
905
+ description: "Filter by delivery status: pending, delivered, failed or dead_letter."
906
+ },
907
+ {
908
+ name: "eventType",
909
+ type: "string",
910
+ required: false,
911
+ description: "Filter by exact event type, e.g. payment.failed."
912
+ },
913
+ { name: "limit", type: "number", required: false, description: "Page size." },
914
+ {
915
+ name: "applicationId",
916
+ type: "string",
917
+ required: false,
918
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
919
+ }
920
+ ],
921
+ requests: [{ method: "GET", path: "/api/v1/webhook-deliveries" }],
922
+ channels: ["mcp", "n8n"]
923
+ },
924
+ {
925
+ id: "get_webhook_delivery",
926
+ title: "Get one webhook delivery",
927
+ description: "Full delivery record: the signed payload we sent, the response we got back, and every attempt.",
928
+ kind: "read",
929
+ scopes: ["webhook_deliveries:read"],
930
+ input: [
931
+ { name: "id", type: "string", required: true, description: "Delivery id." },
932
+ {
933
+ name: "applicationId",
934
+ type: "string",
935
+ required: false,
936
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
937
+ }
938
+ ],
939
+ requests: [{ method: "GET", path: "/api/v1/webhook-deliveries/:id" }],
940
+ channels: ["mcp", "n8n"]
941
+ },
942
+ {
943
+ id: "explain_checkout_failure",
944
+ title: "Explain why a checkout failed",
945
+ description: "Diagnose a failing checkout in one call: session state, its cart, the events it emitted, and any webhook deliveries that followed. Use this before asking the user to reproduce anything.",
946
+ kind: "read",
947
+ scopes: ["checkout_sessions:read"],
948
+ sectionScopes: ["events:read", "webhook_deliveries:read"],
949
+ input: [
950
+ {
951
+ name: "id",
952
+ type: "string",
953
+ required: true,
954
+ description: "Checkout session id (or cart id \u2014 the tool resolves the session)."
955
+ },
956
+ {
957
+ name: "applicationId",
958
+ type: "string",
959
+ required: false,
960
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
961
+ }
962
+ ],
963
+ requests: [
964
+ { method: "GET", path: "/api/v1/checkout-sessions/:id" },
965
+ { method: "GET", path: "/api/v1/checkout-sessions/:id/cart" },
966
+ { method: "GET", path: "/api/v1/events" },
967
+ { method: "GET", path: "/api/v1/webhook-deliveries" }
968
+ ],
969
+ channels: ["mcp"],
970
+ guardrails: [
971
+ "Report which sections were unavailable rather than failing the call when a sub-read is unscoped.",
972
+ OAUTH_WITHHELD
973
+ ]
974
+ },
975
+ // ------------------------------------------------------------- extensions
976
+ {
977
+ id: "list_extension_installations",
978
+ title: "List installed extensions",
979
+ description: "Extensions installed on this application, with status and configuration.",
980
+ kind: "read",
981
+ scopes: ["extensions:read"],
982
+ input: [
983
+ {
984
+ name: "status",
985
+ type: "string",
986
+ required: false,
987
+ description: "Filter by installation status.",
988
+ enum: ["active", "suspended", "uninstalled"]
989
+ },
990
+ { name: "limit", type: "number", required: false, description: "Page size." },
991
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
992
+ {
993
+ name: "applicationId",
994
+ type: "string",
995
+ required: false,
996
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
997
+ }
998
+ ],
999
+ requests: [{ method: "GET", path: "/api/v1/installations" }],
1000
+ channels: ["mcp", "zapier", "make", "n8n"]
1001
+ },
1002
+ // ------------------------------------------------------------- connectors
1003
+ {
1004
+ id: "get_connector_routing",
1005
+ title: "Payment routing and decline rules",
1006
+ description: "Read the payment-routing cascade, active routing connectors, and card/other decline rules \u2014 what determines which processor handles a payment and what happens on a decline.",
1007
+ kind: "read",
1008
+ scopes: ["connectors:read"],
1009
+ input: [
1010
+ {
1011
+ name: "applicationId",
1012
+ type: "string",
1013
+ required: false,
1014
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1015
+ }
1016
+ ],
1017
+ requests: [
1018
+ { method: "GET", path: "/api/v1/payment-routing" },
1019
+ { method: "GET", path: "/api/v1/routing-connectors" },
1020
+ { method: "GET", path: "/api/v1/card-decline" },
1021
+ { method: "GET", path: "/api/v1/other-decline" }
1022
+ ],
1023
+ channels: ["mcp"],
1024
+ guardrails: [
1025
+ "Every sub-read proxies live to the payment provider's routing API and none are guaranteed to succeed together; report any that fail as unavailable rather than failing the whole call."
1026
+ ]
1027
+ },
1028
+ {
1029
+ id: "get_integration_status",
1030
+ title: "Integration readiness",
1031
+ description: 'Answers "am I ready to go live?" \u2014 active connectors, webhook endpoints and their recent delivery health, and the environment this credential is pinned to.',
1032
+ kind: "read",
1033
+ scopes: [],
1034
+ sectionScopes: ["connectors:read", "webhooks:read"],
1035
+ input: [
1036
+ {
1037
+ name: "applicationId",
1038
+ type: "string",
1039
+ required: false,
1040
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1041
+ }
1042
+ ],
1043
+ requests: [
1044
+ { method: "GET", path: "/api/v1/whoami" },
1045
+ { method: "GET", path: "/api/v1/connectors" },
1046
+ { method: "GET", path: "/api/v1/webhook-endpoints" },
1047
+ { method: "GET", path: "/api/v1/webhook-endpoints/coverage" }
1048
+ ],
1049
+ channels: ["mcp"],
1050
+ guardrails: [
1051
+ 'Each section needs its own scope (connectors:read, webhooks:read). Report a section as "not readable with this credential" instead of failing.'
1052
+ ]
1053
+ },
1054
+ // --------------------------------------------------------------- workspace
1055
+ {
1056
+ id: "get_workspace_settings",
1057
+ title: "Get workspace settings",
1058
+ description: "Read this workspace's identity: name, slug, branding and onboarding state. Payment terms are set per application and environment, not here.",
1059
+ kind: "read",
1060
+ scopes: ["merchant:read"],
1061
+ input: [],
1062
+ requests: [{ method: "GET", path: "/api/v1/merchants/me" }],
1063
+ channels: ["mcp", "zapier", "make", "n8n"]
1064
+ },
1065
+ {
1066
+ id: "get_audit_log",
1067
+ title: "Get the workspace audit log",
1068
+ description: "Recent admin actions taken on this workspace \u2014 invites, billing changes, key rotation, suspensions.",
1069
+ kind: "read",
1070
+ scopes: ["audit_logs:read"],
1071
+ input: [
1072
+ { name: "limit", type: "number", required: false, description: "Maximum entries (1-200).", default: 100 }
1073
+ ],
1074
+ requests: [{ method: "GET", path: "/api/v1/audit-log" }],
1075
+ channels: ["mcp", "zapier", "make", "n8n"]
1076
+ },
1077
+ {
1078
+ id: "list_applications",
1079
+ title: "List applications in this workspace",
1080
+ description: 'List every application in this workspace you have access to, so you can pass one of their ids as applicationId on other read and analytics tools. Note: the underlying route is the legacy-named /v1/stores (kept for contract stability) \u2014 the resource it returns is applications, not a separate "stores" concept.',
1081
+ kind: "read",
1082
+ scopes: ["applications:read"],
1083
+ input: [],
1084
+ requests: [{ method: "GET", path: "/api/v1/stores" }],
1085
+ channels: ["mcp", "zapier", "make", "n8n"]
1086
+ },
1087
+ {
1088
+ id: "get_sales_timeseries",
1089
+ title: "Sales over time",
1090
+ description: `Order count and gross revenue bucketed by day, week or month over any date range, in the merchant's timezone, optionally compared to the previous period or the same range last year. Use this for "how much did we sell on/last/between\u2026" instead of paging search_orders and adding it up.`,
1091
+ kind: "read",
1092
+ scopes: ["orders:read"],
1093
+ input: [
1094
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1095
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1096
+ { name: "groupBy", type: "string", required: false, description: "Bucket size.", enum: ["day", "week", "month"], default: "day" },
1097
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for bucket boundaries, e.g. America/New_York. Defaults to UTC \u2014 ask the user rather than assuming UTC when the answer depends on day boundaries." },
1098
+ { name: "compareTo", type: "string", required: false, description: "Add a comparison total.", enum: ["previous_period", "previous_year", "none"], default: "none" },
1099
+ {
1100
+ name: "applicationId",
1101
+ type: "string",
1102
+ required: false,
1103
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1104
+ }
1105
+ ],
1106
+ requests: [{ method: "GET", path: "/api/v1/analytics/orders-timeseries" }],
1107
+ channels: ["mcp", "zapier", "make", "n8n"],
1108
+ guardrails: [
1109
+ "Revenue is GROSS. Refunds are not deducted, because that needs payment_refunds:read, which OAuth connections cannot hold. Never describe the figure as net."
1110
+ ]
1111
+ },
1112
+ {
1113
+ id: "get_business_summary",
1114
+ title: "Business summary",
1115
+ description: "How are we doing? Orders/revenue, subscription MRR, receivables and quotes pipeline over a date range, compared to the immediately preceding window of equal length. Note: period=week|month|quarter means the TRAILING 7/30/90 days, not a calendar period \u2014 pass from/to for calendar months.",
1116
+ kind: "read",
1117
+ scopes: [],
1118
+ sectionScopes: ["orders:read", "subscriptions:read", "invoices:read", "quotes:read"],
1119
+ input: [
1120
+ {
1121
+ name: "period",
1122
+ type: "string",
1123
+ required: false,
1124
+ description: "Reporting window.",
1125
+ enum: ["week", "month", "quarter"],
1126
+ default: "month"
1127
+ },
1128
+ { name: "from", type: "string", required: false, description: "ISO 8601 start of range. Overrides period." },
1129
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range. Defaults to now." },
1130
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for period boundaries. Defaults to UTC." },
1131
+ {
1132
+ name: "applicationId",
1133
+ type: "string",
1134
+ required: false,
1135
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1136
+ }
1137
+ ],
1138
+ requests: [{ method: "GET", path: "/api/v1/analytics/business-summary" }],
1139
+ channels: ["mcp", "zapier", "make", "n8n"],
1140
+ guardrails: [
1141
+ "Each section needs its own scope (orders:read, subscriptions:read, invoices:read, quotes:read). A section without its scope is reported as omitted rather than failing the whole call."
1142
+ ]
1143
+ },
1144
+ {
1145
+ id: "get_top_products",
1146
+ title: "Top products",
1147
+ description: "Best-selling products over a date range, by revenue or units sold. Line items come from the merchant's own cart payload, so a product is identified by its external reference id when present and by name otherwise. The response carries both the top-N rows (by the chosen sort) and range-wide totals across every matching product, so you can compute what share of total revenue/units the top N represent.",
1148
+ kind: "read",
1149
+ scopes: ["orders:read"],
1150
+ input: [
1151
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1152
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1153
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for range boundaries. Defaults to UTC." },
1154
+ { name: "by", type: "string", required: false, description: "Sort key.", enum: ["revenue", "units"], default: "revenue" },
1155
+ { name: "limit", type: "number", required: false, description: "Max rows to return (1-100). Defaults to 20." },
1156
+ {
1157
+ name: "applicationId",
1158
+ type: "string",
1159
+ required: false,
1160
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1161
+ }
1162
+ ],
1163
+ requests: [{ method: "GET", path: "/api/v1/analytics/products" }],
1164
+ channels: ["mcp", "zapier", "make", "n8n"],
1165
+ guardrails: [
1166
+ "Revenue is GROSS. Refunds are not deducted, because that needs payment_refunds:read, which OAuth connections cannot hold. Never describe the figure as net."
1167
+ ]
1168
+ },
1169
+ {
1170
+ id: "get_attention_digest",
1171
+ title: "Attention digest",
1172
+ description: "What needs attention right now? Open disputes, subscriptions in dunning, overdue Net-N invoices, and webhooks with degraded delivery health \u2014 a point-in-time snapshot, not a time range.",
1173
+ kind: "read",
1174
+ scopes: [],
1175
+ sectionScopes: ["payment_disputes:read", "subscriptions:read", "invoices:read", "webhook_deliveries:read"],
1176
+ input: [
1177
+ {
1178
+ name: "applicationId",
1179
+ type: "string",
1180
+ required: false,
1181
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1182
+ }
1183
+ ],
1184
+ requests: [{ method: "GET", path: "/api/v1/analytics/attention-digest" }],
1185
+ channels: ["mcp", "zapier", "make", "n8n"],
1186
+ guardrails: [
1187
+ "Each section needs its own scope (payment_disputes:read, subscriptions:read, invoices:read, webhook_deliveries:read). A section without its scope is reported as omitted rather than failing the whole call."
1188
+ ]
1189
+ },
1190
+ {
1191
+ id: "get_customer_growth",
1192
+ title: "Customer growth",
1193
+ description: "Customer growth over a date range: signups per day/week/month, how many orders came from first-time versus returning customers, and the highest-value customers in the window.",
1194
+ kind: "read",
1195
+ scopes: ["customers:read"],
1196
+ input: [
1197
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1198
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1199
+ { name: "groupBy", type: "string", required: false, description: "Signup series bucket size.", enum: ["day", "week", "month"], default: "day" },
1200
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for bucket boundaries. Defaults to UTC." },
1201
+ { name: "topBy", type: "string", required: false, description: "Sort key for top customers.", enum: ["revenue", "orders"], default: "revenue" },
1202
+ { name: "topLimit", type: "number", required: false, description: "Max top-customers rows to return (1-50). Defaults to 10." },
1203
+ {
1204
+ name: "applicationId",
1205
+ type: "string",
1206
+ required: false,
1207
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1208
+ }
1209
+ ],
1210
+ requests: [{ method: "GET", path: "/api/v1/analytics/customers" }],
1211
+ channels: ["mcp", "zapier", "make", "n8n"],
1212
+ guardrails: [
1213
+ "Revenue is GROSS. Refunds are not deducted, because that needs payment_refunds:read, which OAuth connections cannot hold. Never describe the figure as net.",
1214
+ "topCustomers is a top-N list; use the accompanying totals for range-wide customer/order/revenue counts instead of summing the returned rows.",
1215
+ "topCustomers[].email and .name come back null unless the credential holds customer_pii:read, which OAuth connections cannot hold. Identify a top customer by id, not by a null name."
1216
+ ]
1217
+ },
1218
+ {
1219
+ id: "get_revenue_breakdown",
1220
+ title: "Revenue breakdown",
1221
+ description: "Revenue split by one dimension over a date range \u2014 payment method, order source, or currency. Amounts are gross; refunds are not deducted.",
1222
+ kind: "read",
1223
+ scopes: ["orders:read"],
1224
+ input: [
1225
+ {
1226
+ name: "dimension",
1227
+ type: "string",
1228
+ required: true,
1229
+ description: "Breakdown dimension.",
1230
+ enum: ["paymentMethod", "source", "currency"]
1231
+ },
1232
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1233
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1234
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for range boundaries. Defaults to UTC." },
1235
+ {
1236
+ name: "applicationId",
1237
+ type: "string",
1238
+ required: false,
1239
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1240
+ }
1241
+ ],
1242
+ requests: [{ method: "GET", path: "/api/v1/analytics/revenue-breakdown" }],
1243
+ channels: ["mcp", "zapier", "make", "n8n"],
1244
+ guardrails: [
1245
+ "Revenue is GROSS. Refunds are not deducted, because that needs payment_refunds:read, which OAuth connections cannot hold. Never describe the figure as net.",
1246
+ "dimension is required; it is never defaulted, so omitting it fails the call instead of silently picking a dimension.",
1247
+ "For dimension=paymentMethod, an order paid via more than one method can appear in more than one row, so rows can sum to more than totals (a separate whole-range aggregate over orders alone)."
1248
+ ]
1249
+ },
1250
+ {
1251
+ id: "get_funnel_conversion",
1252
+ title: "Checkout funnel",
1253
+ description: "Checkout funnel over a date range: carts created, checkout sessions started, orders completed, with the drop-off between each stage.",
1254
+ kind: "read",
1255
+ scopes: ["carts:read"],
1256
+ input: [
1257
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1258
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1259
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for range boundaries. Defaults to UTC." },
1260
+ {
1261
+ name: "applicationId",
1262
+ type: "string",
1263
+ required: false,
1264
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1265
+ }
1266
+ ],
1267
+ requests: [{ method: "GET", path: "/api/v1/analytics/funnel" }],
1268
+ channels: ["mcp", "zapier", "make", "n8n"],
1269
+ guardrails: [
1270
+ "Each stage needs its own scope (carts:read, checkout_sessions:read, orders:read), checked independently \u2014 an OAuth grant carries literal scopes with no legacy expansion, so a credential can hold carts:read without the other two. An omitted stage is reported as such rather than failing the whole call.",
1271
+ "conversionRate and dropOff[].ratePct are null (unknown) when a stage they depend on is omitted, and 0 only when the stage was readable with a true zero count. Never treat a null here as 0.",
1272
+ 'dropOff[].ratePct is the percentage of "from" that did NOT reach "to" (loss), not a conversion rate. The step-conversion rate is 100 - ratePct.'
1273
+ ]
1274
+ },
1275
+ {
1276
+ id: "get_subscription_movement",
1277
+ title: "Subscription movement",
1278
+ description: "Subscription movement over a date range: new subscriptions, cancellations and net MRR change per day/week/month. Figures are reconstructed from subscription timestamps, not a stored historical snapshot.",
1279
+ kind: "read",
1280
+ scopes: ["subscriptions:read"],
1281
+ input: [
1282
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1283
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1284
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for bucket boundaries. Defaults to UTC." },
1285
+ { name: "groupBy", type: "string", required: false, description: "Bucket size.", enum: ["day", "week", "month"], default: "day" },
1286
+ {
1287
+ name: "applicationId",
1288
+ type: "string",
1289
+ required: false,
1290
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1291
+ }
1292
+ ],
1293
+ requests: [{ method: "GET", path: "/api/v1/analytics/subscriptions" }],
1294
+ channels: ["mcp", "zapier", "make", "n8n"],
1295
+ guardrails: [
1296
+ "newSubscriptions, churnedSubscriptions and mrrDeltaCents are reconstructed per bucket from subscription created_at/cancelled_at timestamps, not a stored historical MRR snapshot \u2014 never present this series as an audited historical MRR ledger.",
1297
+ "mrrDeltaCents for a bucket only credits subscriptions created in that bucket that are still active right now, and only debits subscriptions cancelled in that bucket that already existed when it started. A subscription created AND cancelled inside the same bucket still counts toward both newSubscriptions and churnedSubscriptions but contributes 0 to mrrDeltaCents.",
1298
+ "totals is the sum over the full series (the series is never truncated), so totals.newSubscriptions/.churnedSubscriptions/.netMrrDeltaCents always equal the corresponding sums of series[]."
1299
+ ]
1300
+ },
1301
+ {
1302
+ id: "get_discount_performance",
1303
+ title: "Discount performance",
1304
+ description: "Discount code performance over a date range: redemption count, revenue on discounted orders, and total discount given per code.",
1305
+ kind: "read",
1306
+ scopes: ["discounts:read"],
1307
+ input: [
1308
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1309
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1310
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for range boundaries. Defaults to UTC." },
1311
+ { name: "limit", type: "number", required: false, description: "Max rows to return. Defaults to 20." },
1312
+ {
1313
+ name: "applicationId",
1314
+ type: "string",
1315
+ required: false,
1316
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1317
+ }
1318
+ ],
1319
+ requests: [{ method: "GET", path: "/api/v1/analytics/discounts" }],
1320
+ channels: ["mcp", "zapier", "make", "n8n"],
1321
+ guardrails: [
1322
+ 'discountedRevenueCents and discountGivenCents are DIFFERENT measures: discountedRevenueCents is the gross order total on orders that redeemed the code; discountGivenCents is the discount amount taken off those same orders. Never describe discountedRevenueCents as "given away" or discountGivenCents as revenue.',
1323
+ "Only orders with status <> draft and a gross-eligible payment_status are counted; refunds are not deducted from discountedRevenueCents, because that needs payment_refunds:read.",
1324
+ "rows are the top N codes by discount given; totals is a SEPARATE whole-range aggregate (not a reduction over rows), so totals.redemptions / totals.discountGivenCents stay valid even when rows is truncated by limit."
1325
+ ]
1326
+ },
1327
+ // ------------------------------------------------------------ sandbox write
1328
+ {
1329
+ id: "seed_test_data",
1330
+ title: "Seed test data (non-production only)",
1331
+ description: "Create a realistic customer, cart, line items and a finalized draft order so an integration has something to read. Refuses to run against production.",
1332
+ kind: "write",
1333
+ scopes: ["customers:write", "carts:write", "cart_items:write"],
1334
+ input: [
1335
+ { name: "customerEmail", type: "string", required: false, description: "Email for the seeded customer." },
1336
+ { name: "lineItems", type: "array", required: false, description: "Line items; a sensible default set is used when omitted." }
1337
+ ],
1338
+ requests: [
1339
+ { method: "POST", path: "/api/v1/customers" },
1340
+ { method: "POST", path: "/api/v1/carts" },
1341
+ { method: "POST", path: "/api/v1/carts/:id/items" },
1342
+ { method: "POST", path: "/api/v1/carts/:id/finalize" }
1343
+ ],
1344
+ channels: ["mcp"],
1345
+ sandboxOnly: true,
1346
+ idempotent: true,
1347
+ guardrails: [
1348
+ "Hard-refuse when the credential resolves to a production environment, regardless of any allow-writes flag.",
1349
+ "Seeds a finalized DRAFT order \u2014 it does not capture a payment, which needs a browser-side tokenised payment method."
1350
+ ]
1351
+ },
1352
+ // ---------------------------------------------------------- inbound writes
1353
+ // The no-code channels are mostly used in the "something happened elsewhere,
1354
+ // put it into Throttle" direction — a form submission, a new CRM contact, a
1355
+ // spreadsheet row. These three are that direction.
1356
+ {
1357
+ id: "create_customer",
1358
+ title: "Create a customer",
1359
+ description: "Create a customer record. Use search_customers first if the buyer may already exist \u2014 this does not de-duplicate by email.",
1360
+ kind: "write",
1361
+ scopes: ["customers:write"],
1362
+ input: [
1363
+ { name: "email", type: "string", required: true, description: "The buyer's email address." },
1364
+ { name: "firstName", type: "string", required: false, description: "Given name." },
1365
+ { name: "lastName", type: "string", required: false, description: "Family name." },
1366
+ { name: "phone", type: "string", required: false, description: "Contact phone number." },
1367
+ { name: "company", type: "string", required: false, description: "The buyer's company." },
1368
+ {
1369
+ name: "externalId",
1370
+ type: "string",
1371
+ required: false,
1372
+ description: "This customer's id in your own system. Resolvable later via get_customer."
1373
+ },
1374
+ {
1375
+ name: "netN",
1376
+ type: "number",
1377
+ required: false,
1378
+ description: "Invoice terms in days for this customer specifically, 0-365. Omit to use the workspace default; 0 means due on issue."
1379
+ },
1380
+ {
1381
+ name: "acceptsMarketing",
1382
+ type: "boolean",
1383
+ required: false,
1384
+ description: "Whether the buyer opted into marketing email.",
1385
+ default: false
1386
+ }
1387
+ ],
1388
+ requests: [{ method: "POST", path: "/api/v1/customers" }],
1389
+ channels: ["mcp", "zapier", "make", "n8n"],
1390
+ idempotent: true,
1391
+ guardrails: [
1392
+ "The created record is echoed back with email, phone, firstName, lastName and name nulled unless the credential holds customer_pii:read, which OAuth connections cannot hold \u2014 the customer is still created with the values sent, only the response is redacted."
1393
+ ]
1394
+ },
1395
+ {
1396
+ id: "update_customer",
1397
+ title: "Update a customer",
1398
+ description: "Change fields on an existing customer. Only the fields supplied are touched; everything else is left alone.",
1399
+ kind: "write",
1400
+ scopes: ["customers:write"],
1401
+ input: [
1402
+ { name: "id", type: "string", required: true, description: "Customer id." },
1403
+ { name: "email", type: "string", required: false, description: "The buyer's email address." },
1404
+ { name: "firstName", type: "string", required: false, description: "Given name." },
1405
+ { name: "lastName", type: "string", required: false, description: "Family name." },
1406
+ { name: "phone", type: "string", required: false, description: "Contact phone number." },
1407
+ { name: "company", type: "string", required: false, description: "The buyer's company." },
1408
+ {
1409
+ name: "externalId",
1410
+ type: "string",
1411
+ required: false,
1412
+ description: "This customer's id in your own system. Pass null to clear it."
1413
+ },
1414
+ {
1415
+ name: "netN",
1416
+ type: "number",
1417
+ required: false,
1418
+ description: "Invoice terms in days for this customer specifically, 0-365."
1419
+ },
1420
+ {
1421
+ name: "acceptsMarketing",
1422
+ type: "boolean",
1423
+ required: false,
1424
+ description: "Whether the buyer opted into marketing email."
1425
+ }
1426
+ ],
1427
+ requests: [{ method: "PATCH", path: "/api/v1/customers/:id" }],
1428
+ channels: ["mcp", "zapier", "make", "n8n"],
1429
+ idempotent: true,
1430
+ guardrails: [
1431
+ "The updated record is echoed back with email, phone, firstName, lastName and name nulled unless the credential holds customer_pii:read, which OAuth connections cannot hold \u2014 the update still applied, only the response is redacted."
1432
+ ]
1433
+ },
1434
+ {
1435
+ id: "create_checkout_link",
1436
+ title: "Create a checkout link",
1437
+ description: "Build a cart from line items and return a hosted checkout URL the buyer can pay at. Prices are in minor units (2500 = $25.00).",
1438
+ kind: "write",
1439
+ scopes: ["carts:write", "cart_items:write", "checkout_sessions:write"],
1440
+ input: [
1441
+ {
1442
+ name: "lineItems",
1443
+ type: "array",
1444
+ required: true,
1445
+ description: "What the buyer is paying for: { name, unitPrice, quantity }. unitPrice is an integer in minor units."
1446
+ },
1447
+ {
1448
+ name: "returnUrl",
1449
+ type: "string",
1450
+ required: true,
1451
+ description: "Absolute URL the buyer lands on after paying."
1452
+ },
1453
+ {
1454
+ name: "cancelUrl",
1455
+ type: "string",
1456
+ required: true,
1457
+ description: "Absolute URL the buyer lands on if they abandon checkout."
1458
+ },
1459
+ {
1460
+ name: "customerId",
1461
+ type: "string",
1462
+ required: false,
1463
+ description: "Attach the cart to an existing customer."
1464
+ },
1465
+ {
1466
+ name: "customerEmail",
1467
+ type: "string",
1468
+ required: false,
1469
+ description: "Pre-fill the buyer email without needing a customer record."
1470
+ },
1471
+ {
1472
+ name: "discountCode",
1473
+ type: "string",
1474
+ required: false,
1475
+ description: "Discount code to apply to the session."
1476
+ },
1477
+ {
1478
+ name: "netN",
1479
+ type: "number",
1480
+ required: false,
1481
+ description: "Invoice terms in days for this cart, 0-365. Omit to pay in full at checkout."
1482
+ }
1483
+ ],
1484
+ // Three calls: the API has no single "checkout link" endpoint, and a
1485
+ // checkout session requires a cart that already holds its line items.
1486
+ requests: [
1487
+ { method: "POST", path: "/api/v1/carts" },
1488
+ { method: "POST", path: "/api/v1/carts/:id/items" },
1489
+ { method: "POST", path: "/api/v1/checkout/sessions" }
1490
+ ],
1491
+ channels: ["mcp", "zapier", "make", "n8n"],
1492
+ idempotent: true,
1493
+ guardrails: [
1494
+ "The returned URL is buyer-facing and takes real payment in a production environment."
1495
+ ]
1496
+ },
1497
+ // -------------------------------------------------------------- money/mail
1498
+ {
1499
+ id: "create_quote",
1500
+ title: "Create a quote",
1501
+ description: "Draft a quote for a customer with line items and terms. Does not send it \u2014 use send_quote.",
1502
+ kind: "write",
1503
+ scopes: ["quotes:write"],
1504
+ input: [
1505
+ { name: "customerId", type: "string", required: true, description: "Customer the quote is for." },
1506
+ { name: "items", type: "array", required: true, description: "Quote line items: { name, unitPrice, quantity }." },
1507
+ { name: "expiresAt", type: "string", required: false, description: "ISO 8601 expiry." },
1508
+ { name: "netN", type: "number", required: false, description: "Payment terms in days. Omit for pay-in-full." },
1509
+ { name: "notes", type: "string", required: false, description: "Buyer-visible notes." }
1510
+ ],
1511
+ requests: [{ method: "POST", path: "/api/v1/quotes" }],
1512
+ channels: ["mcp", "zapier", "make", "n8n"],
1513
+ idempotent: true
1514
+ },
1515
+ {
1516
+ id: "send_quote",
1517
+ title: "Issue and send a quote",
1518
+ description: "Issue a draft quote and email the buyer their quote link. This is buyer-visible \u2014 confirm with the user before calling it.",
1519
+ kind: "write",
1520
+ scopes: ["quotes:write"],
1521
+ input: [{ name: "id", type: "string", required: true, description: "Quote id." }],
1522
+ requests: [{ method: "POST", path: "/api/v1/quotes/:id/issue" }],
1523
+ channels: ["mcp", "zapier", "make", "n8n"],
1524
+ idempotent: true,
1525
+ guardrails: ['Sends mail to a real buyer. Never call it speculatively or to "check" a quote.'],
1526
+ destructive: true
1527
+ },
1528
+ {
1529
+ id: "refund_payment",
1530
+ title: "Refund a payment",
1531
+ description: "Refund a captured payment. The amount is required \u2014 never infer a full refund from an unspecified amount.",
1532
+ kind: "write",
1533
+ scopes: ["payment_refunds:write"],
1534
+ input: [
1535
+ { name: "id", type: "string", required: true, description: "Payment id." },
1536
+ {
1537
+ name: "amount",
1538
+ type: "number",
1539
+ required: true,
1540
+ description: "Amount to refund in minor units. Required \u2014 omitting it would refund the whole payment."
1541
+ },
1542
+ { name: "reason", type: "string", required: false, description: "Reason recorded on the refund." }
1543
+ ],
1544
+ requests: [{ method: "POST", path: "/api/v1/payments/:id/refund" }],
1545
+ // NOT zapier/make. `payment_refunds:write` is `sensitive` in the scope
1546
+ // registry, and `validateScopesForOAuth` rejects every sensitive scope — so
1547
+ // an OAuth grant, which is the only credential a public Zapier or Make app
1548
+ // can hold, can never carry it. Listing the action on those channels would
1549
+ // ship a connector action that 403s on every call. MCP and n8n keep it:
1550
+ // both authenticate with an `sk_` key, which CAN hold the scope.
1551
+ // `oauth-grantable-scopes` in the catalog tests enforces this.
1552
+ channels: ["mcp", "n8n"],
1553
+ idempotent: true,
1554
+ guardrails: [
1555
+ "Moves money and is irreversible. Confirm with the user first.",
1556
+ "The API treats an omitted amount as a FULL refund, so this action requires it explicitly."
1557
+ ],
1558
+ destructive: true
1559
+ },
1560
+ {
1561
+ id: "manage_subscription",
1562
+ title: "Pause or resume a subscription",
1563
+ description: "Pause or resume an active subscription.",
1564
+ kind: "write",
1565
+ scopes: ["subscription_pauses:write"],
1566
+ input: [
1567
+ { name: "id", type: "string", required: true, description: "Subscription id." },
1568
+ {
1569
+ name: "action",
1570
+ type: "string",
1571
+ required: true,
1572
+ description: "What to do.",
1573
+ enum: ["pause", "resume"]
1574
+ }
1575
+ ],
1576
+ requests: [
1577
+ { method: "POST", path: "/api/v1/subscriptions/:id/pause" },
1578
+ { method: "POST", path: "/api/v1/subscriptions/:id/resume" }
1579
+ ],
1580
+ channels: ["mcp", "zapier", "make", "n8n"],
1581
+ idempotent: true
1582
+ },
1583
+ {
1584
+ id: "cancel_subscription",
1585
+ title: "Cancel a subscription",
1586
+ description: "Cancel a subscription. Separate from pause/resume because it needs its own scope.",
1587
+ kind: "write",
1588
+ scopes: ["subscription_cancellations:write"],
1589
+ input: [
1590
+ { name: "id", type: "string", required: true, description: "Subscription id." },
1591
+ {
1592
+ name: "atPeriodEnd",
1593
+ type: "boolean",
1594
+ required: false,
1595
+ description: "Cancel at the end of the current period instead of immediately."
1596
+ }
1597
+ ],
1598
+ requests: [{ method: "POST", path: "/api/v1/subscriptions/:id/cancel" }],
1599
+ channels: ["mcp", "zapier", "make", "n8n"],
1600
+ idempotent: true,
1601
+ guardrails: ["Ends billing for a paying customer. Confirm with the user first."],
1602
+ destructive: true
1603
+ },
1604
+ {
1605
+ id: "replay_webhook_delivery",
1606
+ title: "Replay a webhook delivery",
1607
+ description: "Re-send a past webhook delivery to its endpoint \u2014 useful after fixing a broken handler.",
1608
+ kind: "write",
1609
+ scopes: ["webhook_deliveries:write"],
1610
+ input: [{ name: "id", type: "string", required: true, description: "Delivery id." }],
1611
+ requests: [{ method: "POST", path: "/api/v1/webhook-deliveries/:id/replay" }],
1612
+ channels: ["mcp", "n8n"],
1613
+ idempotent: true,
1614
+ guardrails: ["The receiving system will process the event again. Confirm it is idempotent first."],
1615
+ destructive: true
1616
+ },
1617
+ {
1618
+ id: "send_test_webhook",
1619
+ title: "Send a test webhook",
1620
+ description: "Send a synthetic signed event to a webhook endpoint to verify signature handling and reachability.",
1621
+ kind: "write",
1622
+ scopes: ["webhooks:write"],
1623
+ input: [
1624
+ { name: "id", type: "string", required: true, description: "Endpoint id." },
1625
+ { name: "eventType", type: "string", required: false, description: "Event type to simulate." }
1626
+ ],
1627
+ requests: [{ method: "POST", path: "/api/v1/webhook-endpoints/:id/test" }],
1628
+ channels: ["mcp", "n8n"],
1629
+ idempotent: true,
1630
+ destructive: true
1631
+ }
1632
+ ];
1633
+
1634
+ // ../action-catalog/src/triggers.ts
1635
+ var NO_CODE = ["zapier", "make", "n8n"];
1636
+ function trigger(id, title, eventType, description) {
1637
+ return { id, title, eventType, description, scope: eventReadScope(eventType), channels: NO_CODE };
1638
+ }
1639
+ var TRIGGERS = [
1640
+ trigger("new_order", "New order", "order.created", "Fires when an order is created."),
1641
+ trigger(
1642
+ "payment_captured",
1643
+ "Payment captured",
1644
+ "payment.captured",
1645
+ "Fires when a payment is successfully captured."
1646
+ ),
1647
+ trigger(
1648
+ "payment_failed",
1649
+ "Payment failed",
1650
+ "payment.failed",
1651
+ "Fires when a payment attempt fails."
1652
+ ),
1653
+ trigger(
1654
+ "new_subscription",
1655
+ "New subscription",
1656
+ "subscription.created",
1657
+ "Fires when a subscription is created."
1658
+ ),
1659
+ trigger(
1660
+ "subscription_cancelled",
1661
+ "Subscription cancelled",
1662
+ "subscription.cancelled",
1663
+ "Fires when a subscription is cancelled."
1664
+ ),
1665
+ trigger(
1666
+ "quote_accepted",
1667
+ "Quote accepted",
1668
+ "quote.accepted",
1669
+ "Fires when a buyer accepts a quote."
1670
+ ),
1671
+ trigger(
1672
+ "cart_abandoned",
1673
+ "Cart abandoned",
1674
+ "cart.abandoned",
1675
+ "Fires when a cart is marked abandoned."
1676
+ ),
1677
+ trigger(
1678
+ "invoice_past_due",
1679
+ "Invoice past due",
1680
+ "invoice.past_due",
1681
+ "Fires when an invoice passes its due date unpaid."
1682
+ )
1683
+ ];
1684
+
1685
+ // ../action-catalog/src/index.ts
1686
+ function actionsForChannel(channel) {
1687
+ return ACTIONS.filter((a) => a.channels.includes(channel));
1688
+ }
1689
+ function canRunAction(granted, action) {
1690
+ return action.scopes.every((required) => hasScope(granted, required));
1691
+ }
1692
+
1693
+ // src/format.ts
1694
+ var MAX_CHARS = 24e3;
1695
+ function prune(value) {
1696
+ if (Array.isArray(value)) return value.map(prune);
1697
+ if (value && typeof value === "object") {
1698
+ const out = {};
1699
+ for (const [key, inner] of Object.entries(value)) {
1700
+ if (inner === void 0 || inner === null) continue;
1701
+ out[key] = prune(inner);
1702
+ }
1703
+ return out;
1704
+ }
1705
+ return value;
1706
+ }
1707
+ function formatResult(value) {
1708
+ const text = JSON.stringify(prune(value), null, 2) ?? "null";
1709
+ if (text.length <= MAX_CHARS) return text;
1710
+ return `${text.slice(0, MAX_CHARS)}
1711
+
1712
+ \u2026 truncated at ${MAX_CHARS} characters. Narrow the query (lower "limit", add a filter) or fetch a single record.`;
1713
+ }
1714
+ function formatError(error) {
1715
+ const e = error;
1716
+ if (e?.code && e?.message) {
1717
+ const detail = e.details ? `
1718
+
1719
+ details: ${JSON.stringify(e.details)}` : "";
1720
+ return `Throttle API error ${e.code}${e.statusCode ? ` (HTTP ${e.statusCode})` : ""}: ${e.message}${detail}`;
1721
+ }
1722
+ return `Error: ${e?.message ?? String(error)}`;
1723
+ }
1724
+
1725
+ // ../errors/dist/index.js
1726
+ var ThrottleError = class extends Error {
1727
+ code;
1728
+ statusCode;
1729
+ details;
1730
+ body;
1731
+ constructor(args) {
1732
+ super(args.message);
1733
+ this.name = "ThrottleError";
1734
+ this.code = args.code;
1735
+ this.statusCode = args.statusCode;
1736
+ this.details = args.details;
1737
+ this.body = args.body;
1738
+ Object.setPrototypeOf(this, new.target.prototype);
1739
+ }
1740
+ };
1741
+
1742
+ // src/client.ts
1743
+ var ThrottleApiError = class extends ThrottleError {
1744
+ constructor(code, message, statusCode, details) {
1745
+ super({ code, message, statusCode, details });
1746
+ this.name = "ThrottleApiError";
1747
+ }
1748
+ };
1749
+ function buildQuery(query) {
1750
+ if (!query) return "";
1751
+ const params = new URLSearchParams();
1752
+ for (const [key, value] of Object.entries(query)) {
1753
+ if (value === void 0 || value === null || value === "") continue;
1754
+ params.set(key, String(value));
1755
+ }
1756
+ const qs = params.toString();
1757
+ return qs ? `?${qs}` : "";
1758
+ }
1759
+ var API_KEY_PREFIX_RE = /^(sk_|pk_)/;
1760
+ var ThrottleClient = class {
1761
+ constructor(auth, fetchImpl = fetch) {
1762
+ this.auth = auth;
1763
+ this.fetchImpl = fetchImpl;
1764
+ }
1765
+ /**
1766
+ * Swap the credential every later request is sent with. The hosted remote
1767
+ * server builds one client per MCP session at `initialize`, but an OAuth
1768
+ * access token lives one hour and the MCP client refreshes it underneath
1769
+ * the session — each request then arrives with a newer bearer than the one
1770
+ * the session was opened with. Without this, the session keeps calling the
1771
+ * REST API with the token it was born with and every tool fails with a 401
1772
+ * the moment that first token expires, even though the caller is still
1773
+ * perfectly authenticated.
1774
+ */
1775
+ setApiKey(apiKey) {
1776
+ this.auth = { ...this.auth, apiKey };
1777
+ }
1778
+ async requestJson(method, path, options = {}) {
1779
+ const url = `${this.auth.apiBaseUrl.replace(/\/$/, "")}${path}${buildQuery(options.query)}`;
1780
+ const headers = {
1781
+ "content-type": "application/json",
1782
+ "x-api-key": this.auth.apiKey,
1783
+ // Also sent as Authorization: Bearer — required for an OAuth access
1784
+ // token (the remote server's oauth_token sessions), which the auth
1785
+ // middleware only ever recognizes via this header (peeked by issuer),
1786
+ // never via x-api-key. For an sk_/pk_ key this is a no-op: the
1787
+ // middleware already treats `Authorization: Bearer sk_.../pk_...`
1788
+ // identically to `X-API-Key`, so existing stdio-CLI callers are
1789
+ // unaffected.
1790
+ Authorization: `Bearer ${this.auth.apiKey}`
1791
+ };
1792
+ if (options.idempotencyKey) headers["idempotency-key"] = options.idempotencyKey;
1793
+ if (options.applicationId && !API_KEY_PREFIX_RE.test(this.auth.apiKey)) {
1794
+ headers["x-throttle-application-id"] = options.applicationId;
1795
+ }
1796
+ const sendsBody = method === "POST" || method === "PATCH" || method === "PUT";
1797
+ const body = options.body !== void 0 ? JSON.stringify(options.body) : sendsBody ? "{}" : void 0;
1798
+ const res = await this.fetchImpl(url, { method, headers, body });
1799
+ const text = await res.text();
1800
+ let parsed = {};
1801
+ try {
1802
+ parsed = text ? JSON.parse(text) : {};
1803
+ } catch {
1804
+ }
1805
+ if (!res.ok) {
1806
+ const err = parsed.error;
1807
+ throw new ThrottleApiError(
1808
+ err?.code ?? `http_${res.status}`,
1809
+ err?.message ?? `Request failed (${res.status}): ${text.slice(0, 200)}`,
1810
+ res.status,
1811
+ err?.details
1812
+ );
1813
+ }
1814
+ return parsed;
1815
+ }
1816
+ /** Unwraps the `{ data }` envelope. Key presence, not `??` — `{data: null}` unwraps to null. */
1817
+ async get(path, query, options) {
1818
+ const body = await this.requestJson("GET", path, { query, applicationId: options?.applicationId });
1819
+ return body && typeof body === "object" && "data" in body ? body.data : body;
1820
+ }
1821
+ async list(path, query, options) {
1822
+ const body = await this.requestJson("GET", path, { query, applicationId: options?.applicationId });
1823
+ const data = "data" in body ? body.data : body;
1824
+ const meta = body.meta;
1825
+ return { data: Array.isArray(data) ? data : [], pagination: meta?.pagination ?? null };
1826
+ }
1827
+ async post(path, options = {}) {
1828
+ const body = await this.requestJson("POST", path, options);
1829
+ return body && typeof body === "object" && "data" in body ? body.data : body;
1830
+ }
1831
+ async patch(path, options = {}) {
1832
+ const body = await this.requestJson("PATCH", path, options);
1833
+ return body && typeof body === "object" && "data" in body ? body.data : body;
1834
+ }
1835
+ };
1836
+ async function optional(load) {
1837
+ try {
1838
+ return { ok: true, value: await load() };
1839
+ } catch (error) {
1840
+ const e = error;
1841
+ const code = e?.code ?? "error";
1842
+ const reason = code === "insufficient_scopes" ? "not readable with this credential (missing scope)" : code === "not_found" ? "not found" : `unavailable (${code})`;
1843
+ return { ok: false, unavailable: reason };
1844
+ }
1845
+ }
1846
+
1847
+ // src/contract.ts
1848
+ var RETIRED_FIELDS = {
1849
+ storeId: "applicationId",
1850
+ store_id: "applicationId",
1851
+ merchantId: "workspaceId",
1852
+ merchant_id: "workspaceId",
1853
+ customer_id: "customerId",
1854
+ line_items: "lineItems",
1855
+ unit_price: "unitPrice",
1856
+ external_id: "externalId",
1857
+ payment_method_id: "paymentMethodId",
1858
+ first_name: "firstName",
1859
+ last_name: "lastName",
1860
+ net_n: "netN",
1861
+ hosted_url: "hostedUrl",
1862
+ embed_url: "embedUrl",
1863
+ checkout_url: "checkoutUrl",
1864
+ order_number: "orderNumber",
1865
+ payment_status: "paymentStatus",
1866
+ allowed_origins: "allowedOrigins",
1867
+ enabled_events: "enabledEvents",
1868
+ template_key: "templateKey",
1869
+ request_id: "requestId",
1870
+ signing_secret: "signingSecret (returned once at creation; not accepted as input)"
1871
+ };
1872
+ function snakeToCamel(name) {
1873
+ return name.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
1874
+ }
1875
+ function normalizePath(path) {
1876
+ return path.replace(/\{[^}]+\}/g, "{}").replace(/:[A-Za-z0-9_]+/g, "{}").replace(/\/$/, "");
1877
+ }
1878
+ function segmentsOf(path) {
1879
+ return path.split("?")[0].replace(/\/$/, "").split("/").filter(Boolean);
1880
+ }
1881
+ function isParamSegment(segment) {
1882
+ return /^\{[^}]+\}$/.test(segment) || /^:[A-Za-z0-9_]+$/.test(segment);
1883
+ }
1884
+ function pathMatches(template, candidate) {
1885
+ const t = segmentsOf(template);
1886
+ const c = segmentsOf(candidate);
1887
+ if (t.length !== c.length) return false;
1888
+ return t.every((segment, i) => isParamSegment(segment) || isParamSegment(c[i]) || segment === c[i]);
1889
+ }
1890
+ function paramCount(path) {
1891
+ return segmentsOf(path).filter(isParamSegment).length;
1892
+ }
1893
+ function findOperations(index, query) {
1894
+ const method = query.method?.toUpperCase();
1895
+ const wanted = query.path.trim();
1896
+ const byMethod = (op) => !method || op.method === method;
1897
+ const matched = index.operations.filter((op) => byMethod(op) && pathMatches(op.path, wanted)).sort((a, b) => paramCount(a.path) - paramCount(b.path));
1898
+ if (matched.length > 0) return matched;
1899
+ const needle = normalizePath(wanted).replace(/^\/+/, "");
1900
+ return index.operations.filter((op) => byMethod(op) && normalizePath(op.path).includes(needle));
1901
+ }
1902
+ function validatePayload(index, input) {
1903
+ const errors = [];
1904
+ const warnings = [];
1905
+ const keys = Object.keys(input.body ?? {});
1906
+ for (const key of keys) {
1907
+ const retired = RETIRED_FIELDS[key];
1908
+ if (retired) {
1909
+ errors.push({
1910
+ field: key,
1911
+ problem: "retired public field name \u2014 the API rejects it",
1912
+ suggestion: `use "${retired}"`
1913
+ });
1914
+ continue;
1915
+ }
1916
+ if (key.includes("_")) {
1917
+ errors.push({
1918
+ field: key,
1919
+ problem: "snake_case is not accepted on public request bodies",
1920
+ suggestion: `use "${snakeToCamel(key)}"`
1921
+ });
1922
+ }
1923
+ }
1924
+ const matches = findOperations(index, { path: input.path, method: input.method });
1925
+ const operation = matches[0];
1926
+ if (!operation) {
1927
+ return {
1928
+ ok: false,
1929
+ matched: null,
1930
+ errors: [
1931
+ ...errors,
1932
+ {
1933
+ field: input.path,
1934
+ problem: `no ${input.method.toUpperCase()} operation matches this path in the published contract`,
1935
+ suggestion: "call describe_endpoint to find the right path"
1936
+ }
1937
+ ],
1938
+ warnings
1939
+ };
1940
+ }
1941
+ if (!operation.bodyFields) {
1942
+ return {
1943
+ ok: errors.length === 0,
1944
+ matched: `${operation.method} ${operation.path}`,
1945
+ errors,
1946
+ warnings,
1947
+ note: "The contract publishes no request-body schema for this operation, so field-level checks beyond naming could not run."
1948
+ };
1949
+ }
1950
+ const known = new Map(operation.bodyFields.map((f) => [f.name, f]));
1951
+ for (const key of keys) {
1952
+ if (RETIRED_FIELDS[key] || key.includes("_")) continue;
1953
+ if (!known.has(key)) {
1954
+ warnings.push({
1955
+ field: key,
1956
+ problem: "not documented on this operation \u2014 it will be rejected if the route validates strictly"
1957
+ });
1958
+ }
1959
+ }
1960
+ for (const field of operation.bodyFields) {
1961
+ const value = (input.body ?? {})[field.name];
1962
+ if (value === void 0) {
1963
+ if (field.required) errors.push({ field: field.name, problem: "required field is missing" });
1964
+ continue;
1965
+ }
1966
+ if (field.enum && field.enum.length > 0 && typeof value === "string" && !field.enum.includes(value)) {
1967
+ errors.push({
1968
+ field: field.name,
1969
+ problem: `value "${value}" is not allowed`,
1970
+ suggestion: `one of: ${field.enum.join(", ")}`
1971
+ });
1972
+ }
1973
+ }
1974
+ return {
1975
+ ok: errors.length === 0,
1976
+ matched: `${operation.method} ${operation.path}`,
1977
+ errors,
1978
+ warnings
1979
+ };
1980
+ }
1981
+
1982
+ // src/docs.ts
1983
+ var DOCS_ORIGIN = "https://usethrottle.dev/docs";
1984
+ function excerptAround(text, needle, radius = 160) {
1985
+ const at = text.toLowerCase().indexOf(needle.toLowerCase());
1986
+ if (at < 0) return text.slice(0, radius * 2).trim();
1987
+ const start = Math.max(0, at - radius);
1988
+ const end = Math.min(text.length, at + needle.length + radius);
1989
+ return `${start > 0 ? "\u2026" : ""}${text.slice(start, end).trim()}${end < text.length ? "\u2026" : ""}`;
1990
+ }
1991
+ function searchDocs(index, query, limit = 5) {
1992
+ const phrase = query.trim().toLowerCase();
1993
+ if (!phrase) return [];
1994
+ const terms = phrase.split(/\s+/).filter((t) => t.length > 1);
1995
+ const scored = index.entries.map((entry) => {
1996
+ const title = entry.title.toLowerCase();
1997
+ const headings = entry.headings.join(" \0 ").toLowerCase();
1998
+ const body = entry.text.toLowerCase();
1999
+ let score = 0;
2000
+ if (title === phrase) score += 100;
2001
+ else if (title.startsWith(phrase)) score += 60;
2002
+ else if (title.includes(phrase)) score += 40;
2003
+ if (headings.includes(phrase)) score += 25;
2004
+ if (body.includes(phrase)) score += 15;
2005
+ for (const term of terms) {
2006
+ if (title.includes(term)) score += 8;
2007
+ if (headings.includes(term)) score += 4;
2008
+ if (body.includes(term)) score += 1;
2009
+ }
2010
+ return { entry, score };
2011
+ });
2012
+ return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score || a.entry.route.localeCompare(b.entry.route)).slice(0, Math.max(1, limit)).map(({ entry, score }) => ({
2013
+ title: entry.title,
2014
+ url: `${DOCS_ORIGIN}${entry.route}`,
2015
+ score,
2016
+ excerpt: excerptAround(entry.text, phrase)
2017
+ }));
2018
+ }
2019
+
2020
+ // src/handlers.ts
2021
+ function coveringAction(operation) {
2022
+ const target = normalizePath(operation.path);
2023
+ for (const action of ACTIONS) {
2024
+ const hit = action.requests.some(
2025
+ (r) => r.method === operation.method && normalizePath(r.path) === target
2026
+ );
2027
+ if (hit) return { tool: action.id, requiredScopes: action.scopes };
2028
+ }
2029
+ return {};
2030
+ }
2031
+ var page = (args) => ({ limit: args.limit, cursor: args.cursor });
2032
+ var HANDLERS = {
2033
+ // ---------------------------------------------------------------- identity
2034
+ whoami: async (ctx) => ({
2035
+ ...ctx.identity,
2036
+ note: ctx.identity.environment.providerEnvironment === "production" ? "This credential is pinned to PRODUCTION. Reads return live data." : "This credential is pinned to a non-production environment. Nothing here is live data."
2037
+ }),
2038
+ // ------------------------------------------------------- docs and contract
2039
+ search_docs: async (ctx, args) => {
2040
+ const hits = searchDocs(ctx.docs, String(args.query ?? ""), Number(args.limit ?? 5));
2041
+ return hits.length > 0 ? { hits } : { hits: [], note: "No documentation matched. Try fewer or more general terms." };
2042
+ },
2043
+ describe_endpoint: async (ctx, args) => {
2044
+ const operations = findOperations(ctx.contract, {
2045
+ path: String(args.path ?? ""),
2046
+ method: args.method ? String(args.method) : void 0
2047
+ });
2048
+ if (operations.length === 0) {
2049
+ return {
2050
+ operations: [],
2051
+ note: 'No operation matched. Paths look like /api/v1/orders; try a shorter fragment such as "orders".'
2052
+ };
2053
+ }
2054
+ return {
2055
+ operations: operations.slice(0, 8).map((op) => ({ ...op, ...coveringAction(op) })),
2056
+ truncated: operations.length > 8 ? operations.length - 8 : void 0
2057
+ };
2058
+ },
2059
+ validate_payload: async (ctx, args) => validatePayload(ctx.contract, {
2060
+ method: String(args.method ?? "POST"),
2061
+ path: String(args.path ?? ""),
2062
+ body: args.body ?? {}
2063
+ }),
2064
+ // ------------------------------------------------------------- generic read
2065
+ api_get: async (ctx, args) => {
2066
+ const path = String(args.path ?? "");
2067
+ const resolved = new URL(path, "https://throttle.invalid").pathname;
2068
+ if (!path.startsWith("/api/v1/") || !resolved.startsWith("/api/v1/") || /(^|[\/\\])\.(\.)?([\/\\?#]|$)|%2e/i.test(path)) {
2069
+ throw new Error(
2070
+ `Refusing to call "${path}". api_get only reads paths beginning with /api/v1/ on the configured Throttle host.`
2071
+ );
2072
+ }
2073
+ return ctx.client.get(path, void 0, { applicationId: args.applicationId });
2074
+ },
2075
+ // ------------------------------------------------------------------ orders
2076
+ search_orders: async (ctx, args) => ctx.client.list(
2077
+ "/api/v1/orders",
2078
+ {
2079
+ q: args.q,
2080
+ status: args.status,
2081
+ paymentStatus: args.paymentStatus,
2082
+ createdAfter: args.createdAfter,
2083
+ createdBefore: args.createdBefore,
2084
+ ...page(args)
2085
+ },
2086
+ { applicationId: args.applicationId }
2087
+ ),
2088
+ get_order: async (ctx, args) => {
2089
+ const id = String(args.id);
2090
+ const opts = { applicationId: args.applicationId };
2091
+ const order = await ctx.client.get(`/api/v1/orders/${id}`, void 0, opts);
2092
+ const [payments, returns, fulfillments] = await Promise.all([
2093
+ optional(() => ctx.client.list(`/api/v1/orders/${id}/payments`, void 0, opts)),
2094
+ optional(() => ctx.client.list(`/api/v1/orders/${id}/returns`, void 0, opts)),
2095
+ optional(() => ctx.client.list(`/api/v1/orders/${id}/fulfillments`, void 0, opts))
2096
+ ]);
2097
+ return {
2098
+ order,
2099
+ payments: payments.ok ? payments.value.data : payments.unavailable,
2100
+ returns: returns.ok ? returns.value.data : returns.unavailable,
2101
+ fulfillments: fulfillments.ok ? fulfillments.value.data : fulfillments.unavailable
2102
+ };
2103
+ },
2104
+ // --------------------------------------------------------------- customers
2105
+ search_customers: async (ctx, args) => ctx.client.list(
2106
+ "/api/v1/customers",
2107
+ {
2108
+ q: args.q,
2109
+ createdAfter: args.createdAfter,
2110
+ createdBefore: args.createdBefore,
2111
+ ...page(args)
2112
+ },
2113
+ { applicationId: args.applicationId }
2114
+ ),
2115
+ get_customer: async (ctx, args) => {
2116
+ const id = String(args.id);
2117
+ const opts = { applicationId: args.applicationId };
2118
+ const customer = await ctx.client.get(`/api/v1/customers/${id}`, void 0, opts);
2119
+ const [addresses, subscriptions] = await Promise.all([
2120
+ optional(() => ctx.client.list(`/api/v1/customers/${id}/addresses`, void 0, opts)),
2121
+ optional(() => ctx.client.list("/api/v1/subscriptions", { customerId: id }, opts))
2122
+ ]);
2123
+ return {
2124
+ customer,
2125
+ addresses: addresses.ok ? addresses.value.data : addresses.unavailable,
2126
+ subscriptions: subscriptions.ok ? subscriptions.value.data : subscriptions.unavailable
2127
+ };
2128
+ },
2129
+ // ----------------------------------------------------------- subscriptions
2130
+ list_subscriptions: async (ctx, args) => ctx.client.list(
2131
+ "/api/v1/subscriptions",
2132
+ {
2133
+ customerId: args.customerId,
2134
+ status: args.status,
2135
+ createdAfter: args.createdAfter,
2136
+ createdBefore: args.createdBefore,
2137
+ ...page(args)
2138
+ },
2139
+ { applicationId: args.applicationId }
2140
+ ),
2141
+ get_subscription: async (ctx, args) => ctx.client.get(`/api/v1/subscriptions/${String(args.id)}`, void 0, {
2142
+ applicationId: args.applicationId
2143
+ }),
2144
+ // ------------------------------------------------------------------ quotes
2145
+ search_quotes: async (ctx, args) => ctx.client.list(
2146
+ "/api/v1/quotes",
2147
+ {
2148
+ status: args.status,
2149
+ createdAfter: args.createdAfter,
2150
+ createdBefore: args.createdBefore,
2151
+ ...page(args)
2152
+ },
2153
+ { applicationId: args.applicationId }
2154
+ ),
2155
+ get_quote: async (ctx, args) => ctx.client.get(`/api/v1/quotes/${String(args.id)}`, void 0, {
2156
+ applicationId: args.applicationId
2157
+ }),
2158
+ // ---------------------------------------------------------------- invoices
2159
+ list_invoices: async (ctx, args) => ctx.client.list(
2160
+ "/api/v1/invoices",
2161
+ { status: args.status, ...page(args) },
2162
+ { applicationId: args.applicationId }
2163
+ ),
2164
+ receivables_aging: async (ctx, args) => ctx.client.get("/api/v1/invoices/aging", void 0, { applicationId: args.applicationId }),
2165
+ // --------------------------------------------------------------- discounts
2166
+ search_discounts: async (ctx, args) => ctx.client.list(
2167
+ "/api/v1/discounts",
2168
+ {
2169
+ q: args.q,
2170
+ code: args.code,
2171
+ type: args.type,
2172
+ isActive: args.isActive,
2173
+ createdAfter: args.createdAfter,
2174
+ createdBefore: args.createdBefore,
2175
+ ...page(args)
2176
+ },
2177
+ { applicationId: args.applicationId }
2178
+ ),
2179
+ get_discount: async (ctx, args) => ctx.client.get(`/api/v1/discounts/${String(args.id)}`, void 0, {
2180
+ applicationId: args.applicationId
2181
+ }),
2182
+ // ---------------------------------------------------------------- payments
2183
+ search_payments: async (ctx, args) => ctx.client.list(
2184
+ "/api/v1/payments",
2185
+ {
2186
+ method: args.method,
2187
+ status: args.status,
2188
+ // The route only accepts these as snake_case query params -- confirmed
2189
+ // by reading its querystring schema directly, not assumed from the
2190
+ // platform's usual camelCase convention.
2191
+ order_id: args.orderId,
2192
+ aging: args.aging,
2193
+ due_from: args.dueFrom,
2194
+ due_to: args.dueTo,
2195
+ amount_min: args.amountMin,
2196
+ amount_max: args.amountMax,
2197
+ ...page(args)
2198
+ },
2199
+ { applicationId: args.applicationId }
2200
+ ),
2201
+ get_payment: async (ctx, args) => {
2202
+ const id = String(args.id);
2203
+ const opts = { applicationId: args.applicationId };
2204
+ const payment = await ctx.client.get(`/api/v1/payments/${id}`, void 0, opts);
2205
+ const transactions = await optional(
2206
+ () => ctx.client.list(`/api/v1/payments/${id}/transactions`, void 0, opts)
2207
+ );
2208
+ return {
2209
+ payment,
2210
+ transactions: transactions.ok ? transactions.value.data : transactions.unavailable
2211
+ };
2212
+ },
2213
+ // ---------------------------------------------------------------- invoices
2214
+ get_invoice: async (ctx, args) => ctx.client.get(`/api/v1/invoices/${String(args.id)}`, void 0, {
2215
+ applicationId: args.applicationId
2216
+ }),
2217
+ // ------------------------------------------------------------------ carts
2218
+ list_abandoned_carts: async (ctx, args) => {
2219
+ const opts = { applicationId: args.applicationId };
2220
+ const [carts, summary] = await Promise.all([
2221
+ optional(() => ctx.client.list("/api/v1/abandoned-carts", { ...page(args) }, opts)),
2222
+ optional(
2223
+ () => ctx.client.get("/api/v1/abandoned-carts/summary", { sinceDays: args.sinceDays }, opts)
2224
+ )
2225
+ ]);
2226
+ return {
2227
+ carts: carts.ok ? carts.value.data : carts.unavailable,
2228
+ summary: summary.ok ? summary.value : summary.unavailable
2229
+ };
2230
+ },
2231
+ // --------------------------------------------------------------- debugging
2232
+ search_events: async (ctx, args) => ctx.client.list(
2233
+ "/api/v1/events",
2234
+ {
2235
+ type: args.type,
2236
+ entityType: args.entityType,
2237
+ entityId: args.entityId,
2238
+ ...page(args)
2239
+ },
2240
+ { applicationId: args.applicationId }
2241
+ ),
2242
+ list_webhook_deliveries: async (ctx, args) => ctx.client.list(
2243
+ "/api/v1/webhook-deliveries",
2244
+ {
2245
+ endpointId: args.endpointId,
2246
+ status: args.status,
2247
+ eventType: args.eventType,
2248
+ limit: args.limit
2249
+ },
2250
+ { applicationId: args.applicationId }
2251
+ ),
2252
+ get_webhook_delivery: async (ctx, args) => ctx.client.get(`/api/v1/webhook-deliveries/${String(args.id)}`, void 0, {
2253
+ applicationId: args.applicationId
2254
+ }),
2255
+ send_test_webhook: async (ctx, args) => ctx.client.post(`/api/v1/webhook-endpoints/${String(args.id)}/test`, {
2256
+ body: { eventType: args.eventType },
2257
+ idempotencyKey: `mcp-send-test-webhook-${String(args.id)}-${Date.now()}`
2258
+ }),
2259
+ replay_webhook_delivery: async (ctx, args) => ctx.client.post(`/api/v1/webhook-deliveries/${String(args.id)}/replay`, {
2260
+ idempotencyKey: `mcp-replay-webhook-delivery-${String(args.id)}-${Date.now()}`
2261
+ }),
2262
+ explain_checkout_failure: async (ctx, args) => {
2263
+ const id = String(args.id);
2264
+ const opts = { applicationId: args.applicationId };
2265
+ const session = await optional(
2266
+ () => ctx.client.get(`/api/v1/checkout-sessions/${id}`, void 0, opts)
2267
+ );
2268
+ if (!session.ok) {
2269
+ return {
2270
+ note: `No checkout session ${id} (${session.unavailable}). If this is a cart id, use search_events for its activity (or read /api/v1/carts/${id} with api_get, where that tool is available).`
2271
+ };
2272
+ }
2273
+ const cartId = session.value?.cartId ?? void 0;
2274
+ const [cart, sessionEvents, cartEvents, deliveries] = await Promise.all([
2275
+ optional(() => ctx.client.get(`/api/v1/checkout-sessions/${id}/cart`, void 0, opts)),
2276
+ optional(() => ctx.client.list("/api/v1/events", { entityId: id, limit: 25 }, opts)),
2277
+ cartId ? optional(() => ctx.client.list("/api/v1/events", { entityId: cartId, limit: 25 }, opts)) : Promise.resolve({ ok: true, value: { data: [], pagination: null } }),
2278
+ optional(() => ctx.client.list("/api/v1/webhook-deliveries", { limit: 15 }, opts))
2279
+ ]);
2280
+ return {
2281
+ session: session.value,
2282
+ cart: cart.ok ? cart.value : cart.unavailable,
2283
+ events: [
2284
+ ...sessionEvents.ok ? sessionEvents.value.data : [],
2285
+ ...cartEvents.ok ? cartEvents.value.data : []
2286
+ ],
2287
+ eventsUnavailable: sessionEvents.ok ? void 0 : sessionEvents.unavailable,
2288
+ recentWebhookDeliveries: deliveries.ok ? deliveries.value.data : deliveries.unavailable,
2289
+ hint: "Compare the session state against the events: a session that never emitted checkout_started usually means the browser SDK failed before submitting, not a payment decline."
2290
+ };
2291
+ },
2292
+ // ------------------------------------------------------------ sandbox seed
2293
+ seed_test_data: async (ctx, args) => {
2294
+ if (ctx.identity.environment.providerEnvironment === "production") {
2295
+ throw new Error(
2296
+ "seed_test_data refuses to run against a production environment. Use a non-production API key."
2297
+ );
2298
+ }
2299
+ const stamp = Date.now();
2300
+ const customer = await ctx.client.post("/api/v1/customers", {
2301
+ body: {
2302
+ email: String(args.customerEmail ?? `mcp-seed-${stamp}@example.test`),
2303
+ firstName: "Seed",
2304
+ lastName: "Customer"
2305
+ },
2306
+ idempotencyKey: `mcp-seed-customer-${stamp}`
2307
+ });
2308
+ const cart = await ctx.client.post("/api/v1/carts", {
2309
+ // Unlike /api/v1/customers, cart creation requires applicationId
2310
+ // explicitly in the body (it's cross-checked against the caller's own
2311
+ // authenticated application, not inferred) -- omitting it is a real
2312
+ // 400 missing_application_id from the live API, not a hypothetical.
2313
+ body: { applicationId: ctx.identity.application?.id, customerId: customer?.id },
2314
+ idempotencyKey: `mcp-seed-cart-${stamp}`
2315
+ });
2316
+ const requested = Array.isArray(args.lineItems) && args.lineItems.length > 0 ? args.lineItems : [{ name: "Seed widget", unitPrice: 2500, quantity: 2 }];
2317
+ const items = [];
2318
+ for (const [index, item] of requested.entries()) {
2319
+ items.push(
2320
+ await ctx.client.post(`/api/v1/carts/${cart?.id}/items`, {
2321
+ body: item,
2322
+ idempotencyKey: `mcp-seed-item-${stamp}-${index}`
2323
+ })
2324
+ );
2325
+ }
2326
+ const order = await ctx.client.post(`/api/v1/carts/${cart?.id}/finalize`, {
2327
+ idempotencyKey: `mcp-seed-finalize-${stamp}`
2328
+ });
2329
+ return {
2330
+ environment: ctx.identity.environment.slug,
2331
+ customer,
2332
+ cart,
2333
+ lineItems: items,
2334
+ order,
2335
+ note: "Seeded a finalized DRAFT order. No payment was captured \u2014 that needs a browser-tokenised payment method, which a server-side key cannot produce."
2336
+ };
2337
+ },
2338
+ // ---------------------------------------------------------- inbound writes
2339
+ create_customer: async (ctx, args) => ctx.client.post("/api/v1/customers", {
2340
+ // `createCustomerBody` is `.strict()`, so only fields it declares may be
2341
+ // sent; `undefined` values drop out at JSON.stringify.
2342
+ body: {
2343
+ email: args.email,
2344
+ firstName: args.firstName,
2345
+ lastName: args.lastName,
2346
+ phone: args.phone,
2347
+ company: args.company,
2348
+ externalId: args.externalId,
2349
+ netN: args.netN,
2350
+ acceptsMarketing: args.acceptsMarketing
2351
+ },
2352
+ idempotencyKey: `mcp-create-customer-${String(args.email)}-${Date.now()}`
2353
+ }),
2354
+ update_customer: async (ctx, args) => ctx.client.patch(`/api/v1/customers/${String(args.id)}`, {
2355
+ // `id` is the path parameter, not a body field — `updateCustomerBody` is
2356
+ // `.strict()` and would reject it. A null `externalId` is meaningful
2357
+ // (it clears the value), so only `undefined` is dropped.
2358
+ body: {
2359
+ email: args.email,
2360
+ firstName: args.firstName,
2361
+ lastName: args.lastName,
2362
+ phone: args.phone,
2363
+ company: args.company,
2364
+ externalId: args.externalId,
2365
+ netN: args.netN,
2366
+ acceptsMarketing: args.acceptsMarketing
2367
+ },
2368
+ idempotencyKey: `mcp-update-customer-${String(args.id)}-${Date.now()}`
2369
+ }),
2370
+ create_checkout_link: async (ctx, args) => {
2371
+ const lineItems = Array.isArray(args.lineItems) ? args.lineItems : [];
2372
+ if (lineItems.length === 0) {
2373
+ throw new Error("create_checkout_link: lineItems must contain at least one item.");
2374
+ }
2375
+ const stamp = Date.now();
2376
+ const applicationId = ctx.identity.application?.id;
2377
+ const cart = await ctx.client.post("/api/v1/carts", {
2378
+ body: {
2379
+ applicationId,
2380
+ customerId: args.customerId,
2381
+ customerEmail: args.customerEmail,
2382
+ netN: args.netN
2383
+ },
2384
+ idempotencyKey: `mcp-checkout-link-cart-${stamp}`
2385
+ });
2386
+ for (const [index, item] of lineItems.entries()) {
2387
+ await ctx.client.post(`/api/v1/carts/${cart?.id}/items`, {
2388
+ body: item,
2389
+ idempotencyKey: `mcp-checkout-link-item-${stamp}-${index}`
2390
+ });
2391
+ }
2392
+ const session = await ctx.client.post("/api/v1/checkout/sessions", {
2393
+ body: {
2394
+ applicationId,
2395
+ cartId: cart?.id,
2396
+ returnUrl: args.returnUrl,
2397
+ cancelUrl: args.cancelUrl,
2398
+ customerEmail: args.customerEmail,
2399
+ discountCode: args.discountCode
2400
+ },
2401
+ idempotencyKey: `mcp-checkout-link-session-${stamp}`
2402
+ });
2403
+ return { cartId: cart?.id, ...session };
2404
+ },
2405
+ // -------------------------------------------------------------- money/mail
2406
+ manage_subscription: async (ctx, args) => {
2407
+ const id = String(args.id);
2408
+ const action = String(args.action);
2409
+ if (action !== "pause" && action !== "resume") {
2410
+ throw new Error(`manage_subscription: action must be "pause" or "resume", got "${action}".`);
2411
+ }
2412
+ return ctx.client.post(`/api/v1/subscriptions/${id}/${action}`, {
2413
+ idempotencyKey: `mcp-manage-subscription-${id}-${action}-${Date.now()}`
2414
+ });
2415
+ },
2416
+ create_quote: async (ctx, args) => (
2417
+ // The real route's body shape differs from this tool's flatter,
2418
+ // model-facing input: line items are "items" (not "lineItems"), Net-N
2419
+ // terms are nested under paymentTerms.netN with an explicit mode (not a
2420
+ // flat "netN"), and the buyer-visible note field is "buyerNote" (not
2421
+ // "notes"). Confirmed against packages/platform-api/src/validation/quotes.ts's
2422
+ // actual zod schema after a live call 400'd with unrecognized_keys.
2423
+ ctx.client.post("/api/v1/quotes", {
2424
+ body: {
2425
+ customerId: args.customerId,
2426
+ items: args.items,
2427
+ expiresAt: args.expiresAt,
2428
+ paymentTerms: args.netN !== void 0 ? { mode: "net_terms", netN: args.netN } : void 0,
2429
+ buyerNote: args.notes
2430
+ },
2431
+ idempotencyKey: `mcp-create-quote-${String(args.customerId)}-${Date.now()}`
2432
+ })
2433
+ ),
2434
+ get_sales_timeseries: async (ctx, args) => ctx.client.get(
2435
+ "/api/v1/analytics/orders-timeseries",
2436
+ {
2437
+ from: args.from,
2438
+ to: args.to,
2439
+ groupBy: args.groupBy,
2440
+ timezone: args.timezone,
2441
+ compareTo: args.compareTo
2442
+ },
2443
+ { applicationId: args.applicationId }
2444
+ ),
2445
+ get_business_summary: async (ctx, args) => ctx.client.get(
2446
+ "/api/v1/analytics/business-summary",
2447
+ {
2448
+ period: args.period,
2449
+ from: args.from,
2450
+ to: args.to,
2451
+ timezone: args.timezone
2452
+ },
2453
+ { applicationId: args.applicationId }
2454
+ ),
2455
+ get_attention_digest: async (ctx, args) => ctx.client.get("/api/v1/analytics/attention-digest", void 0, {
2456
+ applicationId: args.applicationId
2457
+ }),
2458
+ get_top_products: async (ctx, args) => ctx.client.get(
2459
+ "/api/v1/analytics/products",
2460
+ {
2461
+ from: args.from,
2462
+ to: args.to,
2463
+ timezone: args.timezone,
2464
+ by: args.by,
2465
+ limit: args.limit
2466
+ },
2467
+ { applicationId: args.applicationId }
2468
+ ),
2469
+ // `.get()`, not `.list()` — the response is a single object (`{ data: {
2470
+ // range, signups, ordersByCustomerType, topCustomers, totals } }`), and
2471
+ // `.list()` only unwraps arrays: it would silently return an empty list
2472
+ // instead of the real payload.
2473
+ get_customer_growth: async (ctx, args) => ctx.client.get(
2474
+ "/api/v1/analytics/customers",
2475
+ {
2476
+ from: args.from,
2477
+ to: args.to,
2478
+ groupBy: args.groupBy,
2479
+ timezone: args.timezone,
2480
+ topBy: args.topBy,
2481
+ topLimit: args.topLimit
2482
+ },
2483
+ { applicationId: args.applicationId }
2484
+ ),
2485
+ // `.get()`, not `.list()` — the response is a single object (`{ data: {
2486
+ // range, dimension, rows, totals, note } }`), not a paginated array.
2487
+ get_revenue_breakdown: async (ctx, args) => ctx.client.get(
2488
+ "/api/v1/analytics/revenue-breakdown",
2489
+ {
2490
+ dimension: args.dimension,
2491
+ from: args.from,
2492
+ to: args.to,
2493
+ timezone: args.timezone
2494
+ },
2495
+ { applicationId: args.applicationId }
2496
+ ),
2497
+ // `.get()`, not `.list()` — the response is a single object (`{ data: {
2498
+ // range, stages, conversionRate, dropOff, note } }`), not a paginated array.
2499
+ get_funnel_conversion: async (ctx, args) => ctx.client.get(
2500
+ "/api/v1/analytics/funnel",
2501
+ {
2502
+ from: args.from,
2503
+ to: args.to,
2504
+ timezone: args.timezone
2505
+ },
2506
+ { applicationId: args.applicationId }
2507
+ ),
2508
+ // `.get()`, not `.list()` — the response is a single object (`{ data: {
2509
+ // range, series, totals, note } }`), not a paginated array.
2510
+ get_subscription_movement: async (ctx, args) => ctx.client.get(
2511
+ "/api/v1/analytics/subscriptions",
2512
+ {
2513
+ from: args.from,
2514
+ to: args.to,
2515
+ timezone: args.timezone,
2516
+ groupBy: args.groupBy
2517
+ },
2518
+ { applicationId: args.applicationId }
2519
+ ),
2520
+ // `.get()`, not `.list()` — the response is a single object (`{ data: {
2521
+ // range, rows, totals, note } }`), not a paginated array.
2522
+ get_discount_performance: async (ctx, args) => ctx.client.get(
2523
+ "/api/v1/analytics/discounts",
2524
+ {
2525
+ from: args.from,
2526
+ to: args.to,
2527
+ timezone: args.timezone,
2528
+ limit: args.limit
2529
+ },
2530
+ { applicationId: args.applicationId }
2531
+ ),
2532
+ // ------------------------------------------------------------- extensions
2533
+ list_extension_installations: async (ctx, args) => ctx.client.list(
2534
+ "/api/v1/installations",
2535
+ { status: args.status, ...page(args) },
2536
+ { applicationId: args.applicationId }
2537
+ ),
2538
+ // ------------------------------------------------------------- connectors
2539
+ get_connector_routing: async (ctx, args) => {
2540
+ const opts = { applicationId: args.applicationId };
2541
+ const [paymentRouting, routingConnectors, cardDecline, otherDecline] = await Promise.all([
2542
+ optional(() => ctx.client.get("/api/v1/payment-routing", void 0, opts)),
2543
+ optional(() => ctx.client.get("/api/v1/routing-connectors", void 0, opts)),
2544
+ optional(() => ctx.client.get("/api/v1/card-decline", void 0, opts)),
2545
+ optional(() => ctx.client.get("/api/v1/other-decline", void 0, opts))
2546
+ ]);
2547
+ return {
2548
+ paymentRouting: paymentRouting.ok ? paymentRouting.value : paymentRouting.unavailable,
2549
+ routingConnectors: routingConnectors.ok ? routingConnectors.value : routingConnectors.unavailable,
2550
+ cardDecline: cardDecline.ok ? cardDecline.value : cardDecline.unavailable,
2551
+ otherDecline: otherDecline.ok ? otherDecline.value : otherDecline.unavailable
2552
+ };
2553
+ },
2554
+ get_integration_status: async (ctx, args) => {
2555
+ const opts = { applicationId: args.applicationId };
2556
+ const [connectors, endpoints, coverage] = await Promise.all([
2557
+ optional(() => ctx.client.list("/api/v1/connectors", void 0, opts)),
2558
+ optional(() => ctx.client.list("/api/v1/webhook-endpoints", void 0, opts)),
2559
+ optional(() => ctx.client.get("/api/v1/webhook-endpoints/coverage", void 0, opts))
2560
+ ]);
2561
+ const overridden = Boolean(args.applicationId) && args.applicationId !== ctx.identity.application?.id;
2562
+ return {
2563
+ environment: ctx.identity.environment,
2564
+ workspace: ctx.identity.workspace,
2565
+ applicationId: args.applicationId ?? ctx.identity.application?.id ?? null,
2566
+ application: overridden ? void 0 : ctx.identity.application,
2567
+ connectors: connectors.ok ? connectors.value.data : connectors.unavailable,
2568
+ webhookEndpoints: endpoints.ok ? endpoints.value.data : endpoints.unavailable,
2569
+ unsubscribedEvents: coverage.ok ? coverage.value : coverage.unavailable
2570
+ };
2571
+ },
2572
+ // --------------------------------------------------------------- workspace
2573
+ // No applicationId forwarding here: /api/v1/merchants/me is workspace-only
2574
+ // (`WHERE workspace_id = ...`, never reads auth.applicationId) — an
2575
+ // applicationId field on this tool would be a silent no-op affordance.
2576
+ get_workspace_settings: async (ctx) => ctx.client.get("/api/v1/merchants/me"),
2577
+ // No applicationId forwarding here either: /api/v1/audit-log filters by
2578
+ // workspaceId (and optionally environmentId), never applicationId.
2579
+ get_audit_log: async (ctx, args) => ctx.client.list("/api/v1/audit-log", { limit: args.limit }),
2580
+ // --------------------------------------------------------- applications
2581
+ // `.get()`, not `.list()` — the legacy /v1/stores route returns a bare
2582
+ // array with no `meta.pagination`, and the brief calls for `.get()` here
2583
+ // specifically.
2584
+ list_applications: async (ctx) => ctx.client.get("/api/v1/stores")
2585
+ };
2586
+
2587
+ // src/input.ts
2588
+ var import_zod = require("zod");
2589
+ function fieldsToZodShape(fields) {
2590
+ const shape = {};
2591
+ for (const field of fields) {
2592
+ shape[field.name] = applyModifiers(field, baseType(field));
2593
+ }
2594
+ return shape;
2595
+ }
2596
+ function baseType(field) {
2597
+ if (field.enum && field.enum.length > 0) {
2598
+ return import_zod.z.enum([...field.enum]);
2599
+ }
2600
+ switch (field.type) {
2601
+ case "string":
2602
+ return import_zod.z.string();
2603
+ case "number":
2604
+ return import_zod.z.number();
2605
+ case "boolean":
2606
+ return import_zod.z.boolean();
2607
+ case "array":
2608
+ return import_zod.z.array(import_zod.z.unknown());
2609
+ case "object":
2610
+ return import_zod.z.record(import_zod.z.unknown());
2611
+ }
2612
+ }
2613
+ function applyModifiers(field, type) {
2614
+ const described = type.describe(field.description);
2615
+ if (field.required) return described;
2616
+ return field.default === void 0 ? described.optional() : described.default(field.default);
2617
+ }
2618
+
2619
+ // src/projection.ts
2620
+ var S = "scalar";
2621
+ var T = "text";
2622
+ var PASS = "pass";
2623
+ var section = (shape) => ({ __section: shape });
2624
+ var oneOf = (values) => ({ __oneOf: new Set(values) });
2625
+ function isPrimitive(value) {
2626
+ return value === null || ["string", "number", "boolean"].includes(typeof value);
2627
+ }
2628
+ var TOKEN = /^[^\s@]{0,200}$/;
2629
+ var OMITTED = { omitted: S, grantable: T, reason: T };
2630
+ var UNAVAILABLE = /^(not readable with this credential \(missing scope\)|not found|unavailable \([^\s@]{0,100}\))$/;
2631
+ function project(value, shape) {
2632
+ if (value === void 0) return void 0;
2633
+ if (shape === PASS) return value;
2634
+ if (shape === S) {
2635
+ if (!isPrimitive(value)) return void 0;
2636
+ return typeof value === "string" && !TOKEN.test(value) ? null : value;
2637
+ }
2638
+ if (shape === T) return isPrimitive(value) ? value : void 0;
2639
+ if ("__oneOf" in shape) {
2640
+ if (!isPrimitive(value)) return void 0;
2641
+ return typeof value === "string" && !shape.__oneOf.has(value) ? null : value;
2642
+ }
2643
+ if (Array.isArray(shape)) {
2644
+ return Array.isArray(value) ? value.map((item) => project(item, shape[0])) : void 0;
2645
+ }
2646
+ if ("__section" in shape) {
2647
+ if (typeof value === "string") return UNAVAILABLE.test(value) ? value : null;
2648
+ if (value && typeof value === "object" && value.omitted === true) {
2649
+ return project(value, OMITTED);
2650
+ }
2651
+ return project(value, shape.__section);
2652
+ }
2653
+ if (value === null) return null;
2654
+ if (typeof value !== "object" || Array.isArray(value)) return void 0;
2655
+ const out = {};
2656
+ for (const [key, child] of Object.entries(shape)) {
2657
+ const projected = project(value[key], child);
2658
+ if (projected !== void 0) out[key] = projected;
2659
+ }
2660
+ return out;
2661
+ }
2662
+ var PAGINATION = { cursor: S, nextCursor: S, hasMore: S };
2663
+ var page2 = (row) => ({ data: [row], pagination: PAGINATION });
2664
+ var ADDRESS = { countryCode: S };
2665
+ var CUSTOMER_REF = { id: S };
2666
+ var APPLICATION = { id: S, name: T, slug: S };
2667
+ var LIFECYCLE_LABELS = ["Draft", "Pending", "Processing", "Partially fulfilled", "Fulfilled", "Cancelled", "Closed"];
2668
+ var DISPLAY_QUALIFIERS = [
2669
+ "Comped",
2670
+ "Awaiting payment",
2671
+ "Authorized",
2672
+ "Partially paid",
2673
+ "Partially refunded",
2674
+ "Refunded",
2675
+ "Payment failed",
2676
+ "Voided",
2677
+ "Payment processing",
2678
+ "Disputed",
2679
+ "Authorization expired",
2680
+ "Returned",
2681
+ "Partially returned",
2682
+ "Delivered",
2683
+ "In transit"
2684
+ ];
2685
+ var ORDER_DISPLAY_STATUSES = new Set(
2686
+ LIFECYCLE_LABELS.flatMap((l) => [l, ...DISPLAY_QUALIFIERS.map((q) => `${l} \xB7 ${q}`)])
2687
+ );
2688
+ var DISPLAY_STATUS = oneOf(ORDER_DISPLAY_STATUSES);
2689
+ var RANGE = { from: S, to: S, timezone: S, groupBy: S };
2690
+ var LINE_ITEM = {
2691
+ id: S,
2692
+ type: S,
2693
+ quantity: S,
2694
+ unitPrice: S,
2695
+ subtotal: S,
2696
+ discountAmount: S,
2697
+ taxAmount: S,
2698
+ total: S,
2699
+ recurring: S
2700
+ };
2701
+ var ORDER = {
2702
+ id: S,
2703
+ orderNumber: S,
2704
+ status: S,
2705
+ displayStatus: DISPLAY_STATUS,
2706
+ paymentStatus: S,
2707
+ type: S,
2708
+ source: S,
2709
+ currency: S,
2710
+ subtotal: S,
2711
+ discountTotal: S,
2712
+ shippingTotal: S,
2713
+ taxTotal: S,
2714
+ total: S,
2715
+ capturedAmount: S,
2716
+ refundedAmount: S,
2717
+ compAmount: S,
2718
+ onHold: S,
2719
+ taxReverseCharge: S,
2720
+ customerId: S,
2721
+ customer: CUSTOMER_REF,
2722
+ subscriptionId: S,
2723
+ shippingAddress: ADDRESS,
2724
+ billingAddress: ADDRESS,
2725
+ fulfillmentSummary: { itemsFulfilled: S, itemsRemaining: S, itemsTotal: S },
2726
+ lineItems: [LINE_ITEM],
2727
+ createdAt: S,
2728
+ completedAt: S,
2729
+ cancelledAt: S,
2730
+ closedAt: S
2731
+ };
2732
+ var PAYMENT = {
2733
+ id: S,
2734
+ orderId: S,
2735
+ status: S,
2736
+ method: S,
2737
+ processor: S,
2738
+ amount: S,
2739
+ capturedAmount: S,
2740
+ currency: S,
2741
+ cardBrand: S,
2742
+ disputed: S,
2743
+ disputeOpenedAt: S,
2744
+ errorCode: S,
2745
+ netN: S,
2746
+ netNSource: S,
2747
+ authorizedAt: S,
2748
+ capturedAt: S,
2749
+ voidedAt: S,
2750
+ createdAt: S
2751
+ };
2752
+ var RETURN = {
2753
+ id: S,
2754
+ orderId: S,
2755
+ status: S,
2756
+ restock: S,
2757
+ refundAmount: S,
2758
+ refundPaymentId: S,
2759
+ createdAt: S,
2760
+ items: [{ lineItemId: S, quantity: S }]
2761
+ };
2762
+ var FULFILLMENT = {
2763
+ id: S,
2764
+ orderId: S,
2765
+ type: S,
2766
+ status: S,
2767
+ createdAt: S,
2768
+ lineItems: [{ lineItemId: S, quantity: S }],
2769
+ detail: { carrier: T, serviceLevel: T, trackingNumber: T, trackingUrl: T, shippedAt: S, estimatedDelivery: S, actualDelivery: S }
2770
+ };
2771
+ var CUSTOMER = { id: S, netN: S, acceptsMarketing: S, createdAt: S };
2772
+ var SUBSCRIPTION = {
2773
+ id: S,
2774
+ status: S,
2775
+ planName: T,
2776
+ planReference: T,
2777
+ amount: S,
2778
+ currency: S,
2779
+ interval: S,
2780
+ quantity: S,
2781
+ intervalUnit: S,
2782
+ intervalCount: S,
2783
+ totalPayments: S,
2784
+ paymentsMade: S,
2785
+ completedAt: S,
2786
+ customerId: S,
2787
+ customer: CUSTOMER_REF,
2788
+ currentPeriodStart: S,
2789
+ currentPeriodEnd: S,
2790
+ billingAnchorAt: S,
2791
+ cancelAtPeriodEnd: S,
2792
+ trialEnd: S,
2793
+ pausedAt: S,
2794
+ cancelledAt: S,
2795
+ lastPaymentAt: S,
2796
+ failureCount: S,
2797
+ pendingPlanName: T,
2798
+ pendingPlanReference: T,
2799
+ pendingAmount: S,
2800
+ pendingInterval: S,
2801
+ pendingQuantity: S,
2802
+ pendingIntervalUnit: S,
2803
+ pendingIntervalCount: S,
2804
+ createdAt: S
2805
+ };
2806
+ var QUOTE_SUMMARY = {
2807
+ id: S,
2808
+ quoteNumber: S,
2809
+ status: S,
2810
+ source: S,
2811
+ currency: S,
2812
+ total: S,
2813
+ customerId: S,
2814
+ orderId: S,
2815
+ expiresAt: S,
2816
+ acceptedAt: S,
2817
+ declinedAt: S,
2818
+ createdAt: S
2819
+ };
2820
+ var QUOTE = {
2821
+ ...QUOTE_SUMMARY,
2822
+ revisions: [{
2823
+ id: S,
2824
+ revisionNumber: S,
2825
+ state: S,
2826
+ subtotal: S,
2827
+ discountTotal: S,
2828
+ shippingTotal: S,
2829
+ taxTotal: S,
2830
+ total: S,
2831
+ depositAmount: S,
2832
+ shippingMode: S,
2833
+ issuedAt: S,
2834
+ expiresAt: S,
2835
+ paymentTerms: { mode: S, netN: S, balanceNetN: S, deposit: { type: S, value: S } },
2836
+ items: [{
2837
+ type: S,
2838
+ quantity: S,
2839
+ unitPrice: S,
2840
+ subtotal: S,
2841
+ discountAmount: S,
2842
+ taxAmount: S,
2843
+ total: S,
2844
+ optional: S,
2845
+ optionalSelected: S
2846
+ }]
2847
+ }],
2848
+ // Who commented and when, never what they wrote.
2849
+ comments: [{ authorType: S, visibility: S, createdAt: S }],
2850
+ timeline: [{ type: S, actorType: S, createdAt: S }]
2851
+ };
2852
+ var INVOICE = {
2853
+ id: S,
2854
+ number: S,
2855
+ status: S,
2856
+ sourceType: S,
2857
+ sourceId: S,
2858
+ customerId: S,
2859
+ currency: S,
2860
+ subtotal: S,
2861
+ discountTotal: S,
2862
+ shippingTotal: S,
2863
+ taxTotal: S,
2864
+ total: S,
2865
+ amountRefunded: S,
2866
+ issuedAt: S,
2867
+ dueAt: S,
2868
+ periodStart: S,
2869
+ periodEnd: S,
2870
+ createdAt: S,
2871
+ billingSnapshot: {
2872
+ planName: T,
2873
+ period: { start: S, end: S },
2874
+ lineItems: [{ quantity: S, unitPrice: S, discountAmount: S, taxAmount: S, total: S }],
2875
+ totals: { currency: S, subtotal: S, discountTotal: S, shippingTotal: S, taxTotal: S, total: S },
2876
+ payment: { method: S, brand: S, status: S }
2877
+ }
2878
+ };
2879
+ var DISCOUNT = {
2880
+ id: S,
2881
+ code: T,
2882
+ name: T,
2883
+ type: S,
2884
+ value: S,
2885
+ isActive: S,
2886
+ startsAt: S,
2887
+ endsAt: S,
2888
+ usageCount: S,
2889
+ usageLimit: S,
2890
+ maxRedemptionsPerCustomer: S,
2891
+ createdAt: S
2892
+ };
2893
+ var EVENT = {
2894
+ id: S,
2895
+ type: S,
2896
+ entityType: S,
2897
+ entityId: S,
2898
+ createdAt: S,
2899
+ data: {
2900
+ status: S,
2901
+ previousStatus: S,
2902
+ action: S,
2903
+ amount: S,
2904
+ currency: S,
2905
+ attempt: S,
2906
+ orderId: S,
2907
+ paymentId: S,
2908
+ order: { id: S, orderNumber: S, status: S, paymentStatus: S, currency: S, total: S },
2909
+ payment: { id: S, orderId: S, status: S, method: S, amount: S, currency: S },
2910
+ subscription: { id: S, status: S, planName: T, amount: S, currency: S, interval: S },
2911
+ fulfillment: { id: S, orderId: S, type: S, status: S }
2912
+ }
2913
+ };
2914
+ var DELIVERY = {
2915
+ id: S,
2916
+ endpointId: S,
2917
+ eventId: S,
2918
+ eventType: S,
2919
+ status: S,
2920
+ attemptCount: S,
2921
+ lastStatus: S,
2922
+ deliveredAt: S,
2923
+ nextRetryAt: S,
2924
+ createdAt: S,
2925
+ replayOf: S
2926
+ };
2927
+ var MONEY_COUNT = { count: S, totalCents: S };
2928
+ var ORDER_PERIOD = { orderCount: S, grossCents: S, netCents: S, refundsCents: S, avgOrderCents: S };
2929
+ var OAUTH_PROJECTIONS = {
2930
+ whoami: {
2931
+ type: S,
2932
+ workspace: { name: T, slug: S },
2933
+ application: APPLICATION,
2934
+ environment: { slug: S, kind: S, providerEnvironment: S },
2935
+ note: T
2936
+ },
2937
+ search_docs: PASS,
2938
+ describe_endpoint: PASS,
2939
+ validate_payload: PASS,
2940
+ search_orders: page2(ORDER),
2941
+ get_order: {
2942
+ order: ORDER,
2943
+ payments: section([PAYMENT]),
2944
+ returns: section([RETURN]),
2945
+ fulfillments: section([FULFILLMENT])
2946
+ },
2947
+ search_customers: page2(CUSTOMER),
2948
+ get_customer: {
2949
+ customer: CUSTOMER,
2950
+ addresses: section([{ id: S, isDefault: S, ...ADDRESS }]),
2951
+ subscriptions: section([SUBSCRIPTION])
2952
+ },
2953
+ list_subscriptions: page2(SUBSCRIPTION),
2954
+ get_subscription: SUBSCRIPTION,
2955
+ search_quotes: page2(QUOTE_SUMMARY),
2956
+ get_quote: QUOTE,
2957
+ list_invoices: page2(INVOICE),
2958
+ get_invoice: INVOICE,
2959
+ receivables_aging: {
2960
+ buckets: [{ key: S, label: T, count: S, total_amount: S }],
2961
+ total_outstanding_amount: S,
2962
+ total_outstanding_count: S
2963
+ },
2964
+ search_discounts: page2(DISCOUNT),
2965
+ get_discount: DISCOUNT,
2966
+ search_payments: page2(PAYMENT),
2967
+ get_payment: {
2968
+ payment: PAYMENT,
2969
+ transactions: section([{ id: S, type: S, status: S, amount: S, currency: S, createdAt: S }])
2970
+ },
2971
+ list_abandoned_carts: {
2972
+ carts: section([{
2973
+ id: S,
2974
+ abandonedAt: S,
2975
+ currency: S,
2976
+ itemCount: S,
2977
+ recoveryStatus: S,
2978
+ total: S,
2979
+ customer: { id: S }
2980
+ }]),
2981
+ summary: section({ abandonedCount: S, abandonedValue: S, recoveryEmailsSent: S, sinceDays: S })
2982
+ },
2983
+ search_events: page2(EVENT),
2984
+ list_webhook_deliveries: page2(DELIVERY),
2985
+ get_webhook_delivery: DELIVERY,
2986
+ explain_checkout_failure: {
2987
+ note: T,
2988
+ hint: T,
2989
+ session: {
2990
+ id: S,
2991
+ status: S,
2992
+ cartId: S,
2993
+ customerId: S,
2994
+ expiresAt: S,
2995
+ completedAt: S,
2996
+ allowedMethods: [S],
2997
+ collect: { shippingAddress: S, billingAddress: S, vatNumber: S },
2998
+ shippingAddress: ADDRESS,
2999
+ billingAddress: ADDRESS,
3000
+ recurringTerms: {
3001
+ interval: S,
3002
+ intervalUnit: S,
3003
+ intervalCount: S,
3004
+ totalPayments: S,
3005
+ taxApplies: S,
3006
+ planChargedToday: S,
3007
+ planName: T,
3008
+ trialDays: S,
3009
+ currency: S,
3010
+ firstChargeAmountMinor: S
3011
+ }
3012
+ },
3013
+ cart: section({
3014
+ id: S,
3015
+ status: S,
3016
+ currency: S,
3017
+ subtotal: S,
3018
+ discountTotal: S,
3019
+ shippingTotal: S,
3020
+ taxTotal: S,
3021
+ total: S,
3022
+ itemCount: S,
3023
+ items: [LINE_ITEM],
3024
+ lineItems: [LINE_ITEM]
3025
+ }),
3026
+ events: [EVENT],
3027
+ eventsUnavailable: T,
3028
+ recentWebhookDeliveries: section([DELIVERY])
3029
+ },
3030
+ list_extension_installations: page2({
3031
+ id: S,
3032
+ extensionId: S,
3033
+ extensionName: T,
3034
+ extensionSlug: S,
3035
+ versionId: S,
3036
+ status: S,
3037
+ environmentSlug: S,
3038
+ installedAt: S,
3039
+ uninstalledAt: S
3040
+ }),
3041
+ get_connector_routing: {
3042
+ paymentRouting: section({
3043
+ defaultCascade: [S],
3044
+ rules: [{ cascade: [S], conditions: [{ type: S, op: S, currency: S, min: S, max: S, values: [S] }] }]
3045
+ }),
3046
+ cardDecline: section({ rules: [{ errorCode: S, conditions: [{ type: S, op: S, values: [S] }] }] }),
3047
+ otherDecline: section({ rules: [{ errorCode: S, conditions: [{ type: S, op: S, values: [S] }] }] }),
3048
+ routingConnectors: section([{ id: S, displayName: T, method: S }])
3049
+ },
3050
+ get_integration_status: {
3051
+ environment: { slug: S, kind: S, providerEnvironment: S },
3052
+ workspace: { name: T, slug: S },
3053
+ applicationId: S,
3054
+ application: APPLICATION,
3055
+ connectors: section([{
3056
+ id: S,
3057
+ displayName: T,
3058
+ method: S,
3059
+ type: S,
3060
+ status: S,
3061
+ active: S,
3062
+ position: S,
3063
+ acceptedCountries: [S],
3064
+ acceptedCurrencies: [S],
3065
+ threeDSecureEnabled: S,
3066
+ createdAt: S
3067
+ }]),
3068
+ webhookEndpoints: section([{ id: S, isActive: S, enabledEvents: [S], createdAt: S }]),
3069
+ unsubscribedEvents: section({ windowDays: S, unsubscribed: [{ type: S, count: S, lastEmittedAt: S }] })
3070
+ },
3071
+ get_workspace_settings: { name: T, slug: S, brandColor: S, logoUrl: T, onboardingCompletedAt: S },
3072
+ // What happened and to which record; not which team member did it.
3073
+ get_audit_log: page2({ id: S, action: S, resourceType: S, resourceId: S, createdAt: S }),
3074
+ list_applications: [{ id: S, name: T, slug: S, currency: S, isActive: S }],
3075
+ // Analytics: computed aggregates, listed field by field all the same.
3076
+ get_business_summary: {
3077
+ period: S,
3078
+ range: RANGE,
3079
+ orders: section({ current: ORDER_PERIOD, previous: ORDER_PERIOD, deltaPct: S }),
3080
+ subscriptions: section({
3081
+ mrr: { currentCents: S, periodStartCents: S, deltaPct: S },
3082
+ newSubscriptions: S,
3083
+ cancelledSubscriptions: S,
3084
+ churnRate: S,
3085
+ note: T
3086
+ }),
3087
+ receivables: section({ outstandingCents: S, outstandingCount: S }),
3088
+ quotes: section({ sent: MONEY_COUNT, accepted: MONEY_COUNT, declined: MONEY_COUNT, expired: MONEY_COUNT })
3089
+ },
3090
+ get_attention_digest: {
3091
+ disputes: section({ open: S, totalCents: S }),
3092
+ subscriptionsInDunning: section(S),
3093
+ overdueInvoices: section(MONEY_COUNT),
3094
+ unhealthyWebhooks: section([{ endpointId: S, failureRate: S, attempts7d: S }])
3095
+ },
3096
+ get_sales_timeseries: {
3097
+ application: APPLICATION,
3098
+ range: RANGE,
3099
+ currency: S,
3100
+ days: S,
3101
+ note: T,
3102
+ series: [{ date: S, grossCents: S, orders: S }],
3103
+ totalGrossCents: S,
3104
+ totalOrders: S
3105
+ },
3106
+ // get_top_products has no entry, so it is not offered over OAuth: it ranks
3107
+ // products by line-item reference id or name, which a buyer can type.
3108
+ get_customer_growth: {
3109
+ application: APPLICATION,
3110
+ range: RANGE,
3111
+ note: T,
3112
+ topBy: S,
3113
+ signups: { total: S, series: [{ date: S, signups: S }] },
3114
+ ordersByCustomerType: { newCustomerOrders: S, returningCustomerOrders: S },
3115
+ // The ranking, by id: a top customer's name and email stay behind.
3116
+ topCustomers: [{ customerId: S, orderCount: S, revenueCents: S }],
3117
+ totals: { customerCount: S, orderCount: S, revenueCents: S }
3118
+ },
3119
+ get_revenue_breakdown: {
3120
+ application: APPLICATION,
3121
+ range: RANGE,
3122
+ dimension: S,
3123
+ note: T,
3124
+ rows: [{ key: S, orderCount: S, grossCents: S }],
3125
+ totals: { orderCount: S, grossCents: S }
3126
+ },
3127
+ get_funnel_conversion: {
3128
+ application: APPLICATION,
3129
+ range: RANGE,
3130
+ note: T,
3131
+ conversionRate: S,
3132
+ stages: [{ name: S, count: S }],
3133
+ dropOff: [{ from: S, to: S, ratePct: S }]
3134
+ },
3135
+ get_subscription_movement: {
3136
+ application: APPLICATION,
3137
+ range: RANGE,
3138
+ note: T,
3139
+ series: [{ date: S, newSubscriptions: S, churnedSubscriptions: S, mrrDeltaCents: S }],
3140
+ totals: { newSubscriptions: S, churnedSubscriptions: S, netMrrDeltaCents: S }
3141
+ },
3142
+ get_discount_performance: {
3143
+ application: APPLICATION,
3144
+ range: RANGE,
3145
+ note: T,
3146
+ rows: [{ discountId: S, code: T, redemptions: S, discountedRevenueCents: S, discountGivenCents: S }],
3147
+ totals: { redemptions: S, discountedRevenueCents: S, discountGivenCents: S }
3148
+ }
3149
+ };
3150
+
3151
+ // src/server.ts
3152
+ var VERSION = true ? "0.2.1" : "0.0.0-dev";
3153
+ function registerableActions(policy) {
3154
+ const isProduction = policy.identity.environment.providerEnvironment === "production";
3155
+ return actionsForChannel("mcp").filter((action) => {
3156
+ if (!HANDLERS[action.id]) return false;
3157
+ if (action.credentialScoped && policy.allowCredentialScoped === false) return false;
3158
+ if (!canRunAction(policy.identity.scopes, action)) return false;
3159
+ if (action.kind === "read") return true;
3160
+ if (action.sandboxOnly) return !isProduction && (policy.allowSandboxWrites ?? true);
3161
+ if (!policy.allowWrites) return false;
3162
+ return !isProduction || policy.allowLiveWrites;
3163
+ });
3164
+ }
3165
+ function buildInstructions(identity, actions, policy) {
3166
+ const isProduction = identity.environment.providerEnvironment === "production";
3167
+ const where = [
3168
+ identity.workspace?.name ?? identity.workspace?.slug ?? "unknown workspace",
3169
+ identity.application?.name ?? identity.application?.slug ?? null
3170
+ ].filter(Boolean).join(" / ");
3171
+ const liveWrites = actions.filter((a) => a.kind === "write" && !a.sandboxOnly);
3172
+ const sandboxWrites = actions.filter((a) => a.kind === "write" && a.sandboxOnly);
3173
+ const lines = [
3174
+ `Throttle MCP ${VERSION}. Connected to ${where}, environment "${identity.environment.slug}" (${isProduction ? "PRODUCTION \u2014 live data" : "non-production \u2014 not live data"}).`,
3175
+ "",
3176
+ "Conventions:",
3177
+ "- The workspace and environment are fixed for this session and cannot be switched from inside a tool call. Call whoami if you need to state where you are.",
3178
+ '- The application can vary per call: pass "applicationId" on a read or analytics tool to reach a different application in this workspace, and call list_applications to see which ones this connection can reach. Omitting applicationId uses the application this connection was authorized for.',
3179
+ "- Every analytics result covers exactly ONE application and names it in `application`. Say which application a figure describes. If the user asks about their business as a whole and this connection reaches more than one application, do not answer from the default alone \u2014 call list_applications, then query each and add them up, or ask which application they mean.",
3180
+ '- List tools return { data, pagination }. When pagination.hasMore is true, pass pagination.cursor back as "cursor" rather than raising "limit".',
3181
+ "- Amounts are integer minor units (2500 = $25.00).",
3182
+ "- Throttle rejects snake_case on public request bodies. Before writing integration code, confirm shapes with describe_endpoint and check bodies with validate_payload instead of guessing field names.",
3183
+ "- API errors come back as text with a stable code. On insufficient_scopes, tell the user which scope to add \u2014 do not retry.",
3184
+ "- Analytics results may mark a section `omitted`. That means UNKNOWN, never zero. When `grantable` is false the data cannot be read over this connection at all \u2014 say so rather than reporting an absence."
3185
+ ];
3186
+ if (liveWrites.length === 0) {
3187
+ lines.push(
3188
+ `- This session is READ-ONLY for real data${policy.allowWrites ? " (no write tools are available to this credential)" : " \u2014 writes were not enabled"}. Say so plainly if asked to change something.`
3189
+ );
3190
+ if (sandboxWrites.length > 0) {
3191
+ lines.push(
3192
+ `- ${sandboxWrites.map((a) => a.id).join(", ")} may create data, but only in this non-production environment.`
3193
+ );
3194
+ }
3195
+ } else {
3196
+ lines.push(
3197
+ `- Write tools are enabled (${liveWrites.map((a) => a.id).join(", ")}). They are irreversible and some are buyer-visible. Confirm with the user before calling one.`
3198
+ );
3199
+ }
3200
+ return lines.join("\n");
3201
+ }
3202
+ function declareSecuritySchemes(server, actions) {
3203
+ const inner = server.server;
3204
+ const handlers = inner._requestHandlers;
3205
+ const original = handlers?.get("tools/list");
3206
+ if (!original) return;
3207
+ const schemes = new Map(
3208
+ actions.map((a) => [
3209
+ a.id,
3210
+ [{
3211
+ type: "oauth2",
3212
+ scopes: [.../* @__PURE__ */ new Set([...a.scopes, ...a.sectionScopes ?? []])].filter((scope) => validateScopesForOAuth([scope]).ok)
3213
+ }]
3214
+ ])
3215
+ );
3216
+ inner.setRequestHandler(import_types.ListToolsRequestSchema, async (request, extra) => {
3217
+ const result = await original(request, extra);
3218
+ return {
3219
+ ...result,
3220
+ tools: result.tools.map((tool) => ({ ...tool, securitySchemes: schemes.get(tool.name) ?? [{ type: "oauth2", scopes: [] }] }))
3221
+ };
3222
+ });
3223
+ }
3224
+ function createServer(options) {
3225
+ const redactPii = options.identity.type === "oauth_token" && !hasScope(options.identity.scopes, "customer_pii:read");
3226
+ const actions = registerableActions(options).filter((a) => !redactPii || OAUTH_PROJECTIONS[a.id] !== void 0);
3227
+ const server = new import_mcp.McpServer(
3228
+ { name: "throttle", version: VERSION },
3229
+ { instructions: buildInstructions(options.identity, actions, options) }
3230
+ );
3231
+ const ctx = {
3232
+ client: options.client,
3233
+ identity: options.identity,
3234
+ contract: options.contract,
3235
+ docs: options.docs
3236
+ };
3237
+ for (const action of actions) {
3238
+ const handler = HANDLERS[action.id];
3239
+ server.registerTool(
3240
+ action.id,
3241
+ {
3242
+ title: action.title,
3243
+ description: describeForModel(action),
3244
+ inputSchema: fieldsToZodShape(action.input),
3245
+ // Every hint is an explicit boolean: directory reviews (OpenAI's in
3246
+ // particular) read a missing value as "unknown", not as false.
3247
+ annotations: {
3248
+ title: action.title,
3249
+ readOnlyHint: action.kind === "read",
3250
+ destructiveHint: action.kind === "write" && Boolean(action.destructive),
3251
+ idempotentHint: Boolean(action.idempotent),
3252
+ // Every tool is bounded to the connected workspace; none reaches the
3253
+ // public internet or an open-ended set of external entities.
3254
+ openWorldHint: false
3255
+ }
3256
+ },
3257
+ async (args) => {
3258
+ try {
3259
+ if (redactPii && action.id === "search_customers" && typeof args?.q === "string" && args.q.trim() !== "") {
3260
+ return {
3261
+ content: [{ type: "text", text: "Customer text search is not available over this connection, because names, emails, phone numbers and external ids are withheld. List customers by creation date (createdAfter/createdBefore), or open one by its Throttle id from an order, quote or subscription." }],
3262
+ isError: true
3263
+ };
3264
+ }
3265
+ const result = await handler(ctx, args ?? {});
3266
+ const shown = redactPii ? redactShopperPii(project(result, OAUTH_PROJECTIONS[action.id])) : result;
3267
+ return { content: [{ type: "text", text: formatResult(shown) }] };
3268
+ } catch (error) {
3269
+ return {
3270
+ content: [{ type: "text", text: formatError(error) }],
3271
+ isError: true,
3272
+ ...options.authChallenge && isCredentialRejection(error) ? {
3273
+ _meta: {
3274
+ "mcp/www_authenticate": [
3275
+ `${options.authChallenge}, error="invalid_token", error_description="The connected credential was rejected. Sign in again to continue."`
3276
+ ]
3277
+ }
3278
+ } : {}
3279
+ };
3280
+ }
3281
+ }
3282
+ );
3283
+ }
3284
+ if (options.authChallenge) declareSecuritySchemes(server, actions);
3285
+ return server;
3286
+ }
3287
+ function isCredentialRejection(error) {
3288
+ return error?.statusCode === 401;
3289
+ }
3290
+ function describeForModel(action) {
3291
+ const parts = [action.description];
3292
+ if (action.guardrails && action.guardrails.length > 0) {
3293
+ parts.push(`Constraints: ${action.guardrails.join(" ")}`);
3294
+ }
3295
+ return parts.join(" ");
3296
+ }
3297
+
3298
+ // src/indexes.ts
3299
+ var import_node_fs = require("fs");
3300
+ var import_meta = {};
3301
+ function loadIndex(file, empty, label) {
3302
+ try {
3303
+ return JSON.parse((0, import_node_fs.readFileSync)(new URL(`./${file}`, import_meta.url), "utf8"));
3304
+ } catch {
3305
+ process.stderr.write(
3306
+ `throttle-mcp: ${label} index missing (${file}); that tool will return no results. Run "pnpm --filter @usethrottle/mcp build:indexes".
3307
+ `
3308
+ );
3309
+ return empty;
3310
+ }
3311
+ }
3312
+ function loadDocsIndex() {
3313
+ return loadIndex("docs-index.json", { generatedAt: "", entries: [] }, "docs");
3314
+ }
3315
+ function loadContractIndex() {
3316
+ return loadIndex("contract-index.json", { generatedAt: "", operations: [] }, "contract");
3317
+ }
3318
+ // Annotate the CommonJS export names for ESM import in node:
3319
+ 0 && (module.exports = {
3320
+ ThrottleClient,
3321
+ VERSION,
3322
+ buildInstructions,
3323
+ createServer,
3324
+ loadContractIndex,
3325
+ loadDocsIndex,
3326
+ registerableActions
3327
+ });
3328
+ //# sourceMappingURL=lib.cjs.map