@carrierllc/mcp 0.9.2 → 0.9.3

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/index.js CHANGED
@@ -1,25 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CARRIER_VERSION,
4
+ OCS_MAX_USAGE_WINDOW_DAYS,
4
5
  ROUTER_RULES,
5
6
  TARGET_IDS,
6
7
  acquireEndpointSlot,
8
+ applyParamRenames,
7
9
  applySubscriberFilters,
8
10
  buildListSubscriberParams,
9
11
  buildRouterCatalog,
10
12
  buildSite,
11
13
  clerkCliDeps,
12
14
  configureClerkInstance,
15
+ daysUntil,
13
16
  deploySite,
14
17
  esimStatusPerAccountParams,
18
+ extractDailyUsage,
15
19
  extractEsimStatusCounts,
20
+ filterActiveSubscribers,
16
21
  fleetScreen,
22
+ formatBytes,
17
23
  formatProbes,
18
24
  generateStorefrontLogo,
19
25
  getLimitForEndpoint,
20
26
  getRateLimitWindowCounts,
21
27
  imsiFromSubscriberRecord,
22
28
  isTargetId,
29
+ lastNDaysPeriod,
23
30
  loadStorefrontBrand,
24
31
  locationParams,
25
32
  mergeEnvLocal,
@@ -33,11 +40,14 @@ import {
33
40
  recurringPackageParams,
34
41
  renderHtml,
35
42
  repairPlanFor,
43
+ runWithBudget,
36
44
  storefrontClerkUrls,
37
45
  subscriberIdParams,
46
+ subscriberRows,
38
47
  usageOverPeriodParams,
39
- verifyStorefront
40
- } from "./chunk-4XHSOF62.js";
48
+ verifyStorefront,
49
+ withOcsListSummary
50
+ } from "./chunk-KHNJNJX3.js";
41
51
  import "./chunk-SHKKVIIA.js";
42
52
 
43
53
  // src/index.ts
