@usethrottle/mcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib.cjs ADDED
@@ -0,0 +1,1702 @@
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
+
36
+ // ../core-scopes/src/scopes.ts
37
+ function hasScope(granted, required) {
38
+ if (granted.includes("*")) return true;
39
+ if (granted.includes(required)) return true;
40
+ const sep = required.lastIndexOf(":");
41
+ if (sep === -1) return false;
42
+ const resource = required.slice(0, sep);
43
+ const action = required.slice(sep + 1);
44
+ if (action === "read" && granted.includes(`${resource}:write`)) return true;
45
+ return false;
46
+ }
47
+ var REG = {};
48
+ function add(group, resource, actions, labels, opts = {}) {
49
+ const hasRead = actions.includes("read");
50
+ for (const action of actions) {
51
+ const id = `${resource}:${action}`;
52
+ const implies = action === "write" && hasRead ? [`${resource}:read`] : [];
53
+ REG[id] = {
54
+ id,
55
+ resource,
56
+ action,
57
+ group,
58
+ label: (action === "read" ? labels.read : labels.write) ?? `${action} ${resource}`,
59
+ sensitive: opts.sensitive ?? false,
60
+ staffOnly: opts.staffOnly ?? false,
61
+ // publishable defaults false; only the curated set below is true
62
+ publishableAllowed: opts.publishableAllowed ?? false,
63
+ // extension default true for commerce scopes; Taxonomy B passes false
64
+ extensionAllowed: opts.extensionAllowed ?? true,
65
+ implies
66
+ };
67
+ }
68
+ }
69
+ add("Orders", "orders", ["read", "write"], { read: "View orders", write: "Manage orders" });
70
+ add("Quotes", "quotes", ["read", "write"], { read: "View quotes", write: "Manage quotes" });
71
+ add("Orders", "order_cancellations", ["write"], { write: "Cancel orders" });
72
+ add("Orders", "order_returns", ["read", "write"], { read: "View returns", write: "Process returns" });
73
+ add("Payments", "payments", ["read", "write"], { read: "View payments", write: "Create payments" });
74
+ add("Payments", "payment_authorizations", ["write"], { write: "Authorize payments" });
75
+ add("Payments", "payment_captures", ["write"], { write: "Capture payments" });
76
+ add("Payments", "payment_refunds", ["read", "write"], { read: "View refunds", write: "Refund payments" }, { sensitive: true });
77
+ add("Payments", "payment_voids", ["write"], { write: "Void payments" }, { sensitive: true });
78
+ add("Payments", "payment_disputes", ["read", "write"], { read: "View disputes", write: "Handle disputes" }, { sensitive: true });
79
+ add("Payments", "payment_methods", ["read", "write"], { read: "View payment methods", write: "Tokenize payment methods" }, { sensitive: true });
80
+ add("Customers", "customers", ["read", "write"], { read: "View customers", write: "Manage customers" });
81
+ add("Customers", "customer_pii", ["read"], { read: "View customer PII" }, { sensitive: true });
82
+ add("Customers", "customer_addresses", ["read", "write"], { read: "View addresses", write: "Manage addresses" });
83
+ add("Customers", "customer_payment_methods", ["read", "write"], { read: "View stored cards", write: "Manage stored cards" }, { sensitive: true });
84
+ add("Carts", "carts", ["read", "write"], { read: "View carts", write: "Manage carts" });
85
+ add("Carts", "cart_items", ["read", "write"], { read: "View line items", write: "Manage line items" });
86
+ add("Carts", "checkout_sessions", ["read", "write"], { read: "View checkout sessions", write: "Manage checkout sessions" });
87
+ add("Discounts", "discounts", ["read", "write"], { read: "View discounts", write: "Manage discounts" });
88
+ add("Subscriptions", "subscriptions", ["read", "write"], { read: "View subscriptions", write: "Manage subscriptions" });
89
+ add("Subscriptions", "subscription_pauses", ["write"], { write: "Pause/resume subscriptions" });
90
+ add("Subscriptions", "subscription_cancellations", ["write"], { write: "Cancel subscriptions" });
91
+ add("Subscriptions", "subscription_usage", ["read", "write"], { read: "View subscription usage", write: "Report subscription usage" }, { extensionAllowed: false });
92
+ add("Fulfillments", "fulfillments", ["read", "write"], { read: "View fulfillments", write: "Manage fulfillments" });
93
+ add("Fulfillments", "fulfillment_shipments", ["write"], { write: "Update shipments" });
94
+ add("Fulfillments", "fulfillment_digital", ["read", "write"], { read: "View digital delivery", write: "Manage digital delivery" });
95
+ add("Fulfillments", "fulfillment_access", ["write"], { write: "Manage access credentials" });
96
+ add("Fulfillments", "fulfillment_services", ["write"], { write: "Manage service fulfillment" });
97
+ add("Invoices / AR", "invoices", ["read"], { read: "View invoices & aging" });
98
+ add("Invoices / AR", "invoice_payments", ["write"], { write: "Mark invoices paid" }, { sensitive: true });
99
+ add("Invoices / AR", "invoice_dunning", ["write"], { write: "Send dunning notices" });
100
+ add("Shipping & Tax", "shipping_tax", ["read", "write"], { read: "View shipping/tax config", write: "Edit shipping/tax config" });
101
+ add("Shipping & Tax", "shipping_tax_publications", ["write"], { write: "Publish shipping/tax config" });
102
+ add("Shipping & Tax", "shipping_quotes", ["write"], { write: "Request shipping quotes" }, { publishableAllowed: true });
103
+ add("Shipping & Tax", "tax_calculations", ["write"], { write: "Calculate tax" }, { publishableAllowed: true });
104
+ add("Shipping & Tax", "shipping_tax_quote_tokens", ["read", "write"], { read: "View quote tokens", write: "Manage quote tokens" });
105
+ add("Shipping & Tax", "shipping_tax_snapshots", ["write"], { write: "Accept external snapshots" });
106
+ add("Connectors", "connectors", ["read", "write"], { read: "View connectors", write: "Manage connectors" });
107
+ add("Connectors", "connector_activations", ["write"], { write: "Activate/deactivate connectors" });
108
+ add("Webhooks & Events", "webhooks", ["read", "write"], { read: "View webhooks", write: "Manage webhooks" });
109
+ add("Webhooks & Events", "webhook_deliveries", ["read", "write"], { read: "View deliveries", write: "Retry deliveries" });
110
+ add("Webhooks & Events", "events", ["read"], { read: "View events" });
111
+ add("Webhooks & Events", "audit_logs", ["read"], { read: "View audit log" });
112
+ add("Applications", "applications", ["read", "write"], { read: "View applications", write: "Manage applications" });
113
+ add("Workspace", "merchant", ["read", "write"], { read: "View workspace settings", write: "Edit workspace settings" });
114
+ add("Workspace", "embed_config", ["read", "write"], { read: "View embed config", write: "Edit embed config" });
115
+ add("Workspace", "branding", ["write"], { write: "Upload branding assets" });
116
+ var HUMAN = { extensionAllowed: false };
117
+ add("Team", "team_members", ["read", "write"], { read: "View members", write: "Manage members" }, HUMAN);
118
+ add("Team", "team_invites", ["read", "write"], { read: "View invites", write: "Manage invites" }, HUMAN);
119
+ add("Platform billing", "billing", ["read", "write"], { read: "View billing", write: "Manage subscription" }, HUMAN);
120
+ add("Platform billing", "billing_payment_methods", ["read", "write"], { read: "View billing cards", write: "Manage billing cards" }, HUMAN);
121
+ add("Email", "email_templates", ["read", "write"], { read: "View email templates", write: "Edit email templates" }, HUMAN);
122
+ add("Email", "email_settings", ["read", "write"], { read: "View email settings", write: "Edit email settings" }, HUMAN);
123
+ add("Go-live", "go_live", ["read", "write"], { read: "View go-live status", write: "Submit go-live" }, HUMAN);
124
+ add("Partner", "partner", ["read", "write"], { read: "View partner profile", write: "Edit partner profile" }, HUMAN);
125
+ add("Partner", "affiliates", ["read", "write"], { read: "View affiliates", write: "Manage affiliates" }, HUMAN);
126
+ add("Notifications", "notifications", ["write"], { write: "Send notifications" }, HUMAN);
127
+ add("Notifications", "preferences", ["read", "write"], { read: "View preferences", write: "Edit preferences" }, HUMAN);
128
+ add("Integrations", "integrations", ["read", "write"], { read: "View integrations", write: "Manage integrations" }, HUMAN);
129
+ add("Support", "support", ["read", "write"], { read: "View support tickets", write: "Manage support tickets" }, HUMAN);
130
+ add("Admin (staff)", "admin", ["read", "write"], { read: "View admin data", write: "Perform admin actions" }, { sensitive: true, staffOnly: true, extensionAllowed: false });
131
+ add(
132
+ "Extensions",
133
+ "extensions",
134
+ ["read", "write"],
135
+ { read: "View extensions & installs", write: "Author, version & publish extensions" },
136
+ HUMAN
137
+ );
138
+ REG["extensions:install"] = {
139
+ id: "extensions:install",
140
+ resource: "extensions",
141
+ action: "install",
142
+ group: "Extensions",
143
+ label: "Install, configure & manage extensions",
144
+ sensitive: false,
145
+ staffOnly: false,
146
+ publishableAllowed: false,
147
+ extensionAllowed: false,
148
+ implies: []
149
+ };
150
+ REG["*"] = {
151
+ id: "*",
152
+ resource: "*",
153
+ action: "write",
154
+ group: "Global",
155
+ label: "Full access",
156
+ sensitive: true,
157
+ staffOnly: false,
158
+ publishableAllowed: false,
159
+ extensionAllowed: false,
160
+ implies: []
161
+ };
162
+ var SCOPE_REGISTRY = REG;
163
+ var ALL_SCOPE_IDS = Object.keys(REG);
164
+ var SCOPE_GROUPS = (() => {
165
+ const out = {};
166
+ for (const def of Object.values(REG)) {
167
+ if (def.id === "*") continue;
168
+ (out[def.group] ??= []).push(def.id);
169
+ }
170
+ return out;
171
+ })();
172
+
173
+ // ../core-scopes/src/events.ts
174
+ var EVENT_SCOPE_OVERRIDES = {
175
+ "payment.refunded": "payment_refunds:read",
176
+ "payment.partially_refunded": "payment_refunds:read",
177
+ "payment.refund_failed": "payment_refunds:read",
178
+ "payment.disputed": "payment_disputes:read",
179
+ "payment.dispute_cleared": "payment_disputes:read",
180
+ "fulfillment.digital.delivered": "fulfillment_digital:read"
181
+ };
182
+ var PREFIX_RULES = [
183
+ ["checkout.session.", "carts:read"],
184
+ ["cart.", "carts:read"],
185
+ ["order.", "orders:read"],
186
+ ["quote.", "quotes:read"],
187
+ ["return.", "order_returns:read"],
188
+ ["payment.", "payments:read"],
189
+ ["customer.", "customers:read"],
190
+ ["subscription.", "subscriptions:read"],
191
+ ["fulfillment.", "fulfillments:read"],
192
+ ["discount.", "discounts:read"],
193
+ ["invoice.", "invoices:read"],
194
+ ["shipping_tax.", "shipping_tax:read"],
195
+ ["workspace.", "billing:read"],
196
+ ["extension.", "merchant:read"]
197
+ ];
198
+ function eventReadScope(type) {
199
+ if (EVENT_SCOPE_OVERRIDES[type]) return EVENT_SCOPE_OVERRIDES[type];
200
+ for (const [prefix, scope] of PREFIX_RULES) {
201
+ if (type.startsWith(prefix)) return scope;
202
+ }
203
+ throw new Error(`Unknown event type: ${type}`);
204
+ }
205
+
206
+ // ../core-scopes/src/issuance.ts
207
+ var PUBLISHABLE_SCOPES = Object.values(SCOPE_REGISTRY).filter((d) => d.publishableAllowed).map((d) => d.id);
208
+
209
+ // ../action-catalog/src/actions.ts
210
+ var ACTIONS = [
211
+ // ---------------------------------------------------------------- identity
212
+ {
213
+ id: "whoami",
214
+ title: "Identify the connected credential",
215
+ 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.",
216
+ kind: "read",
217
+ scopes: [],
218
+ input: [],
219
+ requests: [{ method: "GET", path: "/api/v1/whoami" }],
220
+ channels: ["mcp", "zapier", "make", "n8n"]
221
+ },
222
+ // ------------------------------------------------------- local (no API call)
223
+ {
224
+ id: "search_docs",
225
+ title: "Search the Throttle developer docs",
226
+ description: "Full-text search over the Throttle developer documentation. Returns titles, canonical docs.usethrottle.dev URLs, and matching excerpts. Prefer this over recalling API details from memory.",
227
+ kind: "read",
228
+ scopes: [],
229
+ input: [
230
+ { name: "query", type: "string", required: true, description: "What to search for." },
231
+ { name: "limit", type: "number", required: false, description: "Maximum results." }
232
+ ],
233
+ requests: [],
234
+ channels: ["mcp"]
235
+ },
236
+ {
237
+ id: "describe_endpoint",
238
+ title: "Describe an API endpoint",
239
+ 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.",
240
+ kind: "read",
241
+ scopes: [],
242
+ input: [
243
+ {
244
+ name: "path",
245
+ type: "string",
246
+ required: true,
247
+ description: 'Endpoint path or fragment, e.g. "/api/v1/carts" or "carts".'
248
+ },
249
+ {
250
+ name: "method",
251
+ type: "string",
252
+ required: false,
253
+ description: "Narrow to one HTTP method.",
254
+ enum: ["GET", "POST", "PATCH", "PUT", "DELETE"]
255
+ }
256
+ ],
257
+ requests: [],
258
+ channels: ["mcp"]
259
+ },
260
+ {
261
+ id: "validate_payload",
262
+ title: "Validate a request body against the API contract",
263
+ 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.",
264
+ kind: "read",
265
+ scopes: [],
266
+ input: [
267
+ { name: "method", type: "string", required: true, description: "HTTP method." },
268
+ { name: "path", type: "string", required: true, description: "Endpoint path." },
269
+ { name: "body", type: "object", required: true, description: "The request body to check." }
270
+ ],
271
+ requests: [],
272
+ channels: ["mcp"]
273
+ },
274
+ // ------------------------------------------------------------ generic reads
275
+ {
276
+ id: "api_get",
277
+ title: "Read any Throttle API endpoint",
278
+ 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.",
279
+ kind: "read",
280
+ scopes: [],
281
+ input: [
282
+ {
283
+ name: "path",
284
+ type: "string",
285
+ required: true,
286
+ description: "Path beginning with /api/v1, query string included."
287
+ }
288
+ ],
289
+ requests: [],
290
+ channels: ["mcp"],
291
+ guardrails: [
292
+ "GET only \u2014 reject any other method.",
293
+ "Reject paths outside /api/v1 so the tool cannot be pointed at another host or a non-versioned surface.",
294
+ "Server-side scope enforcement still applies; surface a 403 insufficient_scopes verbatim rather than retrying."
295
+ ]
296
+ },
297
+ // ------------------------------------------------------------------- orders
298
+ {
299
+ id: "search_orders",
300
+ title: "Search orders",
301
+ description: "List orders, newest first, with optional free-text and status filters.",
302
+ kind: "read",
303
+ scopes: ["orders:read"],
304
+ input: [
305
+ { name: "q", type: "string", required: false, description: "Free-text search." },
306
+ { name: "status", type: "string", required: false, description: "Order status filter." },
307
+ { name: "paymentStatus", type: "string", required: false, description: "Payment status filter." },
308
+ { name: "limit", type: "number", required: false, description: "Page size." },
309
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
310
+ ],
311
+ requests: [{ method: "GET", path: "/api/v1/orders" }],
312
+ channels: ["mcp", "zapier", "make", "n8n"]
313
+ },
314
+ {
315
+ id: "get_order",
316
+ title: "Get one order with its returns and fulfillments",
317
+ description: "Full picture of a single order: the order itself plus its payments, returns and fulfillments in one response.",
318
+ kind: "read",
319
+ scopes: ["orders:read"],
320
+ input: [{ name: "id", type: "string", required: true, description: "Order id." }],
321
+ requests: [
322
+ { method: "GET", path: "/api/v1/orders/:id" },
323
+ { method: "GET", path: "/api/v1/orders/:id/payments" },
324
+ { method: "GET", path: "/api/v1/orders/:id/returns" },
325
+ { method: "GET", path: "/api/v1/orders/:orderId/fulfillments" }
326
+ ],
327
+ channels: ["mcp"],
328
+ guardrails: [
329
+ "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."
330
+ ]
331
+ },
332
+ // ---------------------------------------------------------------- customers
333
+ {
334
+ id: "search_customers",
335
+ title: "Search customers",
336
+ description: "Find customers by name, email or external id.",
337
+ kind: "read",
338
+ scopes: ["customers:read"],
339
+ input: [
340
+ { name: "q", type: "string", required: false, description: "Free-text search." },
341
+ { name: "limit", type: "number", required: false, description: "Page size." },
342
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
343
+ ],
344
+ requests: [{ method: "GET", path: "/api/v1/customers" }],
345
+ channels: ["mcp", "zapier", "make", "n8n"]
346
+ },
347
+ {
348
+ id: "get_customer",
349
+ title: "Get one customer with subscriptions and addresses",
350
+ description: "Full picture of a single customer: the record plus their subscriptions and addresses in one response.",
351
+ kind: "read",
352
+ scopes: ["customers:read"],
353
+ input: [{ name: "id", type: "string", required: true, description: "Customer id." }],
354
+ requests: [
355
+ { method: "GET", path: "/api/v1/customers/:id" },
356
+ { method: "GET", path: "/api/v1/customers/:id/addresses" },
357
+ { method: "GET", path: "/api/v1/subscriptions" }
358
+ ],
359
+ channels: ["mcp"],
360
+ guardrails: [
361
+ "Sub-reads need customer_addresses:read / subscriptions:read. Omit a section the credential cannot read instead of failing."
362
+ ]
363
+ },
364
+ // ------------------------------------------------------------- subscriptions
365
+ {
366
+ id: "list_subscriptions",
367
+ title: "List subscriptions",
368
+ description: "List subscriptions with optional customer and status filters.",
369
+ kind: "read",
370
+ scopes: ["subscriptions:read"],
371
+ input: [
372
+ { name: "customerId", type: "string", required: false, description: "Filter by customer." },
373
+ { name: "status", type: "string", required: false, description: "Filter by status." },
374
+ { name: "limit", type: "number", required: false, description: "Page size." },
375
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
376
+ ],
377
+ requests: [{ method: "GET", path: "/api/v1/subscriptions" }],
378
+ channels: ["mcp", "zapier", "make", "n8n"]
379
+ },
380
+ {
381
+ id: "get_subscription",
382
+ title: "Get one subscription",
383
+ description: "Read a single subscription including its current period and pending changes.",
384
+ kind: "read",
385
+ scopes: ["subscriptions:read"],
386
+ input: [{ name: "id", type: "string", required: true, description: "Subscription id." }],
387
+ requests: [{ method: "GET", path: "/api/v1/subscriptions/:id" }],
388
+ channels: ["mcp", "zapier", "make", "n8n"]
389
+ },
390
+ // ------------------------------------------------------------------- quotes
391
+ {
392
+ id: "search_quotes",
393
+ title: "Search quotes",
394
+ description: 'List quotes with optional status filter \u2014 useful for "which quotes expire soon".',
395
+ kind: "read",
396
+ scopes: ["quotes:read"],
397
+ input: [
398
+ { name: "status", type: "string", required: false, description: "Filter by quote status." },
399
+ { name: "limit", type: "number", required: false, description: "Page size." },
400
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
401
+ ],
402
+ requests: [{ method: "GET", path: "/api/v1/quotes" }],
403
+ channels: ["mcp", "zapier", "make", "n8n"]
404
+ },
405
+ {
406
+ id: "get_quote",
407
+ title: "Get one quote",
408
+ description: "Read a single quote with its line items, revisions and buyer link state.",
409
+ kind: "read",
410
+ scopes: ["quotes:read"],
411
+ input: [{ name: "id", type: "string", required: true, description: "Quote id." }],
412
+ requests: [{ method: "GET", path: "/api/v1/quotes/:id" }],
413
+ channels: ["mcp", "zapier", "make", "n8n"]
414
+ },
415
+ // ----------------------------------------------------------------- invoices
416
+ {
417
+ id: "list_invoices",
418
+ title: "List invoices",
419
+ description: "List issued invoices with optional status filter.",
420
+ kind: "read",
421
+ scopes: ["invoices:read"],
422
+ input: [
423
+ { name: "status", type: "string", required: false, description: "Filter by invoice status." },
424
+ { name: "limit", type: "number", required: false, description: "Page size." },
425
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
426
+ ],
427
+ requests: [{ method: "GET", path: "/api/v1/invoices" }],
428
+ channels: ["mcp", "zapier", "make", "n8n"]
429
+ },
430
+ {
431
+ id: "receivables_aging",
432
+ title: "Receivables aging",
433
+ description: 'Outstanding receivables bucketed by age \u2014 answers "what is overdue and by how long".',
434
+ kind: "read",
435
+ scopes: ["invoices:read"],
436
+ input: [],
437
+ requests: [{ method: "GET", path: "/api/v1/invoices/aging" }],
438
+ channels: ["mcp", "zapier", "make", "n8n"]
439
+ },
440
+ // --------------------------------------------------------------- discounts
441
+ {
442
+ id: "search_discounts",
443
+ title: "Search discounts",
444
+ description: "List discount codes with optional filters \u2014 type, active state, and creation date range.",
445
+ kind: "read",
446
+ scopes: ["discounts:read"],
447
+ input: [
448
+ { name: "q", type: "string", required: false, description: "Free-text search." },
449
+ { name: "code", type: "string", required: false, description: "Exact code match." },
450
+ {
451
+ name: "type",
452
+ type: "string",
453
+ required: false,
454
+ description: "Filter by discount type.",
455
+ enum: ["percentage", "fixed_amount"]
456
+ },
457
+ { name: "isActive", type: "boolean", required: false, description: "Filter by active state." },
458
+ { name: "createdAfter", type: "string", required: false, description: "ISO 8601 date lower bound." },
459
+ { name: "createdBefore", type: "string", required: false, description: "ISO 8601 date upper bound." },
460
+ { name: "limit", type: "number", required: false, description: "Page size." },
461
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
462
+ ],
463
+ requests: [{ method: "GET", path: "/api/v1/discounts" }],
464
+ channels: ["mcp", "zapier", "make", "n8n"]
465
+ },
466
+ {
467
+ id: "get_discount",
468
+ title: "Get one discount",
469
+ description: "Read a single discount code: value, conditions, usage limits and redemption count.",
470
+ kind: "read",
471
+ scopes: ["discounts:read"],
472
+ input: [{ name: "id", type: "string", required: true, description: "Discount id." }],
473
+ requests: [{ method: "GET", path: "/api/v1/discounts/:id" }],
474
+ channels: ["mcp", "zapier", "make", "n8n"]
475
+ },
476
+ // ---------------------------------------------------------------- payments
477
+ {
478
+ id: "search_payments",
479
+ title: "Search payments",
480
+ 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.",
481
+ kind: "read",
482
+ scopes: ["payments:read"],
483
+ input: [
484
+ { name: "method", type: "string", required: false, description: "Filter by payment method." },
485
+ { name: "status", type: "string", required: false, description: "Filter by payment status." },
486
+ { name: "orderId", type: "string", required: false, description: "Filter to one order." },
487
+ {
488
+ name: "aging",
489
+ type: "string",
490
+ required: false,
491
+ description: "Overdue bucket filter, Net-N payments only.",
492
+ enum: ["0_30", "31_60", "61_90", "90_plus"]
493
+ },
494
+ { name: "dueFrom", type: "string", required: false, description: "Due-date range start, YYYY-MM-DD." },
495
+ { name: "dueTo", type: "string", required: false, description: "Due-date range end, YYYY-MM-DD." },
496
+ { name: "amountMin", type: "number", required: false, description: "Minimum amount, minor units." },
497
+ { name: "amountMax", type: "number", required: false, description: "Maximum amount, minor units." },
498
+ { name: "limit", type: "number", required: false, description: "Page size." },
499
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
500
+ ],
501
+ requests: [{ method: "GET", path: "/api/v1/payments" }],
502
+ channels: ["mcp", "zapier", "make", "n8n"]
503
+ },
504
+ {
505
+ id: "get_payment",
506
+ title: "Get one payment with its transactions",
507
+ description: "Read a single payment plus its full transaction history (authorize/capture/refund/void attempts).",
508
+ kind: "read",
509
+ scopes: ["payments:read"],
510
+ input: [{ name: "id", type: "string", required: true, description: "Payment id." }],
511
+ requests: [
512
+ { method: "GET", path: "/api/v1/payments/:id" },
513
+ { method: "GET", path: "/api/v1/payments/:id/transactions" }
514
+ ],
515
+ channels: ["mcp"],
516
+ guardrails: [
517
+ "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."
518
+ ]
519
+ },
520
+ // ---------------------------------------------------------------- invoices
521
+ {
522
+ id: "get_invoice",
523
+ title: "Get one invoice",
524
+ description: "Read a single issued invoice: line items, totals, status and the billing snapshot at issue time.",
525
+ kind: "read",
526
+ scopes: ["invoices:read"],
527
+ input: [{ name: "id", type: "string", required: true, description: "Invoice id." }],
528
+ requests: [{ method: "GET", path: "/api/v1/invoices/:id" }],
529
+ channels: ["mcp", "zapier", "make", "n8n"]
530
+ },
531
+ // ------------------------------------------------------------------ carts
532
+ {
533
+ id: "list_abandoned_carts",
534
+ title: "List abandoned carts",
535
+ description: "Carts that were started but never finalized, with recovery status \u2014 the abandoned-cart recovery list.",
536
+ kind: "read",
537
+ scopes: ["carts:read"],
538
+ input: [
539
+ { name: "sinceDays", type: "number", required: false, description: "How many days back to look." },
540
+ { name: "limit", type: "number", required: false, description: "Page size." },
541
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
542
+ ],
543
+ requests: [
544
+ { method: "GET", path: "/api/v1/abandoned-carts" },
545
+ { method: "GET", path: "/api/v1/abandoned-carts/summary" }
546
+ ],
547
+ channels: ["mcp", "zapier", "make", "n8n"],
548
+ guardrails: ["Report the summary as unavailable rather than failing the whole call if that sub-read errors."]
549
+ },
550
+ // ---------------------------------------------------------------- debugging
551
+ {
552
+ id: "search_events",
553
+ title: "Search the event log",
554
+ description: "Search the immutable event log by type or entity. The first stop when asking what actually happened to an order, cart or subscription.",
555
+ kind: "read",
556
+ scopes: ["events:read"],
557
+ input: [
558
+ { name: "type", type: "string", required: false, description: "Event type, e.g. order.created." },
559
+ { name: "entityType", type: "string", required: false, description: "Entity type filter." },
560
+ { name: "entityId", type: "string", required: false, description: "Entity id filter." },
561
+ { name: "limit", type: "number", required: false, description: "Page size." },
562
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
563
+ ],
564
+ requests: [{ method: "GET", path: "/api/v1/events" }],
565
+ channels: ["mcp", "n8n"]
566
+ },
567
+ {
568
+ id: "list_webhook_deliveries",
569
+ title: "List webhook deliveries",
570
+ description: "Recent outbound webhook deliveries, filterable by endpoint and status.",
571
+ kind: "read",
572
+ scopes: ["webhook_deliveries:read"],
573
+ input: [
574
+ { name: "endpointId", type: "string", required: false, description: "Filter by endpoint." },
575
+ { name: "status", type: "string", required: false, description: "Filter by delivery status." },
576
+ { name: "limit", type: "number", required: false, description: "Page size." }
577
+ ],
578
+ requests: [{ method: "GET", path: "/api/v1/webhook-deliveries" }],
579
+ channels: ["mcp", "n8n"]
580
+ },
581
+ {
582
+ id: "get_webhook_delivery",
583
+ title: "Get one webhook delivery",
584
+ description: "Full delivery record: the signed payload we sent, the response we got back, and every attempt.",
585
+ kind: "read",
586
+ scopes: ["webhook_deliveries:read"],
587
+ input: [{ name: "id", type: "string", required: true, description: "Delivery id." }],
588
+ requests: [{ method: "GET", path: "/api/v1/webhook-deliveries/:id" }],
589
+ channels: ["mcp", "n8n"]
590
+ },
591
+ {
592
+ id: "explain_checkout_failure",
593
+ title: "Explain why a checkout failed",
594
+ 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.",
595
+ kind: "read",
596
+ scopes: ["checkout_sessions:read"],
597
+ input: [
598
+ {
599
+ name: "id",
600
+ type: "string",
601
+ required: true,
602
+ description: "Checkout session id (or cart id \u2014 the tool resolves the session)."
603
+ }
604
+ ],
605
+ requests: [
606
+ { method: "GET", path: "/api/v1/checkout-sessions/:id" },
607
+ { method: "GET", path: "/api/v1/checkout-sessions/:id/cart" },
608
+ { method: "GET", path: "/api/v1/events" },
609
+ { method: "GET", path: "/api/v1/webhook-deliveries" }
610
+ ],
611
+ channels: ["mcp"],
612
+ guardrails: [
613
+ "Report which sections were unavailable rather than failing the call when a sub-read is unscoped."
614
+ ]
615
+ },
616
+ // ------------------------------------------------------------- extensions
617
+ {
618
+ id: "list_extension_installations",
619
+ title: "List installed extensions",
620
+ description: "Extensions installed on this application, with status and configuration.",
621
+ kind: "read",
622
+ scopes: ["extensions:read"],
623
+ input: [
624
+ {
625
+ name: "status",
626
+ type: "string",
627
+ required: false,
628
+ description: "Filter by installation status.",
629
+ enum: ["active", "suspended", "uninstalled"]
630
+ },
631
+ { name: "limit", type: "number", required: false, description: "Page size." },
632
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
633
+ ],
634
+ requests: [{ method: "GET", path: "/api/v1/installations" }],
635
+ channels: ["mcp", "zapier", "make", "n8n"]
636
+ },
637
+ // ------------------------------------------------------------- connectors
638
+ {
639
+ id: "get_connector_routing",
640
+ title: "Payment routing and decline rules",
641
+ 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.",
642
+ kind: "read",
643
+ scopes: ["connectors:read"],
644
+ input: [],
645
+ requests: [
646
+ { method: "GET", path: "/api/v1/payment-routing" },
647
+ { method: "GET", path: "/api/v1/routing-connectors" },
648
+ { method: "GET", path: "/api/v1/card-decline" },
649
+ { method: "GET", path: "/api/v1/other-decline" }
650
+ ],
651
+ channels: ["mcp"],
652
+ guardrails: [
653
+ "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."
654
+ ]
655
+ },
656
+ {
657
+ id: "get_integration_status",
658
+ title: "Integration readiness",
659
+ 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.',
660
+ kind: "read",
661
+ scopes: [],
662
+ input: [],
663
+ requests: [
664
+ { method: "GET", path: "/api/v1/whoami" },
665
+ { method: "GET", path: "/api/v1/connectors" },
666
+ { method: "GET", path: "/api/v1/webhook-endpoints" },
667
+ { method: "GET", path: "/api/v1/webhook-endpoints/coverage" }
668
+ ],
669
+ channels: ["mcp"],
670
+ guardrails: [
671
+ 'Each section needs its own scope (connectors:read, webhooks:read). Report a section as "not readable with this credential" instead of failing.'
672
+ ]
673
+ },
674
+ // --------------------------------------------------------------- workspace
675
+ {
676
+ id: "get_workspace_settings",
677
+ title: "Get workspace settings",
678
+ description: "Read this workspace's identity and settings: name, slug, branding, onboarding state, and the default Net-N payment terms.",
679
+ kind: "read",
680
+ scopes: ["merchant:read"],
681
+ input: [],
682
+ requests: [{ method: "GET", path: "/api/v1/merchants/me" }],
683
+ channels: ["mcp", "zapier", "make", "n8n"]
684
+ },
685
+ {
686
+ id: "get_audit_log",
687
+ title: "Get the workspace audit log",
688
+ description: "Recent admin actions taken on this workspace \u2014 invites, billing changes, key rotation, suspensions.",
689
+ kind: "read",
690
+ scopes: ["audit_logs:read"],
691
+ input: [
692
+ { name: "limit", type: "number", required: false, description: "Maximum entries (1-200).", default: 100 }
693
+ ],
694
+ requests: [{ method: "GET", path: "/api/v1/audit-log" }],
695
+ channels: ["mcp", "zapier", "make", "n8n"]
696
+ },
697
+ {
698
+ id: "get_business_summary",
699
+ title: "Business summary",
700
+ description: "How are we doing? Orders/revenue, subscription MRR, receivables and quotes pipeline for a week/month/quarter, always compared to the immediately preceding period of equal length.",
701
+ kind: "read",
702
+ scopes: [],
703
+ input: [
704
+ {
705
+ name: "period",
706
+ type: "string",
707
+ required: false,
708
+ description: "Reporting window.",
709
+ enum: ["week", "month", "quarter"],
710
+ default: "month"
711
+ }
712
+ ],
713
+ requests: [{ method: "GET", path: "/api/v1/analytics/business-summary" }],
714
+ channels: ["mcp", "zapier", "make", "n8n"],
715
+ guardrails: [
716
+ "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."
717
+ ]
718
+ },
719
+ {
720
+ id: "get_attention_digest",
721
+ title: "Attention digest",
722
+ 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.",
723
+ kind: "read",
724
+ scopes: [],
725
+ input: [],
726
+ requests: [{ method: "GET", path: "/api/v1/analytics/attention-digest" }],
727
+ channels: ["mcp", "zapier", "make", "n8n"],
728
+ guardrails: [
729
+ "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."
730
+ ]
731
+ },
732
+ // ------------------------------------------------------------ sandbox write
733
+ {
734
+ id: "seed_test_data",
735
+ title: "Seed test data (non-production only)",
736
+ 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.",
737
+ kind: "write",
738
+ scopes: ["customers:write", "carts:write", "cart_items:write"],
739
+ input: [
740
+ { name: "customerEmail", type: "string", required: false, description: "Email for the seeded customer." },
741
+ { name: "lineItems", type: "array", required: false, description: "Line items; a sensible default set is used when omitted." }
742
+ ],
743
+ requests: [
744
+ { method: "POST", path: "/api/v1/customers" },
745
+ { method: "POST", path: "/api/v1/carts" },
746
+ { method: "POST", path: "/api/v1/carts/:id/items" },
747
+ { method: "POST", path: "/api/v1/carts/:id/finalize" }
748
+ ],
749
+ channels: ["mcp"],
750
+ sandboxOnly: true,
751
+ idempotent: true,
752
+ guardrails: [
753
+ "Hard-refuse when the credential resolves to a production environment, regardless of any allow-writes flag.",
754
+ "Seeds a finalized DRAFT order \u2014 it does not capture a payment, which needs a browser-side tokenised payment method."
755
+ ]
756
+ },
757
+ // -------------------------------------------------------------- money/mail
758
+ {
759
+ id: "create_quote",
760
+ title: "Create a quote",
761
+ description: "Draft a quote for a customer with line items and terms. Does not send it \u2014 use send_quote.",
762
+ kind: "write",
763
+ scopes: ["quotes:write"],
764
+ input: [
765
+ { name: "customerId", type: "string", required: true, description: "Customer the quote is for." },
766
+ { name: "items", type: "array", required: true, description: "Quote line items: { name, unitPrice, quantity }." },
767
+ { name: "expiresAt", type: "string", required: false, description: "ISO 8601 expiry." },
768
+ { name: "netN", type: "number", required: false, description: "Payment terms in days. Omit for pay-in-full." },
769
+ { name: "notes", type: "string", required: false, description: "Buyer-visible notes." }
770
+ ],
771
+ requests: [{ method: "POST", path: "/api/v1/quotes" }],
772
+ channels: ["mcp", "zapier", "make", "n8n"],
773
+ idempotent: true
774
+ },
775
+ {
776
+ id: "send_quote",
777
+ title: "Issue and send a quote",
778
+ description: "Issue a draft quote and email the buyer their quote link. This is buyer-visible \u2014 confirm with the user before calling it.",
779
+ kind: "write",
780
+ scopes: ["quotes:write"],
781
+ input: [{ name: "id", type: "string", required: true, description: "Quote id." }],
782
+ requests: [{ method: "POST", path: "/api/v1/quotes/:id/issue" }],
783
+ channels: ["mcp", "zapier", "make", "n8n"],
784
+ idempotent: true,
785
+ guardrails: ['Sends mail to a real buyer. Never call it speculatively or to "check" a quote.']
786
+ },
787
+ {
788
+ id: "refund_payment",
789
+ title: "Refund a payment",
790
+ description: "Refund a captured payment. The amount is required \u2014 never infer a full refund from an unspecified amount.",
791
+ kind: "write",
792
+ scopes: ["payment_refunds:write"],
793
+ input: [
794
+ { name: "id", type: "string", required: true, description: "Payment id." },
795
+ {
796
+ name: "amount",
797
+ type: "number",
798
+ required: true,
799
+ description: "Amount to refund in minor units. Required \u2014 omitting it would refund the whole payment."
800
+ },
801
+ { name: "reason", type: "string", required: false, description: "Reason recorded on the refund." }
802
+ ],
803
+ requests: [{ method: "POST", path: "/api/v1/payments/:id/refund" }],
804
+ channels: ["mcp", "zapier", "make", "n8n"],
805
+ idempotent: true,
806
+ guardrails: [
807
+ "Moves money and is irreversible. Confirm with the user first.",
808
+ "The API treats an omitted amount as a FULL refund, so this action requires it explicitly."
809
+ ]
810
+ },
811
+ {
812
+ id: "manage_subscription",
813
+ title: "Pause or resume a subscription",
814
+ description: "Pause or resume an active subscription.",
815
+ kind: "write",
816
+ scopes: ["subscription_pauses:write"],
817
+ input: [
818
+ { name: "id", type: "string", required: true, description: "Subscription id." },
819
+ {
820
+ name: "action",
821
+ type: "string",
822
+ required: true,
823
+ description: "What to do.",
824
+ enum: ["pause", "resume"]
825
+ }
826
+ ],
827
+ requests: [
828
+ { method: "POST", path: "/api/v1/subscriptions/:id/pause" },
829
+ { method: "POST", path: "/api/v1/subscriptions/:id/resume" }
830
+ ],
831
+ channels: ["mcp", "zapier", "make", "n8n"],
832
+ idempotent: true
833
+ },
834
+ {
835
+ id: "cancel_subscription",
836
+ title: "Cancel a subscription",
837
+ description: "Cancel a subscription. Separate from pause/resume because it needs its own scope.",
838
+ kind: "write",
839
+ scopes: ["subscription_cancellations:write"],
840
+ input: [
841
+ { name: "id", type: "string", required: true, description: "Subscription id." },
842
+ {
843
+ name: "atPeriodEnd",
844
+ type: "boolean",
845
+ required: false,
846
+ description: "Cancel at the end of the current period instead of immediately."
847
+ }
848
+ ],
849
+ requests: [{ method: "POST", path: "/api/v1/subscriptions/:id/cancel" }],
850
+ channels: ["mcp", "zapier", "make", "n8n"],
851
+ idempotent: true,
852
+ guardrails: ["Ends billing for a paying customer. Confirm with the user first."]
853
+ },
854
+ {
855
+ id: "replay_webhook_delivery",
856
+ title: "Replay a webhook delivery",
857
+ description: "Re-send a past webhook delivery to its endpoint \u2014 useful after fixing a broken handler.",
858
+ kind: "write",
859
+ scopes: ["webhook_deliveries:write"],
860
+ input: [{ name: "id", type: "string", required: true, description: "Delivery id." }],
861
+ requests: [{ method: "POST", path: "/api/v1/webhook-deliveries/:id/replay" }],
862
+ channels: ["mcp", "n8n"],
863
+ idempotent: true,
864
+ guardrails: ["The receiving system will process the event again. Confirm it is idempotent first."]
865
+ },
866
+ {
867
+ id: "send_test_webhook",
868
+ title: "Send a test webhook",
869
+ description: "Send a synthetic signed event to a webhook endpoint to verify signature handling and reachability.",
870
+ kind: "write",
871
+ scopes: ["webhooks:write"],
872
+ input: [
873
+ { name: "id", type: "string", required: true, description: "Endpoint id." },
874
+ { name: "eventType", type: "string", required: false, description: "Event type to simulate." }
875
+ ],
876
+ requests: [{ method: "POST", path: "/api/v1/webhook-endpoints/:id/test" }],
877
+ channels: ["mcp", "n8n"],
878
+ idempotent: true
879
+ }
880
+ ];
881
+
882
+ // ../action-catalog/src/triggers.ts
883
+ var NO_CODE = ["zapier", "make", "n8n"];
884
+ function trigger(id, title, eventType, description) {
885
+ return { id, title, eventType, description, scope: eventReadScope(eventType), channels: NO_CODE };
886
+ }
887
+ var TRIGGERS = [
888
+ trigger("new_order", "New order", "order.created", "Fires when an order is created."),
889
+ trigger(
890
+ "payment_captured",
891
+ "Payment captured",
892
+ "payment.captured",
893
+ "Fires when a payment is successfully captured."
894
+ ),
895
+ trigger(
896
+ "payment_failed",
897
+ "Payment failed",
898
+ "payment.failed",
899
+ "Fires when a payment attempt fails."
900
+ ),
901
+ trigger(
902
+ "new_subscription",
903
+ "New subscription",
904
+ "subscription.created",
905
+ "Fires when a subscription is created."
906
+ ),
907
+ trigger(
908
+ "subscription_cancelled",
909
+ "Subscription cancelled",
910
+ "subscription.cancelled",
911
+ "Fires when a subscription is cancelled."
912
+ ),
913
+ trigger(
914
+ "quote_accepted",
915
+ "Quote accepted",
916
+ "quote.accepted",
917
+ "Fires when a buyer accepts a quote."
918
+ ),
919
+ trigger(
920
+ "cart_abandoned",
921
+ "Cart abandoned",
922
+ "cart.abandoned",
923
+ "Fires when a cart is marked abandoned."
924
+ ),
925
+ trigger(
926
+ "invoice_past_due",
927
+ "Invoice past due",
928
+ "invoice.past_due",
929
+ "Fires when an invoice passes its due date unpaid."
930
+ )
931
+ ];
932
+
933
+ // ../action-catalog/src/index.ts
934
+ function actionsForChannel(channel) {
935
+ return ACTIONS.filter((a) => a.channels.includes(channel));
936
+ }
937
+ function canRunAction(granted, action) {
938
+ return action.scopes.every((required) => hasScope(granted, required));
939
+ }
940
+
941
+ // src/format.ts
942
+ var MAX_CHARS = 24e3;
943
+ function prune(value) {
944
+ if (Array.isArray(value)) return value.map(prune);
945
+ if (value && typeof value === "object") {
946
+ const out = {};
947
+ for (const [key, inner] of Object.entries(value)) {
948
+ if (inner === void 0 || inner === null) continue;
949
+ out[key] = prune(inner);
950
+ }
951
+ return out;
952
+ }
953
+ return value;
954
+ }
955
+ function formatResult(value) {
956
+ const text = JSON.stringify(prune(value), null, 2) ?? "null";
957
+ if (text.length <= MAX_CHARS) return text;
958
+ return `${text.slice(0, MAX_CHARS)}
959
+
960
+ \u2026 truncated at ${MAX_CHARS} characters. Narrow the query (lower "limit", add a filter) or fetch a single record.`;
961
+ }
962
+ function formatError(error) {
963
+ const e = error;
964
+ if (e?.code && e?.message) {
965
+ const detail = e.details ? `
966
+
967
+ details: ${JSON.stringify(e.details)}` : "";
968
+ return `Throttle API error ${e.code}${e.statusCode ? ` (HTTP ${e.statusCode})` : ""}: ${e.message}${detail}`;
969
+ }
970
+ return `Error: ${e?.message ?? String(error)}`;
971
+ }
972
+
973
+ // ../errors/dist/index.js
974
+ var ThrottleError = class extends Error {
975
+ code;
976
+ statusCode;
977
+ details;
978
+ body;
979
+ constructor(args) {
980
+ super(args.message);
981
+ this.name = "ThrottleError";
982
+ this.code = args.code;
983
+ this.statusCode = args.statusCode;
984
+ this.details = args.details;
985
+ this.body = args.body;
986
+ Object.setPrototypeOf(this, new.target.prototype);
987
+ }
988
+ };
989
+
990
+ // src/client.ts
991
+ var ThrottleApiError = class extends ThrottleError {
992
+ constructor(code, message, statusCode, details) {
993
+ super({ code, message, statusCode, details });
994
+ this.name = "ThrottleApiError";
995
+ }
996
+ };
997
+ function buildQuery(query) {
998
+ if (!query) return "";
999
+ const params = new URLSearchParams();
1000
+ for (const [key, value] of Object.entries(query)) {
1001
+ if (value === void 0 || value === null || value === "") continue;
1002
+ params.set(key, String(value));
1003
+ }
1004
+ const qs = params.toString();
1005
+ return qs ? `?${qs}` : "";
1006
+ }
1007
+ var ThrottleClient = class {
1008
+ constructor(auth, fetchImpl = fetch) {
1009
+ this.auth = auth;
1010
+ this.fetchImpl = fetchImpl;
1011
+ }
1012
+ async requestJson(method, path, options = {}) {
1013
+ const url = `${this.auth.apiBaseUrl.replace(/\/$/, "")}${path}${buildQuery(options.query)}`;
1014
+ const headers = {
1015
+ "content-type": "application/json",
1016
+ "x-api-key": this.auth.apiKey
1017
+ };
1018
+ if (options.idempotencyKey) headers["idempotency-key"] = options.idempotencyKey;
1019
+ const sendsBody = method === "POST" || method === "PATCH" || method === "PUT";
1020
+ const body = options.body !== void 0 ? JSON.stringify(options.body) : sendsBody ? "{}" : void 0;
1021
+ const res = await this.fetchImpl(url, { method, headers, body });
1022
+ const text = await res.text();
1023
+ let parsed = {};
1024
+ try {
1025
+ parsed = text ? JSON.parse(text) : {};
1026
+ } catch {
1027
+ }
1028
+ if (!res.ok) {
1029
+ const err = parsed.error;
1030
+ throw new ThrottleApiError(
1031
+ err?.code ?? `http_${res.status}`,
1032
+ err?.message ?? `Request failed (${res.status}): ${text.slice(0, 200)}`,
1033
+ res.status,
1034
+ err?.details
1035
+ );
1036
+ }
1037
+ return parsed;
1038
+ }
1039
+ /** Unwraps the `{ data }` envelope. Key presence, not `??` — `{data: null}` unwraps to null. */
1040
+ async get(path, query) {
1041
+ const body = await this.requestJson("GET", path, { query });
1042
+ return body && typeof body === "object" && "data" in body ? body.data : body;
1043
+ }
1044
+ async list(path, query) {
1045
+ const body = await this.requestJson("GET", path, { query });
1046
+ const data = "data" in body ? body.data : body;
1047
+ const meta = body.meta;
1048
+ return { data: Array.isArray(data) ? data : [], pagination: meta?.pagination ?? null };
1049
+ }
1050
+ async post(path, options = {}) {
1051
+ const body = await this.requestJson("POST", path, options);
1052
+ return body && typeof body === "object" && "data" in body ? body.data : body;
1053
+ }
1054
+ };
1055
+ async function optional(load) {
1056
+ try {
1057
+ return { ok: true, value: await load() };
1058
+ } catch (error) {
1059
+ const e = error;
1060
+ const code = e?.code ?? "error";
1061
+ const reason = code === "insufficient_scopes" ? "not readable with this credential (missing scope)" : code === "not_found" ? "not found" : `unavailable (${code})`;
1062
+ return { ok: false, unavailable: reason };
1063
+ }
1064
+ }
1065
+
1066
+ // src/contract.ts
1067
+ var RETIRED_FIELDS = {
1068
+ storeId: "applicationId",
1069
+ store_id: "applicationId",
1070
+ merchantId: "workspaceId",
1071
+ merchant_id: "workspaceId",
1072
+ customer_id: "customerId",
1073
+ line_items: "lineItems",
1074
+ unit_price: "unitPrice",
1075
+ external_id: "externalId",
1076
+ payment_method_id: "paymentMethodId",
1077
+ first_name: "firstName",
1078
+ last_name: "lastName",
1079
+ net_n: "netN",
1080
+ hosted_url: "hostedUrl",
1081
+ embed_url: "embedUrl",
1082
+ checkout_url: "checkoutUrl",
1083
+ order_number: "orderNumber",
1084
+ payment_status: "paymentStatus",
1085
+ allowed_origins: "allowedOrigins",
1086
+ enabled_events: "enabledEvents",
1087
+ template_key: "templateKey",
1088
+ request_id: "requestId",
1089
+ signing_secret: "signingSecret (returned once at creation; not accepted as input)"
1090
+ };
1091
+ function snakeToCamel(name) {
1092
+ return name.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
1093
+ }
1094
+ function normalizePath(path) {
1095
+ return path.replace(/\{[^}]+\}/g, "{}").replace(/:[A-Za-z0-9_]+/g, "{}").replace(/\/$/, "");
1096
+ }
1097
+ function segmentsOf(path) {
1098
+ return path.split("?")[0].replace(/\/$/, "").split("/").filter(Boolean);
1099
+ }
1100
+ function isParamSegment(segment) {
1101
+ return /^\{[^}]+\}$/.test(segment) || /^:[A-Za-z0-9_]+$/.test(segment);
1102
+ }
1103
+ function pathMatches(template, candidate) {
1104
+ const t = segmentsOf(template);
1105
+ const c = segmentsOf(candidate);
1106
+ if (t.length !== c.length) return false;
1107
+ return t.every((segment, i) => isParamSegment(segment) || isParamSegment(c[i]) || segment === c[i]);
1108
+ }
1109
+ function paramCount(path) {
1110
+ return segmentsOf(path).filter(isParamSegment).length;
1111
+ }
1112
+ function findOperations(index, query) {
1113
+ const method = query.method?.toUpperCase();
1114
+ const wanted = query.path.trim();
1115
+ const byMethod = (op) => !method || op.method === method;
1116
+ const matched = index.operations.filter((op) => byMethod(op) && pathMatches(op.path, wanted)).sort((a, b) => paramCount(a.path) - paramCount(b.path));
1117
+ if (matched.length > 0) return matched;
1118
+ const needle = normalizePath(wanted).replace(/^\/+/, "");
1119
+ return index.operations.filter((op) => byMethod(op) && normalizePath(op.path).includes(needle));
1120
+ }
1121
+ function validatePayload(index, input) {
1122
+ const errors = [];
1123
+ const warnings = [];
1124
+ const keys = Object.keys(input.body ?? {});
1125
+ for (const key of keys) {
1126
+ const retired = RETIRED_FIELDS[key];
1127
+ if (retired) {
1128
+ errors.push({
1129
+ field: key,
1130
+ problem: "retired public field name \u2014 the API rejects it",
1131
+ suggestion: `use "${retired}"`
1132
+ });
1133
+ continue;
1134
+ }
1135
+ if (key.includes("_")) {
1136
+ errors.push({
1137
+ field: key,
1138
+ problem: "snake_case is not accepted on public request bodies",
1139
+ suggestion: `use "${snakeToCamel(key)}"`
1140
+ });
1141
+ }
1142
+ }
1143
+ const matches = findOperations(index, { path: input.path, method: input.method });
1144
+ const operation = matches[0];
1145
+ if (!operation) {
1146
+ return {
1147
+ ok: false,
1148
+ matched: null,
1149
+ errors: [
1150
+ ...errors,
1151
+ {
1152
+ field: input.path,
1153
+ problem: `no ${input.method.toUpperCase()} operation matches this path in the published contract`,
1154
+ suggestion: "call describe_endpoint to find the right path"
1155
+ }
1156
+ ],
1157
+ warnings
1158
+ };
1159
+ }
1160
+ if (!operation.bodyFields) {
1161
+ return {
1162
+ ok: errors.length === 0,
1163
+ matched: `${operation.method} ${operation.path}`,
1164
+ errors,
1165
+ warnings,
1166
+ note: "The contract publishes no request-body schema for this operation, so field-level checks beyond naming could not run."
1167
+ };
1168
+ }
1169
+ const known = new Map(operation.bodyFields.map((f) => [f.name, f]));
1170
+ for (const key of keys) {
1171
+ if (RETIRED_FIELDS[key] || key.includes("_")) continue;
1172
+ if (!known.has(key)) {
1173
+ warnings.push({
1174
+ field: key,
1175
+ problem: "not documented on this operation \u2014 it will be rejected if the route validates strictly"
1176
+ });
1177
+ }
1178
+ }
1179
+ for (const field of operation.bodyFields) {
1180
+ const value = (input.body ?? {})[field.name];
1181
+ if (value === void 0) {
1182
+ if (field.required) errors.push({ field: field.name, problem: "required field is missing" });
1183
+ continue;
1184
+ }
1185
+ if (field.enum && field.enum.length > 0 && typeof value === "string" && !field.enum.includes(value)) {
1186
+ errors.push({
1187
+ field: field.name,
1188
+ problem: `value "${value}" is not allowed`,
1189
+ suggestion: `one of: ${field.enum.join(", ")}`
1190
+ });
1191
+ }
1192
+ }
1193
+ return {
1194
+ ok: errors.length === 0,
1195
+ matched: `${operation.method} ${operation.path}`,
1196
+ errors,
1197
+ warnings
1198
+ };
1199
+ }
1200
+
1201
+ // src/docs.ts
1202
+ var DOCS_ORIGIN = "https://docs.usethrottle.dev";
1203
+ function excerptAround(text, needle, radius = 160) {
1204
+ const at = text.toLowerCase().indexOf(needle.toLowerCase());
1205
+ if (at < 0) return text.slice(0, radius * 2).trim();
1206
+ const start = Math.max(0, at - radius);
1207
+ const end = Math.min(text.length, at + needle.length + radius);
1208
+ return `${start > 0 ? "\u2026" : ""}${text.slice(start, end).trim()}${end < text.length ? "\u2026" : ""}`;
1209
+ }
1210
+ function searchDocs(index, query, limit = 5) {
1211
+ const phrase = query.trim().toLowerCase();
1212
+ if (!phrase) return [];
1213
+ const terms = phrase.split(/\s+/).filter((t) => t.length > 1);
1214
+ const scored = index.entries.map((entry) => {
1215
+ const title = entry.title.toLowerCase();
1216
+ const headings = entry.headings.join(" \0 ").toLowerCase();
1217
+ const body = entry.text.toLowerCase();
1218
+ let score = 0;
1219
+ if (title === phrase) score += 100;
1220
+ else if (title.startsWith(phrase)) score += 60;
1221
+ else if (title.includes(phrase)) score += 40;
1222
+ if (headings.includes(phrase)) score += 25;
1223
+ if (body.includes(phrase)) score += 15;
1224
+ for (const term of terms) {
1225
+ if (title.includes(term)) score += 8;
1226
+ if (headings.includes(term)) score += 4;
1227
+ if (body.includes(term)) score += 1;
1228
+ }
1229
+ return { entry, score };
1230
+ });
1231
+ 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 }) => ({
1232
+ title: entry.title,
1233
+ url: `${DOCS_ORIGIN}${entry.route}`,
1234
+ score,
1235
+ excerpt: excerptAround(entry.text, phrase)
1236
+ }));
1237
+ }
1238
+
1239
+ // src/handlers.ts
1240
+ function coveringAction(operation) {
1241
+ const target = normalizePath(operation.path);
1242
+ for (const action of ACTIONS) {
1243
+ const hit = action.requests.some(
1244
+ (r) => r.method === operation.method && normalizePath(r.path) === target
1245
+ );
1246
+ if (hit) return { tool: action.id, requiredScopes: action.scopes };
1247
+ }
1248
+ return {};
1249
+ }
1250
+ var page = (args) => ({ limit: args.limit, cursor: args.cursor });
1251
+ var HANDLERS = {
1252
+ // ---------------------------------------------------------------- identity
1253
+ whoami: async (ctx) => ({
1254
+ ...ctx.identity,
1255
+ 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."
1256
+ }),
1257
+ // ------------------------------------------------------- docs and contract
1258
+ search_docs: async (ctx, args) => {
1259
+ const hits = searchDocs(ctx.docs, String(args.query ?? ""), Number(args.limit ?? 5));
1260
+ return hits.length > 0 ? { hits } : { hits: [], note: "No documentation matched. Try fewer or more general terms." };
1261
+ },
1262
+ describe_endpoint: async (ctx, args) => {
1263
+ const operations = findOperations(ctx.contract, {
1264
+ path: String(args.path ?? ""),
1265
+ method: args.method ? String(args.method) : void 0
1266
+ });
1267
+ if (operations.length === 0) {
1268
+ return {
1269
+ operations: [],
1270
+ note: 'No operation matched. Paths look like /api/v1/orders; try a shorter fragment such as "orders".'
1271
+ };
1272
+ }
1273
+ return {
1274
+ operations: operations.slice(0, 8).map((op) => ({ ...op, ...coveringAction(op) })),
1275
+ truncated: operations.length > 8 ? operations.length - 8 : void 0
1276
+ };
1277
+ },
1278
+ validate_payload: async (ctx, args) => validatePayload(ctx.contract, {
1279
+ method: String(args.method ?? "POST"),
1280
+ path: String(args.path ?? ""),
1281
+ body: args.body ?? {}
1282
+ }),
1283
+ // ------------------------------------------------------------- generic read
1284
+ api_get: async (ctx, args) => {
1285
+ const path = String(args.path ?? "");
1286
+ if (!path.startsWith("/api/v1/")) {
1287
+ throw new Error(
1288
+ `Refusing to call "${path}". api_get only reads paths beginning with /api/v1/ on the configured Throttle host.`
1289
+ );
1290
+ }
1291
+ return ctx.client.get(path);
1292
+ },
1293
+ // ------------------------------------------------------------------ orders
1294
+ search_orders: async (ctx, args) => ctx.client.list("/api/v1/orders", {
1295
+ q: args.q,
1296
+ status: args.status,
1297
+ paymentStatus: args.paymentStatus,
1298
+ ...page(args)
1299
+ }),
1300
+ get_order: async (ctx, args) => {
1301
+ const id = String(args.id);
1302
+ const order = await ctx.client.get(`/api/v1/orders/${id}`);
1303
+ const [payments, returns, fulfillments] = await Promise.all([
1304
+ optional(() => ctx.client.list(`/api/v1/orders/${id}/payments`)),
1305
+ optional(() => ctx.client.list(`/api/v1/orders/${id}/returns`)),
1306
+ optional(() => ctx.client.list(`/api/v1/orders/${id}/fulfillments`))
1307
+ ]);
1308
+ return {
1309
+ order,
1310
+ payments: payments.ok ? payments.value.data : payments.unavailable,
1311
+ returns: returns.ok ? returns.value.data : returns.unavailable,
1312
+ fulfillments: fulfillments.ok ? fulfillments.value.data : fulfillments.unavailable
1313
+ };
1314
+ },
1315
+ // --------------------------------------------------------------- customers
1316
+ search_customers: async (ctx, args) => ctx.client.list("/api/v1/customers", { q: args.q, ...page(args) }),
1317
+ get_customer: async (ctx, args) => {
1318
+ const id = String(args.id);
1319
+ const customer = await ctx.client.get(`/api/v1/customers/${id}`);
1320
+ const [addresses, subscriptions] = await Promise.all([
1321
+ optional(() => ctx.client.list(`/api/v1/customers/${id}/addresses`)),
1322
+ optional(() => ctx.client.list("/api/v1/subscriptions", { customerId: id }))
1323
+ ]);
1324
+ return {
1325
+ customer,
1326
+ addresses: addresses.ok ? addresses.value.data : addresses.unavailable,
1327
+ subscriptions: subscriptions.ok ? subscriptions.value.data : subscriptions.unavailable
1328
+ };
1329
+ },
1330
+ // ----------------------------------------------------------- subscriptions
1331
+ list_subscriptions: async (ctx, args) => ctx.client.list("/api/v1/subscriptions", {
1332
+ customerId: args.customerId,
1333
+ status: args.status,
1334
+ ...page(args)
1335
+ }),
1336
+ get_subscription: async (ctx, args) => ctx.client.get(`/api/v1/subscriptions/${String(args.id)}`),
1337
+ // ------------------------------------------------------------------ quotes
1338
+ search_quotes: async (ctx, args) => ctx.client.list("/api/v1/quotes", { status: args.status, ...page(args) }),
1339
+ get_quote: async (ctx, args) => ctx.client.get(`/api/v1/quotes/${String(args.id)}`),
1340
+ // ---------------------------------------------------------------- invoices
1341
+ list_invoices: async (ctx, args) => ctx.client.list("/api/v1/invoices", { status: args.status, ...page(args) }),
1342
+ receivables_aging: async (ctx) => ctx.client.get("/api/v1/invoices/aging"),
1343
+ // --------------------------------------------------------------- discounts
1344
+ search_discounts: async (ctx, args) => ctx.client.list("/api/v1/discounts", {
1345
+ q: args.q,
1346
+ code: args.code,
1347
+ type: args.type,
1348
+ isActive: args.isActive,
1349
+ createdAfter: args.createdAfter,
1350
+ createdBefore: args.createdBefore,
1351
+ ...page(args)
1352
+ }),
1353
+ get_discount: async (ctx, args) => ctx.client.get(`/api/v1/discounts/${String(args.id)}`),
1354
+ // ---------------------------------------------------------------- payments
1355
+ search_payments: async (ctx, args) => ctx.client.list("/api/v1/payments", {
1356
+ method: args.method,
1357
+ status: args.status,
1358
+ // The route only accepts these as snake_case query params -- confirmed
1359
+ // by reading its querystring schema directly, not assumed from the
1360
+ // platform's usual camelCase convention.
1361
+ order_id: args.orderId,
1362
+ aging: args.aging,
1363
+ due_from: args.dueFrom,
1364
+ due_to: args.dueTo,
1365
+ amount_min: args.amountMin,
1366
+ amount_max: args.amountMax,
1367
+ ...page(args)
1368
+ }),
1369
+ get_payment: async (ctx, args) => {
1370
+ const id = String(args.id);
1371
+ const payment = await ctx.client.get(`/api/v1/payments/${id}`);
1372
+ const transactions = await optional(() => ctx.client.list(`/api/v1/payments/${id}/transactions`));
1373
+ return {
1374
+ payment,
1375
+ transactions: transactions.ok ? transactions.value.data : transactions.unavailable
1376
+ };
1377
+ },
1378
+ // ---------------------------------------------------------------- invoices
1379
+ get_invoice: async (ctx, args) => ctx.client.get(`/api/v1/invoices/${String(args.id)}`),
1380
+ // ------------------------------------------------------------------ carts
1381
+ list_abandoned_carts: async (ctx, args) => {
1382
+ const [carts, summary] = await Promise.all([
1383
+ optional(() => ctx.client.list("/api/v1/abandoned-carts", { ...page(args) })),
1384
+ optional(() => ctx.client.get("/api/v1/abandoned-carts/summary", { sinceDays: args.sinceDays }))
1385
+ ]);
1386
+ return {
1387
+ carts: carts.ok ? carts.value.data : carts.unavailable,
1388
+ summary: summary.ok ? summary.value : summary.unavailable
1389
+ };
1390
+ },
1391
+ // --------------------------------------------------------------- debugging
1392
+ search_events: async (ctx, args) => ctx.client.list("/api/v1/events", {
1393
+ type: args.type,
1394
+ entityType: args.entityType,
1395
+ entityId: args.entityId,
1396
+ ...page(args)
1397
+ }),
1398
+ list_webhook_deliveries: async (ctx, args) => ctx.client.list("/api/v1/webhook-deliveries", {
1399
+ endpointId: args.endpointId,
1400
+ status: args.status,
1401
+ limit: args.limit
1402
+ }),
1403
+ get_webhook_delivery: async (ctx, args) => ctx.client.get(`/api/v1/webhook-deliveries/${String(args.id)}`),
1404
+ send_test_webhook: async (ctx, args) => ctx.client.post(`/api/v1/webhook-endpoints/${String(args.id)}/test`, {
1405
+ body: { eventType: args.eventType },
1406
+ idempotencyKey: `mcp-send-test-webhook-${String(args.id)}-${Date.now()}`
1407
+ }),
1408
+ replay_webhook_delivery: async (ctx, args) => ctx.client.post(`/api/v1/webhook-deliveries/${String(args.id)}/replay`, {
1409
+ idempotencyKey: `mcp-replay-webhook-delivery-${String(args.id)}-${Date.now()}`
1410
+ }),
1411
+ explain_checkout_failure: async (ctx, args) => {
1412
+ const id = String(args.id);
1413
+ const session = await optional(() => ctx.client.get(`/api/v1/checkout-sessions/${id}`));
1414
+ if (!session.ok) {
1415
+ return {
1416
+ note: `No checkout session ${id} (${session.unavailable}). If this is a cart id, read the cart with api_get /api/v1/carts/${id} and search_events for its activity.`
1417
+ };
1418
+ }
1419
+ const cartId = session.value?.cartId ?? void 0;
1420
+ const [cart, sessionEvents, cartEvents, deliveries] = await Promise.all([
1421
+ optional(() => ctx.client.get(`/api/v1/checkout-sessions/${id}/cart`)),
1422
+ optional(() => ctx.client.list("/api/v1/events", { entityId: id, limit: 25 })),
1423
+ cartId ? optional(() => ctx.client.list("/api/v1/events", { entityId: cartId, limit: 25 })) : Promise.resolve({ ok: true, value: { data: [], pagination: null } }),
1424
+ optional(() => ctx.client.list("/api/v1/webhook-deliveries", { limit: 15 }))
1425
+ ]);
1426
+ return {
1427
+ session: session.value,
1428
+ cart: cart.ok ? cart.value : cart.unavailable,
1429
+ events: [
1430
+ ...sessionEvents.ok ? sessionEvents.value.data : [],
1431
+ ...cartEvents.ok ? cartEvents.value.data : []
1432
+ ],
1433
+ eventsUnavailable: sessionEvents.ok ? void 0 : sessionEvents.unavailable,
1434
+ recentWebhookDeliveries: deliveries.ok ? deliveries.value.data : deliveries.unavailable,
1435
+ 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."
1436
+ };
1437
+ },
1438
+ // ------------------------------------------------------------ sandbox seed
1439
+ seed_test_data: async (ctx, args) => {
1440
+ if (ctx.identity.environment.providerEnvironment === "production") {
1441
+ throw new Error(
1442
+ "seed_test_data refuses to run against a production environment. Use a non-production API key."
1443
+ );
1444
+ }
1445
+ const stamp = Date.now();
1446
+ const customer = await ctx.client.post("/api/v1/customers", {
1447
+ body: {
1448
+ email: String(args.customerEmail ?? `mcp-seed-${stamp}@example.test`),
1449
+ firstName: "Seed",
1450
+ lastName: "Customer"
1451
+ },
1452
+ idempotencyKey: `mcp-seed-customer-${stamp}`
1453
+ });
1454
+ const cart = await ctx.client.post("/api/v1/carts", {
1455
+ // Unlike /api/v1/customers, cart creation requires applicationId
1456
+ // explicitly in the body (it's cross-checked against the caller's own
1457
+ // authenticated application, not inferred) -- omitting it is a real
1458
+ // 400 missing_application_id from the live API, not a hypothetical.
1459
+ body: { applicationId: ctx.identity.application?.id, customerId: customer?.id },
1460
+ idempotencyKey: `mcp-seed-cart-${stamp}`
1461
+ });
1462
+ const requested = Array.isArray(args.lineItems) && args.lineItems.length > 0 ? args.lineItems : [{ name: "Seed widget", unitPrice: 2500, quantity: 2 }];
1463
+ const items = [];
1464
+ for (const [index, item] of requested.entries()) {
1465
+ items.push(
1466
+ await ctx.client.post(`/api/v1/carts/${cart?.id}/items`, {
1467
+ body: item,
1468
+ idempotencyKey: `mcp-seed-item-${stamp}-${index}`
1469
+ })
1470
+ );
1471
+ }
1472
+ const order = await ctx.client.post(`/api/v1/carts/${cart?.id}/finalize`, {
1473
+ idempotencyKey: `mcp-seed-finalize-${stamp}`
1474
+ });
1475
+ return {
1476
+ environment: ctx.identity.environment.slug,
1477
+ customer,
1478
+ cart,
1479
+ lineItems: items,
1480
+ order,
1481
+ 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."
1482
+ };
1483
+ },
1484
+ // -------------------------------------------------------------- money/mail
1485
+ manage_subscription: async (ctx, args) => {
1486
+ const id = String(args.id);
1487
+ const action = String(args.action);
1488
+ if (action !== "pause" && action !== "resume") {
1489
+ throw new Error(`manage_subscription: action must be "pause" or "resume", got "${action}".`);
1490
+ }
1491
+ return ctx.client.post(`/api/v1/subscriptions/${id}/${action}`, {
1492
+ idempotencyKey: `mcp-manage-subscription-${id}-${action}-${Date.now()}`
1493
+ });
1494
+ },
1495
+ create_quote: async (ctx, args) => (
1496
+ // The real route's body shape differs from this tool's flatter,
1497
+ // model-facing input: line items are "items" (not "lineItems"), Net-N
1498
+ // terms are nested under paymentTerms.netN with an explicit mode (not a
1499
+ // flat "netN"), and the buyer-visible note field is "buyerNote" (not
1500
+ // "notes"). Confirmed against packages/platform-api/src/validation/quotes.ts's
1501
+ // actual zod schema after a live call 400'd with unrecognized_keys.
1502
+ ctx.client.post("/api/v1/quotes", {
1503
+ body: {
1504
+ customerId: args.customerId,
1505
+ items: args.items,
1506
+ expiresAt: args.expiresAt,
1507
+ paymentTerms: args.netN !== void 0 ? { mode: "net_terms", netN: args.netN } : void 0,
1508
+ buyerNote: args.notes
1509
+ },
1510
+ idempotencyKey: `mcp-create-quote-${String(args.customerId)}-${Date.now()}`
1511
+ })
1512
+ ),
1513
+ get_business_summary: async (ctx, args) => ctx.client.get("/api/v1/analytics/business-summary", { period: args.period }),
1514
+ get_attention_digest: async (ctx) => ctx.client.get("/api/v1/analytics/attention-digest"),
1515
+ // ------------------------------------------------------------- extensions
1516
+ list_extension_installations: async (ctx, args) => ctx.client.list("/api/v1/installations", { status: args.status, ...page(args) }),
1517
+ // ------------------------------------------------------------- connectors
1518
+ get_connector_routing: async (ctx) => {
1519
+ const [paymentRouting, routingConnectors, cardDecline, otherDecline] = await Promise.all([
1520
+ optional(() => ctx.client.get("/api/v1/payment-routing")),
1521
+ optional(() => ctx.client.get("/api/v1/routing-connectors")),
1522
+ optional(() => ctx.client.get("/api/v1/card-decline")),
1523
+ optional(() => ctx.client.get("/api/v1/other-decline"))
1524
+ ]);
1525
+ return {
1526
+ paymentRouting: paymentRouting.ok ? paymentRouting.value : paymentRouting.unavailable,
1527
+ routingConnectors: routingConnectors.ok ? routingConnectors.value : routingConnectors.unavailable,
1528
+ cardDecline: cardDecline.ok ? cardDecline.value : cardDecline.unavailable,
1529
+ otherDecline: otherDecline.ok ? otherDecline.value : otherDecline.unavailable
1530
+ };
1531
+ },
1532
+ get_integration_status: async (ctx) => {
1533
+ const [connectors, endpoints, coverage] = await Promise.all([
1534
+ optional(() => ctx.client.list("/api/v1/connectors")),
1535
+ optional(() => ctx.client.list("/api/v1/webhook-endpoints")),
1536
+ optional(() => ctx.client.get("/api/v1/webhook-endpoints/coverage"))
1537
+ ]);
1538
+ return {
1539
+ environment: ctx.identity.environment,
1540
+ workspace: ctx.identity.workspace,
1541
+ application: ctx.identity.application,
1542
+ connectors: connectors.ok ? connectors.value.data : connectors.unavailable,
1543
+ webhookEndpoints: endpoints.ok ? endpoints.value.data : endpoints.unavailable,
1544
+ unsubscribedEvents: coverage.ok ? coverage.value : coverage.unavailable
1545
+ };
1546
+ },
1547
+ // --------------------------------------------------------------- workspace
1548
+ get_workspace_settings: async (ctx) => ctx.client.get("/api/v1/merchants/me"),
1549
+ get_audit_log: async (ctx, args) => ctx.client.list("/api/v1/audit-log", { limit: args.limit })
1550
+ };
1551
+
1552
+ // src/input.ts
1553
+ var import_zod = require("zod");
1554
+ function fieldsToZodShape(fields) {
1555
+ const shape = {};
1556
+ for (const field of fields) {
1557
+ shape[field.name] = applyModifiers(field, baseType(field));
1558
+ }
1559
+ return shape;
1560
+ }
1561
+ function baseType(field) {
1562
+ if (field.enum && field.enum.length > 0) {
1563
+ return import_zod.z.enum([...field.enum]);
1564
+ }
1565
+ switch (field.type) {
1566
+ case "string":
1567
+ return import_zod.z.string();
1568
+ case "number":
1569
+ return import_zod.z.number();
1570
+ case "boolean":
1571
+ return import_zod.z.boolean();
1572
+ case "array":
1573
+ return import_zod.z.array(import_zod.z.unknown());
1574
+ case "object":
1575
+ return import_zod.z.record(import_zod.z.unknown());
1576
+ }
1577
+ }
1578
+ function applyModifiers(field, type) {
1579
+ const described = type.describe(field.description);
1580
+ if (field.required) return described;
1581
+ return field.default === void 0 ? described.optional() : described.default(field.default);
1582
+ }
1583
+
1584
+ // src/server.ts
1585
+ var VERSION = typeof __THROTTLE_MCP_VERSION__ === "string" ? __THROTTLE_MCP_VERSION__ : "0.0.0-dev";
1586
+ function registerableActions(policy) {
1587
+ const isProduction = policy.identity.environment.providerEnvironment === "production";
1588
+ return actionsForChannel("mcp").filter((action) => {
1589
+ if (!HANDLERS[action.id]) return false;
1590
+ if (!canRunAction(policy.identity.scopes, action)) return false;
1591
+ if (action.kind === "read") return true;
1592
+ if (action.sandboxOnly) return !isProduction;
1593
+ if (!policy.allowWrites) return false;
1594
+ return !isProduction || policy.allowLiveWrites;
1595
+ });
1596
+ }
1597
+ function buildInstructions(identity, actions, policy) {
1598
+ const isProduction = identity.environment.providerEnvironment === "production";
1599
+ const where = [
1600
+ identity.workspace?.name ?? identity.workspace?.slug ?? "unknown workspace",
1601
+ identity.application?.name ?? identity.application?.slug ?? null
1602
+ ].filter(Boolean).join(" / ");
1603
+ const liveWrites = actions.filter((a) => a.kind === "write" && !a.sandboxOnly);
1604
+ const sandboxWrites = actions.filter((a) => a.kind === "write" && a.sandboxOnly);
1605
+ const lines = [
1606
+ `Throttle MCP ${VERSION}. Connected to ${where}, environment "${identity.environment.slug}" (${isProduction ? "PRODUCTION \u2014 live data" : "non-production \u2014 not live data"}).`,
1607
+ "",
1608
+ "Conventions:",
1609
+ "- Every tool is scoped to this one workspace/application/environment; there is no way to switch from inside a tool call. Call whoami if you need to state where you are.",
1610
+ '- List tools return { data, pagination }. When pagination.hasMore is true, pass pagination.cursor back as "cursor" rather than raising "limit".',
1611
+ "- Amounts are integer minor units (2500 = $25.00).",
1612
+ "- 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.",
1613
+ "- API errors come back as text with a stable code. On insufficient_scopes, tell the user which scope to add \u2014 do not retry."
1614
+ ];
1615
+ if (liveWrites.length === 0) {
1616
+ lines.push(
1617
+ `- 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.`
1618
+ );
1619
+ if (sandboxWrites.length > 0) {
1620
+ lines.push(
1621
+ `- ${sandboxWrites.map((a) => a.id).join(", ")} may create data, but only in this non-production environment.`
1622
+ );
1623
+ }
1624
+ } else {
1625
+ lines.push(
1626
+ `- 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.`
1627
+ );
1628
+ }
1629
+ return lines.join("\n");
1630
+ }
1631
+ function createServer(options) {
1632
+ const actions = registerableActions(options);
1633
+ const server = new import_mcp.McpServer(
1634
+ { name: "throttle", version: VERSION },
1635
+ { instructions: buildInstructions(options.identity, actions, options) }
1636
+ );
1637
+ const ctx = {
1638
+ client: options.client,
1639
+ identity: options.identity,
1640
+ contract: options.contract,
1641
+ docs: options.docs
1642
+ };
1643
+ for (const action of actions) {
1644
+ const handler = HANDLERS[action.id];
1645
+ server.tool(
1646
+ action.id,
1647
+ describeForModel(action),
1648
+ fieldsToZodShape(action.input),
1649
+ async (args) => {
1650
+ try {
1651
+ const result = await handler(ctx, args ?? {});
1652
+ return { content: [{ type: "text", text: formatResult(result) }] };
1653
+ } catch (error) {
1654
+ return {
1655
+ content: [{ type: "text", text: formatError(error) }],
1656
+ isError: true
1657
+ };
1658
+ }
1659
+ }
1660
+ );
1661
+ }
1662
+ return server;
1663
+ }
1664
+ function describeForModel(action) {
1665
+ const parts = [action.description];
1666
+ if (action.guardrails && action.guardrails.length > 0) {
1667
+ parts.push(`Constraints: ${action.guardrails.join(" ")}`);
1668
+ }
1669
+ return parts.join(" ");
1670
+ }
1671
+
1672
+ // src/indexes.ts
1673
+ var import_node_fs = require("fs");
1674
+ var import_meta = {};
1675
+ function loadIndex(file, empty, label) {
1676
+ try {
1677
+ return JSON.parse((0, import_node_fs.readFileSync)(new URL(`./${file}`, import_meta.url), "utf8"));
1678
+ } catch {
1679
+ process.stderr.write(
1680
+ `throttle-mcp: ${label} index missing (${file}); that tool will return no results. Run "pnpm --filter @usethrottle/mcp build:indexes".
1681
+ `
1682
+ );
1683
+ return empty;
1684
+ }
1685
+ }
1686
+ function loadDocsIndex() {
1687
+ return loadIndex("docs-index.json", { generatedAt: "", entries: [] }, "docs");
1688
+ }
1689
+ function loadContractIndex() {
1690
+ return loadIndex("contract-index.json", { generatedAt: "", operations: [] }, "contract");
1691
+ }
1692
+ // Annotate the CommonJS export names for ESM import in node:
1693
+ 0 && (module.exports = {
1694
+ ThrottleClient,
1695
+ VERSION,
1696
+ buildInstructions,
1697
+ createServer,
1698
+ loadContractIndex,
1699
+ loadDocsIndex,
1700
+ registerableActions
1701
+ });
1702
+ //# sourceMappingURL=lib.cjs.map