@usethrottle/mcp 0.2.0 → 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/mcp.js CHANGED
@@ -37,18 +37,43 @@ function buildQuery(query) {
37
37
  const qs = params.toString();
38
38
  return qs ? `?${qs}` : "";
39
39
  }
40
+ var API_KEY_PREFIX_RE = /^(sk_|pk_)/;
40
41
  var ThrottleClient = class {
41
42
  constructor(auth, fetchImpl = fetch) {
42
43
  this.auth = auth;
43
44
  this.fetchImpl = fetchImpl;
44
45
  }
46
+ /**
47
+ * Swap the credential every later request is sent with. The hosted remote
48
+ * server builds one client per MCP session at `initialize`, but an OAuth
49
+ * access token lives one hour and the MCP client refreshes it underneath
50
+ * the session — each request then arrives with a newer bearer than the one
51
+ * the session was opened with. Without this, the session keeps calling the
52
+ * REST API with the token it was born with and every tool fails with a 401
53
+ * the moment that first token expires, even though the caller is still
54
+ * perfectly authenticated.
55
+ */
56
+ setApiKey(apiKey) {
57
+ this.auth = { ...this.auth, apiKey };
58
+ }
45
59
  async requestJson(method, path, options = {}) {
46
60
  const url = `${this.auth.apiBaseUrl.replace(/\/$/, "")}${path}${buildQuery(options.query)}`;
47
61
  const headers = {
48
62
  "content-type": "application/json",
49
- "x-api-key": this.auth.apiKey
63
+ "x-api-key": this.auth.apiKey,
64
+ // Also sent as Authorization: Bearer — required for an OAuth access
65
+ // token (the remote server's oauth_token sessions), which the auth
66
+ // middleware only ever recognizes via this header (peeked by issuer),
67
+ // never via x-api-key. For an sk_/pk_ key this is a no-op: the
68
+ // middleware already treats `Authorization: Bearer sk_.../pk_...`
69
+ // identically to `X-API-Key`, so existing stdio-CLI callers are
70
+ // unaffected.
71
+ Authorization: `Bearer ${this.auth.apiKey}`
50
72
  };
51
73
  if (options.idempotencyKey) headers["idempotency-key"] = options.idempotencyKey;
74
+ if (options.applicationId && !API_KEY_PREFIX_RE.test(this.auth.apiKey)) {
75
+ headers["x-throttle-application-id"] = options.applicationId;
76
+ }
52
77
  const sendsBody = method === "POST" || method === "PATCH" || method === "PUT";
53
78
  const body = options.body !== void 0 ? JSON.stringify(options.body) : sendsBody ? "{}" : void 0;
54
79
  const res = await this.fetchImpl(url, { method, headers, body });
@@ -70,12 +95,12 @@ var ThrottleClient = class {
70
95
  return parsed;
71
96
  }
72
97
  /** Unwraps the `{ data }` envelope. Key presence, not `??` — `{data: null}` unwraps to null. */
73
- async get(path, query) {
74
- const body = await this.requestJson("GET", path, { query });
98
+ async get(path, query, options) {
99
+ const body = await this.requestJson("GET", path, { query, applicationId: options?.applicationId });
75
100
  return body && typeof body === "object" && "data" in body ? body.data : body;
76
101
  }
77
- async list(path, query) {
78
- const body = await this.requestJson("GET", path, { query });
102
+ async list(path, query, options) {
103
+ const body = await this.requestJson("GET", path, { query, applicationId: options?.applicationId });
79
104
  const data = "data" in body ? body.data : body;
80
105
  const meta = body.meta;
81
106
  return { data: Array.isArray(data) ? data : [], pagination: meta?.pagination ?? null };
@@ -84,6 +109,10 @@ var ThrottleClient = class {
84
109
  const body = await this.requestJson("POST", path, options);
85
110
  return body && typeof body === "object" && "data" in body ? body.data : body;
86
111
  }
112
+ async patch(path, options = {}) {
113
+ const body = await this.requestJson("PATCH", path, options);
114
+ return body && typeof body === "object" && "data" in body ? body.data : body;
115
+ }
87
116
  };
88
117
  async function optional(load) {
89
118
  try {
@@ -98,6 +127,7 @@ async function optional(load) {
98
127
 
99
128
  // src/server.ts
100
129
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
130
+ import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
101
131
 
102
132
  // ../core-scopes/src/scopes.ts
103
133
  function hasScope(granted, required) {
@@ -126,6 +156,8 @@ function add(group, resource, actions, labels, opts = {}) {
126
156
  staffOnly: opts.staffOnly ?? false,
127
157
  // publishable defaults false; only the curated set below is true
128
158
  publishableAllowed: opts.publishableAllowed ?? false,
159
+ // publishableOnly defaults false; only customer_accounts:write sets it
160
+ publishableOnly: opts.publishableOnly ?? false,
129
161
  // extension default true for commerce scopes; Taxonomy B passes false
130
162
  extensionAllowed: opts.extensionAllowed ?? true,
131
163
  implies
@@ -136,19 +168,36 @@ add("Orders", "orders", ["read", "write"], { read: "View orders", write: "Manage
136
168
  add("Quotes", "quotes", ["read", "write"], { read: "View quotes", write: "Manage quotes" });
137
169
  add("Orders", "order_cancellations", ["write"], { write: "Cancel orders" });
138
170
  add("Orders", "order_returns", ["read", "write"], { read: "View returns", write: "Process returns" });
171
+ add("Orders", "order_comps", ["write"], { write: "Comp orders (write off the balance)" }, { sensitive: true });
172
+ add("Orders", "order_holds", ["write"], { write: "Freeze and unfreeze orders" });
173
+ add(
174
+ "Imports",
175
+ "imports",
176
+ ["read", "write"],
177
+ { read: "View data imports", write: "Run and undo data imports" },
178
+ { extensionAllowed: false }
179
+ );
139
180
  add("Payments", "payments", ["read", "write"], { read: "View payments", write: "Create payments" });
140
181
  add("Payments", "payment_authorizations", ["write"], { write: "Authorize payments" });
141
182
  add("Payments", "payment_captures", ["write"], { write: "Capture payments" });
142
183
  add("Payments", "payment_refunds", ["read", "write"], { read: "View refunds", write: "Refund payments" }, { sensitive: true });
143
184
  add("Payments", "payment_voids", ["write"], { write: "Void payments" }, { sensitive: true });
185
+ add("Payments", "payment_records", ["write"], { write: "Record payments received outside Throttle" }, { sensitive: true });
144
186
  add("Payments", "payment_disputes", ["read", "write"], { read: "View disputes", write: "Handle disputes" }, { sensitive: true });
145
187
  add("Payments", "payment_methods", ["read", "write"], { read: "View payment methods", write: "Tokenize payment methods" }, { sensitive: true });
146
188
  add("Customers", "customers", ["read", "write"], { read: "View customers", write: "Manage customers" });
147
189
  add("Customers", "customer_pii", ["read"], { read: "View customer PII" }, { sensitive: true });
148
190
  add("Customers", "customer_addresses", ["read", "write"], { read: "View addresses", write: "Manage addresses" });
149
191
  add("Customers", "customer_payment_methods", ["read", "write"], { read: "View stored cards", write: "Manage stored cards" }, { sensitive: true });
192
+ add(
193
+ "Customer accounts",
194
+ "customer_accounts",
195
+ ["write"],
196
+ { write: "Let shoppers register, sign in and manage their account" },
197
+ { publishableAllowed: true, publishableOnly: true, extensionAllowed: false }
198
+ );
150
199
  add("Carts", "carts", ["read", "write"], { read: "View carts", write: "Manage carts" });
151
- add("Carts", "cart_items", ["read", "write"], { read: "View line items", write: "Manage line items" });
200
+ add("Carts", "cart_items", ["write"], { write: "Manage line items" });
152
201
  add("Carts", "checkout_sessions", ["read", "write"], { read: "View checkout sessions", write: "Manage checkout sessions" });
153
202
  add("Discounts", "discounts", ["read", "write"], { read: "View discounts", write: "Manage discounts" });
154
203
  add("Subscriptions", "subscriptions", ["read", "write"], { read: "View subscriptions", write: "Manage subscriptions" });
@@ -169,8 +218,32 @@ add("Shipping & Tax", "shipping_quotes", ["write"], { write: "Request shipping q
169
218
  add("Shipping & Tax", "tax_calculations", ["write"], { write: "Calculate tax" }, { publishableAllowed: true });
170
219
  add("Shipping & Tax", "shipping_tax_quote_tokens", ["read", "write"], { read: "View quote tokens", write: "Manage quote tokens" });
171
220
  add("Shipping & Tax", "shipping_tax_snapshots", ["write"], { write: "Accept external snapshots" });
221
+ add(
222
+ "Scripts",
223
+ "storefront_scripts",
224
+ ["read", "write"],
225
+ {
226
+ read: "Load storefront scripts",
227
+ // `write` covers exactly one thing: reporting a script's own load outcome
228
+ // from the merchant's storefront, so the health panel stops being
229
+ // hosted-checkout-only. On checkout the session id in the URL is the
230
+ // capability that scopes that write; a storefront page has no session, so
231
+ // the pk it already holds is what scopes it instead. It writes nothing but
232
+ // load telemetry — authoring stays on `application_scripts:write`, which
233
+ // is not publishable-allowed.
234
+ write: "Report storefront script load outcomes"
235
+ },
236
+ { publishableAllowed: true }
237
+ );
238
+ add("Scripts", "application_scripts", ["read", "write"], {
239
+ read: "View merchant-authored scripts",
240
+ write: "Create, edit and disable merchant-authored scripts"
241
+ }, { extensionAllowed: false });
242
+ add("Scripts", "extension_scripts", ["read", "write"], {
243
+ read: "View an extension's buyer-facing scripts",
244
+ write: "Create, edit and delete an extension's buyer-facing scripts"
245
+ }, { extensionAllowed: false });
172
246
  add("Connectors", "connectors", ["read", "write"], { read: "View connectors", write: "Manage connectors" });
173
- add("Connectors", "connector_activations", ["write"], { write: "Activate/deactivate connectors" });
174
247
  add("Webhooks & Events", "webhooks", ["read", "write"], { read: "View webhooks", write: "Manage webhooks" });
175
248
  add("Webhooks & Events", "webhook_deliveries", ["read", "write"], { read: "View deliveries", write: "Retry deliveries" });
176
249
  add("Webhooks & Events", "events", ["read"], { read: "View events" });
@@ -186,7 +259,7 @@ add("Platform billing", "billing", ["read", "write"], { read: "View billing", wr
186
259
  add("Platform billing", "billing_payment_methods", ["read", "write"], { read: "View billing cards", write: "Manage billing cards" }, HUMAN);
187
260
  add("Email", "email_templates", ["read", "write"], { read: "View email templates", write: "Edit email templates" }, HUMAN);
188
261
  add("Email", "email_settings", ["read", "write"], { read: "View email settings", write: "Edit email settings" }, HUMAN);
189
- add("Go-live", "go_live", ["read", "write"], { read: "View go-live status", write: "Submit go-live" }, HUMAN);
262
+ add("Go-live", "go_live", ["write"], { write: "Submit go-live" }, HUMAN);
190
263
  add("Partner", "partner", ["read", "write"], { read: "View partner profile", write: "Edit partner profile" }, HUMAN);
191
264
  add("Partner", "affiliates", ["read", "write"], { read: "View affiliates", write: "Manage affiliates" }, HUMAN);
192
265
  add("Notifications", "notifications", ["write"], { write: "Send notifications" }, HUMAN);
@@ -210,6 +283,7 @@ REG["extensions:install"] = {
210
283
  sensitive: false,
211
284
  staffOnly: false,
212
285
  publishableAllowed: false,
286
+ publishableOnly: false,
213
287
  extensionAllowed: false,
214
288
  implies: []
215
289
  };
@@ -222,6 +296,7 @@ REG["*"] = {
222
296
  sensitive: true,
223
297
  staffOnly: false,
224
298
  publishableAllowed: false,
299
+ publishableOnly: false,
225
300
  extensionAllowed: false,
226
301
  implies: []
227
302
  };
@@ -241,6 +316,9 @@ var EVENT_SCOPE_OVERRIDES = {
241
316
  "payment.refunded": "payment_refunds:read",
242
317
  "payment.partially_refunded": "payment_refunds:read",
243
318
  "payment.refund_failed": "payment_refunds:read",
319
+ // Money returned, so it belongs with the other refund events rather than
320
+ // behind the broad subscriptions:read the `subscription.` prefix would give.
321
+ "subscription.invoice_refunded": "payment_refunds:read",
244
322
  "payment.disputed": "payment_disputes:read",
245
323
  "payment.dispute_cleared": "payment_disputes:read",
246
324
  "fulfillment.digital.delivered": "fulfillment_digital:read"
@@ -257,9 +335,17 @@ var PREFIX_RULES = [
257
335
  ["fulfillment.", "fulfillments:read"],
258
336
  ["discount.", "discounts:read"],
259
337
  ["invoice.", "invoices:read"],
338
+ ["import.", "imports:read"],
260
339
  ["shipping_tax.", "shipping_tax:read"],
261
340
  ["workspace.", "billing:read"],
262
- ["extension.", "merchant:read"]
341
+ // `extensions:read` is a human scope (extensions must never watch other
342
+ // extensions), so only a workspace endpoint can SUBSCRIBE to this family.
343
+ // The uninstalled installation's own endpoint is addressed directly by the
344
+ // uninstall path and needs no subscription at all.
345
+ ["extension.", "extensions:read"],
346
+ // Subscribing to script outcomes needs script read — the payload names the
347
+ // script key and its hash, which is the merchant's script inventory.
348
+ ["script.", "application_scripts:read"]
263
349
  ];
264
350
  function eventReadScope(type) {
265
351
  if (EVENT_SCOPE_OVERRIDES[type]) return EVENT_SCOPE_OVERRIDES[type];
@@ -271,8 +357,90 @@ function eventReadScope(type) {
271
357
 
272
358
  // ../core-scopes/src/issuance.ts
273
359
  var PUBLISHABLE_SCOPES = Object.values(SCOPE_REGISTRY).filter((d) => d.publishableAllowed).map((d) => d.id);
360
+ var CONNECTOR_GRANTABLE_SENSITIVE_SCOPES = ["customer_pii:read"];
361
+ function validateScopesForOAuth(scopes, opts = {}) {
362
+ const invalid = [];
363
+ for (const id of scopes) {
364
+ if (id === "*") {
365
+ invalid.push(id);
366
+ continue;
367
+ }
368
+ const def = SCOPE_REGISTRY[id];
369
+ if (!def || def.staffOnly) {
370
+ invalid.push(id);
371
+ continue;
372
+ }
373
+ if (def.sensitive && !(opts.connector && CONNECTOR_GRANTABLE_SENSITIVE_SCOPES.includes(id))) invalid.push(id);
374
+ }
375
+ return { ok: invalid.length === 0, invalid };
376
+ }
377
+
378
+ // ../core-scopes/src/shopper-pii.ts
379
+ var PERSON_KEYS = /* @__PURE__ */ new Set([
380
+ "email",
381
+ "customerEmail",
382
+ "billingEmail",
383
+ "buyerEmail",
384
+ "authorEmail",
385
+ "authorName",
386
+ "phone",
387
+ "firstName",
388
+ "lastName",
389
+ "addressLine1",
390
+ "addressLine2",
391
+ "billingAddressLines",
392
+ "processorResponse",
393
+ "shareToken",
394
+ "company",
395
+ "externalId",
396
+ "externalCustomerId",
397
+ // Free-form: contents cannot be inspected for a person, so the field goes.
398
+ "metadata",
399
+ "notes",
400
+ "internalNotes",
401
+ "buyerNote",
402
+ "buyerReference",
403
+ "changeSummary",
404
+ "declinedReason",
405
+ "compReason",
406
+ "holdReason",
407
+ "body",
408
+ "acceptance",
409
+ "acceptanceSelections",
410
+ "clientContext",
411
+ "ipAddress",
412
+ "userAgent",
413
+ "vatNumber",
414
+ "buyerVatNumber",
415
+ "taxExemptionNumber"
416
+ ]);
417
+ var PERSON_CONTAINERS = /* @__PURE__ */ new Set(["customer", "buyer"]);
418
+ function isPlainObject(value) {
419
+ if (value === null || typeof value !== "object") return false;
420
+ const proto = Object.getPrototypeOf(value);
421
+ return proto === Object.prototype || proto === null;
422
+ }
423
+ function walk(value, parentKey, depth) {
424
+ if (depth > 32) return value;
425
+ if (Array.isArray(value)) return value.map((item) => walk(item, parentKey, depth + 1));
426
+ if (!isPlainObject(value)) return value;
427
+ const out = {};
428
+ const isPerson = parentKey !== null && PERSON_CONTAINERS.has(parentKey);
429
+ for (const [key, child] of Object.entries(value)) {
430
+ if (PERSON_KEYS.has(key) || isPerson && key === "name") {
431
+ out[key] = child === void 0 ? child : null;
432
+ } else {
433
+ out[key] = walk(child, key, depth + 1);
434
+ }
435
+ }
436
+ return out;
437
+ }
438
+ function redactShopperPii(value) {
439
+ return walk(value, null, 0);
440
+ }
274
441
 
275
442
  // ../action-catalog/src/actions.ts
443
+ 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.";
276
444
  var ACTIONS = [
277
445
  // ---------------------------------------------------------------- identity
278
446
  {
@@ -289,7 +457,7 @@ var ACTIONS = [
289
457
  {
290
458
  id: "search_docs",
291
459
  title: "Search the Throttle developer docs",
292
- 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.",
460
+ 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.",
293
461
  kind: "read",
294
462
  scopes: [],
295
463
  input: [
@@ -326,7 +494,7 @@ var ACTIONS = [
326
494
  {
327
495
  id: "validate_payload",
328
496
  title: "Validate a request body against the API contract",
329
- 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.",
497
+ 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.",
330
498
  kind: "read",
331
499
  scopes: [],
332
500
  input: [
@@ -341,15 +509,22 @@ var ACTIONS = [
341
509
  {
342
510
  id: "api_get",
343
511
  title: "Read any Throttle API endpoint",
344
- 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.",
512
+ 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.",
345
513
  kind: "read",
346
514
  scopes: [],
515
+ credentialScoped: true,
347
516
  input: [
348
517
  {
349
518
  name: "path",
350
519
  type: "string",
351
520
  required: true,
352
521
  description: "Path beginning with /api/v1, query string included."
522
+ },
523
+ {
524
+ name: "applicationId",
525
+ type: "string",
526
+ required: false,
527
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
353
528
  }
354
529
  ],
355
530
  requests: [],
@@ -371,11 +546,33 @@ var ACTIONS = [
371
546
  { name: "q", type: "string", required: false, description: "Free-text search." },
372
547
  { name: "status", type: "string", required: false, description: "Order status filter." },
373
548
  { name: "paymentStatus", type: "string", required: false, description: "Payment status filter." },
549
+ {
550
+ name: "createdAfter",
551
+ type: "string",
552
+ required: false,
553
+ description: "ISO 8601 lower bound on creation time, exclusive. For a single day, pass that day's midnight and the next day's midnight."
554
+ },
555
+ {
556
+ name: "createdBefore",
557
+ type: "string",
558
+ required: false,
559
+ description: "ISO 8601 upper bound on creation time, exclusive."
560
+ },
374
561
  { name: "limit", type: "number", required: false, description: "Page size." },
375
- { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
562
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
563
+ {
564
+ name: "applicationId",
565
+ type: "string",
566
+ required: false,
567
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
568
+ }
376
569
  ],
377
570
  requests: [{ method: "GET", path: "/api/v1/orders" }],
378
- channels: ["mcp", "zapier", "make", "n8n"]
571
+ channels: ["mcp", "zapier", "make", "n8n"],
572
+ guardrails: [
573
+ OAUTH_WITHHELD,
574
+ "Over an OAuth connection, q matches the order id, order number and status fields only."
575
+ ]
379
576
  },
380
577
  {
381
578
  id: "get_order",
@@ -383,7 +580,16 @@ var ACTIONS = [
383
580
  description: "Full picture of a single order: the order itself plus its payments, returns and fulfillments in one response.",
384
581
  kind: "read",
385
582
  scopes: ["orders:read"],
386
- input: [{ name: "id", type: "string", required: true, description: "Order id." }],
583
+ sectionScopes: ["payments:read", "order_returns:read", "fulfillments:read"],
584
+ input: [
585
+ { name: "id", type: "string", required: true, description: "Order id." },
586
+ {
587
+ name: "applicationId",
588
+ type: "string",
589
+ required: false,
590
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
591
+ }
592
+ ],
387
593
  requests: [
388
594
  { method: "GET", path: "/api/v1/orders/:id" },
389
595
  { method: "GET", path: "/api/v1/orders/:id/payments" },
@@ -392,23 +598,45 @@ var ACTIONS = [
392
598
  ],
393
599
  channels: ["mcp"],
394
600
  guardrails: [
395
- "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."
601
+ "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.",
602
+ OAUTH_WITHHELD
396
603
  ]
397
604
  },
398
605
  // ---------------------------------------------------------------- customers
399
606
  {
400
607
  id: "search_customers",
401
608
  title: "Search customers",
402
- description: "Find customers by name, email or external id.",
609
+ 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.",
403
610
  kind: "read",
404
611
  scopes: ["customers:read"],
405
612
  input: [
406
613
  { name: "q", type: "string", required: false, description: "Free-text search." },
614
+ {
615
+ name: "createdAfter",
616
+ type: "string",
617
+ required: false,
618
+ description: "ISO 8601 lower bound on creation time, exclusive. For a single day, pass that day's midnight and the next day's midnight."
619
+ },
620
+ {
621
+ name: "createdBefore",
622
+ type: "string",
623
+ required: false,
624
+ description: "ISO 8601 upper bound on creation time, exclusive."
625
+ },
407
626
  { name: "limit", type: "number", required: false, description: "Page size." },
408
- { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
627
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
628
+ {
629
+ name: "applicationId",
630
+ type: "string",
631
+ required: false,
632
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
633
+ }
409
634
  ],
410
635
  requests: [{ method: "GET", path: "/api/v1/customers" }],
411
- channels: ["mcp", "zapier", "make", "n8n"]
636
+ channels: ["mcp", "zapier", "make", "n8n"],
637
+ guardrails: [
638
+ '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.'
639
+ ]
412
640
  },
413
641
  {
414
642
  id: "get_customer",
@@ -416,7 +644,16 @@ var ACTIONS = [
416
644
  description: "Full picture of a single customer: the record plus their subscriptions and addresses in one response.",
417
645
  kind: "read",
418
646
  scopes: ["customers:read"],
419
- input: [{ name: "id", type: "string", required: true, description: "Customer id." }],
647
+ sectionScopes: ["customer_addresses:read", "subscriptions:read"],
648
+ input: [
649
+ { name: "id", type: "string", required: true, description: "Customer id." },
650
+ {
651
+ name: "applicationId",
652
+ type: "string",
653
+ required: false,
654
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
655
+ }
656
+ ],
420
657
  requests: [
421
658
  { method: "GET", path: "/api/v1/customers/:id" },
422
659
  { method: "GET", path: "/api/v1/customers/:id/addresses" },
@@ -424,7 +661,9 @@ var ACTIONS = [
424
661
  ],
425
662
  channels: ["mcp"],
426
663
  guardrails: [
427
- "Sub-reads need customer_addresses:read / subscriptions:read. Omit a section the credential cannot read instead of failing."
664
+ "Sub-reads need customer_addresses:read / subscriptions:read. Omit a section the credential cannot read instead of failing.",
665
+ "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.",
666
+ OAUTH_WITHHELD
428
667
  ]
429
668
  },
430
669
  // ------------------------------------------------------------- subscriptions
@@ -437,8 +676,26 @@ var ACTIONS = [
437
676
  input: [
438
677
  { name: "customerId", type: "string", required: false, description: "Filter by customer." },
439
678
  { name: "status", type: "string", required: false, description: "Filter by status." },
679
+ {
680
+ name: "createdAfter",
681
+ type: "string",
682
+ required: false,
683
+ description: "ISO 8601 lower bound on creation time, exclusive. For a single day, pass that day's midnight and the next day's midnight."
684
+ },
685
+ {
686
+ name: "createdBefore",
687
+ type: "string",
688
+ required: false,
689
+ description: "ISO 8601 upper bound on creation time, exclusive."
690
+ },
440
691
  { name: "limit", type: "number", required: false, description: "Page size." },
441
- { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
692
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
693
+ {
694
+ name: "applicationId",
695
+ type: "string",
696
+ required: false,
697
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
698
+ }
442
699
  ],
443
700
  requests: [{ method: "GET", path: "/api/v1/subscriptions" }],
444
701
  channels: ["mcp", "zapier", "make", "n8n"]
@@ -449,7 +706,15 @@ var ACTIONS = [
449
706
  description: "Read a single subscription including its current period and pending changes.",
450
707
  kind: "read",
451
708
  scopes: ["subscriptions:read"],
452
- input: [{ name: "id", type: "string", required: true, description: "Subscription id." }],
709
+ input: [
710
+ { name: "id", type: "string", required: true, description: "Subscription id." },
711
+ {
712
+ name: "applicationId",
713
+ type: "string",
714
+ required: false,
715
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
716
+ }
717
+ ],
453
718
  requests: [{ method: "GET", path: "/api/v1/subscriptions/:id" }],
454
719
  channels: ["mcp", "zapier", "make", "n8n"]
455
720
  },
@@ -462,8 +727,26 @@ var ACTIONS = [
462
727
  scopes: ["quotes:read"],
463
728
  input: [
464
729
  { name: "status", type: "string", required: false, description: "Filter by quote status." },
730
+ {
731
+ name: "createdAfter",
732
+ type: "string",
733
+ required: false,
734
+ description: "ISO 8601 lower bound on creation time, exclusive. For a single day, pass that day's midnight and the next day's midnight."
735
+ },
736
+ {
737
+ name: "createdBefore",
738
+ type: "string",
739
+ required: false,
740
+ description: "ISO 8601 upper bound on creation time, exclusive."
741
+ },
465
742
  { name: "limit", type: "number", required: false, description: "Page size." },
466
- { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
743
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
744
+ {
745
+ name: "applicationId",
746
+ type: "string",
747
+ required: false,
748
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
749
+ }
467
750
  ],
468
751
  requests: [{ method: "GET", path: "/api/v1/quotes" }],
469
752
  channels: ["mcp", "zapier", "make", "n8n"]
@@ -471,12 +754,21 @@ var ACTIONS = [
471
754
  {
472
755
  id: "get_quote",
473
756
  title: "Get one quote",
474
- description: "Read a single quote with its line items, revisions and buyer link state.",
757
+ description: "Read a single quote: status, totals, and each revision with its line items and payment terms.",
475
758
  kind: "read",
476
759
  scopes: ["quotes:read"],
477
- input: [{ name: "id", type: "string", required: true, description: "Quote id." }],
760
+ input: [
761
+ { name: "id", type: "string", required: true, description: "Quote id." },
762
+ {
763
+ name: "applicationId",
764
+ type: "string",
765
+ required: false,
766
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
767
+ }
768
+ ],
478
769
  requests: [{ method: "GET", path: "/api/v1/quotes/:id" }],
479
- channels: ["mcp", "zapier", "make", "n8n"]
770
+ channels: ["mcp", "zapier", "make", "n8n"],
771
+ guardrails: [OAUTH_WITHHELD]
480
772
  },
481
773
  // ----------------------------------------------------------------- invoices
482
774
  {
@@ -488,10 +780,17 @@ var ACTIONS = [
488
780
  input: [
489
781
  { name: "status", type: "string", required: false, description: "Filter by invoice status." },
490
782
  { name: "limit", type: "number", required: false, description: "Page size." },
491
- { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
783
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
784
+ {
785
+ name: "applicationId",
786
+ type: "string",
787
+ required: false,
788
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
789
+ }
492
790
  ],
493
791
  requests: [{ method: "GET", path: "/api/v1/invoices" }],
494
- channels: ["mcp", "zapier", "make", "n8n"]
792
+ channels: ["mcp", "zapier", "make", "n8n"],
793
+ guardrails: [OAUTH_WITHHELD]
495
794
  },
496
795
  {
497
796
  id: "receivables_aging",
@@ -499,7 +798,14 @@ var ACTIONS = [
499
798
  description: 'Outstanding receivables bucketed by age \u2014 answers "what is overdue and by how long".',
500
799
  kind: "read",
501
800
  scopes: ["invoices:read"],
502
- input: [],
801
+ input: [
802
+ {
803
+ name: "applicationId",
804
+ type: "string",
805
+ required: false,
806
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
807
+ }
808
+ ],
503
809
  requests: [{ method: "GET", path: "/api/v1/invoices/aging" }],
504
810
  channels: ["mcp", "zapier", "make", "n8n"]
505
811
  },
@@ -524,7 +830,13 @@ var ACTIONS = [
524
830
  { name: "createdAfter", type: "string", required: false, description: "ISO 8601 date lower bound." },
525
831
  { name: "createdBefore", type: "string", required: false, description: "ISO 8601 date upper bound." },
526
832
  { name: "limit", type: "number", required: false, description: "Page size." },
527
- { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
833
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
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
+ }
528
840
  ],
529
841
  requests: [{ method: "GET", path: "/api/v1/discounts" }],
530
842
  channels: ["mcp", "zapier", "make", "n8n"]
@@ -535,7 +847,15 @@ var ACTIONS = [
535
847
  description: "Read a single discount code: value, conditions, usage limits and redemption count.",
536
848
  kind: "read",
537
849
  scopes: ["discounts:read"],
538
- input: [{ name: "id", type: "string", required: true, description: "Discount id." }],
850
+ input: [
851
+ { name: "id", type: "string", required: true, description: "Discount id." },
852
+ {
853
+ name: "applicationId",
854
+ type: "string",
855
+ required: false,
856
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
857
+ }
858
+ ],
539
859
  requests: [{ method: "GET", path: "/api/v1/discounts/:id" }],
540
860
  channels: ["mcp", "zapier", "make", "n8n"]
541
861
  },
@@ -562,7 +882,13 @@ var ACTIONS = [
562
882
  { name: "amountMin", type: "number", required: false, description: "Minimum amount, minor units." },
563
883
  { name: "amountMax", type: "number", required: false, description: "Maximum amount, minor units." },
564
884
  { name: "limit", type: "number", required: false, description: "Page size." },
565
- { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
885
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
886
+ {
887
+ name: "applicationId",
888
+ type: "string",
889
+ required: false,
890
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
891
+ }
566
892
  ],
567
893
  requests: [{ method: "GET", path: "/api/v1/payments" }],
568
894
  channels: ["mcp", "zapier", "make", "n8n"]
@@ -573,7 +899,15 @@ var ACTIONS = [
573
899
  description: "Read a single payment plus its full transaction history (authorize/capture/refund/void attempts).",
574
900
  kind: "read",
575
901
  scopes: ["payments:read"],
576
- input: [{ name: "id", type: "string", required: true, description: "Payment id." }],
902
+ input: [
903
+ { name: "id", type: "string", required: true, description: "Payment id." },
904
+ {
905
+ name: "applicationId",
906
+ type: "string",
907
+ required: false,
908
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
909
+ }
910
+ ],
577
911
  requests: [
578
912
  { method: "GET", path: "/api/v1/payments/:id" },
579
913
  { method: "GET", path: "/api/v1/payments/:id/transactions" }
@@ -590,9 +924,18 @@ var ACTIONS = [
590
924
  description: "Read a single issued invoice: line items, totals, status and the billing snapshot at issue time.",
591
925
  kind: "read",
592
926
  scopes: ["invoices:read"],
593
- input: [{ name: "id", type: "string", required: true, description: "Invoice id." }],
927
+ input: [
928
+ { name: "id", type: "string", required: true, description: "Invoice id." },
929
+ {
930
+ name: "applicationId",
931
+ type: "string",
932
+ required: false,
933
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
934
+ }
935
+ ],
594
936
  requests: [{ method: "GET", path: "/api/v1/invoices/:id" }],
595
- channels: ["mcp", "zapier", "make", "n8n"]
937
+ channels: ["mcp", "zapier", "make", "n8n"],
938
+ guardrails: [OAUTH_WITHHELD]
596
939
  },
597
940
  // ------------------------------------------------------------------ carts
598
941
  {
@@ -604,7 +947,13 @@ var ACTIONS = [
604
947
  input: [
605
948
  { name: "sinceDays", type: "number", required: false, description: "How many days back to look." },
606
949
  { name: "limit", type: "number", required: false, description: "Page size." },
607
- { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
950
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
951
+ {
952
+ name: "applicationId",
953
+ type: "string",
954
+ required: false,
955
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
956
+ }
608
957
  ],
609
958
  requests: [
610
959
  { method: "GET", path: "/api/v1/abandoned-carts" },
@@ -625,7 +974,13 @@ var ACTIONS = [
625
974
  { name: "entityType", type: "string", required: false, description: "Entity type filter." },
626
975
  { name: "entityId", type: "string", required: false, description: "Entity id filter." },
627
976
  { name: "limit", type: "number", required: false, description: "Page size." },
628
- { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
977
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
978
+ {
979
+ name: "applicationId",
980
+ type: "string",
981
+ required: false,
982
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
983
+ }
629
984
  ],
630
985
  requests: [{ method: "GET", path: "/api/v1/events" }],
631
986
  channels: ["mcp", "n8n"]
@@ -633,13 +988,30 @@ var ACTIONS = [
633
988
  {
634
989
  id: "list_webhook_deliveries",
635
990
  title: "List webhook deliveries",
636
- description: "Recent outbound webhook deliveries, filterable by endpoint and status.",
991
+ description: "Recent outbound webhook deliveries, filterable by endpoint, status and event type.",
637
992
  kind: "read",
638
993
  scopes: ["webhook_deliveries:read"],
639
994
  input: [
640
995
  { name: "endpointId", type: "string", required: false, description: "Filter by endpoint." },
641
- { name: "status", type: "string", required: false, description: "Filter by delivery status." },
642
- { name: "limit", type: "number", required: false, description: "Page size." }
996
+ {
997
+ name: "status",
998
+ type: "string",
999
+ required: false,
1000
+ description: "Filter by delivery status: pending, delivered, failed or dead_letter."
1001
+ },
1002
+ {
1003
+ name: "eventType",
1004
+ type: "string",
1005
+ required: false,
1006
+ description: "Filter by exact event type, e.g. payment.failed."
1007
+ },
1008
+ { name: "limit", type: "number", required: false, description: "Page size." },
1009
+ {
1010
+ name: "applicationId",
1011
+ type: "string",
1012
+ required: false,
1013
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1014
+ }
643
1015
  ],
644
1016
  requests: [{ method: "GET", path: "/api/v1/webhook-deliveries" }],
645
1017
  channels: ["mcp", "n8n"]
@@ -650,7 +1022,15 @@ var ACTIONS = [
650
1022
  description: "Full delivery record: the signed payload we sent, the response we got back, and every attempt.",
651
1023
  kind: "read",
652
1024
  scopes: ["webhook_deliveries:read"],
653
- input: [{ name: "id", type: "string", required: true, description: "Delivery id." }],
1025
+ input: [
1026
+ { name: "id", type: "string", required: true, description: "Delivery id." },
1027
+ {
1028
+ name: "applicationId",
1029
+ type: "string",
1030
+ required: false,
1031
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1032
+ }
1033
+ ],
654
1034
  requests: [{ method: "GET", path: "/api/v1/webhook-deliveries/:id" }],
655
1035
  channels: ["mcp", "n8n"]
656
1036
  },
@@ -660,12 +1040,19 @@ var ACTIONS = [
660
1040
  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.",
661
1041
  kind: "read",
662
1042
  scopes: ["checkout_sessions:read"],
1043
+ sectionScopes: ["events:read", "webhook_deliveries:read"],
663
1044
  input: [
664
1045
  {
665
1046
  name: "id",
666
1047
  type: "string",
667
1048
  required: true,
668
1049
  description: "Checkout session id (or cart id \u2014 the tool resolves the session)."
1050
+ },
1051
+ {
1052
+ name: "applicationId",
1053
+ type: "string",
1054
+ required: false,
1055
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
669
1056
  }
670
1057
  ],
671
1058
  requests: [
@@ -676,7 +1063,8 @@ var ACTIONS = [
676
1063
  ],
677
1064
  channels: ["mcp"],
678
1065
  guardrails: [
679
- "Report which sections were unavailable rather than failing the call when a sub-read is unscoped."
1066
+ "Report which sections were unavailable rather than failing the call when a sub-read is unscoped.",
1067
+ OAUTH_WITHHELD
680
1068
  ]
681
1069
  },
682
1070
  // ------------------------------------------------------------- extensions
@@ -695,7 +1083,13 @@ var ACTIONS = [
695
1083
  enum: ["active", "suspended", "uninstalled"]
696
1084
  },
697
1085
  { name: "limit", type: "number", required: false, description: "Page size." },
698
- { name: "cursor", type: "string", required: false, description: "Pagination cursor." }
1086
+ { name: "cursor", type: "string", required: false, description: "Pagination cursor." },
1087
+ {
1088
+ name: "applicationId",
1089
+ type: "string",
1090
+ required: false,
1091
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1092
+ }
699
1093
  ],
700
1094
  requests: [{ method: "GET", path: "/api/v1/installations" }],
701
1095
  channels: ["mcp", "zapier", "make", "n8n"]
@@ -707,7 +1101,14 @@ var ACTIONS = [
707
1101
  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.",
708
1102
  kind: "read",
709
1103
  scopes: ["connectors:read"],
710
- input: [],
1104
+ input: [
1105
+ {
1106
+ name: "applicationId",
1107
+ type: "string",
1108
+ required: false,
1109
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1110
+ }
1111
+ ],
711
1112
  requests: [
712
1113
  { method: "GET", path: "/api/v1/payment-routing" },
713
1114
  { method: "GET", path: "/api/v1/routing-connectors" },
@@ -725,7 +1126,15 @@ var ACTIONS = [
725
1126
  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.',
726
1127
  kind: "read",
727
1128
  scopes: [],
728
- input: [],
1129
+ sectionScopes: ["connectors:read", "webhooks:read"],
1130
+ input: [
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
+ ],
729
1138
  requests: [
730
1139
  { method: "GET", path: "/api/v1/whoami" },
731
1140
  { method: "GET", path: "/api/v1/connectors" },
@@ -741,7 +1150,7 @@ var ACTIONS = [
741
1150
  {
742
1151
  id: "get_workspace_settings",
743
1152
  title: "Get workspace settings",
744
- description: "Read this workspace's identity and settings: name, slug, branding, onboarding state, and the default Net-N payment terms.",
1153
+ description: "Read this workspace's identity: name, slug, branding and onboarding state. Payment terms are set per application and environment, not here.",
745
1154
  kind: "read",
746
1155
  scopes: ["merchant:read"],
747
1156
  input: [],
@@ -760,12 +1169,48 @@ var ACTIONS = [
760
1169
  requests: [{ method: "GET", path: "/api/v1/audit-log" }],
761
1170
  channels: ["mcp", "zapier", "make", "n8n"]
762
1171
  },
1172
+ {
1173
+ id: "list_applications",
1174
+ title: "List applications in this workspace",
1175
+ 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.',
1176
+ kind: "read",
1177
+ scopes: ["applications:read"],
1178
+ input: [],
1179
+ requests: [{ method: "GET", path: "/api/v1/stores" }],
1180
+ channels: ["mcp", "zapier", "make", "n8n"]
1181
+ },
1182
+ {
1183
+ id: "get_sales_timeseries",
1184
+ title: "Sales over time",
1185
+ 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.`,
1186
+ kind: "read",
1187
+ scopes: ["orders:read"],
1188
+ input: [
1189
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1190
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1191
+ { name: "groupBy", type: "string", required: false, description: "Bucket size.", enum: ["day", "week", "month"], default: "day" },
1192
+ { 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." },
1193
+ { name: "compareTo", type: "string", required: false, description: "Add a comparison total.", enum: ["previous_period", "previous_year", "none"], default: "none" },
1194
+ {
1195
+ name: "applicationId",
1196
+ type: "string",
1197
+ required: false,
1198
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1199
+ }
1200
+ ],
1201
+ requests: [{ method: "GET", path: "/api/v1/analytics/orders-timeseries" }],
1202
+ channels: ["mcp", "zapier", "make", "n8n"],
1203
+ guardrails: [
1204
+ "Revenue is GROSS. Refunds are not deducted, because that needs payment_refunds:read, which OAuth connections cannot hold. Never describe the figure as net."
1205
+ ]
1206
+ },
763
1207
  {
764
1208
  id: "get_business_summary",
765
1209
  title: "Business summary",
766
- 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.",
1210
+ 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.",
767
1211
  kind: "read",
768
1212
  scopes: [],
1213
+ sectionScopes: ["orders:read", "subscriptions:read", "invoices:read", "quotes:read"],
769
1214
  input: [
770
1215
  {
771
1216
  name: "period",
@@ -774,6 +1219,15 @@ var ACTIONS = [
774
1219
  description: "Reporting window.",
775
1220
  enum: ["week", "month", "quarter"],
776
1221
  default: "month"
1222
+ },
1223
+ { name: "from", type: "string", required: false, description: "ISO 8601 start of range. Overrides period." },
1224
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range. Defaults to now." },
1225
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for period boundaries. Defaults to UTC." },
1226
+ {
1227
+ name: "applicationId",
1228
+ type: "string",
1229
+ required: false,
1230
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
777
1231
  }
778
1232
  ],
779
1233
  requests: [{ method: "GET", path: "/api/v1/analytics/business-summary" }],
@@ -782,19 +1236,189 @@ var ACTIONS = [
782
1236
  "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."
783
1237
  ]
784
1238
  },
1239
+ {
1240
+ id: "get_top_products",
1241
+ title: "Top products",
1242
+ 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.",
1243
+ kind: "read",
1244
+ scopes: ["orders:read"],
1245
+ input: [
1246
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1247
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1248
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for range boundaries. Defaults to UTC." },
1249
+ { name: "by", type: "string", required: false, description: "Sort key.", enum: ["revenue", "units"], default: "revenue" },
1250
+ { name: "limit", type: "number", required: false, description: "Max rows to return (1-100). Defaults to 20." },
1251
+ {
1252
+ name: "applicationId",
1253
+ type: "string",
1254
+ required: false,
1255
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1256
+ }
1257
+ ],
1258
+ requests: [{ method: "GET", path: "/api/v1/analytics/products" }],
1259
+ channels: ["mcp", "zapier", "make", "n8n"],
1260
+ guardrails: [
1261
+ "Revenue is GROSS. Refunds are not deducted, because that needs payment_refunds:read, which OAuth connections cannot hold. Never describe the figure as net."
1262
+ ]
1263
+ },
785
1264
  {
786
1265
  id: "get_attention_digest",
787
1266
  title: "Attention digest",
788
1267
  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.",
789
1268
  kind: "read",
790
1269
  scopes: [],
791
- input: [],
1270
+ sectionScopes: ["payment_disputes:read", "subscriptions:read", "invoices:read", "webhook_deliveries:read"],
1271
+ input: [
1272
+ {
1273
+ name: "applicationId",
1274
+ type: "string",
1275
+ required: false,
1276
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1277
+ }
1278
+ ],
792
1279
  requests: [{ method: "GET", path: "/api/v1/analytics/attention-digest" }],
793
1280
  channels: ["mcp", "zapier", "make", "n8n"],
794
1281
  guardrails: [
795
1282
  "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."
796
1283
  ]
797
1284
  },
1285
+ {
1286
+ id: "get_customer_growth",
1287
+ title: "Customer growth",
1288
+ 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.",
1289
+ kind: "read",
1290
+ scopes: ["customers:read"],
1291
+ input: [
1292
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1293
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1294
+ { name: "groupBy", type: "string", required: false, description: "Signup series bucket size.", enum: ["day", "week", "month"], default: "day" },
1295
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for bucket boundaries. Defaults to UTC." },
1296
+ { name: "topBy", type: "string", required: false, description: "Sort key for top customers.", enum: ["revenue", "orders"], default: "revenue" },
1297
+ { name: "topLimit", type: "number", required: false, description: "Max top-customers rows to return (1-50). Defaults to 10." },
1298
+ {
1299
+ name: "applicationId",
1300
+ type: "string",
1301
+ required: false,
1302
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1303
+ }
1304
+ ],
1305
+ requests: [{ method: "GET", path: "/api/v1/analytics/customers" }],
1306
+ channels: ["mcp", "zapier", "make", "n8n"],
1307
+ guardrails: [
1308
+ "Revenue is GROSS. Refunds are not deducted, because that needs payment_refunds:read, which OAuth connections cannot hold. Never describe the figure as net.",
1309
+ "topCustomers is a top-N list; use the accompanying totals for range-wide customer/order/revenue counts instead of summing the returned rows.",
1310
+ "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."
1311
+ ]
1312
+ },
1313
+ {
1314
+ id: "get_revenue_breakdown",
1315
+ title: "Revenue breakdown",
1316
+ description: "Revenue split by one dimension over a date range \u2014 payment method, order source, or currency. Amounts are gross; refunds are not deducted.",
1317
+ kind: "read",
1318
+ scopes: ["orders:read"],
1319
+ input: [
1320
+ {
1321
+ name: "dimension",
1322
+ type: "string",
1323
+ required: true,
1324
+ description: "Breakdown dimension.",
1325
+ enum: ["paymentMethod", "source", "currency"]
1326
+ },
1327
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1328
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1329
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for range boundaries. Defaults to UTC." },
1330
+ {
1331
+ name: "applicationId",
1332
+ type: "string",
1333
+ required: false,
1334
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1335
+ }
1336
+ ],
1337
+ requests: [{ method: "GET", path: "/api/v1/analytics/revenue-breakdown" }],
1338
+ channels: ["mcp", "zapier", "make", "n8n"],
1339
+ guardrails: [
1340
+ "Revenue is GROSS. Refunds are not deducted, because that needs payment_refunds:read, which OAuth connections cannot hold. Never describe the figure as net.",
1341
+ "dimension is required; it is never defaulted, so omitting it fails the call instead of silently picking a dimension.",
1342
+ "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)."
1343
+ ]
1344
+ },
1345
+ {
1346
+ id: "get_funnel_conversion",
1347
+ title: "Checkout funnel",
1348
+ description: "Checkout funnel over a date range: carts created, checkout sessions started, orders completed, with the drop-off between each stage.",
1349
+ kind: "read",
1350
+ scopes: ["carts:read"],
1351
+ input: [
1352
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1353
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1354
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for range boundaries. Defaults to UTC." },
1355
+ {
1356
+ name: "applicationId",
1357
+ type: "string",
1358
+ required: false,
1359
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1360
+ }
1361
+ ],
1362
+ requests: [{ method: "GET", path: "/api/v1/analytics/funnel" }],
1363
+ channels: ["mcp", "zapier", "make", "n8n"],
1364
+ guardrails: [
1365
+ "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.",
1366
+ "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.",
1367
+ 'dropOff[].ratePct is the percentage of "from" that did NOT reach "to" (loss), not a conversion rate. The step-conversion rate is 100 - ratePct.'
1368
+ ]
1369
+ },
1370
+ {
1371
+ id: "get_subscription_movement",
1372
+ title: "Subscription movement",
1373
+ 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.",
1374
+ kind: "read",
1375
+ scopes: ["subscriptions:read"],
1376
+ input: [
1377
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1378
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1379
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for bucket boundaries. Defaults to UTC." },
1380
+ { name: "groupBy", type: "string", required: false, description: "Bucket size.", enum: ["day", "week", "month"], default: "day" },
1381
+ {
1382
+ name: "applicationId",
1383
+ type: "string",
1384
+ required: false,
1385
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1386
+ }
1387
+ ],
1388
+ requests: [{ method: "GET", path: "/api/v1/analytics/subscriptions" }],
1389
+ channels: ["mcp", "zapier", "make", "n8n"],
1390
+ guardrails: [
1391
+ "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.",
1392
+ "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.",
1393
+ "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[]."
1394
+ ]
1395
+ },
1396
+ {
1397
+ id: "get_discount_performance",
1398
+ title: "Discount performance",
1399
+ description: "Discount code performance over a date range: redemption count, revenue on discounted orders, and total discount given per code.",
1400
+ kind: "read",
1401
+ scopes: ["discounts:read"],
1402
+ input: [
1403
+ { name: "from", type: "string", required: false, description: 'ISO 8601 start of range, exclusive. Defaults to 30 days before "to".' },
1404
+ { name: "to", type: "string", required: false, description: "ISO 8601 end of range, exclusive. Defaults to now." },
1405
+ { name: "timezone", type: "string", required: false, description: "IANA timezone for range boundaries. Defaults to UTC." },
1406
+ { name: "limit", type: "number", required: false, description: "Max rows to return. Defaults to 20." },
1407
+ {
1408
+ name: "applicationId",
1409
+ type: "string",
1410
+ required: false,
1411
+ description: "Application to read from. Defaults to the one this connection was authorized for. Call list_applications to see the others."
1412
+ }
1413
+ ],
1414
+ requests: [{ method: "GET", path: "/api/v1/analytics/discounts" }],
1415
+ channels: ["mcp", "zapier", "make", "n8n"],
1416
+ guardrails: [
1417
+ '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.',
1418
+ "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.",
1419
+ "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."
1420
+ ]
1421
+ },
798
1422
  // ------------------------------------------------------------ sandbox write
799
1423
  {
800
1424
  id: "seed_test_data",
@@ -820,6 +1444,151 @@ var ACTIONS = [
820
1444
  "Seeds a finalized DRAFT order \u2014 it does not capture a payment, which needs a browser-side tokenised payment method."
821
1445
  ]
822
1446
  },
1447
+ // ---------------------------------------------------------- inbound writes
1448
+ // The no-code channels are mostly used in the "something happened elsewhere,
1449
+ // put it into Throttle" direction — a form submission, a new CRM contact, a
1450
+ // spreadsheet row. These three are that direction.
1451
+ {
1452
+ id: "create_customer",
1453
+ title: "Create a customer",
1454
+ description: "Create a customer record. Use search_customers first if the buyer may already exist \u2014 this does not de-duplicate by email.",
1455
+ kind: "write",
1456
+ scopes: ["customers:write"],
1457
+ input: [
1458
+ { name: "email", type: "string", required: true, description: "The buyer's email address." },
1459
+ { name: "firstName", type: "string", required: false, description: "Given name." },
1460
+ { name: "lastName", type: "string", required: false, description: "Family name." },
1461
+ { name: "phone", type: "string", required: false, description: "Contact phone number." },
1462
+ { name: "company", type: "string", required: false, description: "The buyer's company." },
1463
+ {
1464
+ name: "externalId",
1465
+ type: "string",
1466
+ required: false,
1467
+ description: "This customer's id in your own system. Resolvable later via get_customer."
1468
+ },
1469
+ {
1470
+ name: "netN",
1471
+ type: "number",
1472
+ required: false,
1473
+ description: "Invoice terms in days for this customer specifically, 0-365. Omit to use the workspace default; 0 means due on issue."
1474
+ },
1475
+ {
1476
+ name: "acceptsMarketing",
1477
+ type: "boolean",
1478
+ required: false,
1479
+ description: "Whether the buyer opted into marketing email.",
1480
+ default: false
1481
+ }
1482
+ ],
1483
+ requests: [{ method: "POST", path: "/api/v1/customers" }],
1484
+ channels: ["mcp", "zapier", "make", "n8n"],
1485
+ idempotent: true,
1486
+ guardrails: [
1487
+ "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."
1488
+ ]
1489
+ },
1490
+ {
1491
+ id: "update_customer",
1492
+ title: "Update a customer",
1493
+ description: "Change fields on an existing customer. Only the fields supplied are touched; everything else is left alone.",
1494
+ kind: "write",
1495
+ scopes: ["customers:write"],
1496
+ input: [
1497
+ { name: "id", type: "string", required: true, description: "Customer id." },
1498
+ { name: "email", type: "string", required: false, description: "The buyer's email address." },
1499
+ { name: "firstName", type: "string", required: false, description: "Given name." },
1500
+ { name: "lastName", type: "string", required: false, description: "Family name." },
1501
+ { name: "phone", type: "string", required: false, description: "Contact phone number." },
1502
+ { name: "company", type: "string", required: false, description: "The buyer's company." },
1503
+ {
1504
+ name: "externalId",
1505
+ type: "string",
1506
+ required: false,
1507
+ description: "This customer's id in your own system. Pass null to clear it."
1508
+ },
1509
+ {
1510
+ name: "netN",
1511
+ type: "number",
1512
+ required: false,
1513
+ description: "Invoice terms in days for this customer specifically, 0-365."
1514
+ },
1515
+ {
1516
+ name: "acceptsMarketing",
1517
+ type: "boolean",
1518
+ required: false,
1519
+ description: "Whether the buyer opted into marketing email."
1520
+ }
1521
+ ],
1522
+ requests: [{ method: "PATCH", path: "/api/v1/customers/:id" }],
1523
+ channels: ["mcp", "zapier", "make", "n8n"],
1524
+ idempotent: true,
1525
+ guardrails: [
1526
+ "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."
1527
+ ]
1528
+ },
1529
+ {
1530
+ id: "create_checkout_link",
1531
+ title: "Create a checkout link",
1532
+ 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).",
1533
+ kind: "write",
1534
+ scopes: ["carts:write", "cart_items:write", "checkout_sessions:write"],
1535
+ input: [
1536
+ {
1537
+ name: "lineItems",
1538
+ type: "array",
1539
+ required: true,
1540
+ description: "What the buyer is paying for: { name, unitPrice, quantity }. unitPrice is an integer in minor units."
1541
+ },
1542
+ {
1543
+ name: "returnUrl",
1544
+ type: "string",
1545
+ required: true,
1546
+ description: "Absolute URL the buyer lands on after paying."
1547
+ },
1548
+ {
1549
+ name: "cancelUrl",
1550
+ type: "string",
1551
+ required: true,
1552
+ description: "Absolute URL the buyer lands on if they abandon checkout."
1553
+ },
1554
+ {
1555
+ name: "customerId",
1556
+ type: "string",
1557
+ required: false,
1558
+ description: "Attach the cart to an existing customer."
1559
+ },
1560
+ {
1561
+ name: "customerEmail",
1562
+ type: "string",
1563
+ required: false,
1564
+ description: "Pre-fill the buyer email without needing a customer record."
1565
+ },
1566
+ {
1567
+ name: "discountCode",
1568
+ type: "string",
1569
+ required: false,
1570
+ description: "Discount code to apply to the session."
1571
+ },
1572
+ {
1573
+ name: "netN",
1574
+ type: "number",
1575
+ required: false,
1576
+ description: "Invoice terms in days for this cart, 0-365. Omit to pay in full at checkout."
1577
+ }
1578
+ ],
1579
+ // Three calls: the API has no single "checkout link" endpoint, and a
1580
+ // checkout session requires a cart that already holds its line items.
1581
+ requests: [
1582
+ { method: "POST", path: "/api/v1/carts" },
1583
+ { method: "POST", path: "/api/v1/carts/:id/items" },
1584
+ { method: "POST", path: "/api/v1/checkout/sessions" }
1585
+ ],
1586
+ channels: ["mcp", "zapier", "make", "n8n"],
1587
+ idempotent: true,
1588
+ guardrails: [
1589
+ "The returned URL is buyer-facing and takes real payment in a production environment."
1590
+ ]
1591
+ },
823
1592
  // -------------------------------------------------------------- money/mail
824
1593
  {
825
1594
  id: "create_quote",
@@ -848,7 +1617,8 @@ var ACTIONS = [
848
1617
  requests: [{ method: "POST", path: "/api/v1/quotes/:id/issue" }],
849
1618
  channels: ["mcp", "zapier", "make", "n8n"],
850
1619
  idempotent: true,
851
- guardrails: ['Sends mail to a real buyer. Never call it speculatively or to "check" a quote.']
1620
+ guardrails: ['Sends mail to a real buyer. Never call it speculatively or to "check" a quote.'],
1621
+ destructive: true
852
1622
  },
853
1623
  {
854
1624
  id: "refund_payment",
@@ -867,12 +1637,20 @@ var ACTIONS = [
867
1637
  { name: "reason", type: "string", required: false, description: "Reason recorded on the refund." }
868
1638
  ],
869
1639
  requests: [{ method: "POST", path: "/api/v1/payments/:id/refund" }],
870
- channels: ["mcp", "zapier", "make", "n8n"],
1640
+ // NOT zapier/make. `payment_refunds:write` is `sensitive` in the scope
1641
+ // registry, and `validateScopesForOAuth` rejects every sensitive scope — so
1642
+ // an OAuth grant, which is the only credential a public Zapier or Make app
1643
+ // can hold, can never carry it. Listing the action on those channels would
1644
+ // ship a connector action that 403s on every call. MCP and n8n keep it:
1645
+ // both authenticate with an `sk_` key, which CAN hold the scope.
1646
+ // `oauth-grantable-scopes` in the catalog tests enforces this.
1647
+ channels: ["mcp", "n8n"],
871
1648
  idempotent: true,
872
1649
  guardrails: [
873
1650
  "Moves money and is irreversible. Confirm with the user first.",
874
1651
  "The API treats an omitted amount as a FULL refund, so this action requires it explicitly."
875
- ]
1652
+ ],
1653
+ destructive: true
876
1654
  },
877
1655
  {
878
1656
  id: "manage_subscription",
@@ -915,7 +1693,8 @@ var ACTIONS = [
915
1693
  requests: [{ method: "POST", path: "/api/v1/subscriptions/:id/cancel" }],
916
1694
  channels: ["mcp", "zapier", "make", "n8n"],
917
1695
  idempotent: true,
918
- guardrails: ["Ends billing for a paying customer. Confirm with the user first."]
1696
+ guardrails: ["Ends billing for a paying customer. Confirm with the user first."],
1697
+ destructive: true
919
1698
  },
920
1699
  {
921
1700
  id: "replay_webhook_delivery",
@@ -927,7 +1706,8 @@ var ACTIONS = [
927
1706
  requests: [{ method: "POST", path: "/api/v1/webhook-deliveries/:id/replay" }],
928
1707
  channels: ["mcp", "n8n"],
929
1708
  idempotent: true,
930
- guardrails: ["The receiving system will process the event again. Confirm it is idempotent first."]
1709
+ guardrails: ["The receiving system will process the event again. Confirm it is idempotent first."],
1710
+ destructive: true
931
1711
  },
932
1712
  {
933
1713
  id: "send_test_webhook",
@@ -941,7 +1721,8 @@ var ACTIONS = [
941
1721
  ],
942
1722
  requests: [{ method: "POST", path: "/api/v1/webhook-endpoints/:id/test" }],
943
1723
  channels: ["mcp", "n8n"],
944
- idempotent: true
1724
+ idempotent: true,
1725
+ destructive: true
945
1726
  }
946
1727
  ];
947
1728
 
@@ -1172,7 +1953,7 @@ function validatePayload(index, input) {
1172
1953
  }
1173
1954
 
1174
1955
  // src/docs.ts
1175
- var DOCS_ORIGIN = "https://docs.usethrottle.dev";
1956
+ var DOCS_ORIGIN = "https://usethrottle.dev/docs";
1176
1957
  function excerptAround(text, needle, radius = 160) {
1177
1958
  const at = text.toLowerCase().indexOf(needle.toLowerCase());
1178
1959
  if (at < 0) return text.slice(0, radius * 2).trim();
@@ -1256,27 +2037,35 @@ var HANDLERS = {
1256
2037
  // ------------------------------------------------------------- generic read
1257
2038
  api_get: async (ctx, args) => {
1258
2039
  const path = String(args.path ?? "");
1259
- if (!path.startsWith("/api/v1/")) {
2040
+ const resolved = new URL(path, "https://throttle.invalid").pathname;
2041
+ if (!path.startsWith("/api/v1/") || !resolved.startsWith("/api/v1/") || /(^|[\/\\])\.(\.)?([\/\\?#]|$)|%2e/i.test(path)) {
1260
2042
  throw new Error(
1261
2043
  `Refusing to call "${path}". api_get only reads paths beginning with /api/v1/ on the configured Throttle host.`
1262
2044
  );
1263
2045
  }
1264
- return ctx.client.get(path);
2046
+ return ctx.client.get(path, void 0, { applicationId: args.applicationId });
1265
2047
  },
1266
2048
  // ------------------------------------------------------------------ orders
1267
- search_orders: async (ctx, args) => ctx.client.list("/api/v1/orders", {
1268
- q: args.q,
1269
- status: args.status,
1270
- paymentStatus: args.paymentStatus,
1271
- ...page(args)
1272
- }),
2049
+ search_orders: async (ctx, args) => ctx.client.list(
2050
+ "/api/v1/orders",
2051
+ {
2052
+ q: args.q,
2053
+ status: args.status,
2054
+ paymentStatus: args.paymentStatus,
2055
+ createdAfter: args.createdAfter,
2056
+ createdBefore: args.createdBefore,
2057
+ ...page(args)
2058
+ },
2059
+ { applicationId: args.applicationId }
2060
+ ),
1273
2061
  get_order: async (ctx, args) => {
1274
2062
  const id = String(args.id);
1275
- const order = await ctx.client.get(`/api/v1/orders/${id}`);
2063
+ const opts = { applicationId: args.applicationId };
2064
+ const order = await ctx.client.get(`/api/v1/orders/${id}`, void 0, opts);
1276
2065
  const [payments, returns, fulfillments] = await Promise.all([
1277
- optional(() => ctx.client.list(`/api/v1/orders/${id}/payments`)),
1278
- optional(() => ctx.client.list(`/api/v1/orders/${id}/returns`)),
1279
- optional(() => ctx.client.list(`/api/v1/orders/${id}/fulfillments`))
2066
+ optional(() => ctx.client.list(`/api/v1/orders/${id}/payments`, void 0, opts)),
2067
+ optional(() => ctx.client.list(`/api/v1/orders/${id}/returns`, void 0, opts)),
2068
+ optional(() => ctx.client.list(`/api/v1/orders/${id}/fulfillments`, void 0, opts))
1280
2069
  ]);
1281
2070
  return {
1282
2071
  order,
@@ -1286,13 +2075,23 @@ var HANDLERS = {
1286
2075
  };
1287
2076
  },
1288
2077
  // --------------------------------------------------------------- customers
1289
- search_customers: async (ctx, args) => ctx.client.list("/api/v1/customers", { q: args.q, ...page(args) }),
2078
+ search_customers: async (ctx, args) => ctx.client.list(
2079
+ "/api/v1/customers",
2080
+ {
2081
+ q: args.q,
2082
+ createdAfter: args.createdAfter,
2083
+ createdBefore: args.createdBefore,
2084
+ ...page(args)
2085
+ },
2086
+ { applicationId: args.applicationId }
2087
+ ),
1290
2088
  get_customer: async (ctx, args) => {
1291
2089
  const id = String(args.id);
1292
- const customer = await ctx.client.get(`/api/v1/customers/${id}`);
2090
+ const opts = { applicationId: args.applicationId };
2091
+ const customer = await ctx.client.get(`/api/v1/customers/${id}`, void 0, opts);
1293
2092
  const [addresses, subscriptions] = await Promise.all([
1294
- optional(() => ctx.client.list(`/api/v1/customers/${id}/addresses`)),
1295
- optional(() => ctx.client.list("/api/v1/subscriptions", { customerId: id }))
2093
+ optional(() => ctx.client.list(`/api/v1/customers/${id}/addresses`, void 0, opts)),
2094
+ optional(() => ctx.client.list("/api/v1/subscriptions", { customerId: id }, opts))
1296
2095
  ]);
1297
2096
  return {
1298
2097
  customer,
@@ -1301,60 +2100,101 @@ var HANDLERS = {
1301
2100
  };
1302
2101
  },
1303
2102
  // ----------------------------------------------------------- subscriptions
1304
- list_subscriptions: async (ctx, args) => ctx.client.list("/api/v1/subscriptions", {
1305
- customerId: args.customerId,
1306
- status: args.status,
1307
- ...page(args)
2103
+ list_subscriptions: async (ctx, args) => ctx.client.list(
2104
+ "/api/v1/subscriptions",
2105
+ {
2106
+ customerId: args.customerId,
2107
+ status: args.status,
2108
+ createdAfter: args.createdAfter,
2109
+ createdBefore: args.createdBefore,
2110
+ ...page(args)
2111
+ },
2112
+ { applicationId: args.applicationId }
2113
+ ),
2114
+ get_subscription: async (ctx, args) => ctx.client.get(`/api/v1/subscriptions/${String(args.id)}`, void 0, {
2115
+ applicationId: args.applicationId
1308
2116
  }),
1309
- get_subscription: async (ctx, args) => ctx.client.get(`/api/v1/subscriptions/${String(args.id)}`),
1310
2117
  // ------------------------------------------------------------------ quotes
1311
- search_quotes: async (ctx, args) => ctx.client.list("/api/v1/quotes", { status: args.status, ...page(args) }),
1312
- get_quote: async (ctx, args) => ctx.client.get(`/api/v1/quotes/${String(args.id)}`),
2118
+ search_quotes: async (ctx, args) => ctx.client.list(
2119
+ "/api/v1/quotes",
2120
+ {
2121
+ status: args.status,
2122
+ createdAfter: args.createdAfter,
2123
+ createdBefore: args.createdBefore,
2124
+ ...page(args)
2125
+ },
2126
+ { applicationId: args.applicationId }
2127
+ ),
2128
+ get_quote: async (ctx, args) => ctx.client.get(`/api/v1/quotes/${String(args.id)}`, void 0, {
2129
+ applicationId: args.applicationId
2130
+ }),
1313
2131
  // ---------------------------------------------------------------- invoices
1314
- list_invoices: async (ctx, args) => ctx.client.list("/api/v1/invoices", { status: args.status, ...page(args) }),
1315
- receivables_aging: async (ctx) => ctx.client.get("/api/v1/invoices/aging"),
2132
+ list_invoices: async (ctx, args) => ctx.client.list(
2133
+ "/api/v1/invoices",
2134
+ { status: args.status, ...page(args) },
2135
+ { applicationId: args.applicationId }
2136
+ ),
2137
+ receivables_aging: async (ctx, args) => ctx.client.get("/api/v1/invoices/aging", void 0, { applicationId: args.applicationId }),
1316
2138
  // --------------------------------------------------------------- discounts
1317
- search_discounts: async (ctx, args) => ctx.client.list("/api/v1/discounts", {
1318
- q: args.q,
1319
- code: args.code,
1320
- type: args.type,
1321
- isActive: args.isActive,
1322
- createdAfter: args.createdAfter,
1323
- createdBefore: args.createdBefore,
1324
- ...page(args)
2139
+ search_discounts: async (ctx, args) => ctx.client.list(
2140
+ "/api/v1/discounts",
2141
+ {
2142
+ q: args.q,
2143
+ code: args.code,
2144
+ type: args.type,
2145
+ isActive: args.isActive,
2146
+ createdAfter: args.createdAfter,
2147
+ createdBefore: args.createdBefore,
2148
+ ...page(args)
2149
+ },
2150
+ { applicationId: args.applicationId }
2151
+ ),
2152
+ get_discount: async (ctx, args) => ctx.client.get(`/api/v1/discounts/${String(args.id)}`, void 0, {
2153
+ applicationId: args.applicationId
1325
2154
  }),
1326
- get_discount: async (ctx, args) => ctx.client.get(`/api/v1/discounts/${String(args.id)}`),
1327
2155
  // ---------------------------------------------------------------- payments
1328
- search_payments: async (ctx, args) => ctx.client.list("/api/v1/payments", {
1329
- method: args.method,
1330
- status: args.status,
1331
- // The route only accepts these as snake_case query params -- confirmed
1332
- // by reading its querystring schema directly, not assumed from the
1333
- // platform's usual camelCase convention.
1334
- order_id: args.orderId,
1335
- aging: args.aging,
1336
- due_from: args.dueFrom,
1337
- due_to: args.dueTo,
1338
- amount_min: args.amountMin,
1339
- amount_max: args.amountMax,
1340
- ...page(args)
1341
- }),
2156
+ search_payments: async (ctx, args) => ctx.client.list(
2157
+ "/api/v1/payments",
2158
+ {
2159
+ method: args.method,
2160
+ status: args.status,
2161
+ // The route only accepts these as snake_case query params -- confirmed
2162
+ // by reading its querystring schema directly, not assumed from the
2163
+ // platform's usual camelCase convention.
2164
+ order_id: args.orderId,
2165
+ aging: args.aging,
2166
+ due_from: args.dueFrom,
2167
+ due_to: args.dueTo,
2168
+ amount_min: args.amountMin,
2169
+ amount_max: args.amountMax,
2170
+ ...page(args)
2171
+ },
2172
+ { applicationId: args.applicationId }
2173
+ ),
1342
2174
  get_payment: async (ctx, args) => {
1343
2175
  const id = String(args.id);
1344
- const payment = await ctx.client.get(`/api/v1/payments/${id}`);
1345
- const transactions = await optional(() => ctx.client.list(`/api/v1/payments/${id}/transactions`));
2176
+ const opts = { applicationId: args.applicationId };
2177
+ const payment = await ctx.client.get(`/api/v1/payments/${id}`, void 0, opts);
2178
+ const transactions = await optional(
2179
+ () => ctx.client.list(`/api/v1/payments/${id}/transactions`, void 0, opts)
2180
+ );
1346
2181
  return {
1347
2182
  payment,
1348
2183
  transactions: transactions.ok ? transactions.value.data : transactions.unavailable
1349
2184
  };
1350
2185
  },
1351
2186
  // ---------------------------------------------------------------- invoices
1352
- get_invoice: async (ctx, args) => ctx.client.get(`/api/v1/invoices/${String(args.id)}`),
2187
+ get_invoice: async (ctx, args) => ctx.client.get(`/api/v1/invoices/${String(args.id)}`, void 0, {
2188
+ applicationId: args.applicationId
2189
+ }),
1353
2190
  // ------------------------------------------------------------------ carts
1354
2191
  list_abandoned_carts: async (ctx, args) => {
2192
+ const opts = { applicationId: args.applicationId };
1355
2193
  const [carts, summary] = await Promise.all([
1356
- optional(() => ctx.client.list("/api/v1/abandoned-carts", { ...page(args) })),
1357
- optional(() => ctx.client.get("/api/v1/abandoned-carts/summary", { sinceDays: args.sinceDays }))
2194
+ optional(() => ctx.client.list("/api/v1/abandoned-carts", { ...page(args) }, opts)),
2195
+ optional(
2196
+ () => ctx.client.get("/api/v1/abandoned-carts/summary", { sinceDays: args.sinceDays }, opts)
2197
+ )
1358
2198
  ]);
1359
2199
  return {
1360
2200
  carts: carts.ok ? carts.value.data : carts.unavailable,
@@ -1362,18 +2202,29 @@ var HANDLERS = {
1362
2202
  };
1363
2203
  },
1364
2204
  // --------------------------------------------------------------- debugging
1365
- search_events: async (ctx, args) => ctx.client.list("/api/v1/events", {
1366
- type: args.type,
1367
- entityType: args.entityType,
1368
- entityId: args.entityId,
1369
- ...page(args)
1370
- }),
1371
- list_webhook_deliveries: async (ctx, args) => ctx.client.list("/api/v1/webhook-deliveries", {
1372
- endpointId: args.endpointId,
1373
- status: args.status,
1374
- limit: args.limit
2205
+ search_events: async (ctx, args) => ctx.client.list(
2206
+ "/api/v1/events",
2207
+ {
2208
+ type: args.type,
2209
+ entityType: args.entityType,
2210
+ entityId: args.entityId,
2211
+ ...page(args)
2212
+ },
2213
+ { applicationId: args.applicationId }
2214
+ ),
2215
+ list_webhook_deliveries: async (ctx, args) => ctx.client.list(
2216
+ "/api/v1/webhook-deliveries",
2217
+ {
2218
+ endpointId: args.endpointId,
2219
+ status: args.status,
2220
+ eventType: args.eventType,
2221
+ limit: args.limit
2222
+ },
2223
+ { applicationId: args.applicationId }
2224
+ ),
2225
+ get_webhook_delivery: async (ctx, args) => ctx.client.get(`/api/v1/webhook-deliveries/${String(args.id)}`, void 0, {
2226
+ applicationId: args.applicationId
1375
2227
  }),
1376
- get_webhook_delivery: async (ctx, args) => ctx.client.get(`/api/v1/webhook-deliveries/${String(args.id)}`),
1377
2228
  send_test_webhook: async (ctx, args) => ctx.client.post(`/api/v1/webhook-endpoints/${String(args.id)}/test`, {
1378
2229
  body: { eventType: args.eventType },
1379
2230
  idempotencyKey: `mcp-send-test-webhook-${String(args.id)}-${Date.now()}`
@@ -1383,18 +2234,21 @@ var HANDLERS = {
1383
2234
  }),
1384
2235
  explain_checkout_failure: async (ctx, args) => {
1385
2236
  const id = String(args.id);
1386
- const session = await optional(() => ctx.client.get(`/api/v1/checkout-sessions/${id}`));
2237
+ const opts = { applicationId: args.applicationId };
2238
+ const session = await optional(
2239
+ () => ctx.client.get(`/api/v1/checkout-sessions/${id}`, void 0, opts)
2240
+ );
1387
2241
  if (!session.ok) {
1388
2242
  return {
1389
- 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.`
2243
+ 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).`
1390
2244
  };
1391
2245
  }
1392
2246
  const cartId = session.value?.cartId ?? void 0;
1393
2247
  const [cart, sessionEvents, cartEvents, deliveries] = await Promise.all([
1394
- optional(() => ctx.client.get(`/api/v1/checkout-sessions/${id}/cart`)),
1395
- optional(() => ctx.client.list("/api/v1/events", { entityId: id, limit: 25 })),
1396
- cartId ? optional(() => ctx.client.list("/api/v1/events", { entityId: cartId, limit: 25 })) : Promise.resolve({ ok: true, value: { data: [], pagination: null } }),
1397
- optional(() => ctx.client.list("/api/v1/webhook-deliveries", { limit: 15 }))
2248
+ optional(() => ctx.client.get(`/api/v1/checkout-sessions/${id}/cart`, void 0, opts)),
2249
+ optional(() => ctx.client.list("/api/v1/events", { entityId: id, limit: 25 }, opts)),
2250
+ cartId ? optional(() => ctx.client.list("/api/v1/events", { entityId: cartId, limit: 25 }, opts)) : Promise.resolve({ ok: true, value: { data: [], pagination: null } }),
2251
+ optional(() => ctx.client.list("/api/v1/webhook-deliveries", { limit: 15 }, opts))
1398
2252
  ]);
1399
2253
  return {
1400
2254
  session: session.value,
@@ -1454,6 +2308,73 @@ var HANDLERS = {
1454
2308
  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."
1455
2309
  };
1456
2310
  },
2311
+ // ---------------------------------------------------------- inbound writes
2312
+ create_customer: async (ctx, args) => ctx.client.post("/api/v1/customers", {
2313
+ // `createCustomerBody` is `.strict()`, so only fields it declares may be
2314
+ // sent; `undefined` values drop out at JSON.stringify.
2315
+ body: {
2316
+ email: args.email,
2317
+ firstName: args.firstName,
2318
+ lastName: args.lastName,
2319
+ phone: args.phone,
2320
+ company: args.company,
2321
+ externalId: args.externalId,
2322
+ netN: args.netN,
2323
+ acceptsMarketing: args.acceptsMarketing
2324
+ },
2325
+ idempotencyKey: `mcp-create-customer-${String(args.email)}-${Date.now()}`
2326
+ }),
2327
+ update_customer: async (ctx, args) => ctx.client.patch(`/api/v1/customers/${String(args.id)}`, {
2328
+ // `id` is the path parameter, not a body field — `updateCustomerBody` is
2329
+ // `.strict()` and would reject it. A null `externalId` is meaningful
2330
+ // (it clears the value), so only `undefined` is dropped.
2331
+ body: {
2332
+ email: args.email,
2333
+ firstName: args.firstName,
2334
+ lastName: args.lastName,
2335
+ phone: args.phone,
2336
+ company: args.company,
2337
+ externalId: args.externalId,
2338
+ netN: args.netN,
2339
+ acceptsMarketing: args.acceptsMarketing
2340
+ },
2341
+ idempotencyKey: `mcp-update-customer-${String(args.id)}-${Date.now()}`
2342
+ }),
2343
+ create_checkout_link: async (ctx, args) => {
2344
+ const lineItems = Array.isArray(args.lineItems) ? args.lineItems : [];
2345
+ if (lineItems.length === 0) {
2346
+ throw new Error("create_checkout_link: lineItems must contain at least one item.");
2347
+ }
2348
+ const stamp = Date.now();
2349
+ const applicationId = ctx.identity.application?.id;
2350
+ const cart = await ctx.client.post("/api/v1/carts", {
2351
+ body: {
2352
+ applicationId,
2353
+ customerId: args.customerId,
2354
+ customerEmail: args.customerEmail,
2355
+ netN: args.netN
2356
+ },
2357
+ idempotencyKey: `mcp-checkout-link-cart-${stamp}`
2358
+ });
2359
+ for (const [index, item] of lineItems.entries()) {
2360
+ await ctx.client.post(`/api/v1/carts/${cart?.id}/items`, {
2361
+ body: item,
2362
+ idempotencyKey: `mcp-checkout-link-item-${stamp}-${index}`
2363
+ });
2364
+ }
2365
+ const session = await ctx.client.post("/api/v1/checkout/sessions", {
2366
+ body: {
2367
+ applicationId,
2368
+ cartId: cart?.id,
2369
+ returnUrl: args.returnUrl,
2370
+ cancelUrl: args.cancelUrl,
2371
+ customerEmail: args.customerEmail,
2372
+ discountCode: args.discountCode
2373
+ },
2374
+ idempotencyKey: `mcp-checkout-link-session-${stamp}`
2375
+ });
2376
+ return { cartId: cart?.id, ...session };
2377
+ },
1457
2378
  // -------------------------------------------------------------- money/mail
1458
2379
  manage_subscription: async (ctx, args) => {
1459
2380
  const id = String(args.id);
@@ -1483,17 +2404,118 @@ var HANDLERS = {
1483
2404
  idempotencyKey: `mcp-create-quote-${String(args.customerId)}-${Date.now()}`
1484
2405
  })
1485
2406
  ),
1486
- get_business_summary: async (ctx, args) => ctx.client.get("/api/v1/analytics/business-summary", { period: args.period }),
1487
- get_attention_digest: async (ctx) => ctx.client.get("/api/v1/analytics/attention-digest"),
2407
+ get_sales_timeseries: async (ctx, args) => ctx.client.get(
2408
+ "/api/v1/analytics/orders-timeseries",
2409
+ {
2410
+ from: args.from,
2411
+ to: args.to,
2412
+ groupBy: args.groupBy,
2413
+ timezone: args.timezone,
2414
+ compareTo: args.compareTo
2415
+ },
2416
+ { applicationId: args.applicationId }
2417
+ ),
2418
+ get_business_summary: async (ctx, args) => ctx.client.get(
2419
+ "/api/v1/analytics/business-summary",
2420
+ {
2421
+ period: args.period,
2422
+ from: args.from,
2423
+ to: args.to,
2424
+ timezone: args.timezone
2425
+ },
2426
+ { applicationId: args.applicationId }
2427
+ ),
2428
+ get_attention_digest: async (ctx, args) => ctx.client.get("/api/v1/analytics/attention-digest", void 0, {
2429
+ applicationId: args.applicationId
2430
+ }),
2431
+ get_top_products: async (ctx, args) => ctx.client.get(
2432
+ "/api/v1/analytics/products",
2433
+ {
2434
+ from: args.from,
2435
+ to: args.to,
2436
+ timezone: args.timezone,
2437
+ by: args.by,
2438
+ limit: args.limit
2439
+ },
2440
+ { applicationId: args.applicationId }
2441
+ ),
2442
+ // `.get()`, not `.list()` — the response is a single object (`{ data: {
2443
+ // range, signups, ordersByCustomerType, topCustomers, totals } }`), and
2444
+ // `.list()` only unwraps arrays: it would silently return an empty list
2445
+ // instead of the real payload.
2446
+ get_customer_growth: async (ctx, args) => ctx.client.get(
2447
+ "/api/v1/analytics/customers",
2448
+ {
2449
+ from: args.from,
2450
+ to: args.to,
2451
+ groupBy: args.groupBy,
2452
+ timezone: args.timezone,
2453
+ topBy: args.topBy,
2454
+ topLimit: args.topLimit
2455
+ },
2456
+ { applicationId: args.applicationId }
2457
+ ),
2458
+ // `.get()`, not `.list()` — the response is a single object (`{ data: {
2459
+ // range, dimension, rows, totals, note } }`), not a paginated array.
2460
+ get_revenue_breakdown: async (ctx, args) => ctx.client.get(
2461
+ "/api/v1/analytics/revenue-breakdown",
2462
+ {
2463
+ dimension: args.dimension,
2464
+ from: args.from,
2465
+ to: args.to,
2466
+ timezone: args.timezone
2467
+ },
2468
+ { applicationId: args.applicationId }
2469
+ ),
2470
+ // `.get()`, not `.list()` — the response is a single object (`{ data: {
2471
+ // range, stages, conversionRate, dropOff, note } }`), not a paginated array.
2472
+ get_funnel_conversion: async (ctx, args) => ctx.client.get(
2473
+ "/api/v1/analytics/funnel",
2474
+ {
2475
+ from: args.from,
2476
+ to: args.to,
2477
+ timezone: args.timezone
2478
+ },
2479
+ { applicationId: args.applicationId }
2480
+ ),
2481
+ // `.get()`, not `.list()` — the response is a single object (`{ data: {
2482
+ // range, series, totals, note } }`), not a paginated array.
2483
+ get_subscription_movement: async (ctx, args) => ctx.client.get(
2484
+ "/api/v1/analytics/subscriptions",
2485
+ {
2486
+ from: args.from,
2487
+ to: args.to,
2488
+ timezone: args.timezone,
2489
+ groupBy: args.groupBy
2490
+ },
2491
+ { applicationId: args.applicationId }
2492
+ ),
2493
+ // `.get()`, not `.list()` — the response is a single object (`{ data: {
2494
+ // range, rows, totals, note } }`), not a paginated array.
2495
+ get_discount_performance: async (ctx, args) => ctx.client.get(
2496
+ "/api/v1/analytics/discounts",
2497
+ {
2498
+ from: args.from,
2499
+ to: args.to,
2500
+ timezone: args.timezone,
2501
+ limit: args.limit
2502
+ },
2503
+ { applicationId: args.applicationId }
2504
+ ),
1488
2505
  // ------------------------------------------------------------- extensions
1489
- list_extension_installations: async (ctx, args) => ctx.client.list("/api/v1/installations", { status: args.status, ...page(args) }),
2506
+ list_extension_installations: async (ctx, args) => ctx.client.list(
2507
+ "/api/v1/installations",
2508
+ { status: args.status, ...page(args) },
2509
+ { applicationId: args.applicationId }
2510
+ ),
1490
2511
  // ------------------------------------------------------------- connectors
1491
- get_connector_routing: async (ctx) => {
2512
+ get_connector_routing: async (ctx, args) => {
2513
+ const opts = { applicationId: args.applicationId };
1492
2514
  const [paymentRouting, routingConnectors, cardDecline, otherDecline] = await Promise.all([
1493
- optional(() => ctx.client.get("/api/v1/payment-routing")),
1494
- optional(() => ctx.client.get("/api/v1/routing-connectors")),
1495
- optional(() => ctx.client.get("/api/v1/card-decline")),
1496
- optional(() => ctx.client.get("/api/v1/other-decline"))
2515
+ optional(() => ctx.client.get("/api/v1/payment-routing", void 0, opts)),
2516
+ optional(() => ctx.client.get("/api/v1/routing-connectors", void 0, opts)),
2517
+ optional(() => ctx.client.get("/api/v1/card-decline", void 0, opts)),
2518
+ optional(() => ctx.client.get("/api/v1/other-decline", void 0, opts))
1497
2519
  ]);
1498
2520
  return {
1499
2521
  paymentRouting: paymentRouting.ok ? paymentRouting.value : paymentRouting.unavailable,
@@ -1502,24 +2524,37 @@ var HANDLERS = {
1502
2524
  otherDecline: otherDecline.ok ? otherDecline.value : otherDecline.unavailable
1503
2525
  };
1504
2526
  },
1505
- get_integration_status: async (ctx) => {
2527
+ get_integration_status: async (ctx, args) => {
2528
+ const opts = { applicationId: args.applicationId };
1506
2529
  const [connectors, endpoints, coverage] = await Promise.all([
1507
- optional(() => ctx.client.list("/api/v1/connectors")),
1508
- optional(() => ctx.client.list("/api/v1/webhook-endpoints")),
1509
- optional(() => ctx.client.get("/api/v1/webhook-endpoints/coverage"))
2530
+ optional(() => ctx.client.list("/api/v1/connectors", void 0, opts)),
2531
+ optional(() => ctx.client.list("/api/v1/webhook-endpoints", void 0, opts)),
2532
+ optional(() => ctx.client.get("/api/v1/webhook-endpoints/coverage", void 0, opts))
1510
2533
  ]);
2534
+ const overridden = Boolean(args.applicationId) && args.applicationId !== ctx.identity.application?.id;
1511
2535
  return {
1512
2536
  environment: ctx.identity.environment,
1513
2537
  workspace: ctx.identity.workspace,
1514
- application: ctx.identity.application,
2538
+ applicationId: args.applicationId ?? ctx.identity.application?.id ?? null,
2539
+ application: overridden ? void 0 : ctx.identity.application,
1515
2540
  connectors: connectors.ok ? connectors.value.data : connectors.unavailable,
1516
2541
  webhookEndpoints: endpoints.ok ? endpoints.value.data : endpoints.unavailable,
1517
2542
  unsubscribedEvents: coverage.ok ? coverage.value : coverage.unavailable
1518
2543
  };
1519
2544
  },
1520
2545
  // --------------------------------------------------------------- workspace
2546
+ // No applicationId forwarding here: /api/v1/merchants/me is workspace-only
2547
+ // (`WHERE workspace_id = ...`, never reads auth.applicationId) — an
2548
+ // applicationId field on this tool would be a silent no-op affordance.
1521
2549
  get_workspace_settings: async (ctx) => ctx.client.get("/api/v1/merchants/me"),
1522
- get_audit_log: async (ctx, args) => ctx.client.list("/api/v1/audit-log", { limit: args.limit })
2550
+ // No applicationId forwarding here either: /api/v1/audit-log filters by
2551
+ // workspaceId (and optionally environmentId), never applicationId.
2552
+ get_audit_log: async (ctx, args) => ctx.client.list("/api/v1/audit-log", { limit: args.limit }),
2553
+ // --------------------------------------------------------- applications
2554
+ // `.get()`, not `.list()` — the legacy /v1/stores route returns a bare
2555
+ // array with no `meta.pagination`, and the brief calls for `.get()` here
2556
+ // specifically.
2557
+ list_applications: async (ctx) => ctx.client.get("/api/v1/stores")
1523
2558
  };
1524
2559
 
1525
2560
  // src/input.ts
@@ -1554,15 +2589,548 @@ function applyModifiers(field, type) {
1554
2589
  return field.default === void 0 ? described.optional() : described.default(field.default);
1555
2590
  }
1556
2591
 
2592
+ // src/projection.ts
2593
+ var S = "scalar";
2594
+ var T = "text";
2595
+ var PASS = "pass";
2596
+ var section = (shape) => ({ __section: shape });
2597
+ var oneOf = (values) => ({ __oneOf: new Set(values) });
2598
+ function isPrimitive(value) {
2599
+ return value === null || ["string", "number", "boolean"].includes(typeof value);
2600
+ }
2601
+ var TOKEN = /^[^\s@]{0,200}$/;
2602
+ var OMITTED = { omitted: S, grantable: T, reason: T };
2603
+ var UNAVAILABLE = /^(not readable with this credential \(missing scope\)|not found|unavailable \([^\s@]{0,100}\))$/;
2604
+ function project(value, shape) {
2605
+ if (value === void 0) return void 0;
2606
+ if (shape === PASS) return value;
2607
+ if (shape === S) {
2608
+ if (!isPrimitive(value)) return void 0;
2609
+ return typeof value === "string" && !TOKEN.test(value) ? null : value;
2610
+ }
2611
+ if (shape === T) return isPrimitive(value) ? value : void 0;
2612
+ if ("__oneOf" in shape) {
2613
+ if (!isPrimitive(value)) return void 0;
2614
+ return typeof value === "string" && !shape.__oneOf.has(value) ? null : value;
2615
+ }
2616
+ if (Array.isArray(shape)) {
2617
+ return Array.isArray(value) ? value.map((item) => project(item, shape[0])) : void 0;
2618
+ }
2619
+ if ("__section" in shape) {
2620
+ if (typeof value === "string") return UNAVAILABLE.test(value) ? value : null;
2621
+ if (value && typeof value === "object" && value.omitted === true) {
2622
+ return project(value, OMITTED);
2623
+ }
2624
+ return project(value, shape.__section);
2625
+ }
2626
+ if (value === null) return null;
2627
+ if (typeof value !== "object" || Array.isArray(value)) return void 0;
2628
+ const out = {};
2629
+ for (const [key, child] of Object.entries(shape)) {
2630
+ const projected = project(value[key], child);
2631
+ if (projected !== void 0) out[key] = projected;
2632
+ }
2633
+ return out;
2634
+ }
2635
+ var PAGINATION = { cursor: S, nextCursor: S, hasMore: S };
2636
+ var page2 = (row) => ({ data: [row], pagination: PAGINATION });
2637
+ var ADDRESS = { countryCode: S };
2638
+ var CUSTOMER_REF = { id: S };
2639
+ var APPLICATION = { id: S, name: T, slug: S };
2640
+ var LIFECYCLE_LABELS = ["Draft", "Pending", "Processing", "Partially fulfilled", "Fulfilled", "Cancelled", "Closed"];
2641
+ var DISPLAY_QUALIFIERS = [
2642
+ "Comped",
2643
+ "Awaiting payment",
2644
+ "Authorized",
2645
+ "Partially paid",
2646
+ "Partially refunded",
2647
+ "Refunded",
2648
+ "Payment failed",
2649
+ "Voided",
2650
+ "Payment processing",
2651
+ "Disputed",
2652
+ "Authorization expired",
2653
+ "Returned",
2654
+ "Partially returned",
2655
+ "Delivered",
2656
+ "In transit"
2657
+ ];
2658
+ var ORDER_DISPLAY_STATUSES = new Set(
2659
+ LIFECYCLE_LABELS.flatMap((l) => [l, ...DISPLAY_QUALIFIERS.map((q) => `${l} \xB7 ${q}`)])
2660
+ );
2661
+ var DISPLAY_STATUS = oneOf(ORDER_DISPLAY_STATUSES);
2662
+ var RANGE = { from: S, to: S, timezone: S, groupBy: S };
2663
+ var LINE_ITEM = {
2664
+ id: S,
2665
+ type: S,
2666
+ quantity: S,
2667
+ unitPrice: S,
2668
+ subtotal: S,
2669
+ discountAmount: S,
2670
+ taxAmount: S,
2671
+ total: S,
2672
+ recurring: S
2673
+ };
2674
+ var ORDER = {
2675
+ id: S,
2676
+ orderNumber: S,
2677
+ status: S,
2678
+ displayStatus: DISPLAY_STATUS,
2679
+ paymentStatus: S,
2680
+ type: S,
2681
+ source: S,
2682
+ currency: S,
2683
+ subtotal: S,
2684
+ discountTotal: S,
2685
+ shippingTotal: S,
2686
+ taxTotal: S,
2687
+ total: S,
2688
+ capturedAmount: S,
2689
+ refundedAmount: S,
2690
+ compAmount: S,
2691
+ onHold: S,
2692
+ taxReverseCharge: S,
2693
+ customerId: S,
2694
+ customer: CUSTOMER_REF,
2695
+ subscriptionId: S,
2696
+ shippingAddress: ADDRESS,
2697
+ billingAddress: ADDRESS,
2698
+ fulfillmentSummary: { itemsFulfilled: S, itemsRemaining: S, itemsTotal: S },
2699
+ lineItems: [LINE_ITEM],
2700
+ createdAt: S,
2701
+ completedAt: S,
2702
+ cancelledAt: S,
2703
+ closedAt: S
2704
+ };
2705
+ var PAYMENT = {
2706
+ id: S,
2707
+ orderId: S,
2708
+ status: S,
2709
+ method: S,
2710
+ processor: S,
2711
+ amount: S,
2712
+ capturedAmount: S,
2713
+ currency: S,
2714
+ cardBrand: S,
2715
+ disputed: S,
2716
+ disputeOpenedAt: S,
2717
+ errorCode: S,
2718
+ netN: S,
2719
+ netNSource: S,
2720
+ authorizedAt: S,
2721
+ capturedAt: S,
2722
+ voidedAt: S,
2723
+ createdAt: S
2724
+ };
2725
+ var RETURN = {
2726
+ id: S,
2727
+ orderId: S,
2728
+ status: S,
2729
+ restock: S,
2730
+ refundAmount: S,
2731
+ refundPaymentId: S,
2732
+ createdAt: S,
2733
+ items: [{ lineItemId: S, quantity: S }]
2734
+ };
2735
+ var FULFILLMENT = {
2736
+ id: S,
2737
+ orderId: S,
2738
+ type: S,
2739
+ status: S,
2740
+ createdAt: S,
2741
+ lineItems: [{ lineItemId: S, quantity: S }],
2742
+ detail: { carrier: T, serviceLevel: T, trackingNumber: T, trackingUrl: T, shippedAt: S, estimatedDelivery: S, actualDelivery: S }
2743
+ };
2744
+ var CUSTOMER = { id: S, netN: S, acceptsMarketing: S, createdAt: S };
2745
+ var SUBSCRIPTION = {
2746
+ id: S,
2747
+ status: S,
2748
+ planName: T,
2749
+ planReference: T,
2750
+ amount: S,
2751
+ currency: S,
2752
+ interval: S,
2753
+ quantity: S,
2754
+ intervalUnit: S,
2755
+ intervalCount: S,
2756
+ totalPayments: S,
2757
+ paymentsMade: S,
2758
+ completedAt: S,
2759
+ customerId: S,
2760
+ customer: CUSTOMER_REF,
2761
+ currentPeriodStart: S,
2762
+ currentPeriodEnd: S,
2763
+ billingAnchorAt: S,
2764
+ cancelAtPeriodEnd: S,
2765
+ trialEnd: S,
2766
+ pausedAt: S,
2767
+ cancelledAt: S,
2768
+ lastPaymentAt: S,
2769
+ failureCount: S,
2770
+ pendingPlanName: T,
2771
+ pendingPlanReference: T,
2772
+ pendingAmount: S,
2773
+ pendingInterval: S,
2774
+ pendingQuantity: S,
2775
+ pendingIntervalUnit: S,
2776
+ pendingIntervalCount: S,
2777
+ createdAt: S
2778
+ };
2779
+ var QUOTE_SUMMARY = {
2780
+ id: S,
2781
+ quoteNumber: S,
2782
+ status: S,
2783
+ source: S,
2784
+ currency: S,
2785
+ total: S,
2786
+ customerId: S,
2787
+ orderId: S,
2788
+ expiresAt: S,
2789
+ acceptedAt: S,
2790
+ declinedAt: S,
2791
+ createdAt: S
2792
+ };
2793
+ var QUOTE = {
2794
+ ...QUOTE_SUMMARY,
2795
+ revisions: [{
2796
+ id: S,
2797
+ revisionNumber: S,
2798
+ state: S,
2799
+ subtotal: S,
2800
+ discountTotal: S,
2801
+ shippingTotal: S,
2802
+ taxTotal: S,
2803
+ total: S,
2804
+ depositAmount: S,
2805
+ shippingMode: S,
2806
+ issuedAt: S,
2807
+ expiresAt: S,
2808
+ paymentTerms: { mode: S, netN: S, balanceNetN: S, deposit: { type: S, value: S } },
2809
+ items: [{
2810
+ type: S,
2811
+ quantity: S,
2812
+ unitPrice: S,
2813
+ subtotal: S,
2814
+ discountAmount: S,
2815
+ taxAmount: S,
2816
+ total: S,
2817
+ optional: S,
2818
+ optionalSelected: S
2819
+ }]
2820
+ }],
2821
+ // Who commented and when, never what they wrote.
2822
+ comments: [{ authorType: S, visibility: S, createdAt: S }],
2823
+ timeline: [{ type: S, actorType: S, createdAt: S }]
2824
+ };
2825
+ var INVOICE = {
2826
+ id: S,
2827
+ number: S,
2828
+ status: S,
2829
+ sourceType: S,
2830
+ sourceId: S,
2831
+ customerId: S,
2832
+ currency: S,
2833
+ subtotal: S,
2834
+ discountTotal: S,
2835
+ shippingTotal: S,
2836
+ taxTotal: S,
2837
+ total: S,
2838
+ amountRefunded: S,
2839
+ issuedAt: S,
2840
+ dueAt: S,
2841
+ periodStart: S,
2842
+ periodEnd: S,
2843
+ createdAt: S,
2844
+ billingSnapshot: {
2845
+ planName: T,
2846
+ period: { start: S, end: S },
2847
+ lineItems: [{ quantity: S, unitPrice: S, discountAmount: S, taxAmount: S, total: S }],
2848
+ totals: { currency: S, subtotal: S, discountTotal: S, shippingTotal: S, taxTotal: S, total: S },
2849
+ payment: { method: S, brand: S, status: S }
2850
+ }
2851
+ };
2852
+ var DISCOUNT = {
2853
+ id: S,
2854
+ code: T,
2855
+ name: T,
2856
+ type: S,
2857
+ value: S,
2858
+ isActive: S,
2859
+ startsAt: S,
2860
+ endsAt: S,
2861
+ usageCount: S,
2862
+ usageLimit: S,
2863
+ maxRedemptionsPerCustomer: S,
2864
+ createdAt: S
2865
+ };
2866
+ var EVENT = {
2867
+ id: S,
2868
+ type: S,
2869
+ entityType: S,
2870
+ entityId: S,
2871
+ createdAt: S,
2872
+ data: {
2873
+ status: S,
2874
+ previousStatus: S,
2875
+ action: S,
2876
+ amount: S,
2877
+ currency: S,
2878
+ attempt: S,
2879
+ orderId: S,
2880
+ paymentId: S,
2881
+ order: { id: S, orderNumber: S, status: S, paymentStatus: S, currency: S, total: S },
2882
+ payment: { id: S, orderId: S, status: S, method: S, amount: S, currency: S },
2883
+ subscription: { id: S, status: S, planName: T, amount: S, currency: S, interval: S },
2884
+ fulfillment: { id: S, orderId: S, type: S, status: S }
2885
+ }
2886
+ };
2887
+ var DELIVERY = {
2888
+ id: S,
2889
+ endpointId: S,
2890
+ eventId: S,
2891
+ eventType: S,
2892
+ status: S,
2893
+ attemptCount: S,
2894
+ lastStatus: S,
2895
+ deliveredAt: S,
2896
+ nextRetryAt: S,
2897
+ createdAt: S,
2898
+ replayOf: S
2899
+ };
2900
+ var MONEY_COUNT = { count: S, totalCents: S };
2901
+ var ORDER_PERIOD = { orderCount: S, grossCents: S, netCents: S, refundsCents: S, avgOrderCents: S };
2902
+ var OAUTH_PROJECTIONS = {
2903
+ whoami: {
2904
+ type: S,
2905
+ workspace: { name: T, slug: S },
2906
+ application: APPLICATION,
2907
+ environment: { slug: S, kind: S, providerEnvironment: S },
2908
+ note: T
2909
+ },
2910
+ search_docs: PASS,
2911
+ describe_endpoint: PASS,
2912
+ validate_payload: PASS,
2913
+ search_orders: page2(ORDER),
2914
+ get_order: {
2915
+ order: ORDER,
2916
+ payments: section([PAYMENT]),
2917
+ returns: section([RETURN]),
2918
+ fulfillments: section([FULFILLMENT])
2919
+ },
2920
+ search_customers: page2(CUSTOMER),
2921
+ get_customer: {
2922
+ customer: CUSTOMER,
2923
+ addresses: section([{ id: S, isDefault: S, ...ADDRESS }]),
2924
+ subscriptions: section([SUBSCRIPTION])
2925
+ },
2926
+ list_subscriptions: page2(SUBSCRIPTION),
2927
+ get_subscription: SUBSCRIPTION,
2928
+ search_quotes: page2(QUOTE_SUMMARY),
2929
+ get_quote: QUOTE,
2930
+ list_invoices: page2(INVOICE),
2931
+ get_invoice: INVOICE,
2932
+ receivables_aging: {
2933
+ buckets: [{ key: S, label: T, count: S, total_amount: S }],
2934
+ total_outstanding_amount: S,
2935
+ total_outstanding_count: S
2936
+ },
2937
+ search_discounts: page2(DISCOUNT),
2938
+ get_discount: DISCOUNT,
2939
+ search_payments: page2(PAYMENT),
2940
+ get_payment: {
2941
+ payment: PAYMENT,
2942
+ transactions: section([{ id: S, type: S, status: S, amount: S, currency: S, createdAt: S }])
2943
+ },
2944
+ list_abandoned_carts: {
2945
+ carts: section([{
2946
+ id: S,
2947
+ abandonedAt: S,
2948
+ currency: S,
2949
+ itemCount: S,
2950
+ recoveryStatus: S,
2951
+ total: S,
2952
+ customer: { id: S }
2953
+ }]),
2954
+ summary: section({ abandonedCount: S, abandonedValue: S, recoveryEmailsSent: S, sinceDays: S })
2955
+ },
2956
+ search_events: page2(EVENT),
2957
+ list_webhook_deliveries: page2(DELIVERY),
2958
+ get_webhook_delivery: DELIVERY,
2959
+ explain_checkout_failure: {
2960
+ note: T,
2961
+ hint: T,
2962
+ session: {
2963
+ id: S,
2964
+ status: S,
2965
+ cartId: S,
2966
+ customerId: S,
2967
+ expiresAt: S,
2968
+ completedAt: S,
2969
+ allowedMethods: [S],
2970
+ collect: { shippingAddress: S, billingAddress: S, vatNumber: S },
2971
+ shippingAddress: ADDRESS,
2972
+ billingAddress: ADDRESS,
2973
+ recurringTerms: {
2974
+ interval: S,
2975
+ intervalUnit: S,
2976
+ intervalCount: S,
2977
+ totalPayments: S,
2978
+ taxApplies: S,
2979
+ planChargedToday: S,
2980
+ planName: T,
2981
+ trialDays: S,
2982
+ currency: S,
2983
+ firstChargeAmountMinor: S
2984
+ }
2985
+ },
2986
+ cart: section({
2987
+ id: S,
2988
+ status: S,
2989
+ currency: S,
2990
+ subtotal: S,
2991
+ discountTotal: S,
2992
+ shippingTotal: S,
2993
+ taxTotal: S,
2994
+ total: S,
2995
+ itemCount: S,
2996
+ items: [LINE_ITEM],
2997
+ lineItems: [LINE_ITEM]
2998
+ }),
2999
+ events: [EVENT],
3000
+ eventsUnavailable: T,
3001
+ recentWebhookDeliveries: section([DELIVERY])
3002
+ },
3003
+ list_extension_installations: page2({
3004
+ id: S,
3005
+ extensionId: S,
3006
+ extensionName: T,
3007
+ extensionSlug: S,
3008
+ versionId: S,
3009
+ status: S,
3010
+ environmentSlug: S,
3011
+ installedAt: S,
3012
+ uninstalledAt: S
3013
+ }),
3014
+ get_connector_routing: {
3015
+ paymentRouting: section({
3016
+ defaultCascade: [S],
3017
+ rules: [{ cascade: [S], conditions: [{ type: S, op: S, currency: S, min: S, max: S, values: [S] }] }]
3018
+ }),
3019
+ cardDecline: section({ rules: [{ errorCode: S, conditions: [{ type: S, op: S, values: [S] }] }] }),
3020
+ otherDecline: section({ rules: [{ errorCode: S, conditions: [{ type: S, op: S, values: [S] }] }] }),
3021
+ routingConnectors: section([{ id: S, displayName: T, method: S }])
3022
+ },
3023
+ get_integration_status: {
3024
+ environment: { slug: S, kind: S, providerEnvironment: S },
3025
+ workspace: { name: T, slug: S },
3026
+ applicationId: S,
3027
+ application: APPLICATION,
3028
+ connectors: section([{
3029
+ id: S,
3030
+ displayName: T,
3031
+ method: S,
3032
+ type: S,
3033
+ status: S,
3034
+ active: S,
3035
+ position: S,
3036
+ acceptedCountries: [S],
3037
+ acceptedCurrencies: [S],
3038
+ threeDSecureEnabled: S,
3039
+ createdAt: S
3040
+ }]),
3041
+ webhookEndpoints: section([{ id: S, isActive: S, enabledEvents: [S], createdAt: S }]),
3042
+ unsubscribedEvents: section({ windowDays: S, unsubscribed: [{ type: S, count: S, lastEmittedAt: S }] })
3043
+ },
3044
+ get_workspace_settings: { name: T, slug: S, brandColor: S, logoUrl: T, onboardingCompletedAt: S },
3045
+ // What happened and to which record; not which team member did it.
3046
+ get_audit_log: page2({ id: S, action: S, resourceType: S, resourceId: S, createdAt: S }),
3047
+ list_applications: [{ id: S, name: T, slug: S, currency: S, isActive: S }],
3048
+ // Analytics: computed aggregates, listed field by field all the same.
3049
+ get_business_summary: {
3050
+ period: S,
3051
+ range: RANGE,
3052
+ orders: section({ current: ORDER_PERIOD, previous: ORDER_PERIOD, deltaPct: S }),
3053
+ subscriptions: section({
3054
+ mrr: { currentCents: S, periodStartCents: S, deltaPct: S },
3055
+ newSubscriptions: S,
3056
+ cancelledSubscriptions: S,
3057
+ churnRate: S,
3058
+ note: T
3059
+ }),
3060
+ receivables: section({ outstandingCents: S, outstandingCount: S }),
3061
+ quotes: section({ sent: MONEY_COUNT, accepted: MONEY_COUNT, declined: MONEY_COUNT, expired: MONEY_COUNT })
3062
+ },
3063
+ get_attention_digest: {
3064
+ disputes: section({ open: S, totalCents: S }),
3065
+ subscriptionsInDunning: section(S),
3066
+ overdueInvoices: section(MONEY_COUNT),
3067
+ unhealthyWebhooks: section([{ endpointId: S, failureRate: S, attempts7d: S }])
3068
+ },
3069
+ get_sales_timeseries: {
3070
+ application: APPLICATION,
3071
+ range: RANGE,
3072
+ currency: S,
3073
+ days: S,
3074
+ note: T,
3075
+ series: [{ date: S, grossCents: S, orders: S }],
3076
+ totalGrossCents: S,
3077
+ totalOrders: S
3078
+ },
3079
+ // get_top_products has no entry, so it is not offered over OAuth: it ranks
3080
+ // products by line-item reference id or name, which a buyer can type.
3081
+ get_customer_growth: {
3082
+ application: APPLICATION,
3083
+ range: RANGE,
3084
+ note: T,
3085
+ topBy: S,
3086
+ signups: { total: S, series: [{ date: S, signups: S }] },
3087
+ ordersByCustomerType: { newCustomerOrders: S, returningCustomerOrders: S },
3088
+ // The ranking, by id: a top customer's name and email stay behind.
3089
+ topCustomers: [{ customerId: S, orderCount: S, revenueCents: S }],
3090
+ totals: { customerCount: S, orderCount: S, revenueCents: S }
3091
+ },
3092
+ get_revenue_breakdown: {
3093
+ application: APPLICATION,
3094
+ range: RANGE,
3095
+ dimension: S,
3096
+ note: T,
3097
+ rows: [{ key: S, orderCount: S, grossCents: S }],
3098
+ totals: { orderCount: S, grossCents: S }
3099
+ },
3100
+ get_funnel_conversion: {
3101
+ application: APPLICATION,
3102
+ range: RANGE,
3103
+ note: T,
3104
+ conversionRate: S,
3105
+ stages: [{ name: S, count: S }],
3106
+ dropOff: [{ from: S, to: S, ratePct: S }]
3107
+ },
3108
+ get_subscription_movement: {
3109
+ application: APPLICATION,
3110
+ range: RANGE,
3111
+ note: T,
3112
+ series: [{ date: S, newSubscriptions: S, churnedSubscriptions: S, mrrDeltaCents: S }],
3113
+ totals: { newSubscriptions: S, churnedSubscriptions: S, netMrrDeltaCents: S }
3114
+ },
3115
+ get_discount_performance: {
3116
+ application: APPLICATION,
3117
+ range: RANGE,
3118
+ note: T,
3119
+ rows: [{ discountId: S, code: T, redemptions: S, discountedRevenueCents: S, discountGivenCents: S }],
3120
+ totals: { redemptions: S, discountedRevenueCents: S, discountGivenCents: S }
3121
+ }
3122
+ };
3123
+
1557
3124
  // src/server.ts
1558
- var VERSION = true ? "0.2.0" : "0.0.0-dev";
3125
+ var VERSION = true ? "0.2.1" : "0.0.0-dev";
1559
3126
  function registerableActions(policy) {
1560
3127
  const isProduction = policy.identity.environment.providerEnvironment === "production";
1561
3128
  return actionsForChannel("mcp").filter((action) => {
1562
3129
  if (!HANDLERS[action.id]) return false;
3130
+ if (action.credentialScoped && policy.allowCredentialScoped === false) return false;
1563
3131
  if (!canRunAction(policy.identity.scopes, action)) return false;
1564
3132
  if (action.kind === "read") return true;
1565
- if (action.sandboxOnly) return !isProduction;
3133
+ if (action.sandboxOnly) return !isProduction && (policy.allowSandboxWrites ?? true);
1566
3134
  if (!policy.allowWrites) return false;
1567
3135
  return !isProduction || policy.allowLiveWrites;
1568
3136
  });
@@ -1579,11 +3147,14 @@ function buildInstructions(identity, actions, policy) {
1579
3147
  `Throttle MCP ${VERSION}. Connected to ${where}, environment "${identity.environment.slug}" (${isProduction ? "PRODUCTION \u2014 live data" : "non-production \u2014 not live data"}).`,
1580
3148
  "",
1581
3149
  "Conventions:",
1582
- "- 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.",
3150
+ "- 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.",
3151
+ '- 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.',
3152
+ "- 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.",
1583
3153
  '- List tools return { data, pagination }. When pagination.hasMore is true, pass pagination.cursor back as "cursor" rather than raising "limit".',
1584
3154
  "- Amounts are integer minor units (2500 = $25.00).",
1585
3155
  "- 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.",
1586
- "- API errors come back as text with a stable code. On insufficient_scopes, tell the user which scope to add \u2014 do not retry."
3156
+ "- API errors come back as text with a stable code. On insufficient_scopes, tell the user which scope to add \u2014 do not retry.",
3157
+ "- 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."
1587
3158
  ];
1588
3159
  if (liveWrites.length === 0) {
1589
3160
  lines.push(
@@ -1601,8 +3172,31 @@ function buildInstructions(identity, actions, policy) {
1601
3172
  }
1602
3173
  return lines.join("\n");
1603
3174
  }
3175
+ function declareSecuritySchemes(server, actions) {
3176
+ const inner = server.server;
3177
+ const handlers = inner._requestHandlers;
3178
+ const original = handlers?.get("tools/list");
3179
+ if (!original) return;
3180
+ const schemes = new Map(
3181
+ actions.map((a) => [
3182
+ a.id,
3183
+ [{
3184
+ type: "oauth2",
3185
+ scopes: [.../* @__PURE__ */ new Set([...a.scopes, ...a.sectionScopes ?? []])].filter((scope) => validateScopesForOAuth([scope]).ok)
3186
+ }]
3187
+ ])
3188
+ );
3189
+ inner.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
3190
+ const result = await original(request, extra);
3191
+ return {
3192
+ ...result,
3193
+ tools: result.tools.map((tool) => ({ ...tool, securitySchemes: schemes.get(tool.name) ?? [{ type: "oauth2", scopes: [] }] }))
3194
+ };
3195
+ });
3196
+ }
1604
3197
  function createServer(options) {
1605
- const actions = registerableActions(options);
3198
+ const redactPii = options.identity.type === "oauth_token" && !hasScope(options.identity.scopes, "customer_pii:read");
3199
+ const actions = registerableActions(options).filter((a) => !redactPii || OAUTH_PROJECTIONS[a.id] !== void 0);
1606
3200
  const server = new McpServer(
1607
3201
  { name: "throttle", version: VERSION },
1608
3202
  { instructions: buildInstructions(options.identity, actions, options) }
@@ -1615,25 +3209,57 @@ function createServer(options) {
1615
3209
  };
1616
3210
  for (const action of actions) {
1617
3211
  const handler = HANDLERS[action.id];
1618
- server.tool(
3212
+ server.registerTool(
1619
3213
  action.id,
1620
- describeForModel(action),
1621
- fieldsToZodShape(action.input),
3214
+ {
3215
+ title: action.title,
3216
+ description: describeForModel(action),
3217
+ inputSchema: fieldsToZodShape(action.input),
3218
+ // Every hint is an explicit boolean: directory reviews (OpenAI's in
3219
+ // particular) read a missing value as "unknown", not as false.
3220
+ annotations: {
3221
+ title: action.title,
3222
+ readOnlyHint: action.kind === "read",
3223
+ destructiveHint: action.kind === "write" && Boolean(action.destructive),
3224
+ idempotentHint: Boolean(action.idempotent),
3225
+ // Every tool is bounded to the connected workspace; none reaches the
3226
+ // public internet or an open-ended set of external entities.
3227
+ openWorldHint: false
3228
+ }
3229
+ },
1622
3230
  async (args) => {
1623
3231
  try {
3232
+ if (redactPii && action.id === "search_customers" && typeof args?.q === "string" && args.q.trim() !== "") {
3233
+ return {
3234
+ 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." }],
3235
+ isError: true
3236
+ };
3237
+ }
1624
3238
  const result = await handler(ctx, args ?? {});
1625
- return { content: [{ type: "text", text: formatResult(result) }] };
3239
+ const shown = redactPii ? redactShopperPii(project(result, OAUTH_PROJECTIONS[action.id])) : result;
3240
+ return { content: [{ type: "text", text: formatResult(shown) }] };
1626
3241
  } catch (error) {
1627
3242
  return {
1628
3243
  content: [{ type: "text", text: formatError(error) }],
1629
- isError: true
3244
+ isError: true,
3245
+ ...options.authChallenge && isCredentialRejection(error) ? {
3246
+ _meta: {
3247
+ "mcp/www_authenticate": [
3248
+ `${options.authChallenge}, error="invalid_token", error_description="The connected credential was rejected. Sign in again to continue."`
3249
+ ]
3250
+ }
3251
+ } : {}
1630
3252
  };
1631
3253
  }
1632
3254
  }
1633
3255
  );
1634
3256
  }
3257
+ if (options.authChallenge) declareSecuritySchemes(server, actions);
1635
3258
  return server;
1636
3259
  }
3260
+ function isCredentialRejection(error) {
3261
+ return error?.statusCode === 401;
3262
+ }
1637
3263
  function describeForModel(action) {
1638
3264
  const parts = [action.description];
1639
3265
  if (action.guardrails && action.guardrails.length > 0) {