@@ -69,32 +79,35 @@ var OcsClient = class {
69
79
  async call(method, params = {}) {
70
80
  await acquireEndpointSlot(this.token, method, "interactive");
71
81
  const url = `${this.baseUrl}/v1?token=${this.token}`;
72
- const body2 = JSON.stringify({ [method]: params });
73
- const res = await fetch(url, {
74
- method: "POST",
75
- headers: { "Content-Type": "application/json" },
76
- body: body2
77
- });
78
- if (!res.ok) {
79
- throw new OcsApiError(res.status, `HTTP ${res.status} ${res.statusText}`, method);
80
- }
81
- const json = await res.json();
82
- if (json.status?.code !== 0) {
83
- throw new OcsApiError(json.status?.code ?? -1, json.status?.msg ?? "Unknown error", method);
84
- }
85
- if (method === "getCustomerTariff" && json["listTariffRule"] !== void 0) {
86
- return json["listTariffRule"];
87
- }
88
- if (method === "getSubscriberLocationByCellId") {
89
- const byMethod = json[method];
90
- if (byMethod !== void 0) {
91
- return byMethod;
82
+ const body2 = JSON.stringify({ [method]: applyParamRenames(method, params) });
83
+ return runWithBudget(method, async (signal) => {
84
+ const res = await fetch(url, {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json" },
87
+ body: body2,
88
+ signal
89
+ });
90
+ if (!res.ok) {
91
+ throw new OcsApiError(res.status, `HTTP ${res.status} ${res.statusText}`, method);
92
92
  }
93
- if (json["subscriberLocation"] !== void 0) {
94
- return json["subscriberLocation"];
93
+ const json = await res.json();
94
+ if (json.status?.code !== 0) {
95
+ throw new OcsApiError(json.status?.code ?? -1, json.status?.msg ?? "Unknown error", method);
95
96
  }
96
- }
97
- return json[method] ?? json;
97
+ if (method === "getCustomerTariff" && json["listTariffRule"] !== void 0) {
98
+ return json["listTariffRule"];
99
+ }
100
+ if (method === "getSubscriberLocationByCellId") {
101
+ const byMethod = json[method];
102
+ if (byMethod !== void 0) {
103
+ return byMethod;
104
+ }
105
+ if (json["subscriberLocation"] !== void 0) {
106
+ return json["subscriberLocation"];
107
+ }
108
+ }
109
+ return json[method] ?? json;
110
+ });
98
111
  }
99
112
  };
100
113
 
@@ -189,6 +202,9 @@ function estimateCostUsd(u, rates = HAIKU_RATES) {
189
202
  var DEFAULT_BEDROCK_REGION = "us-east-1";
190
203
  var DEFAULT_MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0";
191
204
  var BEDROCK_TIMEOUT_MS = 3e4;
205
+ var MAX_ATTEMPTS = 4;
206
+ var INITIAL_RETRY_MS = 100;
207
+ var MIN_ATTEMPT_BUDGET_MS = 250;
192
208
  var BedrockRateLimitError = class extends Error {
193
209
  isRateLimit = true;
194
210
  /**
@@ -218,7 +234,11 @@ function client(creds) {
218
234
  secretAccessKey: creds.secretAccessKey,
219
235
  ...creds.sessionToken ? { sessionToken: creds.sessionToken } : {},
220
236
  region,
221
- service: "bedrock"
237
+ service: "bedrock",
238
+ // See MAX_ATTEMPTS. aws4fetch's retry loop cannot be cancelled, so this
239
+ // layer owns the policy instead. Changing this back to the default 10
240
+ // silently restores the ~50s stall that timeoutMs is supposed to prevent.
241
+ retries: 0
222
242
  }),
223
243
  region,
224
244
  modelId: creds.modelId ?? DEFAULT_MODEL_ID
@@ -260,6 +280,47 @@ async function fail(resp) {
260
280
  }
261
281
  throw new BedrockError(text || `Bedrock returned HTTP ${resp.status}`, resp.status);
262
282
  }
283
+ function timedOut() {
284
+ const err7 = new Error("Bedrock call timed out before a usable response");
285
+ err7.name = "TimeoutError";
286
+ return err7;
287
+ }
288
+ function sleep(ms, signal) {
289
+ if (signal.aborted) return Promise.resolve();
290
+ return new Promise((resolve) => {
291
+ const done = () => {
292
+ clearTimeout(timer);
293
+ signal.removeEventListener("abort", done);
294
+ resolve();
295
+ };
296
+ const timer = setTimeout(done, ms);
297
+ signal.addEventListener("abort", done, { once: true });
298
+ });
299
+ }
300
+ function retryable(status) {
301
+ return status === 429 || status >= 500;
302
+ }
303
+ async function sendWithRetry(aws, url, init, signal, deadlineAt) {
304
+ let throttled = null;
305
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
306
+ if (signal.aborted) break;
307
+ let resp;
308
+ try {
309
+ resp = await aws.fetch(url, init);
310
+ } catch (err7) {
311
+ if (signal.aborted && throttled) return throttled;
312
+ throw err7;
313
+ }
314
+ if (!retryable(resp.status)) return resp;
315
+ throttled = resp;
316
+ if (attempt === MAX_ATTEMPTS - 1) break;
317
+ const backoff = Math.random() * INITIAL_RETRY_MS * 2 ** attempt;
318
+ if (Date.now() + backoff + MIN_ATTEMPT_BUDGET_MS > deadlineAt) break;
319
+ await sleep(backoff, signal);
320
+ }
321
+ if (throttled) return throttled;
322
+ throw timedOut();
323
+ }
263
324
  async function invokeTool(creds, req, signal) {
264
325
  const json = await invokeModelRaw(
265
326
  creds,
@@ -273,19 +334,24 @@ async function invokeTool(creds, req, signal) {
273
334
  }
274
335
  async function invokeModelRaw(creds, payload, opts = {}, signal) {
275
336
  const { aws, region, modelId } = client(creds);
337
+ const timeoutMs = opts.timeoutMs ?? BEDROCK_TIMEOUT_MS;
338
+ const deadlineAt = Date.now() + timeoutMs;
276
339
  const controller = new AbortController();
277
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? BEDROCK_TIMEOUT_MS);
340
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
278
341
  const onAbort = () => controller.abort();
279
342
  signal?.addEventListener("abort", onAbort, { once: true });
280
343
  try {
281
- const resp = await aws.fetch(
344
+ const resp = await sendWithRetry(
345
+ aws,
282
346
  `https://bedrock-runtime.${region}.amazonaws.com/model/${encodeURIComponent(modelId)}/invoke`,
283
347
  {
284
348
  method: "POST",
285
349
  headers: headersFor(opts.attribution, "application/json"),
286
350
  body: JSON.stringify(payload),
287
351
  signal: controller.signal
288
- }
352
+ },
353
+ controller.signal,
354
+ deadlineAt
289
355
  );
290
356
  if (!resp.ok) await fail(resp);
291
357
  return await resp.json();
@@ -769,11 +835,12 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
769
835
  return result2;
770
836
  };
771
837
  }
772
- async function ocsCall(env, token2, method, params = {}) {
838
+ async function ocsCall(env, token2, method, params = {}, listKind) {
773
839
  const client2 = new OcsClient(env.CARRIER_OCS_BASE_URL, token2);
774
840
  const result2 = await client2.call(method, params);
841
+ const payload = listKind ? withOcsListSummary(result2, listKind) : result2;
775
842
  return {
776
- content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
843
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
777
844
  };
778
845
  }
779
846
  async function resolveSubscriberByIccid(env, token2, iccid, cache) {
@@ -803,7 +870,7 @@ function registerAllTools(server2, ctx) {
803
870
  "list_reseller_accounts",
804
871
  {
805
872
  title: "List Reseller Accounts",
806
- description: "Use this to enumerate the accounts (sub-resellers or customer accounts) BELOW a reseller. Params: `resellerId` (integer, optional \u2014 omit to list accounts under the token owner's reseller). Returns: `{ reseller: [ { id, name, account: [ { id, name, balance, packageOnly } ] } ] }` \u2014 the accounts are NESTED under each reseller, not a flat top-level array. Do NOT use this to fetch a single subscriber's details \u2014 use `get_subscriber` instead. Do NOT use this to check eSIM activation counts \u2014 use `esim_status_per_account` for that. Do NOT use this to find a reseller's PARENT, balance or charging plans \u2014 those are above or on the reseller itself, not in this list; use `get_reseller_info`.",
873
+ description: "Use this to enumerate the accounts (sub-resellers or customer accounts) BELOW a reseller. Params: `resellerId` (integer, optional \u2014 omit to list accounts under the token owner's reseller). Returns: `{ summary, reseller: [ { id, name, account: [ { id, name, balance, packageOnly } ] } ] }` \u2014 the accounts are NESTED under each reseller, not a flat top-level array. `summary.total` therefore counts RESELLERS; the account count is `summary.nested.accounts`. For any question about accounts use that, never `total`. Do NOT use this to fetch a single subscriber's details \u2014 use `get_subscriber` instead. Do NOT use this to check eSIM activation counts \u2014 use `esim_status_per_account` for that. Do NOT use this to find a reseller's PARENT, balance or charging plans \u2014 those are above or on the reseller itself, not in this list; use `get_reseller_info`.",
807
874
  inputSchema: {
808
875
  resellerId: z.number().optional().describe("Filter to a specific reseller by ID (omit for token owner's reseller)")
809
876
  },
@@ -817,7 +884,7 @@ function registerAllTools(server2, ctx) {
817
884
  async ({ resellerId }, token2) => {
818
885
  const params = {};
819
886
  if (resellerId !== void 0) params.resellerId = resellerId;
820
- return ocsCall(ctx.env, token2, "listResellerAccount", params);
887
+ return ocsCall(ctx.env, token2, "listResellerAccount", params, "resellers");
821
888
  }
822
889
  )
823
890
  );
@@ -965,7 +1032,7 @@ function registerAllTools(server2, ctx) {
965
1032
  "list_subscribers",
966
1033
  {
967
1034
  title: "List Subscribers",
968
- description: "Use this to list subscribers with optional filters and pagination. Good for fleet enumeration, bulk status checks, and finding subscribers by account or status. OCS REQUIRES at least one search key \u2014 provide exactly one of: `imsi`, `iccid`, `activationCode`, `accountId`, or `msisdn`. Calling with no key will be rejected before reaching OCS. Params: `imsi` (string), `iccid` (string), `activationCode` (string), `accountId` (integer), `msisdn` (string), `status` (string, e.g. 'ACTIVE'/'SUSPENDED'), `offset` (integer, pagination \u2014 default 0), `limit` (integer, max results \u2014 always set to avoid unbounded fetches; recommended max 100 per call). Returns: array of subscriber summary records with ICCID, status, and account. Do NOT use this to fetch full details for a specific subscriber \u2014 use `get_subscriber` for that.",
1035
+ description: "Use this to list subscribers with optional filters and pagination. Good for fleet enumeration, bulk status checks, and finding subscribers by account or status. OCS REQUIRES at least one search key \u2014 provide exactly one of: `imsi`, `iccid`, `activationCode`, `accountId`, or `msisdn`. Calling with no key will be rejected before reaching OCS. Params: `imsi` (string), `iccid` (string), `activationCode` (string), `accountId` (integer), `msisdn` (string), `status` (string, e.g. 'ACTIVE'/'SUSPENDED'), `offset` (integer, pagination \u2014 default 0), `limit` (integer, max results \u2014 always set to avoid unbounded fetches; recommended max 100 per call). Returns: `{ summary, subscriberList: [...] }` \u2014 subscriber summary records with ICCID, status and account, behind a server-counted `summary.total` (rows returned) and `summary.breakdown.status`. Answer 'how many' from `summary`, not by counting rows \u2014 but when `summary.moreAvailable` is set this is one page, so report `summary.upstreamTotal` for the fleet-wide figure, never `total`. Do NOT use this to fetch full details for a specific subscriber \u2014 use `get_subscriber` for that.",
969
1036
  inputSchema: z.object({
970
1037
  imsi: z.string().optional().describe("Filter by IMSI"),
971
1038
  iccid: z.string().optional().describe("Filter by ICCID"),
@@ -994,7 +1061,12 @@ function registerAllTools(server2, ctx) {
994
1061
  const raw = await client2.call("listSubscriber", params);
995
1062
  const payload = applySubscriberFilters(raw, args);
996
1063
  return {
997
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
1064
+ content: [
1065
+ {
1066
+ type: "text",
1067
+ text: JSON.stringify(withOcsListSummary(payload, "subscribers"), null, 2)
1068
+ }
1069
+ ]
998
1070
  };
999
1071
  }
1000
1072
  )
@@ -1321,7 +1393,7 @@ function registerAllTools(server2, ctx) {
1321
1393
  "list_subscriber_packages",
1322
1394
  {
1323
1395
  title: "List Subscriber Packages",
1324
- description: "Use this to retrieve all prepaid packages currently assigned to a subscriber. Returns each package's allowance (data/voice/SMS), consumed usage, expiry date, status, and packageId. Always call this before any package modification tool (`modify_package_limits`, `modify_package_expiry`, `modify_package_status`, `delete_subscriber_package`) to confirm the correct packageId and current state. Params: `iccid` (subscriber identifier). Returns: array of package records with `packageId`, `name`, `status`, `dataLimit`, `dataUsed`, `expirationDate`, `recurring` flag. Do NOT use this to browse the product catalog \u2014 use `list_package_templates` for that.",
1396
+ description: "Use this to retrieve all prepaid packages currently assigned to a subscriber. Returns each package's allowance (data/voice/SMS), consumed usage, expiry date, status, and packageId. Always call this before any package modification tool (`modify_package_limits`, `modify_package_expiry`, `modify_package_status`, `delete_subscriber_package`) to confirm the correct packageId and current state. Params: `iccid` (subscriber identifier). Returns: `{ summary, prepaidPackage: [...] }` \u2014 package records with `packageId`, `name`, `status`, `dataLimit`, `dataUsed`, `expirationDate` and a `recurring` flag, behind a server-counted `summary.total` and a `summary.breakdown` over status and recurring. Answer 'how many' from `summary`, not by counting rows. Do NOT use this to browse the product catalog \u2014 use `list_package_templates` for that.",
1325
1397
  inputSchema: { iccid: z.string().describe("The subscriber ICCID") },
1326
1398
  annotations: { readOnlyHint: true }
1327
1399
  },
@@ -1330,7 +1402,7 @@ function registerAllTools(server2, ctx) {
1330
1402
  "listSubscriberPrepaidPackages",
1331
1403
  TOOL_SCOPES["list_subscriber_packages"],
1332
1404
  ctx,
1333
- async ({ iccid }, token2) => ocsCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid })
1405
+ async ({ iccid }, token2) => ocsCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }, "packages")
1334
1406
  )
1335
1407
  );
1336
1408
  server2.registerTool(
@@ -1563,10 +1635,11 @@ function registerAllTools(server2, ctx) {
1563
1635
  "list_package_templates",
1564
1636
  {
1565
1637
  title: "List Package Templates",
1566
- description: "Use this to browse the product catalog of prepaid package templates available for assignment. Returns each template's name, data/voice/SMS limits, pricing, validity period, location zone, and recurring configuration. Call this before `assign_package` or `assign_recurring_package` to obtain valid `packageTemplateId` values. Params: `accountId` (integer, optional \u2014 filter templates visible to a specific account). Returns: array of template records with `templateId`, `name`, `dataLimit`, `price`, `validityDays`, `locationZoneId`, `recurring`. Do NOT use this to list packages assigned to a specific subscriber \u2014 use `list_subscriber_packages`.",
1567
- inputSchema: {
1568
- accountId: z.number().optional().describe("Filter templates by account ID")
1569
- },
1638
+ description: "Use this to browse the product catalog of prepaid package templates available for assignment. Returns each template's name, data/voice/SMS limits, pricing, validity period, location zone, and recurring configuration. Call this before `assign_package` or `assign_recurring_package` to obtain valid `packageTemplateId` values. Params: none \u2014 returns the whole catalog for the token owner's reseller. Returns: `{ summary, template: [...] }`. `summary.total` is the number of templates, counted by the server over the complete catalog, and `summary.breakdown` counts the templates per `recurring` value and per location zone. Answer any 'how many' question from `summary` \u2014 it is authoritative, and counting the rows yourself gets it wrong. Each row carries `prepaidpackagetemplateid`, `prepaidpackagetemplatename`, `databyte` (bytes), `cost`, `perioddays`, `locationzoneid`, `rdbLocationZones.locationzonename` and `recurring`. Do NOT use this to list packages assigned to a specific subscriber \u2014 use `list_subscriber_packages`.",
1639
+ // No `accountId` — see the matching note in apps/mcp-server/src/tools.ts.
1640
+ // OCS rejected every call that carried it, and it is not a rename of any
1641
+ // of the five properties OCS does accept.
1642
+ inputSchema: {},
1570
1643
  annotations: { readOnlyHint: true }
1571
1644
  },
1572
1645
  wrapHandler(
@@ -1574,11 +1647,7 @@ function registerAllTools(server2, ctx) {
1574
1647
  "listPrepaidPackageTemplate",
1575
1648
  TOOL_SCOPES["list_package_templates"],
1576
1649
  ctx,
1577
- async ({ accountId }, token2) => {
1578
- const params = {};
1579
- if (accountId !== void 0) params.accountId = accountId;
1580
- return ocsCall(ctx.env, token2, "listPrepaidPackageTemplate", params);
1581
- }
1650
+ async (_args, token2) => ocsCall(ctx.env, token2, "listPrepaidPackageTemplate", {}, "templates")
1582
1651
  )
1583
1652
  );
1584
1653
  server2.registerTool(
@@ -1831,7 +1900,7 @@ function registerAllTools(server2, ctx) {
1831
1900
  "get_tariff",
1832
1901
  {
1833
1902
  title: "Get Customer Tariff",
1834
- description: `Use this to retrieve the tariff table for a reseller: per-country, per-operator wholesale data/voice/SMS rates. Useful for cost analysis, margin calculations, and identifying expensive roaming countries before steering decisions. Params: \`resellerId\` (integer, optional \u2014 omit to use the token owner's reseller); \`country\` (optional, ISO-3166 alpha-2 such as "nl" OR a full country name such as "Netherlands", case-insensitive); \`trafficType\` (optional, "data" | "voice" | "sms" \u2014 keeps only rules whose corresponding rate is greater than zero); \`verbose\` (optional boolean, default false \u2014 returns the full untouched OCS rule shape, capped at ${TARIFF_VERBOSE_MAX_RULES} rows, so combine it with the filters). Returns BY DEFAULT a projected result: \`totalRules\`, \`matchedRules\`, \`returnedRules\`, \`truncated\`, a hoisted \`currency\`, a \`fields\` legend and \`rules[]\` of short-keyed rows (\`iso\`, \`op\`, \`data\`, \`moCall\`, \`mtCall\`, \`moSms\`, \`mtSms\`, \`active\`). Zero rates and inactive flags are omitted per row. Nested operator detail (mccMncs, tadigs, continent, countryCode, utcOffset), the sponsor object, plan ids and the validity/discount flags are dropped unless \`verbose\` is true. Rows are capped at ${TARIFF_MAX_RULES}; when the cap bites, \`truncated\` is true and \`note\` says so explicitly and names the filter params \u2014 the result is never silently shortened. Filtering happens server-side on the complete OCS table, so a filtered call sees every matching rule. Response key in OCS is \`listTariffRule\`. Do NOT use this to assign a pricing plan to a subscriber \u2014 use \`modify_subscriber_mobile_plan\`. This shows the RESELLER's wholesale cost, not what end-users are charged.`,
1903
+ description: `Use this to retrieve the tariff table for a reseller: per-country, per-operator wholesale data/voice/SMS rates. Useful for cost analysis, margin calculations, and identifying expensive roaming countries before steering decisions. Params: \`resellerId\` (integer, optional \u2014 omit to use the token owner's reseller); \`country\` (optional, ISO-3166 alpha-2 such as "nl" OR a full country name such as "Netherlands", case-insensitive); \`trafficType\` (optional, "data" | "voice" | "sms" \u2014 keeps only rules whose corresponding rate is greater than zero); \`verbose\` (optional boolean, default false \u2014 returns the full untouched OCS rule shape, capped at ${TARIFF_VERBOSE_MAX_RULES} rows, so combine it with the filters). Returns BY DEFAULT a projected result: \`totalRules\`, \`matchedRules\`, \`returnedRules\`, \`truncated\`, a hoisted \`currency\`, a \`fields\` legend and \`rules[]\` of short-keyed rows (\`iso\`, \`op\`, \`data\`, \`moCall\`, \`mtCall\`, \`moSms\`, \`mtSms\`, \`active\`). Zero rates and inactive flags are omitted per row. Nested operator detail (mccMncs, tadigs, continent, countryCode, utcOffset), the sponsor object, plan ids and the validity/discount flags are dropped unless \`verbose\` is true. Rows are capped at ${TARIFF_MAX_RULES}; when the cap bites, \`truncated\` is true and \`note\` says so explicitly and names the filter params \u2014 the result is never silently shortened. Filtering happens server-side on the complete OCS table, so a filtered call sees every matching rule. Response key in OCS is \`listTariffRule\`. This is the slowest tool on the platform: the OCS table is ~3.4 MB and takes 3 s in a fast window and up to ~2 minutes in a slow one, so the call carries a 180 s budget. Expect to wait, and do not call it in a loop. Do NOT use this to assign a pricing plan to a subscriber \u2014 use \`modify_subscriber_mobile_plan\`. This shows the RESELLER's wholesale cost, not what end-users are charged.`,
1835
1904
  inputSchema: {
1836
1905
  resellerId: z.number().optional().describe("Reseller ID (omit to use token owner's reseller)"),
1837
1906
  country: z.string().optional().describe(
@@ -1957,8 +2026,9 @@ var ocs_methods_default = {
1957
2026
  scope: "read",
1958
2027
  description: "Retrieve reseller details",
1959
2028
  params: {
1960
- resellerId: { type: "number", required: false }
2029
+ resellerId: { type: "number", required: false, ocs_field: "id" }
1961
2030
  },
2031
+ params_note: "`resellerId` is the MCP-facing name. OCS getResellerInfo accepts exactly one property, `id`; @carrier/ocs-client translates it at the serialisation chokepoint (OCS_PARAM_RENAMES).",
1962
2032
  response: {},
1963
2033
  annotations: "readOnlyHint",
1964
2034
  verified_against_server: true,
@@ -2363,9 +2433,8 @@ var ocs_methods_default = {
2363
2433
  category: "templates",
2364
2434
  scope: "read",
2365
2435
  description: "List all prepaid package templates",
2366
- params: {
2367
- accountId: { type: "number", required: false }
2368
- },
2436
+ params: {},
2437
+ params_note: "No account filter. OCS listPrepaidPackageTemplate accepts only locationZoneId, templateId, destinationListId, resellerId, sponsorId. The former `accountId` param was rejected by OCS on every call and is not a rename of any of those five: accounts are a different id space from resellers and sponsors.",
2369
2438
  response: {},
2370
2439
  annotations: "readOnlyHint",
2371
2440
  verified_against_server: true,
@@ -3499,26 +3568,9 @@ async function fetchActiveSubscribers(env, token2, accountId, resellerId) {
3499
3568
  { accountId: acctId }
3500
3569
  );
3501
3570
  if (subResult.error) return { data: null, error: subResult.error };
3502
- const raw = subResult.data;
3503
- const list = Array.isArray(raw) ? raw : raw?.subscriberList ?? [];
3504
- aggregated.push(...list);
3571
+ aggregated.push(...subscriberRows(subResult.data));
3505
3572
  }
3506
- const active = aggregated.filter((s) => String(s.status ?? "").toUpperCase() === "ACTIVE");
3507
- return { data: active, error: null };
3508
- }
3509
- function formatBytes(bytes) {
3510
- if (bytes === 0) return "0 B";
3511
- const units = ["B", "KB", "MB", "GB", "TB"];
3512
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
3513
- return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
3514
- }
3515
- function daysUntil(dateStr) {
3516
- const now = /* @__PURE__ */ new Date();
3517
- const target = new Date(dateStr);
3518
- return Math.ceil((target.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24));
3519
- }
3520
- function toISODate(d) {
3521
- return d.toISOString().split("T")[0];
3573
+ return { data: filterActiveSubscribers(aggregated), error: null };
3522
3574
  }
3523
3575
  function result(text, isError = false) {
3524
3576
  return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
@@ -3598,10 +3650,10 @@ function registerIntelligenceTools(server2, ctx) {
3598
3650
  }
3599
3651
  }
3600
3652
  const now = /* @__PURE__ */ new Date();
3601
- const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1e3);
3653
+ const eventWindow = lastNDaysPeriod(3, now);
3602
3654
  const events = await safeCall(ctx.env, token2, "subscriberNetworkEventsOverPeriod", {
3603
3655
  subscriber: { iccid },
3604
- period: { start: toISODate(twoDaysAgo), end: toISODate(now) }
3656
+ period: eventWindow
3605
3657
  });
3606
3658
  if (events.data && Array.isArray(events.data)) {
3607
3659
  if (events.data.length === 0) {
@@ -3778,11 +3830,11 @@ Package-only accounts at 0 balance require no action.`);
3778
3830
  }, async ({ iccid }) => {
3779
3831
  const token2 = await ctx.getUserToken(ctx.props.sub);
3780
3832
  const now = /* @__PURE__ */ new Date();
3781
- const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
3833
+ const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
3782
3834
  const [usageResult, pkgResult] = await Promise.all([
3783
3835
  safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
3784
3836
  subscriber: { iccid },
3785
- period: { start: toISODate(weekAgo), end: toISODate(now) }
3837
+ period: usagePeriod
3786
3838
  }),
3787
3839
  safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid })
3788
3840
  ]);
@@ -3790,13 +3842,8 @@ Package-only accounts at 0 balance require no action.`);
3790
3842
  const sections = [`# Usage Anomaly Report: ${iccid}
3791
3843
  `];
3792
3844
  const anomalies = [];
3793
- if (usageResult.data && Array.isArray(usageResult.data) && usageResult.data.length > 0) {
3794
- const dailyData = [];
3795
- for (const entry of usageResult.data) {
3796
- const bytes = Number(entry.dataBytes ?? entry.dataVolume ?? entry.totalData ?? 0);
3797
- const date = String(entry.date ?? entry.day ?? "?");
3798
- dailyData.push({ date, bytes });
3799
- }
3845
+ const dailyData = extractDailyUsage(usageResult.data);
3846
+ if (dailyData.length > 0) {
3800
3847
  if (dailyData.length >= 2) {
3801
3848
  const volumes = dailyData.map((d) => d.bytes);
3802
3849
  const mean = volumes.reduce((a, b) => a + b, 0) / volumes.length;
@@ -3864,24 +3911,21 @@ Package-only accounts at 0 balance require no action.`);
3864
3911
  }, async ({ iccid }) => {
3865
3912
  const token2 = await ctx.getUserToken(ctx.props.sub);
3866
3913
  const now = /* @__PURE__ */ new Date();
3867
- const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
3914
+ const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
3868
3915
  const [usageResult, pkgResult, templatesResult] = await Promise.all([
3869
3916
  safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
3870
3917
  subscriber: { iccid },
3871
- period: { start: toISODate(weekAgo), end: toISODate(now) }
3918
+ period: usagePeriod
3872
3919
  }),
3873
3920
  safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }),
3874
3921
  safeCall(ctx.env, token2, "listPrepaidPackageTemplate", {})
3875
3922
  ]);
3876
3923
  const sections = [`# Package Optimization: ${iccid}
3877
3924
  `];
3925
+ const optimizeRows = extractDailyUsage(usageResult.data);
3878
3926
  let avgDailyData = 0;
3879
- if (usageResult.data && Array.isArray(usageResult.data) && usageResult.data.length > 0) {
3880
- const totalData = usageResult.data.reduce(
3881
- (sum, e) => sum + Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0),
3882
- 0
3883
- );
3884
- avgDailyData = totalData / usageResult.data.length;
3927
+ if (optimizeRows.length > 0) {
3928
+ avgDailyData = optimizeRows.reduce((sum, r) => sum + r.bytes, 0) / optimizeRows.length;
3885
3929
  sections.push(`## Current Usage Pattern`);
3886
3930
  sections.push(`- Average daily data: ${formatBytes(avgDailyData)}`);
3887
3931
  sections.push(`- Projected monthly: ${formatBytes(avgDailyData * 30)}`);
@@ -3952,22 +3996,21 @@ Package-only accounts at 0 balance require no action.`);
3952
3996
  }, async ({ iccid }) => {
3953
3997
  const token2 = await ctx.getUserToken(ctx.props.sub);
3954
3998
  const now = /* @__PURE__ */ new Date();
3955
- const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
3999
+ const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
3956
4000
  const [subResult, usageResult, pkgResult, activeResult] = await Promise.all([
3957
4001
  safeCall(ctx.env, token2, "getSingleSubscriber", { iccid }),
3958
4002
  safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
3959
4003
  subscriber: { iccid },
3960
- period: { start: toISODate(weekAgo), end: toISODate(now) }
4004
+ period: usagePeriod
3961
4005
  }),
3962
4006
  safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }),
3963
4007
  safeCall(ctx.env, token2, "getSubscriberActivePeriod", { iccid })
3964
4008
  ]);
3965
4009
  let riskScore = 0;
3966
4010
  const factors = [];
3967
- if (usageResult.data && Array.isArray(usageResult.data) && usageResult.data.length >= 3) {
3968
- const volumes = usageResult.data.map(
3969
- (e) => Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0)
3970
- );
4011
+ const churnRows = extractDailyUsage(usageResult.data);
4012
+ if (churnRows.length >= 3) {
4013
+ const volumes = churnRows.map((r) => r.bytes);
3971
4014
  const firstHalf = volumes.slice(0, Math.floor(volumes.length / 2));
3972
4015
  const secondHalf = volumes.slice(Math.floor(volumes.length / 2));
3973
4016
  const avgFirst = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length;
@@ -3984,7 +4027,7 @@ Package-only accounts at 0 balance require no action.`);
3984
4027
  factors.push({ factor: "Moderately declining usage", impact, detail: `Usage dropped ${Math.abs(trend * 100).toFixed(0)}%` });
3985
4028
  }
3986
4029
  }
3987
- } else if (!usageResult.data || Array.isArray(usageResult.data) && usageResult.data.length === 0) {
4030
+ } else if (churnRows.length === 0) {
3988
4031
  riskScore += 25;
3989
4032
  factors.push({ factor: "No recent usage", impact: 25, detail: "Zero data activity in last 7 days" });
3990
4033
  }
@@ -4279,7 +4322,7 @@ ${networksSorted.length} different networks in ${country} \u2014 possible steeri
4279
4322
  const sections = [`# High Cost Subscriber Report
4280
4323
  `];
4281
4324
  const now = /* @__PURE__ */ new Date();
4282
- const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
4325
+ const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
4283
4326
  const highCostSubs = [];
4284
4327
  const batchSize = 5;
4285
4328
  const subs = subsResult.data.slice(0, sampleSize);
@@ -4291,19 +4334,13 @@ ${networksSorted.length} different networks in ${country} \u2014 possible steeri
4291
4334
  const [usage, pkgs, loc] = await Promise.all([
4292
4335
  safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
4293
4336
  subscriber: { iccid },
4294
- period: { start: toISODate(weekAgo), end: toISODate(now) }
4337
+ period: usagePeriod
4295
4338
  }),
4296
4339
  safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }),
