@carrierllc/mcp 0.3.2 → 0.4.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/index.js CHANGED
@@ -1,16 +1,39 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CARRIER_VERSION,
4
+ TARGET_IDS,
4
5
  acquireEndpointSlot,
6
+ buildSite,
7
+ clerkCliDeps,
8
+ configureClerkInstance,
9
+ deploySite,
10
+ esimStatusPerAccountParams,
5
11
  extractEsimStatusCounts,
12
+ fleetScreen,
13
+ formatProbes,
6
14
  generateStorefrontLogo,
7
15
  getLimitForEndpoint,
8
16
  getRateLimitWindowCounts,
17
+ imsiFromSubscriberRecord,
18
+ isTargetId,
19
+ loadStorefrontBrand,
9
20
  locationParams,
21
+ mergeEnvLocal,
10
22
  networkEventsOverPeriodParams,
23
+ normalizePackageTemplate,
24
+ normalizePackageTemplateChanges,
25
+ probeAll,
26
+ provisionClerk,
27
+ rankTargets,
28
+ recurringPackageParams,
29
+ renderHtml,
30
+ repairPlanFor,
31
+ storefrontClerkUrls,
11
32
  subscriberIdParams,
12
- usageOverPeriodParams
13
- } from "./chunk-IYCGWBQU.js";
33
+ usageOverPeriodParams,
34
+ verifyStorefront
35
+ } from "./chunk-6OI56RSR.js";
36
+ import "./chunk-SHKKVIIA.js";
14
37
 
15
38
  // src/index.ts
16
39
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -158,6 +181,212 @@ function firstDayNextMonth() {
158
181
  return new Date(Date.UTC(y, m, 1)).toISOString();
159
182
  }
160
183
 
184
+ // src/bounded-results.ts
185
+ var TARIFF_MAX_RULES = 3e3;
186
+ var TARIFF_VERBOSE_MAX_RULES = 250;
187
+ var ZONES_MAX_ZONES = 100;
188
+ var ZONES_MAX_OPERATORS = 2e3;
189
+ var ZONES_VERBOSE_MAX_OPERATORS = 400;
190
+ function extractTariffRules(raw) {
191
+ if (Array.isArray(raw)) return raw;
192
+ if (raw && typeof raw === "object") {
193
+ const obj = raw;
194
+ for (const key of ["rule", "listTariffRule", "rules"]) {
195
+ const v = obj[key];
196
+ if (Array.isArray(v)) return v;
197
+ if (v && typeof v === "object" && Array.isArray(v.rule)) {
198
+ return v.rule;
199
+ }
200
+ }
201
+ }
202
+ return [];
203
+ }
204
+ function extractZones(raw) {
205
+ if (Array.isArray(raw)) return raw;
206
+ if (raw && typeof raw === "object") {
207
+ const obj = raw;
208
+ for (const key of ["listDetailedLocationZone", "locationZone", "zone", "zones"]) {
209
+ const v = obj[key];
210
+ if (Array.isArray(v)) return v;
211
+ }
212
+ }
213
+ return [];
214
+ }
215
+ function matchesCountry(op, country) {
216
+ const want = country.trim().toLowerCase();
217
+ if (!want) return true;
218
+ const iso = (op?.countryIso2 ?? "").trim().toLowerCase();
219
+ const name = (op?.countryName ?? "").trim().toLowerCase();
220
+ return iso === want || name === want;
221
+ }
222
+ function matchesTrafficType(rule, trafficType) {
223
+ const gt0 = (v) => typeof v === "number" && v > 0;
224
+ switch (trafficType) {
225
+ case "data":
226
+ return gt0(rule.datarate);
227
+ case "voice":
228
+ return gt0(rule.mocallrate) || gt0(rule.mtcallrate);
229
+ case "sms":
230
+ return gt0(rule.mosmsrate) || gt0(rule.mtsmsrate);
231
+ }
232
+ }
233
+ var RATE_COLUMNS = {
234
+ data: [["datarate", "data"]],
235
+ voice: [
236
+ ["mocallrate", "moCall"],
237
+ ["mtcallrate", "mtCall"]
238
+ ],
239
+ sms: [
240
+ ["mosmsrate", "moSms"],
241
+ ["mtsmsrate", "mtSms"]
242
+ ]
243
+ };
244
+ var ALL_RATE_COLUMNS = [
245
+ ["datarate", "data"],
246
+ ["mocallrate", "moCall"],
247
+ ["mtcallrate", "mtCall"],
248
+ ["mosmsrate", "moSms"],
249
+ ["mtsmsrate", "mtSms"]
250
+ ];
251
+ var FIELD_LEGEND = {
252
+ iso: "ISO-3166 alpha-2 country code",
253
+ op: "operator / network name",
254
+ data: "data rate per MB",
255
+ moCall: "mobile-originated call rate per minute",
256
+ mtCall: "mobile-terminated call rate per minute",
257
+ moSms: "mobile-originated SMS rate",
258
+ mtSms: "mobile-terminated SMS rate",
259
+ active: "present only when the rule is active; absent means inactive"
260
+ };
261
+ function truncationNote(kind, returned, matched) {
262
+ if (kind === "tariff") {
263
+ return `TRUNCATED: showing ${returned.toLocaleString("en-US")} of ${matched.toLocaleString("en-US")} matching tariff rules (cap ${TARIFF_MAX_RULES.toLocaleString("en-US")} rows, imposed so the result fits the MCP transport \u2014 see issue #663). Narrow the request to see the rest: pass 'country' (ISO2 such as "nl" or a full name such as "Netherlands") and/or 'trafficType' ("data" | "voice" | "sms"). These filters are applied server-side to the complete OCS table, so a filtered call sees every matching rule, not just the rules in this sample.`;
264
+ }
265
+ return `TRUNCATED: showing ${returned.toLocaleString("en-US")} of ${matched.toLocaleString("en-US")} operator entries across the returned zones (cap ${ZONES_MAX_OPERATORS.toLocaleString("en-US")} entries, imposed so the result fits the MCP transport \u2014 see issue #663). Pass 'locationZoneId' to retrieve a single zone in full.`;
266
+ }
267
+ function boundTariffResult(raw, opts = {}) {
268
+ const all = extractTariffRules(raw);
269
+ const { country, trafficType, verbose = false, resellerId } = opts;
270
+ const matched = all.filter(
271
+ (r) => (country === void 0 || matchesCountry(r.operator, country)) && (trafficType === void 0 || matchesTrafficType(r, trafficType))
272
+ );
273
+ const cap = opts.maxRules ?? (verbose ? TARIFF_VERBOSE_MAX_RULES : TARIFF_MAX_RULES);
274
+ const kept = matched.slice(0, cap);
275
+ const truncated = kept.length < matched.length;
276
+ const result2 = {
277
+ resellerId,
278
+ totalRules: all.length,
279
+ matchedRules: matched.length,
280
+ returnedRules: kept.length,
281
+ truncated,
282
+ projection: verbose ? "verbose" : "compact",
283
+ rules: []
284
+ };
285
+ if (country !== void 0 || trafficType !== void 0) {
286
+ result2.filters = {};
287
+ if (country !== void 0) result2.filters.country = country;
288
+ if (trafficType !== void 0) result2.filters.trafficType = trafficType;
289
+ }
290
+ if (verbose) {
291
+ result2.rules = kept;
292
+ if (truncated) result2.note = truncationNote("tariff", kept.length, matched.length);
293
+ return result2;
294
+ }
295
+ const currencies = new Set(
296
+ kept.map((r) => r.currency?.currencycode).filter((c) => typeof c === "string")
297
+ );
298
+ const hoistCurrency = currencies.size === 1;
299
+ if (hoistCurrency) result2.currency = [...currencies][0];
300
+ const columns = trafficType ? RATE_COLUMNS[trafficType] : ALL_RATE_COLUMNS;
301
+ result2.rules = kept.map((r) => {
302
+ const op = r.operator ?? {};
303
+ const row = {};
304
+ if (op.countryIso2 !== void 0) row.iso = op.countryIso2;
305
+ if (op.operatorName !== void 0) row.op = op.operatorName;
306
+ for (const [src, dst] of columns) {
307
+ const v = r[src];
308
+ if (typeof v === "number" && v !== 0) row[dst] = v;
309
+ }
310
+ if (!hoistCurrency && typeof r.currency?.currencycode === "string") {
311
+ row.cur = r.currency.currencycode;
312
+ }
313
+ if (r.active === true) row.active = true;
314
+ return row;
315
+ });
316
+ const legend = { ...FIELD_LEGEND };
317
+ if (trafficType) {
318
+ for (const key of ["data", "moCall", "mtCall", "moSms", "mtSms"]) {
319
+ if (!columns.some(([, dst]) => dst === key)) delete legend[key];
320
+ }
321
+ }
322
+ if (!hoistCurrency) legend.cur = "ISO-4217 currency code";
323
+ result2.fields = legend;
324
+ const notes = [];
325
+ if (truncated) notes.push(truncationNote("tariff", kept.length, matched.length));
326
+ notes.push(
327
+ `Projected result: nested operator detail (mccMncs, tadigs, continent, countryCode, utcOffset), the sponsor object, plan ids (roamingplanruleid, roamingplanid, sponsoridx) and the validity/discount flags (startdate, enddate, isDiscounted, hidden, dailyCap) are omitted. Pass verbose=true for the full untouched rule shape (capped at ${TARIFF_VERBOSE_MAX_RULES} rows \u2014 use 'country'/'trafficType' with it).`
328
+ );
329
+ result2.note = notes.join(" ");
330
+ return result2;
331
+ }
332
+ var ZONE_FIELD_LEGEND = {
333
+ zoneId: "location zone id \u2014 pass to package-template tools",
334
+ zoneName: "human-readable zone name",
335
+ operatorCount: "total operator entries in this zone upstream",
336
+ operators: "projected operator entries: net (networkId), iso (country ISO2), op (operator name)"
337
+ };
338
+ function boundZonesResult(raw, opts = {}) {
339
+ const all = extractZones(raw);
340
+ const { verbose = false, resellerId } = opts;
341
+ const maxZones = opts.maxZones ?? ZONES_MAX_ZONES;
342
+ const maxOperators = opts.maxOperators ?? (verbose ? ZONES_VERBOSE_MAX_OPERATORS : ZONES_MAX_OPERATORS);
343
+ const totalOperators = all.reduce((n, z23) => n + (z23.operators?.length ?? 0), 0);
344
+ const keptZones = all.slice(0, maxZones);
345
+ let budget = maxOperators;
346
+ let returnedOperators = 0;
347
+ const zones = keptZones.map((z23) => {
348
+ const ops = z23.operators ?? [];
349
+ const take = Math.max(0, Math.min(ops.length, budget));
350
+ budget -= take;
351
+ returnedOperators += take;
352
+ const sliced = ops.slice(0, take);
353
+ if (verbose) {
354
+ return { ...z23, operators: sliced, operatorCount: ops.length };
355
+ }
356
+ return {
357
+ zoneId: z23.zoneId,
358
+ zoneName: z23.zoneName,
359
+ operatorCount: ops.length,
360
+ operators: sliced.map((o) => ({
361
+ net: o.networkId,
362
+ iso: o.countryIso2,
363
+ op: o.operatorName
364
+ }))
365
+ };
366
+ });
367
+ const truncated = keptZones.length < all.length || returnedOperators < totalOperators;
368
+ const result2 = {
369
+ resellerId,
370
+ totalZones: all.length,
371
+ returnedZones: keptZones.length,
372
+ totalOperators,
373
+ returnedOperators,
374
+ truncated,
375
+ projection: verbose ? "verbose" : "compact",
376
+ zones
377
+ };
378
+ const notes = [];
379
+ if (truncated) notes.push(truncationNote("zones", returnedOperators, totalOperators));
380
+ if (!verbose) {
381
+ result2.fields = ZONE_FIELD_LEGEND;
382
+ notes.push(
383
+ `Projected result: per-operator mccMncs, tadigs, continent, countryCode, countryName and utcOffset are omitted, as is the repeated reseller object. Pass verbose=true for the full untouched zone shape (capped at ${ZONES_VERBOSE_MAX_OPERATORS} operator entries).`
384
+ );
385
+ }
386
+ if (notes.length) result2.note = notes.join(" ");
387
+ return result2;
388
+ }
389
+
161
390
  // src/tools.ts