4297
4340
  safeCall(ctx.env, token2, "getSubscriberLocation", { iccid })
4298
4341
  ]);
4299
- let dailyAvgBytes = 0;
4300
- if (usage.data && Array.isArray(usage.data) && usage.data.length > 0) {
4301
- const totalBytes = usage.data.reduce(
4302
- (sum, e) => sum + Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0),
4303
- 0
4304
- );
4305
- dailyAvgBytes = totalBytes / usage.data.length;
4306
- }
4342
+ const dailyRows = extractDailyUsage(usage.data);
4343
+ const dailyAvgBytes = dailyRows.length > 0 ? dailyRows.reduce((sum, r) => sum + r.bytes, 0) / dailyRows.length : 0;
4307
4344
  if (pkgs.data && Array.isArray(pkgs.data)) {
4308
4345
  const activePkg = pkgs.data.find(
4309
4346
  (p) => String(p.status ?? "").toUpperCase() === "ACTIVE"
@@ -4378,7 +4415,7 @@ ${networksSorted.length} different networks in ${country} \u2014 possible steeri
4378
4415
  });
4379
4416
  server2.registerTool("detect_country_entry", {
4380
4417
  title: "Detect Country Entry",
4381
- description: "Detects when a subscriber has entered a new country by reading networkInfo.lastMcc from getSingleSubscriber (one cheap OCS call \u2014 avoids the per-call cost of getSubscriberLocationByCellId). Resolves MCC \u2192 ISO 3166-1 alpha-2 and optionally diffs against a caller-supplied expectedCountry to return countryChanged. Designed for downstream country-entry upsell workflows (e.g. mango.talk SMS/push offers). COST NOTE: This tool makes exactly one OCS call per invocation. Consumers running polling crons MUST enforce their own rate floor \u2014 this layer provides no throttle.",
4418
+ description: "Detects when a subscriber has entered a new country by reading networkInfo.lastMcc from getSingleSubscriber (one cheap OCS call \u2014 avoids the per-call cost of getSubscriberLocationByCellId). Resolves MCC \u2192 ISO 3166-1 alpha-2 and optionally diffs against a caller-supplied expectedCountry to return countryChanged. Designed for downstream country-entry upsell workflows (e.g. mango.talk SMS/push offers). NOT AN AI TOOL: this is a deterministic MCC lookup and returns no model-written analysis, unlike the eight intelligence composites. COST NOTE: This tool makes exactly one OCS call per invocation. Consumers running polling crons MUST enforce their own rate floor \u2014 this layer provides no throttle.",
4382
4419
  inputSchema: {
4383
4420
  subscriber: z2.union([
4384
4421
  z2.object({ subscriberId: z2.number() }).describe("Internal subscriber ID"),
@@ -6925,8 +6962,11 @@ var WALLET_TOOL_SCOPES = {
6925
6962
  wallet_topup_checkout: "write",
6926
6963
  wallet_auto_topup: "write"
6927
6964
  };
6928
- function ok(text) {
6929
- return { content: [{ type: "text", text }] };
6965
+ function okStructured(payload) {
6966
+ return {
6967
+ content: [{ type: "text", text: JSON.stringify(payload) }],
6968
+ structuredContent: payload
6969
+ };
6930
6970
  }
6931
6971
  function err(text) {
6932
6972
  return { isError: true, content: [{ type: "text", text }] };
@@ -6940,6 +6980,17 @@ function registerWalletTools(server2, ctx) {
6940
6980
  title: "Wallet Balance",
6941
6981
  description: "Read the caller organisation's Carrier prepaid wallet: current balance and auto-top-up settings. The wallet is what Carrier bills your own eSIM spend against \u2014 it is not an OCS balance and not your MCP plan credits. Params: none (the organisation is taken from the session). Returns: { org_id, balance_eur_cents, auto_topup_enabled, auto_topup_threshold_cents, auto_topup_pack_cents } \u2014 all amounts in EUR cents, not euros. Do NOT use this for a reseller account balance in OCS \u2014 use `get_reseller_info` or `list_reseller_accounts`. Do NOT use this for a subscriber's OCS balance \u2014 use `get_subscriber`. Do NOT use this for MCP plan credits \u2014 use `credit_balance`. Do NOT use this for money held at Stripe \u2014 use `stripe_connect_balance`.",
6942
6982
  inputSchema: {},
6983
+ // Derived from the object literal this handler builds below, not from the
6984
+ // description. Every field is unconditional on the success path and comes
6985
+ // straight off the Wallet record (wallet-client.ts:31-34), so all five are
6986
+ // required rather than optional.
6987
+ outputSchema: {
6988
+ org_id: z7.string().describe("Carrier organisation the wallet belongs to"),
6989
+ balance_eur_cents: z7.number().int().describe("Current balance in EUR cents"),
6990
+ auto_topup_enabled: z7.boolean(),
6991
+ auto_topup_threshold_cents: z7.number().int().describe("Balance at or below which an auto top-up fires, EUR cents"),
6992
+ auto_topup_pack_cents: z7.number().int().describe("Amount an auto top-up charges, EUR cents")
6993
+ },
6943
6994
  annotations: annotationsFor("wallet_balance", "read")
6944
6995
  },
6945
6996
  async () => {
@@ -6958,15 +7009,13 @@ function registerWalletTools(server2, ctx) {
6958
7009
  sub: props2.sub,
6959
7010
  reseller_id: props2.reseller_id
6960
7011
  });
6961
- return ok(
6962
- JSON.stringify({
6963
- org_id: orgId,
6964
- balance_eur_cents: wallet.balance_eur_cents,
6965
- auto_topup_enabled: wallet.auto_topup_enabled,
6966
- auto_topup_threshold_cents: wallet.auto_topup_threshold_cents,
6967
- auto_topup_pack_cents: wallet.auto_topup_pack_cents
6968
- })
6969
- );
7012
+ return okStructured({
7013
+ org_id: orgId,
7014
+ balance_eur_cents: wallet.balance_eur_cents,
7015
+ auto_topup_enabled: wallet.auto_topup_enabled,
7016
+ auto_topup_threshold_cents: wallet.auto_topup_threshold_cents,
7017
+ auto_topup_pack_cents: wallet.auto_topup_pack_cents
7018
+ });
6970
7019
  } catch (e) {
6971
7020
  if (e instanceof WalletClientError) return err(`Wallet error: ${e.message}`);
6972
7021
  Sentry2.captureException(e);
@@ -6984,6 +7033,31 @@ function registerWalletTools(server2, ctx) {
6984
7033
  "Pack id (pack_500/pack_1000/pack_2500/pack_5000) or exact EUR-cents amount. Omit to list packs."
6985
7034
  )
6986
7035
  },
7036
+ /**
7037
+ * This tool really does return two different shapes, so the schema says
7038
+ * so rather than pretending otherwise. `pack` omitted returns the
7039
+ * catalogue arm (`packs`); `pack` supplied returns the session arm. An
7040
+ * `outputSchema` is an object shape, not a union type, so the two arms
7041
+ * are expressed as mutually exclusive optional groups and the pairing is
7042
+ * documented here. Marking either arm required would break the other.
7043
+ */
7044
+ outputSchema: {
7045
+ packs: z7.array(
7046
+ z7.object({
7047
+ id: z7.string(),
7048
+ eur_cents: z7.number().int(),
7049
+ bonus_pct: z7.number(),
7050
+ bonus_eur_cents: z7.number().int(),
7051
+ credited_eur_cents: z7.number().int()
7052
+ })
7053
+ ).optional().describe("Catalogue arm \u2014 present only when `pack` was omitted."),
7054
+ org_id: z7.string().optional().describe("Session arm."),
7055
+ pack_id: z7.string().optional().describe("Session arm."),
7056
+ pack_eur_cents: z7.number().int().optional().describe("Session arm."),
7057
+ bonus_eur_cents: z7.number().int().optional().describe("Session arm."),
7058
+ checkout_session_id: z7.string().optional().describe("Session arm."),
7059
+ checkout_url: z7.string().nullable().optional().describe("Session arm. Stripe may return null for a session URL.")
7060
+ },
6987
7061
  annotations: annotationsFor("wallet_topup_checkout", "write")
6988
7062
  },
6989
7063
  async ({ pack }) => {
@@ -6992,17 +7066,15 @@ function registerWalletTools(server2, ctx) {
6992
7066
  const orgId = defaultOrgId;
6993
7067
  const start = Date.now();
6994
7068
  if (!pack) {
6995
- return ok(
6996
- JSON.stringify({
6997
- packs: PACKS.map((p) => ({
6998
- id: p.id,
6999
- eur_cents: p.eurCents,
7000
- bonus_pct: p.bonusPct,
7001
- bonus_eur_cents: bonusCentsFor(p),
7002
- credited_eur_cents: p.eurCents + bonusCentsFor(p)
7003
- }))
7004
- })
7005
- );
7069
+ return okStructured({
7070
+ packs: PACKS.map((p) => ({
7071
+ id: p.id,
7072
+ eur_cents: p.eurCents,
7073
+ bonus_pct: p.bonusPct,
7074
+ bonus_eur_cents: bonusCentsFor(p),
7075
+ credited_eur_cents: p.eurCents + bonusCentsFor(p)
7076
+ }))
7077
+ });
7006
7078
  }
7007
7079
  const selected = resolvePack(pack);
7008
7080
  if (!selected) {
@@ -7021,16 +7093,14 @@ function registerWalletTools(server2, ctx) {
7021
7093
  sub: props2.sub,
7022
7094
  reseller_id: props2.reseller_id
7023
7095
  });
7024
- return ok(
7025
- JSON.stringify({
7026
- org_id: orgId,
7027
- pack_id: selected.id,
7028
- pack_eur_cents: selected.eurCents,
7029
- bonus_eur_cents: bonusCentsFor(selected),
7030
- checkout_session_id: session.id,
7031
- checkout_url: session.url
7032
- })
7033
- );
7096
+ return okStructured({
7097
+ org_id: orgId,
7098
+ pack_id: selected.id,
7099
+ pack_eur_cents: selected.eurCents,
7100
+ bonus_eur_cents: bonusCentsFor(selected),
7101
+ checkout_session_id: session.id,
7102
+ checkout_url: session.url
7103
+ });
7034
7104
  } catch (e) {
7035
7105
  Sentry2.captureException(e);
7036
7106
  return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
@@ -7045,6 +7115,26 @@ function registerWalletTools(server2, ctx) {
7045
7115
  inputSchema: {
7046
7116
  pack_cents: z7.number().int().positive().optional().describe("Override pack amount in EUR cents (must match a catalog pack).")
7047
7117
  },
7118
+ /**
7119
+ * Only the charged arm is described here — `charged: false` returns
7120
+ * through `err()`, and the SDK skips output validation on an `isError`
7121
+ * result.
7122
+ *
7123
+ * `balance_after_cents` is optional and the description above is wrong to
7124
+ * promise it. runAutoTopup has a second success arm (wallet-tools.ts:337)
7125
+ * where the card was charged but crediting Atlas failed: it carries
7126
+ * `paymentIntentId` and a `reason`, and no balance. Requiring the field
7127
+ * would turn that arm — a real charge the caller must be told about —
7128
+ * into a validation error.
7129
+ */
7130
+ outputSchema: {
7131
+ org_id: z7.string(),
7132
+ charged: z7.literal(true),
7133
+ credited_eur_cents: z7.number().int(),
7134
+ balance_after_cents: z7.number().int().optional().describe("Absent when the charge succeeded but crediting is pending."),
7135
+ payment_intent_id: z7.string().optional(),
7136
+ note: z7.string().optional().describe("Present when the charge landed but the credit is still pending.")
7137
+ },
7048
7138
  annotations: annotationsFor("wallet_auto_topup", "write")
7049
7139
  },
7050
7140
  async ({ pack_cents }) => {
@@ -7068,16 +7158,18 @@ function registerWalletTools(server2, ctx) {
7068
7158
  JSON.stringify({ org_id: orgId, charged: false, reason: result2.reason })
7069
7159
  );
7070
7160
  }
7071
- return ok(
7072
- JSON.stringify({
7073
- org_id: orgId,
7074
- charged: true,
7075
- credited_eur_cents: result2.creditedCents,
7076
- balance_after_cents: result2.balanceAfterCents,
7077
- payment_intent_id: result2.paymentIntentId,
7078
- ...result2.reason ? { note: result2.reason } : {}
7079
- })
7080
- );
7161
+ return okStructured({
7162
+ org_id: orgId,
7163
+ charged: true,
7164
+ credited_eur_cents: result2.creditedCents,
7165
+ // Spread rather than assign, so the key is absent instead of set to
7166
+ // `undefined`. JSON.stringify drops an undefined value anyway, so
7167
+ // assigning it would make `content` and `structuredContent` disagree
7168
+ // about whether the field is there.
7169
+ ...result2.balanceAfterCents !== void 0 ? { balance_after_cents: result2.balanceAfterCents } : {},
7170
+ ...result2.paymentIntentId !== void 0 ? { payment_intent_id: result2.paymentIntentId } : {},
7171
+ ...result2.reason ? { note: result2.reason } : {}
7172
+ });
7081
7173
  } catch (e) {
7082
7174
  Sentry2.captureException(e);
7083
7175
  return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
@@ -7103,25 +7195,30 @@ async function issueConfirmToken(env, sub, toolName) {
7103
7195
  await env.OAUTH_KV.put(key, token2, { expirationTtl: 300 });
7104
7196
  return token2;
7105
7197
  }
7106
- function ok2(text) {
7198
+ function ok(text) {
7107
7199
  return { content: [{ type: "text", text }] };
7108
7200
  }
7109
7201
  function err2(text) {
7110
7202
  return { isError: true, content: [{ type: "text", text }] };
7111
7203
  }
7112
7204
  async function stripeGet(stripeKey, path, connectedAccountId) {
7113
- const headers = { Authorization: `Bearer ${stripeKey}` };
7205
+ const headers = {
7206
+ Authorization: `Bearer ${stripeKey}`,
7207
+ "Stripe-Version": "2025-08-27.basil"
7208
+ };
7114
7209
  if (connectedAccountId) headers["Stripe-Account"] = connectedAccountId;
7115
7210
  const res = await fetch(`https://api.stripe.com${path}`, { headers });
7116
7211
  const data = await res.json();
7117
7212
  return { ok: res.ok, status: res.status, data };
7118
7213
  }
7119
- async function stripePost(stripeKey, path, params, connectedAccountId) {
7214
+ async function stripePost(stripeKey, path, params, connectedAccountId, idempotencyKey) {
7120
7215
  const headers = {
7121
7216
  Authorization: `Bearer ${stripeKey}`,
7217
+ "Stripe-Version": "2025-08-27.basil",
7122
7218
  "Content-Type": "application/x-www-form-urlencoded"
7123
7219
  };
7124
7220
  if (connectedAccountId) headers["Stripe-Account"] = connectedAccountId;
7221
+ if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
7125
7222
  const res = await fetch(`https://api.stripe.com${path}`, {
7126
7223
  method: "POST",
7127
7224
  headers,
@@ -7130,6 +7227,9 @@ async function stripePost(stripeKey, path, params, connectedAccountId) {
7130
7227
  const data = await res.json();
7131
7228
  return { ok: res.ok, status: res.status, data };
7132
7229
  }
7230
+ function refundIdempotencyKey(sub, charge, amount, reason) {
7231
+ return `carrier-refund:${sub}:${charge}:${amount ?? "full"}:${reason ?? "none"}`;
7232
+ }
7133
7233
  var STRIPE_CONNECT_TOOL_SCOPES = {
7134
7234
  stripe_connect_status: "read",
7135
7235
  stripe_connect_payouts: "read",
@@ -7177,7 +7277,7 @@ function registerStripeConnectTools(server2, ctx) {
7177
7277
  if (!stripeKey) return err2("Stripe not configured");
7178
7278
  const accountId = await env.CARRIER_USERS.get(opKvKey);
7179
7279
  if (!accountId) {
7180
- return ok2(JSON.stringify({ status: "not_connected", operator_id: opId }));
7280
+ return ok(JSON.stringify({ status: "not_connected", operator_id: opId }));
7181
7281
  }
7182
7282
  const result2 = await stripeGet(stripeKey, `/v1/accounts/${accountId}`);
7183
7283
  if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
@@ -7192,7 +7292,7 @@ function registerStripeConnectTools(server2, ctx) {
7192
7292
  sub: props2.sub,
7193
7293
  reseller_id: props2.reseller_id
7194
7294
  });
7195
- return ok2(JSON.stringify({
7295
+ return ok(JSON.stringify({
7196
7296
  status,
7197
7297
  operator_id: opId,
7198
7298
  account_id: accountId,
@@ -7243,7 +7343,7 @@ function registerStripeConnectTools(server2, ctx) {
7243
7343
  sub: props2.sub,
7244
7344
  reseller_id: props2.reseller_id
7245
7345
  });
7246
- return ok2(JSON.stringify({ payouts: result2.data.data, has_more: result2.data.has_more }));
7346
+ return ok(JSON.stringify({ payouts: result2.data.data, has_more: result2.data.has_more }));
7247
7347
  } catch (e) {
7248
7348
  Sentry3.captureException(e);
7249
7349
  return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
@@ -7278,7 +7378,7 @@ function registerStripeConnectTools(server2, ctx) {
7278
7378
  sub: props2.sub,
7279
7379
  reseller_id: props2.reseller_id
7280
7380
  });
7281
- return ok2(JSON.stringify({
7381
+ return ok(JSON.stringify({
7282
7382
  account_id: accountId,
7283
7383
  available: result2.data.available ?? [],
7284
7384
  pending: result2.data.pending ?? []
@@ -7309,7 +7409,7 @@ function registerStripeConnectTools(server2, ctx) {
7309
7409
  const toolName = "stripe_connect_refund";
7310
7410
  if (!confirm_token) {
7311
7411
  const token2 = await issueConfirmToken(env, props2.sub, toolName);
7312
- return ok2(
7412
+ return ok(
7313
7413
  `HARD_BLOCK: Refund ${amount_cents ? `${amount_cents} cents on` : "(full) on"} charge ${charge_id} requires confirmation.
7314
7414
  confirm_token: ${token2}
7315
7415
  Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes.`
@@ -7327,7 +7427,13 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
7327
7427
  const params = { charge: charge_id };
7328
7428
  if (amount_cents) params.amount = String(amount_cents);
7329
7429
  if (reason) params.reason = reason;
7330
- const result2 = await stripePost(stripeKey, "/v1/refunds", params, accountId);
7430
+ const result2 = await stripePost(
7431
+ stripeKey,
7432
+ "/v1/refunds",
7433
+ params,
7434
+ accountId,
7435
+ refundIdempotencyKey(props2.sub, charge_id, amount_cents, reason)
7436
+ );
7331
7437
  if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
7332
7438
  writeAudit(env, {
7333
7439
  tool_name: toolName,
@@ -7338,7 +7444,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
7338
7444
  sub: props2.sub,
7339
7445
  reseller_id: props2.reseller_id
7340
7446
  });
7341
- return ok2(`Refund issued: ${result2.data.id} \u2014 status: ${result2.data.status}`);
7447
+ return ok(`Refund issued: ${result2.data.id} \u2014 status: ${result2.data.status}`);
7342
7448
  } catch (e) {
7343
7449
  Sentry3.captureException(e);
7344
7450
  return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
@@ -7378,7 +7484,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
7378
7484
  sub: props2.sub,
7379
7485
  reseller_id: props2.reseller_id
7380
7486
  });
7381
- return ok2(JSON.stringify({ disputes: result2.data.data, has_more: result2.data.has_more }));
7487
+ return ok(JSON.stringify({ disputes: result2.data.data, has_more: result2.data.has_more }));
7382
7488
  } catch (e) {
7383
7489
  Sentry3.captureException(e);
7384
7490
  return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
@@ -7421,7 +7527,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
7421
7527
  sub: props2.sub,
7422
7528
  reseller_id: props2.reseller_id
7423
7529
  });
7424
- return ok2(JSON.stringify({ reviews: result2.data.data, has_more: result2.data.has_more }));
7530
+ return ok(JSON.stringify({ reviews: result2.data.data, has_more: result2.data.has_more }));
7425
7531
  } catch (e) {
7426
7532
  Sentry3.captureException(e);
7427
7533
  return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
@@ -7451,7 +7557,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
7451
7557
  const start = Date.now();
7452
7558
  if (!confirm_token) {
7453
7559
  const token2 = await issueConfirmToken(env, props2.sub, toolName);
7454
- return ok2(
7560
+ return ok(
7455
7561
  `HARD_BLOCK: Approving review ${review_id} allows the charge to proceed.
7456
7562
  confirm_token: ${token2}
7457
7563
  Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
@@ -7477,7 +7583,7 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
7477
7583
  sub: props2.sub,
7478
7584
  reseller_id: props2.reseller_id
7479
7585
  });
7480
- return ok2(`Review ${review_id} approved. Charge will proceed.`);
7586
+ return ok(`Review ${review_id} approved. Charge will proceed.`);
7481
7587
  } catch (e) {
7482
7588
  Sentry3.captureException(e);
7483
7589
  return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
@@ -7507,7 +7613,7 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
7507
7613
  const start = Date.now();
7508
7614
  if (!confirm_token) {
7509
7615
  const token2 = await issueConfirmToken(env, props2.sub, toolName);
7510
- return ok2(
7616
+ return ok(
7511
7617
  `HARD_BLOCK: Declining review ${review_id} will close/block the charge.
7512
7618
  confirm_token: ${token2}
7513
7619
  Expires in 5 minutes.`
@@ -7533,7 +7639,7 @@ Expires in 5 minutes.`
7533
7639
  sub: props2.sub,
7534
7640
  reseller_id: props2.reseller_id
7535
7641
  });
7536
- return ok2(`Review ${review_id} declined.`);
7642
+ return ok(`Review ${review_id} declined.`);
7537
7643
  } catch (e) {
7538
7644
  Sentry3.captureException(e);
7539
7645
  return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
@@ -7564,7 +7670,7 @@ Expires in 5 minutes.`
7564
7670
  const start = Date.now();
7565
7671
  if (!confirm_token) {
7566
7672
  const token2 = await issueConfirmToken(env, props2.sub, toolName);
7567
- return ok2(
7673
+ return ok(
7568
7674
  `HARD_BLOCK: Adding "${value}" to list ${value_list_id} will affect future charge decisions.
7569
7675
  confirm_token: ${token2}
7570
7676
  Expires in 5 minutes.`
@@ -7590,7 +7696,7 @@ Expires in 5 minutes.`
7590
7696
  sub: props2.sub,
7591
7697
  reseller_id: props2.reseller_id
7592
7698
  });
7593
- return ok2(`Added "${value}" to Radar list ${value_list_id}.`);
7699
+ return ok(`Added "${value}" to Radar list ${value_list_id}.`);
7594
7700
  } catch (e) {
7595
7701
  Sentry3.captureException(e);
7596
7702
  return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
@@ -7616,7 +7722,7 @@ Expires in 5 minutes.`
7616
7722
  "This tool acts on Carrier's platform Stripe account and is restricted to Carrier's own organization."
7617
7723
  );
7618
7724
  }
7619
- return ok2(
7725
+ return ok(
7620
7726
  `Stripe Radar does not expose rule enable/disable via the public API.
7621
7727
  To ${enabled ? "enable" : "disable"} rule ${rule_id}:
7622
7728
  1. Open https://dashboard.stripe.com/radar/rules
@@ -7633,9 +7739,9 @@ import { z as z9 } from "zod";
7633
7739
  var STOREFRONT_LOGO_TOOL_SCOPES = {
7634
7740
  generate_storefront_logo: "read"
7635
7741
  };
7636
- var ok3 = (text) => ({ content: [{ type: "text", text }] });
7742
+ var ok2 = (text) => ({ content: [{ type: "text", text }] });
7637
7743
  var err3 = (text) => ({ isError: true, content: [{ type: "text", text }] });
7638
- function registerStorefrontLogoTools(server2, env = {}) {
7744
+ function registerStorefrontLogoTools(server2, env = {}, props2) {
7639
7745
  server2.registerTool(
7640
7746
  "generate_storefront_logo",
7641
7747
  {
@@ -7649,6 +7755,8 @@ function registerStorefrontLogoTools(server2, env = {}) {
7649
7755
  annotations: annotationsFor("generate_storefront_logo", "read")
7650
7756
  },
7651
7757
  async ({ name, accent, tagline }) => {
7758
+ const denied = denyUnlessScoped(props2, "generate_storefront_logo", STOREFRONT_LOGO_TOOL_SCOPES);
7759
+ if (denied) return denied;
7652
7760
  try {
7653
7761
  const hex = accent ? accent.startsWith("#") ? accent : `#${accent}` : void 0;
7654
7762
  const logo = await generateStorefrontLogo({
@@ -7657,7 +7765,7 @@ function registerStorefrontLogoTools(server2, env = {}) {
7657
7765
  tagline,
7658
7766
  env: { ...env, ...process.env }
7659
7767
  });
7660
- return ok3(
7768
+ return ok2(
7661
7769
  JSON.stringify({
7662
7770
  brand: name,
7663
7771
  source: logo.source,
@@ -7682,11 +7790,11 @@ var STOREFRONT_DEPLOY_TOOL_SCOPES = {
7682
7790
  deploy_storefront: "write",
7683
7791
  provision_storefront_clerk: "write"
7684
7792
  };
7685
- var ok4 = (value) => ({
7793
+ var ok3 = (value) => ({
7686
7794
  content: [{ type: "text", text: JSON.stringify(value) }]
7687
7795
  });
7688
7796
  var err4 = (text) => ({ isError: true, content: [{ type: "text", text }] });
7689
- function registerStorefrontDeployTools(server2) {
7797
+ function registerStorefrontDeployTools(server2, props2) {
7690
7798
  server2.registerTool(
7691
7799
  "list_deploy_targets",
7692
7800
  {
@@ -7698,10 +7806,12 @@ function registerStorefrontDeployTools(server2) {
7698
7806
  annotations: annotationsFor("list_deploy_targets", "read")
7699
7807
  },
7700
7808
  async ({ dir }) => {
7809
+ const denied = denyUnlessScoped(props2, "list_deploy_targets", STOREFRONT_DEPLOY_TOOL_SCOPES);
7810
+ if (denied) return denied;
7701
7811
  try {
7702
7812
  const statuses = await probeAll(dir);
7703
7813
  const ranked = rankTargets(statuses);
7704
- return ok4({
7814
+ return ok3({
7705
7815
  targets: statuses,
7706
7816
  default: ranked[0]?.id ?? null,
7707
7817
  reason: ranked.length ? void 0 : "No host is installed and logged in."
@@ -7728,6 +7838,8 @@ function registerStorefrontDeployTools(server2) {
7728
7838
  annotations: annotationsFor("deploy_storefront", "write")
7729
7839
  },
7730
7840
  async ({ dir, target, name, skip_build, push_secrets, verify, domain }) => {
7841
+ const denied = denyUnlessScoped(props2, "deploy_storefront", STOREFRONT_DEPLOY_TOOL_SCOPES);
7842
+ if (denied) return denied;
7731
7843
  try {
7732
7844
  if (target && !isTargetId(target)) return err4(`Unknown target "${target}".`);
7733
7845
  const brand = await loadStorefrontBrand(dir, name ? { name } : void 0);
@@ -7747,7 +7859,7 @@ function registerStorefrontDeployTools(server2) {
7747
7859
  return err4(result2.reason ?? "Deploy failed.");
7748
7860
  }
7749
7861
  const verification = verify !== false && result2.url ? await verifyStorefront(result2.url, dir, process.env) : void 0;
7750
- return ok4({
7862
+ return ok3({
7751
7863
  deployed: true,
7752
7864
  target: result2.target,
7753
7865
  project: result2.projectName,
@@ -7780,6 +7892,12 @@ function registerStorefrontDeployTools(server2) {
7780
7892
  annotations: annotationsFor("provision_storefront_clerk", "write")
7781
7893
  },
7782
7894
  async ({ dir, name, production, no_create, url }) => {
7895
+ const denied = denyUnlessScoped(
7896
+ props2,
7897
+ "provision_storefront_clerk",
7898
+ STOREFRONT_DEPLOY_TOOL_SCOPES
7899
+ );
7900
+ if (denied) return denied;
7783
7901
  try {
7784
7902
  const brand = await loadStorefrontBrand(dir, name ? { name } : void 0);
7785
7903
  const result2 = await provisionClerk({
@@ -7792,7 +7910,7 @@ function registerStorefrontDeployTools(server2) {
7792
7910
  cli: clerkCliDeps()
7793
7911
  });
7794
7912
  if (!result2.ok || !result2.credentials) {
7795
- return ok4({
7913
+ return ok3({
7796
7914
  provisioned: false,
7797
7915
  tier: result2.tier,
7798
7916
  reason: result2.reason,
@@ -7808,7 +7926,7 @@ function registerStorefrontDeployTools(server2) {
7808
7926
  allowedOrigins,
7809
7927
  redirectUrls
7810
7928
  });
7811
- return ok4({
7929
+ return ok3({
7812
7930
  provisioned: true,
7813
7931
  tier: result2.tier,
7814
7932
  application_id: result2.credentials.applicationId ?? null,
@@ -10415,7 +10533,7 @@ function registerRateLimitStatusTool(server2, ctx) {
10415
10533
 
10416
10534
  // src/tools-depletion-events.ts
10417
10535
  import { z as z14 } from "zod";
10418
- function ok5(payload) {
10536
+ function ok4(payload) {
10419
10537
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
10420
10538
  }
10421
10539
  function err5(message) {
@@ -10475,7 +10593,7 @@ function registerDepletionEventsTool(server2, ctx) {
10475
10593
  if (subscriberId) {
10476
10594
  const raw = await kv.get(`bundle-depleted:${subscriberId}`, "text");
10477
10595
  if (!raw) {
10478
- return ok5({
10596
+ return ok4({
10479
10597
  subscriber_id: subscriberId,
10480
10598
  depleted: false,
10481
10599
  event: null,
@@ -10486,7 +10604,7 @@ function registerDepletionEventsTool(server2, ctx) {
10486
10604
  try {
10487
10605
  event = JSON.parse(raw);
10488
10606
  } catch {
10489
- return ok5({
10607
+ return ok4({
10490
10608
  subscriber_id: subscriberId,
10491
10609
  depleted: false,
10492
10610
  event: null,
@@ -10494,7 +10612,7 @@ function registerDepletionEventsTool(server2, ctx) {
10494
10612
  });
10495
10613
  }
10496
10614
  if (!await ownsEvent(ctx, scopeToken, event.iccid)) {
10497
- return ok5({
10615
+ return ok4({
10498
10616
  subscriber_id: subscriberId,
10499
10617
  depleted: false,
10500
10618
  event: null,
@@ -10502,14 +10620,14 @@ function registerDepletionEventsTool(server2, ctx) {
10502
10620
  });
10503
10621
  }
10504
10622
  if (since && event.depletedAt < since) {
10505
- return ok5({
10623
+ return ok4({
10506
10624
  subscriber_id: subscriberId,
10507
10625
  depleted: false,
10508
10626
  event: null,
10509
10627
  note: `No bundle depletion after ${since}.`
10510
10628
  });
10511
10629
  }
10512
- return ok5({
10630
+ return ok4({
10513
10631
  subscriber_id: subscriberId,
10514
10632
  depleted: true,
10515
10633
  event: {
@@ -10523,7 +10641,7 @@ function registerDepletionEventsTool(server2, ctx) {
10523
10641
  }
10524
10642
  const listResult = await kv.list({ prefix: "bundle-depleted:", limit: 20 });
10525
10643
  if (listResult.keys.length === 0) {
10526
- return ok5({
10644
+ return ok4({
10527
10645
  depletions: [],
10528
10646
  total: 0,
10529
10647
  note: "No bundle depletion events in the last 7 days."
@@ -10550,7 +10668,7 @@ function registerDepletionEventsTool(server2, ctx) {
10550
10668
  })
10551
10669
  );
10552
10670
  events.sort((a, b) => b.depleted_at.localeCompare(a.depleted_at));
10553
- return ok5({
10671
+ return ok4({
10554
10672
  depletions: events,
10555
10673
  total: events.length,
10556
10674
  list_truncated: !listResult.list_complete
@@ -11926,7 +12044,7 @@ function resolvePortalBaseUrl(env) {
11926
12044
  function getGreenzoneKv(env) {
11927
12045
  return env.GREENZONE_STATE_KV ?? null;
11928
12046
  }
11929
- function ok6(payload) {
12047
+ function ok5(payload) {
11930
12048
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
11931
12049
  }
11932
12050
  function err6(message) {
@@ -11962,7 +12080,7 @@ function registerGreenzoneTools(server2, ctx) {
11962
12080
  ...ip ? buildAddIpSteps(portalBaseUrl, ip) : []
11963
12081
  ];
11964
12082
  if (dry_run) {
11965
- return ok6({
12083
+ return ok5({
11966
12084
  dry_run: true,
11967
12085
  message: "Would execute the following Kapture steps against the OCS portal:",
11968
12086
  kapture_steps: steps,
@@ -11985,7 +12103,7 @@ function registerGreenzoneTools(server2, ctx) {
11985
12103
  } else {
11986
12104
  kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
11987
12105
  }
11988
- return ok6({
12106
+ return ok5({
11989
12107
  requires_kapture: true,
11990
12108
  message: "Execute the Kapture steps below to apply the change in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
11991
12109
  kapture_steps: steps,
@@ -12023,7 +12141,7 @@ function registerGreenzoneTools(server2, ctx) {
12023
12141
  ...ip ? buildRemoveIpSteps(portalBaseUrl, ip) : []
12024
12142
  ];
12025
12143
  if (dry_run) {
12026
- return ok6({
12144
+ return ok5({
12027
12145
  dry_run: true,
12028
12146
  message: "Would execute the following Kapture steps to remove from OCS portal:",
12029
12147
  kapture_steps: steps,
@@ -12046,7 +12164,7 @@ function registerGreenzoneTools(server2, ctx) {
12046
12164
  } else {
12047
12165
  kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
12048
12166
  }
12049
- return ok6({
12167
+ return ok5({
12050
12168
  requires_kapture: true,
12051
12169
  message: "Execute the Kapture steps below to apply the removal in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
12052
12170
  kapture_steps: steps,
@@ -12077,7 +12195,7 @@ function registerGreenzoneTools(server2, ctx) {
12077
12195
  const { from_portal } = args;
12078
12196
  if (from_portal) {
12079
12197
  const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
12080
- return ok6({
12198
+ return ok5({
12081
12199
  requires_kapture: true,
12082
12200
  message: "Execute the Kapture steps below to read live Greenzone whitelist state from the OCS portal.",
12083
12201
  kapture_steps: buildListSteps(portalBaseUrl),
@@ -12086,7 +12204,7 @@ function registerGreenzoneTools(server2, ctx) {
12086
12204
  }
12087
12205
  const kv = getGreenzoneKv(ctx.env);
12088
12206
  if (!kv) {
12089
- return ok6({
12207
+ return ok5({
12090
12208
  source: "kv_cache",
12091
12209
  hosts: [],
12092
12210
  ips: [],
@@ -12098,7 +12216,7 @@ function registerGreenzoneTools(server2, ctx) {
12098
12216
  }
12099
12217
  try {
12100
12218
  const state = await readState(kv);
12101
- return ok6({
12219
+ return ok5({
12102
12220
  source: "kv_cache",
12103
12221
  hosts: state.hosts,
12104
12222
  ips: state.ips,
@@ -12229,8 +12347,12 @@ registerStripeConnectTools(server, { env: stdioEnv, props });
12229
12347
  registerWalletTools(server, { env: stdioEnv, props });
12230
12348
  registerGreenzoneTools(server, toolCtx);
12231
12349
  registerUiAgentGenericTools(server, toolCtx);
12232
- registerStorefrontLogoTools(server, stdioEnv);
12233
- registerStorefrontDeployTools(server);
12350
+ registerStorefrontLogoTools(
12351
+ server,
12352
+ stdioEnv,
12353
+ props
12354
+ );
12355
+ registerStorefrontDeployTools(server, props);
12234
12356
  registerAllPrompts(server);
12235
12357
  var transport = new StdioServerTransport();
12236
12358
  await server.connect(transport);