162
391
  var TOOL_SCOPES = {
163
392
  // --- read ---
@@ -203,7 +432,12 @@ var TOOL_SCOPES = {
203
432
  modify_account_balance: "admin",
204
433
  // corrected per ocs-spec PR #6
205
434
  change_sim_status: "admin",
206
- delete_subscriber_package: "admin",
435
+ // Removing ONE package from ONE subscriber is a write, not an admin
436
+ // action. Admin-gating it made package lifecycle write-once for any
437
+ // operator on a read/write token: they could assign a package but had no
438
+ // way to remove one, including one assigned in error. clean_all_packages
439
+ // stays admin — wiping every package is a different blast radius.
440
+ delete_subscriber_package: "write",
207
441
  clean_all_packages: "admin",
208
442
  modify_template_core: "admin",
209
443
  modify_template_recurring: "admin",
@@ -310,9 +544,9 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
310
544
  try {
311
545
  const token2 = await ctx.getUserToken(ctx.props.sub);
312
546
  result2 = await handler(args, token2);
313
- } catch (err6) {
547
+ } catch (err7) {
314
548
  try {
315
- Sentry.captureException(err6, {
549
+ Sentry.captureException(err7, {
316
550
  tags: {
317
551
  tool: toolName,
318
552
  feature: "mcp",
@@ -321,8 +555,8 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
321
555
  });
322
556
  } catch {
323
557
  }
324
- const message = err6 instanceof Error ? err6.message : String(err6);
325
- const ocsCode = err6 instanceof OcsApiError ? err6.code : void 0;
558
+ const message = err7 instanceof Error ? err7.message : String(err7);
559
+ const ocsCode = err7 instanceof OcsApiError ? err7.code : void 0;
326
560
  ctx.audit({
327
561
  tool_name: toolName,
328
562
  ocs_method: ocsMethod,
@@ -464,12 +698,9 @@ function registerAllTools(server2, ctx) {
464
698
  TOOL_SCOPES["esim_status_per_account"],
465
699
  ctx,
466
700
  async ({ accountId, resellerId }, token2) => {
467
- const params = {};
468
- if (accountId !== void 0) {
469
- params.accountId = accountId;
470
- } else {
471
- params.resellerId = resellerId ?? await getDefaultResellerId(ctx.env, token2);
472
- }
701
+ const params = esimStatusPerAccountParams(
702
+ accountId !== void 0 ? { accountId } : { resellerId: resellerId ?? await getDefaultResellerId(ctx.env, token2) }
703
+ );
473
704
  return ocsCall(ctx.env, token2, "esimStatusPerAccount", params);
474
705
  }
475
706
  )
@@ -869,7 +1100,7 @@ function registerAllTools(server2, ctx) {
869
1100
  async ({ iccid, bitrate, bitrate_string }, token2) => {
870
1101
  const cache = /* @__PURE__ */ new Map();
871
1102
  const sub = await resolveSubscriberByIccid(ctx.env, token2, iccid, cache);
872
- const imsi = sub.imsi;
1103
+ const imsi = imsiFromSubscriberRecord(sub);
873
1104
  if (typeof imsi !== "string" || imsi.length === 0) {
874
1105
  return {
875
1106
  isError: true,
@@ -897,7 +1128,7 @@ function registerAllTools(server2, ctx) {
897
1128
  async ({ iccid }, token2) => {
898
1129
  const cache = /* @__PURE__ */ new Map();
899
1130
  const sub = await resolveSubscriberByIccid(ctx.env, token2, iccid, cache);
900
- const imsi = sub.imsi;
1131
+ const imsi = imsiFromSubscriberRecord(sub);
901
1132
  if (typeof imsi !== "string" || imsi.length === 0) {
902
1133
  return {
903
1134
  isError: true,
@@ -987,21 +1218,10 @@ function registerAllTools(server2, ctx) {
987
1218
  TOOL_SCOPES["assign_recurring_package"],
988
1219
  ctx,
989
1220
  async ({ iccid, packageTemplateId, activation_at_first_use, start_time_utc }, token2) => {
990
- const cache = /* @__PURE__ */ new Map();
991
- const sub = await resolveSubscriberByIccid(ctx.env, token2, iccid, cache);
992
- const subscriberId = sub.id ?? sub.subscriberId;
993
- if (subscriberId === void 0) {
994
- return {
995
- isError: true,
996
- content: [{ type: "text", text: `Error: Could not resolve subscriberId for ICCID ${iccid}` }]
997
- };
998
- }
999
- const params = {
1000
- subscriber: Number(subscriberId),
1001
- packageTemplateId
1002
- };
1003
- if (activation_at_first_use === true) params.activationAtFirstUse = true;
1004
- if (start_time_utc !== void 0) params.startTimeUTC = start_time_utc;
1221
+ const params = recurringPackageParams(iccid, packageTemplateId, {
1222
+ activationAtFirstUse: activation_at_first_use,
1223
+ startTimeUTC: start_time_utc
1224
+ });
1005
1225
  return ocsCall(ctx.env, token2, "affectRecurringPackageToSubscriber", params);
1006
1226
  }
1007
1227
  )
@@ -1124,8 +1344,8 @@ function registerAllTools(server2, ctx) {
1124
1344
  title: "Delete Subscriber Package",
1125
1345
  description: "Use this to permanently remove a single prepaid package from a subscriber. This is irreversible \u2014 the package record and any unused allowance are deleted. Always call `list_subscriber_packages` first to confirm the correct packageId and snapshot the current state. Use `dry_run=true` on the first call. Params: `iccid` (subscriber identifier), `packageId` (integer from `list_subscriber_packages`). Returns: OCS confirmation of deletion. Do NOT use this to remove ALL packages at once \u2014 use `clean_all_packages` for that (requires separate confirm). Do NOT use this to pause a package \u2014 use `modify_package_status` to deactivate it instead.",
1126
1346
  inputSchema: {
1127
- iccid: z.string().describe("The subscriber ICCID"),
1128
- packageId: z.number().describe("The package ID to delete"),
1347
+ iccid: z.string().describe("The subscriber ICCID (for audit logging; OCS itself does not take it \u2014 see below)"),
1348
+ packageId: z.number().describe("The package ID to delete (OCS's sole parameter \u2014 a bare integer)"),
1129
1349
  ...DRY_RUN_FIELD
1130
1350
  },
1131
1351
  annotations: { destructiveHint: true }
@@ -1135,7 +1355,11 @@ function registerAllTools(server2, ctx) {
1135
1355
  "deleteSubscriberPackage",
1136
1356
  TOOL_SCOPES["delete_subscriber_package"],
1137
1357
  ctx,
1138
- async ({ iccid, packageId }, token2) => ocsCall(ctx.env, token2, "deleteSubscriberPackage", { iccid, packageId })
1358
+ // OCS deleteSubscriberPackage takes a BARE INTEGER package id
1359
+ // (endpoints-full.md 3.8, "Numeric value = package ID") — not an
1360
+ // { iccid, packageId } object. iccid is kept in the tool's input schema
1361
+ // for audit/description purposes only; it is not an OCS parameter.
1362
+ async ({ packageId }, token2) => ocsCall(ctx.env, token2, "deleteSubscriberPackage", packageId)
1139
1363
  )
1140
1364
  );
1141
1365
  server2.registerTool(
@@ -1183,7 +1407,7 @@ function registerAllTools(server2, ctx) {
1183
1407
  "create_package_template",
1184
1408
  {
1185
1409
  title: "Create Package Template",
1186
- description: "Use this to create a new prepaid package template in the product catalog. Templates define allowances, pricing, location zones, validity, and throttling thresholds that are reused each time the template is assigned to a subscriber. Params: `template` (full template configuration as a JSON string \u2014 fields include `name`, `dataLimit` in bytes, `price`, `validityDays`, `locationZoneId`, `recurring`, `throttlingActive`). Returns: created template record with the new `templateId`. Do NOT use this to modify an existing template \u2014 use `modify_template_core`. After creation, call `list_package_templates` to confirm the template is visible.",
1410
+ description: 'Use this to create a new prepaid package template in the product catalog. Templates define allowances, pricing, location zones, validity, and throttling thresholds that are reused each time the template is assigned to a subscriber. Params: `template` (full template configuration as a JSON string \u2014 fields include `name`, `dataLimit` in bytes, `price`, `validityDays`, `locationZoneId`, `recurring`, `throttlingActive`). NEVER express an unlimited plan as a zero data allowance: OCS reads 0 as a literal zero, and because `throttlingThreshold1Perc` is a percentage OF THE BUNDLE, a zero bundle leaves the throttle trigger uncomputable and every storefront renders "0 MB". Describe the plan as it is sold and the OCS fields are derived for you: `unlimited: true`, `throttleAfterMb` (the full-speed window, e.g. 500 for "500 MB/day"), `throttleKbps` (the speed AFTER that window) and `perioddays` (1 for a daily bucket). The bundle ceiling is sized so the window is a whole trigger percentage and the bundle cannot be drained even at the throttled rate for the whole period, because a plan sold as unlimited must not run out. `throttleKbps` is a SPEED and must be an OCS bitrate (128, 256, 384, 512, 1024, 3072, 5120, 7680, 10240, 20480, 51200, 102400); passing megabytes here is rejected. Returns: created template record with the new `templateId`. Do NOT use this to modify an existing template \u2014 use `modify_template_core`. After creation, call `list_package_templates` to confirm the template is visible.',
1187
1411
  inputSchema: {
1188
1412
  template: z.string().describe("Full template configuration as JSON string"),
1189
1413
  ...DRY_RUN_FIELD
@@ -1199,7 +1423,7 @@ function registerAllTools(server2, ctx) {
1199
1423
  ctx.env,
1200
1424
  token2,
1201
1425
  "createPrepaidPackageTemplate",
1202
- JSON.parse(template)
1426
+ normalizePackageTemplate(JSON.parse(template))
1203
1427
  )
1204
1428
  )
1205
1429
  );
@@ -1222,7 +1446,7 @@ function registerAllTools(server2, ctx) {
1222
1446
  ctx,
1223
1447
  async ({ templateId, changes }, token2) => ocsCall(ctx.env, token2, "modifyPPTCore", {
1224
1448
  templateId,
1225
- ...JSON.parse(changes)
1449
+ ...normalizePackageTemplateChanges(JSON.parse(changes))
1226
1450
  })
1227
1451
  )
1228
1452
  );
@@ -1276,9 +1500,9 @@ function registerAllTools(server2, ctx) {
1276
1500
  "list_location_zones",
1277
1501
  {
1278
1502
  title: "List Location Zone Elements",
1279
- description: "Use this to list countries and networks within a specific location zone. WARNING: this method has a known Jackson deserialization bug in the upstream OCS API that may return malformed responses. Prefer `list_detailed_location_zones` for reliable results. Params: `locationZoneId` (integer, optional \u2014 filter to a specific zone). Returns: array of zone element records with country and operator entries. Do NOT use this for reliable zone data \u2014 use `list_detailed_location_zones` instead. Do NOT use this to create zones \u2014 use `create_location_zone`.",
1503
+ description: "Use this to list countries and networks within a specific location zone. WARNING: this method has a known Jackson deserialization bug in the upstream OCS API that may return malformed responses. Prefer `list_detailed_location_zones` for reliable results. Params: `locationZoneId` (integer, REQUIRED \u2014 OCS takes a bare zone id, not an object; there is no unfiltered call). Returns: array of zone element records with country and operator entries. Do NOT use this for reliable zone data \u2014 use `list_detailed_location_zones` instead. Do NOT use this to create zones \u2014 use `create_location_zone`.",
1280
1504
  inputSchema: {
1281
- locationZoneId: z.number().optional().describe("Filter by zone ID")
1505
+ locationZoneId: z.number().describe("Zone ID (required \u2014 OCS has no unfiltered call for this method)")
1282
1506
  },
1283
1507
  annotations: { readOnlyHint: true }
1284
1508
  },
@@ -1288,9 +1512,7 @@ function registerAllTools(server2, ctx) {
1288
1512
  TOOL_SCOPES["list_location_zones"],
1289
1513
  ctx,
1290
1514
  async ({ locationZoneId }, token2) => {
1291
- const params = {};
1292
- if (locationZoneId !== void 0) params.locationZoneId = locationZoneId;
1293
- return ocsCall(ctx.env, token2, "listLocationZoneElement", params);
1515
+ return ocsCall(ctx.env, token2, "listLocationZoneElement", locationZoneId);
1294
1516
  }
1295
1517
  )
1296
1518
  );
@@ -1298,9 +1520,12 @@ function registerAllTools(server2, ctx) {
1298
1520
  "list_detailed_location_zones",
1299
1521
  {
1300
1522
  title: "List Detailed Location Zones",
1301
- description: "Use this as the preferred way to list location zones with full detail: included countries, operator networks, zone IDs, and names. This is the working alternative to `list_location_zones` which has a known upstream deserialization bug. Use `locationZoneId` values from this response when creating or editing package templates. Params: `resellerId` (integer, optional \u2014 omit to use the token owner's reseller). Returns: array of zone objects each containing `locationZoneId`, `name`, `countries`, and `operators`. Do NOT use `list_location_zones` when you need reliable data \u2014 always use this tool instead.",
1523
+ description: `Use this as the preferred way to list location zones with full detail: included countries, operator networks, zone IDs, and names. This is the working alternative to \`list_location_zones\` which has a known upstream deserialization bug. Use \`locationZoneId\` values from this response when creating or editing package templates. Params: \`resellerId\` (integer, optional \u2014 omit to use the token owner's reseller); \`verbose\` (optional boolean, default false \u2014 returns the full untouched zone shape with a smaller operator-entry budget). Returns BY DEFAULT a projected result: \`totalZones\`, \`returnedZones\`, \`totalOperators\`, \`returnedOperators\`, \`truncated\`, a \`fields\` legend, and \`zones[]\` each with \`zoneId\`, \`zoneName\`, \`operatorCount\` and short-keyed \`operators[]\` (\`net\`, \`iso\`, \`op\`). Per-operator mccMncs, tadigs, continent, countryCode, countryName, utcOffset and the repeated reseller object are dropped unless \`verbose\` is true. Operator entries are capped at ${ZONES_MAX_OPERATORS} so the result fits the MCP transport (issue #663); when the cap bites, \`truncated\` is true and \`note\` says so \u2014 never silently. Do NOT use \`list_location_zones\` when you need reliable data \u2014 always use this tool instead.`,
1302
1524
  inputSchema: {
1303
- resellerId: z.number().optional().describe("Reseller ID (omit to use token owner's reseller)")
1525
+ resellerId: z.number().optional().describe("Reseller ID (omit to use token owner's reseller)"),
1526
+ verbose: z.boolean().optional().describe(
1527
+ "Return the full untouched OCS zone shape instead of the projected zones (smaller operator budget)"
1528
+ )
1304
1529
  },
1305
1530
  annotations: { readOnlyHint: true }
1306
1531
  },
@@ -1309,9 +1534,16 @@ function registerAllTools(server2, ctx) {
1309
1534
  "listDetailedLocationZone",
1310
1535
  TOOL_SCOPES["list_detailed_location_zones"],
1311
1536
  ctx,
1312
- async ({ resellerId }, token2) => {
1537
+ async ({ resellerId, verbose }, token2) => {
1313
1538
  const id = resellerId ?? await getDefaultResellerId(ctx.env, token2);
1314
- return ocsCall(ctx.env, token2, "listDetailedLocationZone", id);
1539
+ const raw = await new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2).call(
1540
+ "listDetailedLocationZone",
1541
+ id
1542
+ );
1543
+ const bounded = boundZonesResult(raw, { resellerId: id, verbose });
1544
+ return {
1545
+ content: [{ type: "text", text: JSON.stringify(bounded) }]
1546
+ };
1315
1547
  }
1316
1548
  )
1317
1549
  );
@@ -1319,9 +1551,9 @@ function registerAllTools(server2, ctx) {
1319
1551
  "list_destination_prefixes",
1320
1552
  {
1321
1553
  title: "List Destination List Prefixes",
1322
- description: "Use this to list the phone number prefixes (country dialling codes) within a specific named destination list. Destination lists control which countries a subscriber may call on voice/SMS packages. You must already know the `destinationListId` to use this tool. Params: `destinationListId` (integer, optional \u2014 omit to list all known prefixes). Returns: array of prefix records with country code and E.164 prefix. Do NOT use this to discover the destination list catalog \u2014 use `list_destination_lists` for that. For data-only eSIM products without MOC voice, destination lists are irrelevant.",
1554
+ description: "Use this to list the phone number prefixes (country dialling codes) within a specific named destination list. Destination lists control which countries a subscriber may call on voice/SMS packages. You must already know the `destinationListId` to use this tool. Params: `destinationListId` (integer, REQUIRED \u2014 OCS takes a bare list id, not an object; there is no unfiltered call). Returns: array of prefix records with country code and E.164 prefix. Do NOT use this to discover the destination list catalog \u2014 use `list_destination_lists` for that. For data-only eSIM products without MOC voice, destination lists are irrelevant.",
1323
1555
  inputSchema: {
1324
- destinationListId: z.number().optional().describe("Filter by destination list ID")
1556
+ destinationListId: z.number().describe("Destination list ID (required \u2014 OCS has no unfiltered call for this method)")
1325
1557
  },
1326
1558
  annotations: { readOnlyHint: true }
1327
1559
  },
@@ -1331,10 +1563,7 @@ function registerAllTools(server2, ctx) {
1331
1563
  TOOL_SCOPES["list_destination_prefixes"],
1332
1564
  ctx,
1333
1565
  async ({ destinationListId }, token2) => {
1334
- const params = {};
1335
- if (destinationListId !== void 0)
1336
- params.destinationListId = destinationListId;
1337
- return ocsCall(ctx.env, token2, "listDestinationListPrefix", params);
1566
+ return ocsCall(ctx.env, token2, "listDestinationListPrefix", destinationListId);
1338
1567
  }
1339
1568
  )
1340
1569
  );
@@ -1424,9 +1653,18 @@ function registerAllTools(server2, ctx) {
1424
1653
  "get_tariff",
1425
1654
  {
1426
1655
  title: "Get Customer Tariff",
1427
- description: "Use this to retrieve the complete tariff table for a reseller: per-country, per-traffic-type (data/voice/SMS) wholesale 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). Returns: array of tariff rules, each with `country`, `trafficType`, `rate`, and `currency`. 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.",
1656
+ 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.`,
1428
1657
  inputSchema: {
1429
- resellerId: z.number().optional().describe("Reseller ID (omit to use token owner's reseller)")
1658
+ resellerId: z.number().optional().describe("Reseller ID (omit to use token owner's reseller)"),
1659
+ country: z.string().optional().describe(
1660
+ 'Filter by country \u2014 ISO-3166 alpha-2 ("nl") or full name ("Netherlands"), case-insensitive'
1661
+ ),
1662
+ trafficType: z.enum(["data", "voice", "sms"]).optional().describe(
1663
+ 'Filter to rules whose rate for this traffic type is greater than zero ("data" | "voice" | "sms")'
1664
+ ),
1665
+ verbose: z.boolean().optional().describe(
1666
+ `Return the full untouched OCS rule shape instead of the projected rows (capped at ${TARIFF_VERBOSE_MAX_RULES} rows)`
1667
+ )
1430
1668
  },
1431
1669
  annotations: { readOnlyHint: true }
1432
1670
  },
@@ -1435,9 +1673,21 @@ function registerAllTools(server2, ctx) {
1435
1673
  "getCustomerTariff",
1436
1674
  TOOL_SCOPES["get_tariff"],
1437
1675
  ctx,
1438
- async ({ resellerId }, token2) => {
1676
+ async ({ resellerId, country, trafficType, verbose }, token2) => {
1439
1677
  const id = resellerId ?? await getDefaultResellerId(ctx.env, token2);
1440
- return ocsCall(ctx.env, token2, "getCustomerTariff", id);
1678
+ const raw = await new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2).call(
1679
+ "getCustomerTariff",
1680
+ id
1681
+ );
1682
+ const bounded = boundTariffResult(raw, {
1683
+ resellerId: id,
1684
+ country,
1685
+ trafficType,
1686
+ verbose
1687
+ });
1688
+ return {
1689
+ content: [{ type: "text", text: JSON.stringify(bounded) }]
1690
+ };
1441
1691
  }
1442
1692
  )
1443
1693
  );
@@ -1463,7 +1713,7 @@ function registerAllTools(server2, ctx) {
1463
1713
  async ({ iccid, msisdn, message, sender }, token2) => {
1464
1714
  const cache = /* @__PURE__ */ new Map();
1465
1715
  const sub = await resolveSubscriberByIccid(ctx.env, token2, iccid, cache);
1466
- const imsi = sub.imsi;
1716
+ const imsi = imsiFromSubscriberRecord(sub);
1467
1717
  if (typeof imsi !== "string" || imsi.length === 0) {
1468
1718
  return {
1469
1719
  isError: true,
@@ -3042,8 +3292,8 @@ async function safeCall(env, token2, method, params = {}) {
3042
3292
  const client = new OcsClient(env.CARRIER_OCS_BASE_URL, token2);
3043
3293
  const data = await client.call(method, params);
3044
3294
  return { data, error: null };
3045
- } catch (err6) {
3046
- return { data: null, error: err6 instanceof Error ? err6.message : String(err6) };
3295
+ } catch (err7) {
3296
+ return { data: null, error: err7 instanceof Error ? err7.message : String(err7) };
3047
3297
  }
3048
3298
  }
3049
3299
  async function fetchActiveSubscribers(env, token2, accountId, resellerId) {
@@ -3284,7 +3534,10 @@ High suspension rate (${totalSuspended} suspended vs ${totalActive} active)`);
3284
3534
  (a) => Number(a.balance ?? 0) < 10
3285
3535
  );
3286
3536
  const packageOnlyZero = accounts.filter(
3287
- (a) => Boolean(a.packageOnly) && Number(a.balance ?? 0) <= 0
3537
+ (a) => a.packageOnly === true && Number(a.balance ?? 0) === 0
3538
+ );
3539
+ const criticalZero = accounts.filter(
3540
+ (a) => a.packageOnly === false && Number(a.balance ?? 0) <= 0
3288
3541
  );
3289
3542
  sections.push(`
3290
3543
  ## Account Summary`);
@@ -3300,23 +3553,28 @@ High suspension rate (${totalSuspended} suspended vs ${totalActive} active)`);
3300
3553
  sections.push(`| ${a.name ?? a.accountId ?? "?"} | ${Number(a.balance ?? 0).toFixed(2)} | ${a.packageOnly ? "yes" : "no"} |`);
3301
3554
  }
3302
3555
  }
3303
- const critical = packageOnlyZero.length;
3304
- const warning = lowBalance.length - critical;
3305
- const healthy = accounts.length - lowBalance.length;
3556
+ const informational = packageOnlyZero.length;
3557
+ const critical = criticalZero.length;
3558
+ const warning = lowBalance.length - informational - critical;
3559
+ const healthy = accounts.length - informational - critical - Math.max(warning, 0);
3306
3560
  sections.push(`
3307
3561
  ## Fleet Health Verdict`);
3308
3562
  sections.push(`- Healthy: ${healthy}`);
3309
- sections.push(`- Warning (low balance, not package-only-zero): ${Math.max(warning, 0)}`);
3310
- sections.push(`- Critical (package-only and 0 balance): ${critical}`);
3563
+ sections.push(`- Informational (package-only at 0 balance): ${informational}`);
3564
+ sections.push(`- Warning (low balance): ${Math.max(warning, 0)}`);
3565
+ sections.push(`- Critical (non-package-only at 0 balance): ${critical}`);
3311
3566
  if (accounts.length === 0) {
3312
3567
  sections.push(`
3313
3568
  No accounts found under this reseller.`);
3314
3569
  } else if (critical > 0) {
3315
3570
  sections.push(`
3316
- Action: top up package-only accounts at 0 balance to keep packages assignable.`);
3571
+ Action: top up non-package-only accounts at 0 balance to restore rated traffic.`);
3317
3572
  } else if (lowBalance.length === 0) {
3318
3573
  sections.push(`
3319
3574
  All accounts healthy.`);
3575
+ } else if (informational > 0 && warning <= 0) {
3576
+ sections.push(`
3577
+ Package-only accounts at 0 balance require no action.`);
3320
3578
  }
3321
3579
  } else {
3322
3580
  sections.push(`
@@ -4925,10 +5183,10 @@ async function callBedrock(env, payload) {
4925
5183
  const body = await resp.text();
4926
5184
  if (resp.status === 429) {
4927
5185
  const retryAfter = resp.headers.get("retry-after");
4928
- const err6 = new Error(`Bedrock rate limit: ${body}`);
4929
- err6.isRateLimit = true;
4930
- err6.retryAfter = retryAfter;
4931
- throw err6;
5186
+ const err7 = new Error(`Bedrock rate limit: ${body}`);
5187
+ err7.isRateLimit = true;
5188
+ err7.retryAfter = retryAfter;
5189
+ throw err7;
4932
5190
  }
4933
5191
  throw new Error(`Bedrock invoke failed: ${resp.status} ${body}`);
4934
5192
  }
@@ -4966,9 +5224,9 @@ Pre-resolved context: ${JSON.stringify(definedContext)}` : "";
4966
5224
  AWS_REGION: env.AWS_REGION,
4967
5225
  BEDROCK_MODEL_ID: env.BEDROCK_MODEL_ID
4968
5226
  }, payload);
4969
- } catch (err6) {
4970
- if (err6 instanceof Error && err6.isRateLimit) {
4971
- const retryAfter = err6.retryAfter;
5227
+ } catch (err7) {
5228
+ if (err7 instanceof Error && err7.isRateLimit) {
5229
+ const retryAfter = err7.retryAfter;
4972
5230
  const parsedSeconds = retryAfter ? parseInt(retryAfter, 10) : 60;
4973
5231
  return {
4974
5232
  match: "rate_limited",
@@ -4976,7 +5234,7 @@ Pre-resolved context: ${JSON.stringify(definedContext)}` : "";
4976
5234
  suggestion: "Bedrock rate limit reached. Please retry after the indicated delay."
4977
5235
  };
4978
5236
  }
4979
- throw err6;
5237
+ throw err7;
4980
5238
  }
4981
5239
  const toolUseBlock = response.content.find(
4982
5240
  (block) => block.type === "tool_use"
@@ -5150,7 +5408,7 @@ function registerAllCarrierAskTools(server2, ctx) {
5150
5408
  let route;
5151
5409
  try {
5152
5410
  route = await _routeIntent(intent, context, ctx.env);
5153
- } catch (err6) {
5411
+ } catch (err7) {
5154
5412
  writeCarrierAskAudit(ctx.env, {
5155
5413
  intent_hash: intentHash,
5156
5414
  match: "error",
@@ -5167,7 +5425,7 @@ function registerAllCarrierAskTools(server2, ctx) {
5167
5425
  type: "text",
5168
5426
  text: JSON.stringify({
5169
5427
  error: "routing_error",
5170
- message: err6 instanceof Error ? err6.message : "An unexpected error occurred during routing."
5428
+ message: err7 instanceof Error ? err7.message : "An unexpected error occurred during routing."
5171
5429
  })
5172
5430
  }
5173
5431
  ],
@@ -5419,8 +5677,8 @@ function registerListRecentOcsEventsTool(server2, env) {
5419
5677
  return {
5420
5678
  content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
5421
5679
  };
5422
- } catch (err6) {
5423
- const message = err6 instanceof Error ? err6.message : String(err6);
5680
+ } catch (err7) {
5681
+ const message = err7 instanceof Error ? err7.message : String(err7);
5424
5682
  return {
5425
5683
  isError: true,
5426
5684
  content: [{ type: "text", text: `Error reading OCS event buffer: ${message}` }]
@@ -5700,47 +5958,100 @@ import {
5700
5958
  registerAppResource,
5701
5959
  RESOURCE_MIME_TYPE
5702
5960
  } from "@modelcontextprotocol/ext-apps/server";
5703
- async function safeCallWithToken(client, _token, method, params = {}) {
5961
+
5962
+ // src/apps/fleet-data.ts
5963
+ function extractAccounts(json) {
5964
+ const raw = json;
5965
+ if (Array.isArray(raw)) return raw;
5966
+ return (raw?.reseller ?? []).flatMap((r) => r.account ?? []);
5967
+ }
5968
+ function toFleetInput(raw) {
5969
+ const counts = extractEsimStatusCounts(raw.status);
5970
+ const accountRecords = extractAccounts(raw.accounts);
5971
+ const accounts = accountRecords.map((a) => ({
5972
+ name: String(a.name ?? a.accountId ?? "?"),
5973
+ balance: Number(a.balance ?? 0),
5974
+ // Per-account status breakdown is not in listResellerAccount; the fleet
5975
+ // totals come from the status call. Showing 0 here would read as "this
5976
+ // account has no eSIMs", so the per-account columns stay at the balance
5977
+ // and flags this call actually carries.
5978
+ active: 0,
5979
+ suspended: 0,
5980
+ inventory: 0,
5981
+ other: 0,
5982
+ packageOnly: Boolean(a.packageOnly)
5983
+ }));
5984
+ const unavailable = [];
5985
+ if (raw.statusError) unavailable.push(`esimStatusPerAccount: ${raw.statusError}`);
5986
+ if (raw.accountsError) unavailable.push(`listResellerAccount: ${raw.accountsError}`);
5987
+ return {
5988
+ active: counts.active,
5989
+ suspended: counts.suspended,
5990
+ inventory: counts.inventory,
5991
+ other: counts.other,
5992
+ accounts,
5993
+ unavailable: unavailable.length ? unavailable : void 0
5994
+ };
5995
+ }
5996
+
5997
+ // src/apps/fleet-health-app.ts
5998
+ var FLEET_APP_RESOURCE_URI = "ui://fleet-health-dashboard";
5999
+ async function safeCall2(client, method, params) {
5704
6000
  try {
5705
6001
  return { data: await client.call(method, params), error: null };
5706
- } catch (err6) {
5707
- return {
5708
- data: null,
5709
- error: err6 instanceof Error ? err6.message : String(err6)
5710
- };
6002
+ } catch (err7) {
6003
+ return { data: null, error: err7 instanceof Error ? err7.message : String(err7) };
5711
6004
  }
5712
6005
  }
6006
+ async function loadFleet(ctx, accountId) {
6007
+ const token2 = await ctx.getUserToken(ctx.props.sub);
6008
+ const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
6009
+ const resellerId = await getDefaultResellerId(ctx.env, token2).catch(() => void 0);
6010
+ const statusParams = accountId !== void 0 ? { accountId } : resellerId !== void 0 ? { resellerId } : {};
6011
+ const accountParams = resellerId !== void 0 ? { resellerId } : {};
6012
+ const [status, accounts] = await Promise.all([
6013
+ safeCall2(client, "esimStatusPerAccount", statusParams),
6014
+ safeCall2(client, "listResellerAccount", accountParams)
6015
+ ]);
6016
+ return toFleetInput({
6017
+ status: status.data,
6018
+ accounts: accounts.data,
6019
+ statusError: status.error,
6020
+ accountsError: accounts.error
6021
+ });
6022
+ }
5713
6023
  function registerFleetHealthApp(server2, ctx) {
5714
6024
  registerAppResource(
5715
6025
  server2,
5716
6026
  "Fleet Health Dashboard",
5717
- "ui://fleet-health-dashboard",
6027
+ FLEET_APP_RESOURCE_URI,
5718
6028
  {
5719
- description: "Interactive Fleet Health Dashboard \u2014 eSIM status charts, account breakdown, low-balance alerts."
6029
+ description: "Fleet overview panel \u2014 eSIM status, per-account breakdown and low-balance alerts."
5720
6030
  },
5721
6031
  async () => {
5722
6032
  let html;
5723
6033
  try {
5724
- const resp = await ctx.env.ASSETS.fetch(
5725
- new Request("https://internal/views/fleet-health/index.html")
5726
- );
5727
- html = await resp.text();
5728
- } catch {
5729
- html = "<html><body><p>Dashboard unavailable.</p></body></html>";
6034
+ html = renderHtml(fleetScreen(await loadFleet(ctx)));
6035
+ } catch (err7) {
6036
+ html = renderHtml({
6037
+ id: "fleet-health",
6038
+ title: "Fleet health",
6039
+ sections: [
6040
+ {
6041
+ kind: "note",
6042
+ tone: "critical",
6043
+ text: `Could not load fleet data: ${err7 instanceof Error ? err7.message : String(err7)}`
6044
+ }
6045
+ ]
6046
+ });
5730
6047
  }
5731
6048
  return {
5732
6049
  contents: [
5733
6050
  {
5734
- uri: "ui://fleet-health-dashboard",
6051
+ uri: FLEET_APP_RESOURCE_URI,
5735
6052
  mimeType: RESOURCE_MIME_TYPE,
5736
- text: html,
5737
- _meta: {
5738
- ui: {
5739
- csp: {
5740
- resourceDomains: ["https://cdn.jsdelivr.net"]
5741
- }
5742
- }
5743
- }
6053
+ text: html
6054
+ // Self-contained: no external origins, so no CSP exceptions.
5744
6055
  }
5745
6056
  ]
5746
6057
  };
@@ -5751,100 +6062,45 @@ function registerFleetHealthApp(server2, ctx) {
5751
6062
  "fleet_health_app",
5752
6063
  {
5753
6064
  title: "Fleet Health Dashboard",
5754
- description: "Renders an interactive Fleet Health Dashboard with eSIM status charts, top-10 account breakdown, and low-balance alerts. Returns structuredContent for the chart panel.",
6065
+ description: "Draw the fleet overview as an interactive panel: eSIM status, per-account breakdown and low-balance alerts. Returns structuredContent for the panel, so it only helps a client that can render MCP UI resources. Do NOT use this when the answer has to be readable text, or in any headless or scripted context \u2014 `fleet_health` returns the same data as prose.",
5755
6066
  inputSchema: {
5756
6067
  accountId: z9.number().optional().describe("Filter to a specific account (omit for all)")
5757
6068
  },
5758
6069
  annotations: { readOnlyHint: true },
5759
6070
  _meta: {
5760
6071
  ui: {
5761
- resourceUri: "ui://fleet-health-dashboard",
6072
+ resourceUri: FLEET_APP_RESOURCE_URI,
5762
6073
  visibility: ["model", "app"]
5763
6074
  }
5764
6075
  }
5765
6076
  },
5766
6077
  async ({ accountId }) => {
5767
- const token2 = await ctx.getUserToken(ctx.props.sub);
5768
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
5769
- const [statusResult, accountsResult] = await Promise.all([
5770
- safeCallWithToken(
5771
- client,
5772
- token2,
5773
- "esimStatusPerAccount",
5774
- accountId !== void 0 ? { accountId } : {}
5775
- ),
5776
- safeCallWithToken(
5777
- client,
5778
- token2,
5779
- "listResellerAccount",
5780
- {}
5781
- )
5782
- ]);
5783
- let totalActive = 0;
5784
- let totalSuspended = 0;
5785
- let totalInventory = 0;
5786
- let totalOther = 0;
5787
- const accountList = [];
5788
- if (statusResult.data && Array.isArray(statusResult.data)) {
5789
- for (const account of statusResult.data) {
5790
- const active = Number(account["active"] ?? 0);
5791
- const suspended = Number(account["suspended"] ?? 0);
5792
- const inventory = Number(
5793
- account["inventory"] ?? account["notActivated"] ?? 0
5794
- );
5795
- const other = Number(
5796
- account["other"] ?? account["terminated"] ?? 0
5797
- );
5798
- totalActive += active;
5799
- totalSuspended += suspended;
5800
- totalInventory += inventory;
5801
- totalOther += other;
5802
- accountList.push({
5803
- name: String(account["name"] ?? account["accountId"] ?? "?"),
5804
- balance: 0,
5805
- active,
5806
- suspended,
5807
- inventory,
5808
- other
5809
- });
5810
- }
5811
- }
5812
- if (accountsResult.data && Array.isArray(accountsResult.data)) {
5813
- for (const a of accountsResult.data) {
5814
- const aName = String(a["name"] ?? a["accountId"] ?? "?");
5815
- const entry = accountList.find((e) => e.name === aName);
5816
- if (entry) {
5817
- entry.balance = Number(a["balance"] ?? 0);
6078
+ const input = await loadFleet(ctx, accountId);
6079
+ const screen = fleetScreen(input);
6080
+ const total = input.active + input.suspended + input.inventory + input.other;
6081
+ const lowBalanceCount = input.accounts.filter((a) => a.balance < 10).length;
6082
+ return {
6083
+ content: [
6084
+ {
6085
+ type: "text",
6086
+ text: total === 0 && input.accounts.length === 0 ? "Fleet view returned no eSIMs and no accounts \u2014 check the account scope and credentials." : `${total} eSIMs across ${input.accounts.length} account(s); ${input.active} active. Fleet Utilization: ${total > 0 ? (input.active / total * 100).toFixed(1) : "0"}%`
5818
6087
  }
6088
+ ],
6089
+ structuredContent: {
6090
+ // `screen` is the shared model — the panel and `carrier dash` both
6091
+ // render this exact object, so the two cannot disagree.
6092
+ screen,
6093
+ // Flat fields are the original published contract. Kept so anything
6094
+ // already reading them keeps working; the fix was the data, not the shape.
6095
+ utilization: total > 0 ? Math.round(input.active / total * 1e3) / 10 : 0,
6096
+ totalActive: input.active,
6097
+ totalSuspended: input.suspended,
6098
+ totalInventory: input.inventory,
6099
+ totalOther: input.other,
6100
+ totalAccounts: input.accounts.length,
6101
+ lowBalanceCount,
6102
+ accountList: input.accounts
5819
6103
  }
5820
- }
5821
- const total = totalActive + totalSuspended + totalInventory + totalOther;
5822
- const utilization = total > 0 ? Math.round(totalActive / total * 1e3) / 10 : 0;
5823
- const totalAccounts = accountsResult.data && Array.isArray(accountsResult.data) ? accountsResult.data.length : accountList.length;
5824
- const lowBalanceCount = accountsResult.data && Array.isArray(accountsResult.data) ? accountsResult.data.filter((a) => Number(a["balance"] ?? 0) < 10).length : 0;
5825
- const sortedAccounts = [...accountList].sort(
5826
- (a, b) => b.active + b.suspended + b.inventory + b.other - (a.active + a.suspended + a.inventory + a.other)
5827
- ).slice(0, 10);
5828
- const errors = [statusResult.error, accountsResult.error].filter(Boolean).join("; ");
5829
- const summaryLines = [
5830
- `Fleet Utilization: ${utilization}%`,
5831
- `Active: ${totalActive} | Suspended: ${totalSuspended} | Inventory: ${totalInventory} | Other: ${totalOther}`,
5832
- `Total Accounts: ${totalAccounts} | Low Balance (<10): ${lowBalanceCount}`,
5833
- ...errors ? [`Errors: ${errors}`] : []
5834
- ];
5835
- const structuredContent = {
5836
- utilization,
5837
- totalActive,
5838
- totalSuspended,
5839
- totalInventory,
5840
- totalOther,
5841
- totalAccounts,
5842
- lowBalanceCount,
5843
- accountList: sortedAccounts
5844
- };
5845
- return {
5846
- content: [{ type: "text", text: summaryLines.join("\n") }],
5847
- structuredContent
5848
6104
  };
5849
6105
  }
5850
6106
  );
@@ -5901,13 +6157,13 @@ async function deleteWizardSession(env, sub, wizardId) {
5901
6157
  }
5902
6158
 
5903
6159
  // src/apps/provisioning-wizard.ts
5904
- async function safeCallWithToken2(client, _token, method, params = {}) {
6160
+ async function safeCallWithToken(client, _token, method, params = {}) {
5905
6161
  try {
5906
6162
  return { data: await client.call(method, params), error: null };
5907
- } catch (err6) {
6163
+ } catch (err7) {
5908
6164
  return {
5909
6165
  data: null,
5910
- error: err6 instanceof Error ? err6.message : String(err6)
6166
+ error: err7 instanceof Error ? err7.message : String(err7)
5911
6167
  };
5912
6168
  }
5913
6169
  }
@@ -5973,7 +6229,7 @@ function registerProvisioningWizard(server2, ctx) {
5973
6229
  const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
5974
6230
  if (step === "init") {
5975
6231
  const newWizardId = generateWizardId();
5976
- const subscribersResult = await safeCallWithToken2(
6232
+ const subscribersResult = await safeCallWithToken(
5977
6233
  client,
5978
6234
  token2,
5979
6235
  "listResellerAccount",
@@ -6051,7 +6307,7 @@ function registerProvisioningWizard(server2, ctx) {
6051
6307
  };
6052
6308
  }
6053
6309
  const resellerIdForTemplates = await getDefaultResellerId(ctx.env, token2);
6054
- const packagesResult = await safeCallWithToken2(
6310
+ const packagesResult = await safeCallWithToken(
6055
6311
  client,
6056
6312
  token2,
6057
6313
  "listPrepaidPackageTemplate",
@@ -6118,7 +6374,7 @@ function registerProvisioningWizard(server2, ctx) {
6118
6374
  isError: true
6119
6375
  };
6120
6376
  }
6121
- const previewResult = await safeCallWithToken2(
6377
+ const previewResult = await safeCallWithToken(
6122
6378
  client,
6123
6379
  token2,
6124
6380
  "listPrepaidPackageTemplate",
@@ -6190,7 +6446,7 @@ function registerProvisioningWizard(server2, ctx) {
6190
6446
  isError: true
6191
6447
  };
6192
6448
  }
6193
- const subRecord = await safeCallWithToken2(
6449
+ const subRecord = await safeCallWithToken(
6194
6450
  client,
6195
6451
  token2,
6196
6452
  "getSingleSubscriber",
@@ -6219,7 +6475,7 @@ function registerProvisioningWizard(server2, ctx) {
6219
6475
  isError: true
6220
6476
  };
6221
6477
  }
6222
- const result2 = await safeCallWithToken2(
6478
+ const result2 = await safeCallWithToken(
6223
6479
  client,
6224
6480
  token2,
6225
6481
  "affectPackageToSubscriber",
@@ -6264,13 +6520,13 @@ import {
6264
6520
  registerAppResource as registerAppResource3,
6265
6521
  RESOURCE_MIME_TYPE as RESOURCE_MIME_TYPE3
6266
6522
  } from "@modelcontextprotocol/ext-apps/server";
6267
- async function safeCallWithToken3(client, _token, method, params = {}) {
6523
+ async function safeCallWithToken2(client, _token, method, params = {}) {
6268
6524
  try {
6269
6525
  return { data: await client.call(method, params), error: null };
6270
- } catch (err6) {
6526
+ } catch (err7) {
6271
6527
  return {
6272
6528
  data: null,
6273
- error: err6 instanceof Error ? err6.message : String(err6)
6529
+ error: err7 instanceof Error ? err7.message : String(err7)
6274
6530
  };
6275
6531
  }
6276
6532
  }
@@ -6332,7 +6588,7 @@ function registerBalanceTopupApp(server2, ctx) {
6332
6588
  const token2 = await ctx.getUserToken(ctx.props.sub);
6333
6589
  const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
6334
6590
  if (preview === true) {
6335
- const result2 = await safeCallWithToken3(
6591
+ const result2 = await safeCallWithToken2(
6336
6592
  client,
6337
6593
  token2,
6338
6594
  "getSingleSubscriber",
@@ -6357,7 +6613,7 @@ function registerBalanceTopupApp(server2, ctx) {
6357
6613
  structuredContent: structured2
6358
6614
  };
6359
6615
  }
6360
- const execResult = await safeCallWithToken3(
6616
+ const execResult = await safeCallWithToken2(
6361
6617
  client,
6362
6618
  token2,
6363
6619
  // CAR-78: subscriber top-up uses modifySubscriberBalance with { subscriber, amount }
@@ -7931,8 +8187,8 @@ function buildPricingHandler(tool, ctx) {
7931
8187
  }
7932
8188
  ]
7933
8189
  };
7934
- } catch (err6) {
7935
- const message = err6 instanceof Error ? err6.message : String(err6);
8190
+ } catch (err7) {
8191
+ const message = err7 instanceof Error ? err7.message : String(err7);
7936
8192
  ctx.audit({
7937
8193
  tool_name: tool.name,
7938
8194
  ocs_method: "billing",
@@ -8252,8 +8508,8 @@ function writeUsageThresholdAudit(env, severity, remainingCredits, month, taskCo
8252
8508
  doubles: [remainingCredits],
8253
8509
  indexes: ["manus_usage"]
8254
8510
  });
8255
- } catch (err6) {
8256
- console.error(`[manus-usage] threshold audit write failed: ${err6.message}`);
8511
+ } catch (err7) {
8512
+ console.error(`[manus-usage] threshold audit write failed: ${err7.message}`);
8257
8513
  }
8258
8514
  }
8259
8515
 
@@ -8363,7 +8619,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8363
8619
  }
8364
8620
  ]
8365
8621
  };
8366
- } catch (err6) {
8622
+ } catch (err7) {
8367
8623
  ctx.audit({
8368
8624
  tool_name: "ui_agent_schedule_create",
8369
8625
  ocs_method: "[manus:schedule.create]",
@@ -8372,7 +8628,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8372
8628
  duration_ms: Date.now() - start,
8373
8629
  event_type: "ui_agent_dispatch"
8374
8630
  });
8375
- const msg = err6 instanceof ManusScheduleError ? `Manus API error (HTTP ${err6.statusCode}): ${err6.message}` : err6 instanceof Error ? err6.message : String(err6);
8631
+ const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
8376
8632
  return { isError: true, content: [{ type: "text", text: msg }] };
8377
8633
  }
8378
8634
  }
@@ -8430,7 +8686,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8430
8686
  }
8431
8687
  ]
8432
8688
  };
8433
- } catch (err6) {
8689
+ } catch (err7) {
8434
8690
  ctx.audit({
8435
8691
  tool_name: "ui_agent_schedule_list",
8436
8692
  ocs_method: "[manus:schedule.list]",
@@ -8438,7 +8694,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8438
8694
  dry_run: false,
8439
8695
  duration_ms: Date.now() - start
8440
8696
  });
8441
- const msg = err6 instanceof ManusScheduleError ? `Manus API error (HTTP ${err6.statusCode}): ${err6.message}` : err6 instanceof Error ? err6.message : String(err6);
8697
+ const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
8442
8698
  return { isError: true, content: [{ type: "text", text: msg }] };
8443
8699
  }
8444
8700
  }
@@ -8499,7 +8755,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8499
8755
  }
8500
8756
  ]
8501
8757
  };
8502
- } catch (err6) {
8758
+ } catch (err7) {
8503
8759
  ctx.audit({
8504
8760
  tool_name: "ui_agent_schedule_delete",
8505
8761
  ocs_method: "[manus:schedule.delete]",
@@ -8508,7 +8764,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8508
8764
  duration_ms: Date.now() - start,
8509
8765
  event_type: "ui_agent_dispatch"
8510
8766
  });
8511
- const msg = err6 instanceof ManusScheduleError ? `Manus API error (HTTP ${err6.statusCode}): ${err6.message}` : err6 instanceof Error ? err6.message : String(err6);
8767
+ const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
8512
8768
  return { isError: true, content: [{ type: "text", text: msg }] };
8513
8769
  }
8514
8770
  }
@@ -8569,7 +8825,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8569
8825
  }
8570
8826
  ]
8571
8827
  };
8572
- } catch (err6) {
8828
+ } catch (err7) {
8573
8829
  ctx.audit({
8574
8830
  tool_name: "ui_agent_schedule_pause",
8575
8831
  ocs_method: "[manus:schedule.pause]",
@@ -8578,7 +8834,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8578
8834
  duration_ms: Date.now() - start,
8579
8835
  event_type: "ui_agent_dispatch"
8580
8836
  });
8581
- const msg = err6 instanceof ManusScheduleError ? `Manus API error (HTTP ${err6.statusCode}): ${err6.message}` : err6 instanceof Error ? err6.message : String(err6);
8837
+ const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
8582
8838
  return { isError: true, content: [{ type: "text", text: msg }] };
8583
8839
  }
8584
8840
  }
@@ -8639,7 +8895,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8639
8895
  }
8640
8896
  ]
8641
8897
  };
8642
- } catch (err6) {
8898
+ } catch (err7) {
8643
8899
  ctx.audit({
8644
8900
  tool_name: "ui_agent_schedule_resume",
8645
8901
  ocs_method: "[manus:schedule.resume]",
@@ -8648,7 +8904,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8648
8904
  duration_ms: Date.now() - start,
8649
8905
  event_type: "ui_agent_dispatch"
8650
8906
  });
8651
- const msg = err6 instanceof ManusScheduleError ? `Manus API error (HTTP ${err6.statusCode}): ${err6.message}` : err6 instanceof Error ? err6.message : String(err6);
8907
+ const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
8652
8908
  return { isError: true, content: [{ type: "text", text: msg }] };
8653
8909
  }
8654
8910
  }
@@ -8693,7 +8949,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8693
8949
  }
8694
8950
  ]
8695
8951
  };
8696
- } catch (err6) {
8952
+ } catch (err7) {
8697
8953
  ctx.audit({
8698
8954
  tool_name: "ui_agent_usage",
8699
8955
  ocs_method: "[manus:usage.get]",
@@ -8701,7 +8957,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8701
8957
  dry_run: false,
8702
8958
  duration_ms: Date.now() - start
8703
8959
  });
8704
- const msg = err6 instanceof Error ? err6.message : String(err6);
8960
+ const msg = err7 instanceof Error ? err7.message : String(err7);
8705
8961
  return { isError: true, content: [{ type: "text", text: msg }] };
8706
8962
  }
8707
8963
  }
@@ -8944,7 +9200,7 @@ Report success or failure with a clear summary.`;
8944
9200
  }
8945
9201
  ]
8946
9202
  };
8947
- } catch (err6) {
9203
+ } catch (err7) {
8948
9204
  ctx.audit({
8949
9205
  tool_name: toolName,
8950
9206
  ocs_method: `[ui-agent:${gapId}]`,
@@ -8958,7 +9214,7 @@ Report success or failure with a clear summary.`;
8958
9214
  content: [
8959
9215
  {
8960
9216
  type: "text",
8961
- text: `Error dispatching UI agent: ${err6 instanceof Error ? err6.message : String(err6)}`
9217
+ text: `Error dispatching UI agent: ${err7 instanceof Error ? err7.message : String(err7)}`
8962
9218
  }
8963
9219
  ]
8964
9220
  };
@@ -9382,7 +9638,7 @@ function registerUiAgentAskTools(server2, ctx) {
9382
9638
  try {
9383
9639
  const replyOutcome = await askReply(keys, task_id, reply);
9384
9640
  result2 = replyOutcome.data;
9385
- } catch (err6) {
9641
+ } catch (err7) {
9386
9642
  ctx.audit({
9387
9643
  tool_name: "ui_agent_reply",
9388
9644
  ocs_method: "[ui-agent:ask-reply]",
@@ -9397,7 +9653,7 @@ function registerUiAgentAskTools(server2, ctx) {
9397
9653
  content: [
9398
9654
  {
9399
9655
  type: "text",
9400
- text: `Error calling Manus task.reply: ${err6 instanceof Error ? err6.message : String(err6)}`
9656
+ text: `Error calling Manus task.reply: ${err7 instanceof Error ? err7.message : String(err7)}`
9401
9657
  }
9402
9658
  ]
9403
9659
  };
@@ -9475,13 +9731,13 @@ function registerUiAgentAskTools(server2, ctx) {
9475
9731
  try {
9476
9732
  const listing = await ctx.env.CARRIER_USERS.list({ prefix: PENDING_ASK_PREFIX });
9477
9733
  keys = listing.keys;
9478
- } catch (err6) {
9734
+ } catch (err7) {
9479
9735
  return {
9480
9736
  isError: true,
9481
9737
  content: [
9482
9738
  {
9483
9739
  type: "text",
9484
- text: `Error listing pending tasks: ${err6 instanceof Error ? err6.message : String(err6)}`
9740
+ text: `Error listing pending tasks: ${err7 instanceof Error ? err7.message : String(err7)}`
9485
9741
  }
9486
9742
  ]
9487
9743
  };
@@ -10935,6 +11191,138 @@ function registerStorefrontLogoTools(server2, env = {}) {
10935
11191
  );
10936
11192
  }
10937
11193
 
11194
+ // src/tools-storefront-deploy.ts
11195
+ import { z as z22 } from "zod";
11196
+ var ok6 = (value) => ({
11197
+ content: [{ type: "text", text: JSON.stringify(value) }]
11198
+ });
11199
+ var err6 = (text) => ({ isError: true, content: [{ type: "text", text }] });
11200
+ function registerStorefrontDeployTools(server2) {
11201
+ server2.tool(
11202
+ "list_deploy_targets",
11203
+ "Report which storefront deploy hosts (Cloudflare, Vercel, Netlify, Fly) are installed, logged in, and ready on this machine, and which one a deploy would pick.",
11204
+ {
11205
+ dir: z22.string().min(1).describe("Path to the scaffolded storefront directory")
11206
+ },
11207
+ async ({ dir }) => {
11208
+ try {
11209
+ const statuses = await probeAll(dir);
11210
+ const ranked = rankTargets(statuses);
11211
+ return ok6({
11212
+ targets: statuses,
11213
+ default: ranked[0]?.id ?? null,
11214
+ reason: ranked.length ? void 0 : "No host is installed and logged in."
11215
+ });
11216
+ } catch (e) {
11217
+ return err6(e instanceof Error ? e.message : "target probe failed");
11218
+ }
11219
+ }
11220
+ );
11221
+ server2.tool(
11222
+ "deploy_storefront",
11223
+ "Build and deploy a scaffolded storefront to whichever host is available on this machine, then push its runtime secrets (CARRIER_API_KEY, CLERK_SECRET_KEY) to that host.",
11224
+ {
11225
+ dir: z22.string().min(1).describe("Path to the scaffolded storefront directory"),
11226
+ target: z22.enum(TARGET_IDS).optional().describe("Force a host instead of auto-detecting"),
11227
+ name: z22.string().optional().describe("Project/worker name (defaults from brand)"),
11228
+ skip_build: z22.boolean().optional().describe("Deploy the existing build without rebuilding"),
11229
+ push_secrets: z22.boolean().optional().describe("Stage runtime secrets on the host before deploy (default true)"),
11230
+ verify: z22.boolean().optional().describe("Probe the deployed URL and report whether it actually serves (default true)"),
11231
+ domain: z22.boolean().optional().describe("Route the brand domain to the deployment (Cloudflare provisions DNS + cert)")
11232
+ },
11233
+ async ({ dir, target, name, skip_build, push_secrets, verify, domain }) => {
11234
+ try {
11235
+ if (target && !isTargetId(target)) return err6(`Unknown target "${target}".`);
11236
+ const brand = await loadStorefrontBrand(dir, name ? { name } : void 0);
11237
+ const preferred = target;
11238
+ if (!skip_build) {
11239
+ const ranked = rankTargets(await probeAll(dir));
11240
+ const buildFor = preferred ?? ranked[0]?.id ?? "cloudflare";
11241
+ const built = await buildSite(dir, buildFor);
11242
+ if (!built) return err6(`Build failed for ${buildFor}. Run the build locally to see why.`);
11243
+ }
11244
+ const result2 = await deploySite(dir, brand, {
11245
+ preferred,
11246
+ pushSecrets: push_secrets !== false,
11247
+ customDomain: domain === true
11248
+ });
11249
+ if (!result2.ok) {
11250
+ return err6(result2.reason ?? "Deploy failed.");
11251
+ }
11252
+ const verification = verify !== false && result2.url ? await verifyStorefront(result2.url, dir, process.env) : void 0;
11253
+ return ok6({
11254
+ deployed: true,
11255
+ target: result2.target,
11256
+ project: result2.projectName,
11257
+ url: result2.url ?? null,
11258
+ secrets_staged: result2.secrets?.pushed ?? [],
11259
+ secrets_failed: result2.secrets?.failed ?? [],
11260
+ verified: verification ? verification.ok : null,
11261
+ diagnosis: verification?.ok === false ? verification.diagnosis : void 0,
11262
+ probes: verification ? formatProbes(verification.probes) : void 0,
11263
+ repair_hint: verification && !verification.ok ? repairPlanFor(verification.diagnosis)?.note ?? verification.summary : void 0,
11264
+ warning: result2.secrets?.pushed.includes("CARRIER_API_KEY") ? void 0 : "CARRIER_API_KEY was not staged \u2014 / and /shop will return 500 until it is set."
11265
+ });
11266
+ } catch (e) {
11267
+ return err6(e instanceof Error ? e.message : "deploy failed");
11268
+ }
11269
+ }
11270
+ );
11271
+ server2.tool(
11272
+ "provision_storefront_clerk",
11273
+ "Create a Clerk application (Platform API) or reuse Clerk keys already on this machine, write them into the storefront .env.local, and configure allowed origins and redirect URLs on the instance.",
11274
+ {
11275
+ dir: z22.string().min(1).describe("Path to the scaffolded storefront directory"),
11276
+ name: z22.string().optional().describe("Clerk application name (defaults from brand)"),
11277
+ production: z22.boolean().optional().describe("Also create a production instance (Platform API only)"),
11278
+ no_create: z22.boolean().optional().describe("Never create a new application; only reuse existing keys"),
11279
+ url: z22.string().optional().describe("Deployed URL to whitelist as origin and redirect")
11280
+ },
11281
+ async ({ dir, name, production, no_create, url }) => {
11282
+ try {
11283
+ const brand = await loadStorefrontBrand(dir, name ? { name } : void 0);
11284
+ const result2 = await provisionClerk({
11285
+ name: name ?? brand.name,
11286
+ domain: brand.domain,
11287
+ production: production === true,
11288
+ noCreate: no_create === true,
11289
+ env: process.env,
11290
+ storefront: dir,
11291
+ cli: clerkCliDeps()
11292
+ });
11293
+ if (!result2.ok || !result2.credentials) {
11294
+ return ok6({
11295
+ provisioned: false,
11296
+ tier: result2.tier,
11297
+ reason: result2.reason,
11298
+ guidance: result2.guidance ?? []
11299
+ });
11300
+ }
11301
+ const written = await mergeEnvLocal(dir, {
11302
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: result2.credentials.publishableKey,
11303
+ CLERK_SECRET_KEY: result2.credentials.secretKey
11304
+ });
11305
+ const { allowedOrigins, redirectUrls } = storefrontClerkUrls(url, brand.domain);
11306
+ const configured = await configureClerkInstance(result2.credentials.secretKey, {
11307
+ allowedOrigins,
11308
+ redirectUrls
11309
+ });
11310
+ return ok6({
11311
+ provisioned: true,
11312
+ tier: result2.tier,
11313
+ application_id: result2.credentials.applicationId ?? null,
11314
+ instance_id: result2.credentials.instanceId ?? null,
11315
+ env_written: written,
11316
+ configured: configured.applied,
11317
+ configure_failures: configured.failed
11318
+ });
11319
+ } catch (e) {
11320
+ return err6(e instanceof Error ? e.message : "clerk provisioning failed");
11321
+ }
11322
+ }
11323
+ );
11324
+ }
11325
+
10938
11326
  // src/index.ts
10939
11327
  var baseUrl = process.env.CARRIER_OCS_BASE_URL ?? process.env.ESIMVAULT_BASE_URL ?? "https://ocs.esimvault.cloud";
10940
11328
  var token = process.env.CARRIER_OCS_API_TOKEN ?? process.env.ESIMVAULT_API_TOKEN;
@@ -11003,6 +11391,7 @@ registerWalletTools(server, { env: stdioEnv, props });
11003
11391
  registerGreenzoneTools(server, toolCtx);
11004
11392
  registerUiAgentGenericTools(server, toolCtx);
11005
11393
  registerStorefrontLogoTools(server, stdioEnv);
11394
+ registerStorefrontDeployTools(server);
11006
11395
  registerAllPrompts(server);
11007
11396
  var transport = new StdioServerTransport();
11008
11397
  await server.connect(transport);