@carrierllc/mcp 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-RBG4MKMW.js +2441 -0
- package/dist/chunk-RBG4MKMW.js.map +1 -0
- package/dist/chunk-SHKKVIIA.js +114 -0
- package/dist/chunk-SHKKVIIA.js.map +1 -0
- package/dist/cli.js +927 -371
- package/dist/cli.js.map +1 -1
- package/dist/fsx-BDDIQ3Y7.js +27 -0
- package/dist/fsx-BDDIQ3Y7.js.map +1 -0
- package/dist/index.js +585 -194
- package/dist/index.js.map +1 -1
- package/package.json +14 -4
- package/plugin/carrier/commands/storefront.md +55 -6
- package/templates/storefront/next-env.d.ts +7 -0
- package/templates/storefront/package-lock.json +0 -174
- package/templates/storefront/tsconfig.json +24 -6
- package/dist/chunk-TABEJ7SM.js +0 -461
- package/dist/chunk-TABEJ7SM.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
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
|
+
renderHtml,
|
|
29
|
+
repairPlanFor,
|
|
30
|
+
storefrontClerkUrls,
|
|
11
31
|
subscriberIdParams,
|
|
12
|
-
usageOverPeriodParams
|
|
13
|
-
|
|
32
|
+
usageOverPeriodParams,
|
|
33
|
+
verifyStorefront
|
|
34
|
+
} from "./chunk-RBG4MKMW.js";
|
|
35
|
+
import "./chunk-SHKKVIIA.js";
|
|
14
36
|
|
|
15
37
|
// src/index.ts
|
|
16
38
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -158,6 +180,212 @@ function firstDayNextMonth() {
|
|
|
158
180
|
return new Date(Date.UTC(y, m, 1)).toISOString();
|
|
159
181
|
}
|
|
160
182
|
|
|
183
|
+
// src/bounded-results.ts
|
|
184
|
+
var TARIFF_MAX_RULES = 3e3;
|
|
185
|
+
var TARIFF_VERBOSE_MAX_RULES = 250;
|
|
186
|
+
var ZONES_MAX_ZONES = 100;
|
|
187
|
+
var ZONES_MAX_OPERATORS = 2e3;
|
|
188
|
+
var ZONES_VERBOSE_MAX_OPERATORS = 400;
|
|
189
|
+
function extractTariffRules(raw) {
|
|
190
|
+
if (Array.isArray(raw)) return raw;
|
|
191
|
+
if (raw && typeof raw === "object") {
|
|
192
|
+
const obj = raw;
|
|
193
|
+
for (const key of ["rule", "listTariffRule", "rules"]) {
|
|
194
|
+
const v = obj[key];
|
|
195
|
+
if (Array.isArray(v)) return v;
|
|
196
|
+
if (v && typeof v === "object" && Array.isArray(v.rule)) {
|
|
197
|
+
return v.rule;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
function extractZones(raw) {
|
|
204
|
+
if (Array.isArray(raw)) return raw;
|
|
205
|
+
if (raw && typeof raw === "object") {
|
|
206
|
+
const obj = raw;
|
|
207
|
+
for (const key of ["listDetailedLocationZone", "locationZone", "zone", "zones"]) {
|
|
208
|
+
const v = obj[key];
|
|
209
|
+
if (Array.isArray(v)) return v;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
function matchesCountry(op, country) {
|
|
215
|
+
const want = country.trim().toLowerCase();
|
|
216
|
+
if (!want) return true;
|
|
217
|
+
const iso = (op?.countryIso2 ?? "").trim().toLowerCase();
|
|
218
|
+
const name = (op?.countryName ?? "").trim().toLowerCase();
|
|
219
|
+
return iso === want || name === want;
|
|
220
|
+
}
|
|
221
|
+
function matchesTrafficType(rule, trafficType) {
|
|
222
|
+
const gt0 = (v) => typeof v === "number" && v > 0;
|
|
223
|
+
switch (trafficType) {
|
|
224
|
+
case "data":
|
|
225
|
+
return gt0(rule.datarate);
|
|
226
|
+
case "voice":
|
|
227
|
+
return gt0(rule.mocallrate) || gt0(rule.mtcallrate);
|
|
228
|
+
case "sms":
|
|
229
|
+
return gt0(rule.mosmsrate) || gt0(rule.mtsmsrate);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
var RATE_COLUMNS = {
|
|
233
|
+
data: [["datarate", "data"]],
|
|
234
|
+
voice: [
|
|
235
|
+
["mocallrate", "moCall"],
|
|
236
|
+
["mtcallrate", "mtCall"]
|
|
237
|
+
],
|
|
238
|
+
sms: [
|
|
239
|
+
["mosmsrate", "moSms"],
|
|
240
|
+
["mtsmsrate", "mtSms"]
|
|
241
|
+
]
|
|
242
|
+
};
|
|
243
|
+
var ALL_RATE_COLUMNS = [
|
|
244
|
+
["datarate", "data"],
|
|
245
|
+
["mocallrate", "moCall"],
|
|
246
|
+
["mtcallrate", "mtCall"],
|
|
247
|
+
["mosmsrate", "moSms"],
|
|
248
|
+
["mtsmsrate", "mtSms"]
|
|
249
|
+
];
|
|
250
|
+
var FIELD_LEGEND = {
|
|
251
|
+
iso: "ISO-3166 alpha-2 country code",
|
|
252
|
+
op: "operator / network name",
|
|
253
|
+
data: "data rate per MB",
|
|
254
|
+
moCall: "mobile-originated call rate per minute",
|
|
255
|
+
mtCall: "mobile-terminated call rate per minute",
|
|
256
|
+
moSms: "mobile-originated SMS rate",
|
|
257
|
+
mtSms: "mobile-terminated SMS rate",
|
|
258
|
+
active: "present only when the rule is active; absent means inactive"
|
|
259
|
+
};
|
|
260
|
+
function truncationNote(kind, returned, matched) {
|
|
261
|
+
if (kind === "tariff") {
|
|
262
|
+
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.`;
|
|
263
|
+
}
|
|
264
|
+
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.`;
|
|
265
|
+
}
|
|
266
|
+
function boundTariffResult(raw, opts = {}) {
|
|
267
|
+
const all = extractTariffRules(raw);
|
|
268
|
+
const { country, trafficType, verbose = false, resellerId } = opts;
|
|
269
|
+
const matched = all.filter(
|
|
270
|
+
(r) => (country === void 0 || matchesCountry(r.operator, country)) && (trafficType === void 0 || matchesTrafficType(r, trafficType))
|
|
271
|
+
);
|
|
272
|
+
const cap = opts.maxRules ?? (verbose ? TARIFF_VERBOSE_MAX_RULES : TARIFF_MAX_RULES);
|
|
273
|
+
const kept = matched.slice(0, cap);
|
|
274
|
+
const truncated = kept.length < matched.length;
|
|
275
|
+
const result2 = {
|
|
276
|
+
resellerId,
|
|
277
|
+
totalRules: all.length,
|
|
278
|
+
matchedRules: matched.length,
|
|
279
|
+
returnedRules: kept.length,
|
|
280
|
+
truncated,
|
|
281
|
+
projection: verbose ? "verbose" : "compact",
|
|
282
|
+
rules: []
|
|
283
|
+
};
|
|
284
|
+
if (country !== void 0 || trafficType !== void 0) {
|
|
285
|
+
result2.filters = {};
|
|
286
|
+
if (country !== void 0) result2.filters.country = country;
|
|
287
|
+
if (trafficType !== void 0) result2.filters.trafficType = trafficType;
|
|
288
|
+
}
|
|
289
|
+
if (verbose) {
|
|
290
|
+
result2.rules = kept;
|
|
291
|
+
if (truncated) result2.note = truncationNote("tariff", kept.length, matched.length);
|
|
292
|
+
return result2;
|
|
293
|
+
}
|
|
294
|
+
const currencies = new Set(
|
|
295
|
+
kept.map((r) => r.currency?.currencycode).filter((c) => typeof c === "string")
|
|
296
|
+
);
|
|
297
|
+
const hoistCurrency = currencies.size === 1;
|
|
298
|
+
if (hoistCurrency) result2.currency = [...currencies][0];
|
|
299
|
+
const columns = trafficType ? RATE_COLUMNS[trafficType] : ALL_RATE_COLUMNS;
|
|
300
|
+
result2.rules = kept.map((r) => {
|
|
301
|
+
const op = r.operator ?? {};
|
|
302
|
+
const row = {};
|
|
303
|
+
if (op.countryIso2 !== void 0) row.iso = op.countryIso2;
|
|
304
|
+
if (op.operatorName !== void 0) row.op = op.operatorName;
|
|
305
|
+
for (const [src, dst] of columns) {
|
|
306
|
+
const v = r[src];
|
|
307
|
+
if (typeof v === "number" && v !== 0) row[dst] = v;
|
|
308
|
+
}
|
|
309
|
+
if (!hoistCurrency && typeof r.currency?.currencycode === "string") {
|
|
310
|
+
row.cur = r.currency.currencycode;
|
|
311
|
+
}
|
|
312
|
+
if (r.active === true) row.active = true;
|
|
313
|
+
return row;
|
|
314
|
+
});
|
|
315
|
+
const legend = { ...FIELD_LEGEND };
|
|
316
|
+
if (trafficType) {
|
|
317
|
+
for (const key of ["data", "moCall", "mtCall", "moSms", "mtSms"]) {
|
|
318
|
+
if (!columns.some(([, dst]) => dst === key)) delete legend[key];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (!hoistCurrency) legend.cur = "ISO-4217 currency code";
|
|
322
|
+
result2.fields = legend;
|
|
323
|
+
const notes = [];
|
|
324
|
+
if (truncated) notes.push(truncationNote("tariff", kept.length, matched.length));
|
|
325
|
+
notes.push(
|
|
326
|
+
`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).`
|
|
327
|
+
);
|
|
328
|
+
result2.note = notes.join(" ");
|
|
329
|
+
return result2;
|
|
330
|
+
}
|
|
331
|
+
var ZONE_FIELD_LEGEND = {
|
|
332
|
+
zoneId: "location zone id \u2014 pass to package-template tools",
|
|
333
|
+
zoneName: "human-readable zone name",
|
|
334
|
+
operatorCount: "total operator entries in this zone upstream",
|
|
335
|
+
operators: "projected operator entries: net (networkId), iso (country ISO2), op (operator name)"
|
|
336
|
+
};
|
|
337
|
+
function boundZonesResult(raw, opts = {}) {
|
|
338
|
+
const all = extractZones(raw);
|
|
339
|
+
const { verbose = false, resellerId } = opts;
|
|
340
|
+
const maxZones = opts.maxZones ?? ZONES_MAX_ZONES;
|
|
341
|
+
const maxOperators = opts.maxOperators ?? (verbose ? ZONES_VERBOSE_MAX_OPERATORS : ZONES_MAX_OPERATORS);
|
|
342
|
+
const totalOperators = all.reduce((n, z23) => n + (z23.operators?.length ?? 0), 0);
|
|
343
|
+
const keptZones = all.slice(0, maxZones);
|
|
344
|
+
let budget = maxOperators;
|
|
345
|
+
let returnedOperators = 0;
|
|
346
|
+
const zones = keptZones.map((z23) => {
|
|
347
|
+
const ops = z23.operators ?? [];
|
|
348
|
+
const take = Math.max(0, Math.min(ops.length, budget));
|
|
349
|
+
budget -= take;
|
|
350
|
+
returnedOperators += take;
|
|
351
|
+
const sliced = ops.slice(0, take);
|
|
352
|
+
if (verbose) {
|
|
353
|
+
return { ...z23, operators: sliced, operatorCount: ops.length };
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
zoneId: z23.zoneId,
|
|
357
|
+
zoneName: z23.zoneName,
|
|
358
|
+
operatorCount: ops.length,
|
|
359
|
+
operators: sliced.map((o) => ({
|
|
360
|
+
net: o.networkId,
|
|
361
|
+
iso: o.countryIso2,
|
|
362
|
+
op: o.operatorName
|
|
363
|
+
}))
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
const truncated = keptZones.length < all.length || returnedOperators < totalOperators;
|
|
367
|
+
const result2 = {
|
|
368
|
+
resellerId,
|
|
369
|
+
totalZones: all.length,
|
|
370
|
+
returnedZones: keptZones.length,
|
|
371
|
+
totalOperators,
|
|
372
|
+
returnedOperators,
|
|
373
|
+
truncated,
|
|
374
|
+
projection: verbose ? "verbose" : "compact",
|
|
375
|
+
zones
|
|
376
|
+
};
|
|
377
|
+
const notes = [];
|
|
378
|
+
if (truncated) notes.push(truncationNote("zones", returnedOperators, totalOperators));
|
|
379
|
+
if (!verbose) {
|
|
380
|
+
result2.fields = ZONE_FIELD_LEGEND;
|
|
381
|
+
notes.push(
|
|
382
|
+
`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).`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (notes.length) result2.note = notes.join(" ");
|
|
386
|
+
return result2;
|
|
387
|
+
}
|
|
388
|
+
|
|
161
389
|
// src/tools.ts
|
|
162
390
|
var TOOL_SCOPES = {
|
|
163
391
|
// --- read ---
|
|
@@ -203,7 +431,12 @@ var TOOL_SCOPES = {
|
|
|
203
431
|
modify_account_balance: "admin",
|
|
204
432
|
// corrected per ocs-spec PR #6
|
|
205
433
|
change_sim_status: "admin",
|
|
206
|
-
|
|
434
|
+
// Removing ONE package from ONE subscriber is a write, not an admin
|
|
435
|
+
// action. Admin-gating it made package lifecycle write-once for any
|
|
436
|
+
// operator on a read/write token: they could assign a package but had no
|
|
437
|
+
// way to remove one, including one assigned in error. clean_all_packages
|
|
438
|
+
// stays admin — wiping every package is a different blast radius.
|
|
439
|
+
delete_subscriber_package: "write",
|
|
207
440
|
clean_all_packages: "admin",
|
|
208
441
|
modify_template_core: "admin",
|
|
209
442
|
modify_template_recurring: "admin",
|
|
@@ -310,9 +543,9 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
|
310
543
|
try {
|
|
311
544
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
312
545
|
result2 = await handler(args, token2);
|
|
313
|
-
} catch (
|
|
546
|
+
} catch (err7) {
|
|
314
547
|
try {
|
|
315
|
-
Sentry.captureException(
|
|
548
|
+
Sentry.captureException(err7, {
|
|
316
549
|
tags: {
|
|
317
550
|
tool: toolName,
|
|
318
551
|
feature: "mcp",
|
|
@@ -321,8 +554,8 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
|
321
554
|
});
|
|
322
555
|
} catch {
|
|
323
556
|
}
|
|
324
|
-
const message =
|
|
325
|
-
const ocsCode =
|
|
557
|
+
const message = err7 instanceof Error ? err7.message : String(err7);
|
|
558
|
+
const ocsCode = err7 instanceof OcsApiError ? err7.code : void 0;
|
|
326
559
|
ctx.audit({
|
|
327
560
|
tool_name: toolName,
|
|
328
561
|
ocs_method: ocsMethod,
|
|
@@ -464,12 +697,9 @@ function registerAllTools(server2, ctx) {
|
|
|
464
697
|
TOOL_SCOPES["esim_status_per_account"],
|
|
465
698
|
ctx,
|
|
466
699
|
async ({ accountId, resellerId }, token2) => {
|
|
467
|
-
const params =
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
} else {
|
|
471
|
-
params.resellerId = resellerId ?? await getDefaultResellerId(ctx.env, token2);
|
|
472
|
-
}
|
|
700
|
+
const params = esimStatusPerAccountParams(
|
|
701
|
+
accountId !== void 0 ? { accountId } : { resellerId: resellerId ?? await getDefaultResellerId(ctx.env, token2) }
|
|
702
|
+
);
|
|
473
703
|
return ocsCall(ctx.env, token2, "esimStatusPerAccount", params);
|
|
474
704
|
}
|
|
475
705
|
)
|
|
@@ -869,7 +1099,7 @@ function registerAllTools(server2, ctx) {
|
|
|
869
1099
|
async ({ iccid, bitrate, bitrate_string }, token2) => {
|
|
870
1100
|
const cache = /* @__PURE__ */ new Map();
|
|
871
1101
|
const sub = await resolveSubscriberByIccid(ctx.env, token2, iccid, cache);
|
|
872
|
-
const imsi = sub
|
|
1102
|
+
const imsi = imsiFromSubscriberRecord(sub);
|
|
873
1103
|
if (typeof imsi !== "string" || imsi.length === 0) {
|
|
874
1104
|
return {
|
|
875
1105
|
isError: true,
|
|
@@ -897,7 +1127,7 @@ function registerAllTools(server2, ctx) {
|
|
|
897
1127
|
async ({ iccid }, token2) => {
|
|
898
1128
|
const cache = /* @__PURE__ */ new Map();
|
|
899
1129
|
const sub = await resolveSubscriberByIccid(ctx.env, token2, iccid, cache);
|
|
900
|
-
const imsi = sub
|
|
1130
|
+
const imsi = imsiFromSubscriberRecord(sub);
|
|
901
1131
|
if (typeof imsi !== "string" || imsi.length === 0) {
|
|
902
1132
|
return {
|
|
903
1133
|
isError: true,
|
|
@@ -1124,8 +1354,8 @@ function registerAllTools(server2, ctx) {
|
|
|
1124
1354
|
title: "Delete Subscriber Package",
|
|
1125
1355
|
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
1356
|
inputSchema: {
|
|
1127
|
-
iccid: z.string().describe("The subscriber ICCID"),
|
|
1128
|
-
packageId: z.number().describe("The package ID to delete"),
|
|
1357
|
+
iccid: z.string().describe("The subscriber ICCID (for audit logging; OCS itself does not take it \u2014 see below)"),
|
|
1358
|
+
packageId: z.number().describe("The package ID to delete (OCS's sole parameter \u2014 a bare integer)"),
|
|
1129
1359
|
...DRY_RUN_FIELD
|
|
1130
1360
|
},
|
|
1131
1361
|
annotations: { destructiveHint: true }
|
|
@@ -1135,7 +1365,11 @@ function registerAllTools(server2, ctx) {
|
|
|
1135
1365
|
"deleteSubscriberPackage",
|
|
1136
1366
|
TOOL_SCOPES["delete_subscriber_package"],
|
|
1137
1367
|
ctx,
|
|
1138
|
-
|
|
1368
|
+
// OCS deleteSubscriberPackage takes a BARE INTEGER package id
|
|
1369
|
+
// (endpoints-full.md 3.8, "Numeric value = package ID") — not an
|
|
1370
|
+
// { iccid, packageId } object. iccid is kept in the tool's input schema
|
|
1371
|
+
// for audit/description purposes only; it is not an OCS parameter.
|
|
1372
|
+
async ({ packageId }, token2) => ocsCall(ctx.env, token2, "deleteSubscriberPackage", packageId)
|
|
1139
1373
|
)
|
|
1140
1374
|
);
|
|
1141
1375
|
server2.registerTool(
|
|
@@ -1183,7 +1417,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1183
1417
|
"create_package_template",
|
|
1184
1418
|
{
|
|
1185
1419
|
title: "Create Package Template",
|
|
1186
|
-
description:
|
|
1420
|
+
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
1421
|
inputSchema: {
|
|
1188
1422
|
template: z.string().describe("Full template configuration as JSON string"),
|
|
1189
1423
|
...DRY_RUN_FIELD
|
|
@@ -1199,7 +1433,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1199
1433
|
ctx.env,
|
|
1200
1434
|
token2,
|
|
1201
1435
|
"createPrepaidPackageTemplate",
|
|
1202
|
-
JSON.parse(template)
|
|
1436
|
+
normalizePackageTemplate(JSON.parse(template))
|
|
1203
1437
|
)
|
|
1204
1438
|
)
|
|
1205
1439
|
);
|
|
@@ -1222,7 +1456,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1222
1456
|
ctx,
|
|
1223
1457
|
async ({ templateId, changes }, token2) => ocsCall(ctx.env, token2, "modifyPPTCore", {
|
|
1224
1458
|
templateId,
|
|
1225
|
-
...JSON.parse(changes)
|
|
1459
|
+
...normalizePackageTemplateChanges(JSON.parse(changes))
|
|
1226
1460
|
})
|
|
1227
1461
|
)
|
|
1228
1462
|
);
|
|
@@ -1276,9 +1510,9 @@ function registerAllTools(server2, ctx) {
|
|
|
1276
1510
|
"list_location_zones",
|
|
1277
1511
|
{
|
|
1278
1512
|
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,
|
|
1513
|
+
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
1514
|
inputSchema: {
|
|
1281
|
-
locationZoneId: z.number().
|
|
1515
|
+
locationZoneId: z.number().describe("Zone ID (required \u2014 OCS has no unfiltered call for this method)")
|
|
1282
1516
|
},
|
|
1283
1517
|
annotations: { readOnlyHint: true }
|
|
1284
1518
|
},
|
|
@@ -1288,9 +1522,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1288
1522
|
TOOL_SCOPES["list_location_zones"],
|
|
1289
1523
|
ctx,
|
|
1290
1524
|
async ({ locationZoneId }, token2) => {
|
|
1291
|
-
|
|
1292
|
-
if (locationZoneId !== void 0) params.locationZoneId = locationZoneId;
|
|
1293
|
-
return ocsCall(ctx.env, token2, "listLocationZoneElement", params);
|
|
1525
|
+
return ocsCall(ctx.env, token2, "listLocationZoneElement", locationZoneId);
|
|
1294
1526
|
}
|
|
1295
1527
|
)
|
|
1296
1528
|
);
|
|
@@ -1298,9 +1530,12 @@ function registerAllTools(server2, ctx) {
|
|
|
1298
1530
|
"list_detailed_location_zones",
|
|
1299
1531
|
{
|
|
1300
1532
|
title: "List Detailed Location Zones",
|
|
1301
|
-
description:
|
|
1533
|
+
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
1534
|
inputSchema: {
|
|
1303
|
-
resellerId: z.number().optional().describe("Reseller ID (omit to use token owner's reseller)")
|
|
1535
|
+
resellerId: z.number().optional().describe("Reseller ID (omit to use token owner's reseller)"),
|
|
1536
|
+
verbose: z.boolean().optional().describe(
|
|
1537
|
+
"Return the full untouched OCS zone shape instead of the projected zones (smaller operator budget)"
|
|
1538
|
+
)
|
|
1304
1539
|
},
|
|
1305
1540
|
annotations: { readOnlyHint: true }
|
|
1306
1541
|
},
|
|
@@ -1309,9 +1544,16 @@ function registerAllTools(server2, ctx) {
|
|
|
1309
1544
|
"listDetailedLocationZone",
|
|
1310
1545
|
TOOL_SCOPES["list_detailed_location_zones"],
|
|
1311
1546
|
ctx,
|
|
1312
|
-
async ({ resellerId }, token2) => {
|
|
1547
|
+
async ({ resellerId, verbose }, token2) => {
|
|
1313
1548
|
const id = resellerId ?? await getDefaultResellerId(ctx.env, token2);
|
|
1314
|
-
|
|
1549
|
+
const raw = await new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2).call(
|
|
1550
|
+
"listDetailedLocationZone",
|
|
1551
|
+
id
|
|
1552
|
+
);
|
|
1553
|
+
const bounded = boundZonesResult(raw, { resellerId: id, verbose });
|
|
1554
|
+
return {
|
|
1555
|
+
content: [{ type: "text", text: JSON.stringify(bounded) }]
|
|
1556
|
+
};
|
|
1315
1557
|
}
|
|
1316
1558
|
)
|
|
1317
1559
|
);
|
|
@@ -1319,9 +1561,9 @@ function registerAllTools(server2, ctx) {
|
|
|
1319
1561
|
"list_destination_prefixes",
|
|
1320
1562
|
{
|
|
1321
1563
|
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,
|
|
1564
|
+
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
1565
|
inputSchema: {
|
|
1324
|
-
destinationListId: z.number().
|
|
1566
|
+
destinationListId: z.number().describe("Destination list ID (required \u2014 OCS has no unfiltered call for this method)")
|
|
1325
1567
|
},
|
|
1326
1568
|
annotations: { readOnlyHint: true }
|
|
1327
1569
|
},
|
|
@@ -1331,10 +1573,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1331
1573
|
TOOL_SCOPES["list_destination_prefixes"],
|
|
1332
1574
|
ctx,
|
|
1333
1575
|
async ({ destinationListId }, token2) => {
|
|
1334
|
-
|
|
1335
|
-
if (destinationListId !== void 0)
|
|
1336
|
-
params.destinationListId = destinationListId;
|
|
1337
|
-
return ocsCall(ctx.env, token2, "listDestinationListPrefix", params);
|
|
1576
|
+
return ocsCall(ctx.env, token2, "listDestinationListPrefix", destinationListId);
|
|
1338
1577
|
}
|
|
1339
1578
|
)
|
|
1340
1579
|
);
|
|
@@ -1424,9 +1663,18 @@ function registerAllTools(server2, ctx) {
|
|
|
1424
1663
|
"get_tariff",
|
|
1425
1664
|
{
|
|
1426
1665
|
title: "Get Customer Tariff",
|
|
1427
|
-
description:
|
|
1666
|
+
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
1667
|
inputSchema: {
|
|
1429
|
-
resellerId: z.number().optional().describe("Reseller ID (omit to use token owner's reseller)")
|
|
1668
|
+
resellerId: z.number().optional().describe("Reseller ID (omit to use token owner's reseller)"),
|
|
1669
|
+
country: z.string().optional().describe(
|
|
1670
|
+
'Filter by country \u2014 ISO-3166 alpha-2 ("nl") or full name ("Netherlands"), case-insensitive'
|
|
1671
|
+
),
|
|
1672
|
+
trafficType: z.enum(["data", "voice", "sms"]).optional().describe(
|
|
1673
|
+
'Filter to rules whose rate for this traffic type is greater than zero ("data" | "voice" | "sms")'
|
|
1674
|
+
),
|
|
1675
|
+
verbose: z.boolean().optional().describe(
|
|
1676
|
+
`Return the full untouched OCS rule shape instead of the projected rows (capped at ${TARIFF_VERBOSE_MAX_RULES} rows)`
|
|
1677
|
+
)
|
|
1430
1678
|
},
|
|
1431
1679
|
annotations: { readOnlyHint: true }
|
|
1432
1680
|
},
|
|
@@ -1435,9 +1683,21 @@ function registerAllTools(server2, ctx) {
|
|
|
1435
1683
|
"getCustomerTariff",
|
|
1436
1684
|
TOOL_SCOPES["get_tariff"],
|
|
1437
1685
|
ctx,
|
|
1438
|
-
async ({ resellerId }, token2) => {
|
|
1686
|
+
async ({ resellerId, country, trafficType, verbose }, token2) => {
|
|
1439
1687
|
const id = resellerId ?? await getDefaultResellerId(ctx.env, token2);
|
|
1440
|
-
|
|
1688
|
+
const raw = await new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2).call(
|
|
1689
|
+
"getCustomerTariff",
|
|
1690
|
+
id
|
|
1691
|
+
);
|
|
1692
|
+
const bounded = boundTariffResult(raw, {
|
|
1693
|
+
resellerId: id,
|
|
1694
|
+
country,
|
|
1695
|
+
trafficType,
|
|
1696
|
+
verbose
|
|
1697
|
+
});
|
|
1698
|
+
return {
|
|
1699
|
+
content: [{ type: "text", text: JSON.stringify(bounded) }]
|
|
1700
|
+
};
|
|
1441
1701
|
}
|
|
1442
1702
|
)
|
|
1443
1703
|
);
|
|
@@ -1463,7 +1723,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1463
1723
|
async ({ iccid, msisdn, message, sender }, token2) => {
|
|
1464
1724
|
const cache = /* @__PURE__ */ new Map();
|
|
1465
1725
|
const sub = await resolveSubscriberByIccid(ctx.env, token2, iccid, cache);
|
|
1466
|
-
const imsi = sub
|
|
1726
|
+
const imsi = imsiFromSubscriberRecord(sub);
|
|
1467
1727
|
if (typeof imsi !== "string" || imsi.length === 0) {
|
|
1468
1728
|
return {
|
|
1469
1729
|
isError: true,
|
|
@@ -3042,8 +3302,8 @@ async function safeCall(env, token2, method, params = {}) {
|
|
|
3042
3302
|
const client = new OcsClient(env.CARRIER_OCS_BASE_URL, token2);
|
|
3043
3303
|
const data = await client.call(method, params);
|
|
3044
3304
|
return { data, error: null };
|
|
3045
|
-
} catch (
|
|
3046
|
-
return { data: null, error:
|
|
3305
|
+
} catch (err7) {
|
|
3306
|
+
return { data: null, error: err7 instanceof Error ? err7.message : String(err7) };
|
|
3047
3307
|
}
|
|
3048
3308
|
}
|
|
3049
3309
|
async function fetchActiveSubscribers(env, token2, accountId, resellerId) {
|
|
@@ -4925,10 +5185,10 @@ async function callBedrock(env, payload) {
|
|
|
4925
5185
|
const body = await resp.text();
|
|
4926
5186
|
if (resp.status === 429) {
|
|
4927
5187
|
const retryAfter = resp.headers.get("retry-after");
|
|
4928
|
-
const
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
throw
|
|
5188
|
+
const err7 = new Error(`Bedrock rate limit: ${body}`);
|
|
5189
|
+
err7.isRateLimit = true;
|
|
5190
|
+
err7.retryAfter = retryAfter;
|
|
5191
|
+
throw err7;
|
|
4932
5192
|
}
|
|
4933
5193
|
throw new Error(`Bedrock invoke failed: ${resp.status} ${body}`);
|
|
4934
5194
|
}
|
|
@@ -4966,9 +5226,9 @@ Pre-resolved context: ${JSON.stringify(definedContext)}` : "";
|
|
|
4966
5226
|
AWS_REGION: env.AWS_REGION,
|
|
4967
5227
|
BEDROCK_MODEL_ID: env.BEDROCK_MODEL_ID
|
|
4968
5228
|
}, payload);
|
|
4969
|
-
} catch (
|
|
4970
|
-
if (
|
|
4971
|
-
const retryAfter =
|
|
5229
|
+
} catch (err7) {
|
|
5230
|
+
if (err7 instanceof Error && err7.isRateLimit) {
|
|
5231
|
+
const retryAfter = err7.retryAfter;
|
|
4972
5232
|
const parsedSeconds = retryAfter ? parseInt(retryAfter, 10) : 60;
|
|
4973
5233
|
return {
|
|
4974
5234
|
match: "rate_limited",
|
|
@@ -4976,7 +5236,7 @@ Pre-resolved context: ${JSON.stringify(definedContext)}` : "";
|
|
|
4976
5236
|
suggestion: "Bedrock rate limit reached. Please retry after the indicated delay."
|
|
4977
5237
|
};
|
|
4978
5238
|
}
|
|
4979
|
-
throw
|
|
5239
|
+
throw err7;
|
|
4980
5240
|
}
|
|
4981
5241
|
const toolUseBlock = response.content.find(
|
|
4982
5242
|
(block) => block.type === "tool_use"
|
|
@@ -5150,7 +5410,7 @@ function registerAllCarrierAskTools(server2, ctx) {
|
|
|
5150
5410
|
let route;
|
|
5151
5411
|
try {
|
|
5152
5412
|
route = await _routeIntent(intent, context, ctx.env);
|
|
5153
|
-
} catch (
|
|
5413
|
+
} catch (err7) {
|
|
5154
5414
|
writeCarrierAskAudit(ctx.env, {
|
|
5155
5415
|
intent_hash: intentHash,
|
|
5156
5416
|
match: "error",
|
|
@@ -5167,7 +5427,7 @@ function registerAllCarrierAskTools(server2, ctx) {
|
|
|
5167
5427
|
type: "text",
|
|
5168
5428
|
text: JSON.stringify({
|
|
5169
5429
|
error: "routing_error",
|
|
5170
|
-
message:
|
|
5430
|
+
message: err7 instanceof Error ? err7.message : "An unexpected error occurred during routing."
|
|
5171
5431
|
})
|
|
5172
5432
|
}
|
|
5173
5433
|
],
|
|
@@ -5419,8 +5679,8 @@ function registerListRecentOcsEventsTool(server2, env) {
|
|
|
5419
5679
|
return {
|
|
5420
5680
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
5421
5681
|
};
|
|
5422
|
-
} catch (
|
|
5423
|
-
const message =
|
|
5682
|
+
} catch (err7) {
|
|
5683
|
+
const message = err7 instanceof Error ? err7.message : String(err7);
|
|
5424
5684
|
return {
|
|
5425
5685
|
isError: true,
|
|
5426
5686
|
content: [{ type: "text", text: `Error reading OCS event buffer: ${message}` }]
|
|
@@ -5700,47 +5960,100 @@ import {
|
|
|
5700
5960
|
registerAppResource,
|
|
5701
5961
|
RESOURCE_MIME_TYPE
|
|
5702
5962
|
} from "@modelcontextprotocol/ext-apps/server";
|
|
5703
|
-
|
|
5963
|
+
|
|
5964
|
+
// src/apps/fleet-data.ts
|
|
5965
|
+
function extractAccounts(json) {
|
|
5966
|
+
const raw = json;
|
|
5967
|
+
if (Array.isArray(raw)) return raw;
|
|
5968
|
+
return (raw?.reseller ?? []).flatMap((r) => r.account ?? []);
|
|
5969
|
+
}
|
|
5970
|
+
function toFleetInput(raw) {
|
|
5971
|
+
const counts = extractEsimStatusCounts(raw.status);
|
|
5972
|
+
const accountRecords = extractAccounts(raw.accounts);
|
|
5973
|
+
const accounts = accountRecords.map((a) => ({
|
|
5974
|
+
name: String(a.name ?? a.accountId ?? "?"),
|
|
5975
|
+
balance: Number(a.balance ?? 0),
|
|
5976
|
+
// Per-account status breakdown is not in listResellerAccount; the fleet
|
|
5977
|
+
// totals come from the status call. Showing 0 here would read as "this
|
|
5978
|
+
// account has no eSIMs", so the per-account columns stay at the balance
|
|
5979
|
+
// and flags this call actually carries.
|
|
5980
|
+
active: 0,
|
|
5981
|
+
suspended: 0,
|
|
5982
|
+
inventory: 0,
|
|
5983
|
+
other: 0,
|
|
5984
|
+
packageOnly: Boolean(a.packageOnly)
|
|
5985
|
+
}));
|
|
5986
|
+
const unavailable = [];
|
|
5987
|
+
if (raw.statusError) unavailable.push(`esimStatusPerAccount: ${raw.statusError}`);
|
|
5988
|
+
if (raw.accountsError) unavailable.push(`listResellerAccount: ${raw.accountsError}`);
|
|
5989
|
+
return {
|
|
5990
|
+
active: counts.active,
|
|
5991
|
+
suspended: counts.suspended,
|
|
5992
|
+
inventory: counts.inventory,
|
|
5993
|
+
other: counts.other,
|
|
5994
|
+
accounts,
|
|
5995
|
+
unavailable: unavailable.length ? unavailable : void 0
|
|
5996
|
+
};
|
|
5997
|
+
}
|
|
5998
|
+
|
|
5999
|
+
// src/apps/fleet-health-app.ts
|
|
6000
|
+
var FLEET_APP_RESOURCE_URI = "ui://fleet-health-dashboard";
|
|
6001
|
+
async function safeCall2(client, method, params) {
|
|
5704
6002
|
try {
|
|
5705
6003
|
return { data: await client.call(method, params), error: null };
|
|
5706
|
-
} catch (
|
|
5707
|
-
return {
|
|
5708
|
-
data: null,
|
|
5709
|
-
error: err6 instanceof Error ? err6.message : String(err6)
|
|
5710
|
-
};
|
|
6004
|
+
} catch (err7) {
|
|
6005
|
+
return { data: null, error: err7 instanceof Error ? err7.message : String(err7) };
|
|
5711
6006
|
}
|
|
5712
6007
|
}
|
|
6008
|
+
async function loadFleet(ctx, accountId) {
|
|
6009
|
+
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
6010
|
+
const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
6011
|
+
const resellerId = await getDefaultResellerId(ctx.env, token2).catch(() => void 0);
|
|
6012
|
+
const statusParams = accountId !== void 0 ? { accountId } : resellerId !== void 0 ? { resellerId } : {};
|
|
6013
|
+
const accountParams = resellerId !== void 0 ? { resellerId } : {};
|
|
6014
|
+
const [status, accounts] = await Promise.all([
|
|
6015
|
+
safeCall2(client, "esimStatusPerAccount", statusParams),
|
|
6016
|
+
safeCall2(client, "listResellerAccount", accountParams)
|
|
6017
|
+
]);
|
|
6018
|
+
return toFleetInput({
|
|
6019
|
+
status: status.data,
|
|
6020
|
+
accounts: accounts.data,
|
|
6021
|
+
statusError: status.error,
|
|
6022
|
+
accountsError: accounts.error
|
|
6023
|
+
});
|
|
6024
|
+
}
|
|
5713
6025
|
function registerFleetHealthApp(server2, ctx) {
|
|
5714
6026
|
registerAppResource(
|
|
5715
6027
|
server2,
|
|
5716
6028
|
"Fleet Health Dashboard",
|
|
5717
|
-
|
|
6029
|
+
FLEET_APP_RESOURCE_URI,
|
|
5718
6030
|
{
|
|
5719
|
-
description: "
|
|
6031
|
+
description: "Fleet overview panel \u2014 eSIM status, per-account breakdown and low-balance alerts."
|
|
5720
6032
|
},
|
|
5721
6033
|
async () => {
|
|
5722
6034
|
let html;
|
|
5723
6035
|
try {
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
6036
|
+
html = renderHtml(fleetScreen(await loadFleet(ctx)));
|
|
6037
|
+
} catch (err7) {
|
|
6038
|
+
html = renderHtml({
|
|
6039
|
+
id: "fleet-health",
|
|
6040
|
+
title: "Fleet health",
|
|
6041
|
+
sections: [
|
|
6042
|
+
{
|
|
6043
|
+
kind: "note",
|
|
6044
|
+
tone: "critical",
|
|
6045
|
+
text: `Could not load fleet data: ${err7 instanceof Error ? err7.message : String(err7)}`
|
|
6046
|
+
}
|
|
6047
|
+
]
|
|
6048
|
+
});
|
|
5730
6049
|
}
|
|
5731
6050
|
return {
|
|
5732
6051
|
contents: [
|
|
5733
6052
|
{
|
|
5734
|
-
uri:
|
|
6053
|
+
uri: FLEET_APP_RESOURCE_URI,
|
|
5735
6054
|
mimeType: RESOURCE_MIME_TYPE,
|
|
5736
|
-
text: html
|
|
5737
|
-
|
|
5738
|
-
ui: {
|
|
5739
|
-
csp: {
|
|
5740
|
-
resourceDomains: ["https://cdn.jsdelivr.net"]
|
|
5741
|
-
}
|
|
5742
|
-
}
|
|
5743
|
-
}
|
|
6055
|
+
text: html
|
|
6056
|
+
// Self-contained: no external origins, so no CSP exceptions.
|
|
5744
6057
|
}
|
|
5745
6058
|
]
|
|
5746
6059
|
};
|
|
@@ -5751,100 +6064,45 @@ function registerFleetHealthApp(server2, ctx) {
|
|
|
5751
6064
|
"fleet_health_app",
|
|
5752
6065
|
{
|
|
5753
6066
|
title: "Fleet Health Dashboard",
|
|
5754
|
-
description: "
|
|
6067
|
+
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
6068
|
inputSchema: {
|
|
5756
6069
|
accountId: z9.number().optional().describe("Filter to a specific account (omit for all)")
|
|
5757
6070
|
},
|
|
5758
6071
|
annotations: { readOnlyHint: true },
|
|
5759
6072
|
_meta: {
|
|
5760
6073
|
ui: {
|
|
5761
|
-
resourceUri:
|
|
6074
|
+
resourceUri: FLEET_APP_RESOURCE_URI,
|
|
5762
6075
|
visibility: ["model", "app"]
|
|
5763
6076
|
}
|
|
5764
6077
|
}
|
|
5765
6078
|
},
|
|
5766
6079
|
async ({ accountId }) => {
|
|
5767
|
-
const
|
|
5768
|
-
const
|
|
5769
|
-
const
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
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);
|
|
6080
|
+
const input = await loadFleet(ctx, accountId);
|
|
6081
|
+
const screen = fleetScreen(input);
|
|
6082
|
+
const total = input.active + input.suspended + input.inventory + input.other;
|
|
6083
|
+
const lowBalanceCount = input.accounts.filter((a) => a.balance < 10).length;
|
|
6084
|
+
return {
|
|
6085
|
+
content: [
|
|
6086
|
+
{
|
|
6087
|
+
type: "text",
|
|
6088
|
+
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
6089
|
}
|
|
6090
|
+
],
|
|
6091
|
+
structuredContent: {
|
|
6092
|
+
// `screen` is the shared model — the panel and `carrier dash` both
|
|
6093
|
+
// render this exact object, so the two cannot disagree.
|
|
6094
|
+
screen,
|
|
6095
|
+
// Flat fields are the original published contract. Kept so anything
|
|
6096
|
+
// already reading them keeps working; the fix was the data, not the shape.
|
|
6097
|
+
utilization: total > 0 ? Math.round(input.active / total * 1e3) / 10 : 0,
|
|
6098
|
+
totalActive: input.active,
|
|
6099
|
+
totalSuspended: input.suspended,
|
|
6100
|
+
totalInventory: input.inventory,
|
|
6101
|
+
totalOther: input.other,
|
|
6102
|
+
totalAccounts: input.accounts.length,
|
|
6103
|
+
lowBalanceCount,
|
|
6104
|
+
accountList: input.accounts
|
|
5819
6105
|
}
|
|
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
6106
|
};
|
|
5849
6107
|
}
|
|
5850
6108
|
);
|
|
@@ -5901,13 +6159,13 @@ async function deleteWizardSession(env, sub, wizardId) {
|
|
|
5901
6159
|
}
|
|
5902
6160
|
|
|
5903
6161
|
// src/apps/provisioning-wizard.ts
|
|
5904
|
-
async function
|
|
6162
|
+
async function safeCallWithToken(client, _token, method, params = {}) {
|
|
5905
6163
|
try {
|
|
5906
6164
|
return { data: await client.call(method, params), error: null };
|
|
5907
|
-
} catch (
|
|
6165
|
+
} catch (err7) {
|
|
5908
6166
|
return {
|
|
5909
6167
|
data: null,
|
|
5910
|
-
error:
|
|
6168
|
+
error: err7 instanceof Error ? err7.message : String(err7)
|
|
5911
6169
|
};
|
|
5912
6170
|
}
|
|
5913
6171
|
}
|
|
@@ -5973,7 +6231,7 @@ function registerProvisioningWizard(server2, ctx) {
|
|
|
5973
6231
|
const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
5974
6232
|
if (step === "init") {
|
|
5975
6233
|
const newWizardId = generateWizardId();
|
|
5976
|
-
const subscribersResult = await
|
|
6234
|
+
const subscribersResult = await safeCallWithToken(
|
|
5977
6235
|
client,
|
|
5978
6236
|
token2,
|
|
5979
6237
|
"listResellerAccount",
|
|
@@ -6051,7 +6309,7 @@ function registerProvisioningWizard(server2, ctx) {
|
|
|
6051
6309
|
};
|
|
6052
6310
|
}
|
|
6053
6311
|
const resellerIdForTemplates = await getDefaultResellerId(ctx.env, token2);
|
|
6054
|
-
const packagesResult = await
|
|
6312
|
+
const packagesResult = await safeCallWithToken(
|
|
6055
6313
|
client,
|
|
6056
6314
|
token2,
|
|
6057
6315
|
"listPrepaidPackageTemplate",
|
|
@@ -6118,7 +6376,7 @@ function registerProvisioningWizard(server2, ctx) {
|
|
|
6118
6376
|
isError: true
|
|
6119
6377
|
};
|
|
6120
6378
|
}
|
|
6121
|
-
const previewResult = await
|
|
6379
|
+
const previewResult = await safeCallWithToken(
|
|
6122
6380
|
client,
|
|
6123
6381
|
token2,
|
|
6124
6382
|
"listPrepaidPackageTemplate",
|
|
@@ -6190,7 +6448,7 @@ function registerProvisioningWizard(server2, ctx) {
|
|
|
6190
6448
|
isError: true
|
|
6191
6449
|
};
|
|
6192
6450
|
}
|
|
6193
|
-
const subRecord = await
|
|
6451
|
+
const subRecord = await safeCallWithToken(
|
|
6194
6452
|
client,
|
|
6195
6453
|
token2,
|
|
6196
6454
|
"getSingleSubscriber",
|
|
@@ -6219,7 +6477,7 @@ function registerProvisioningWizard(server2, ctx) {
|
|
|
6219
6477
|
isError: true
|
|
6220
6478
|
};
|
|
6221
6479
|
}
|
|
6222
|
-
const result2 = await
|
|
6480
|
+
const result2 = await safeCallWithToken(
|
|
6223
6481
|
client,
|
|
6224
6482
|
token2,
|
|
6225
6483
|
"affectPackageToSubscriber",
|
|
@@ -6264,13 +6522,13 @@ import {
|
|
|
6264
6522
|
registerAppResource as registerAppResource3,
|
|
6265
6523
|
RESOURCE_MIME_TYPE as RESOURCE_MIME_TYPE3
|
|
6266
6524
|
} from "@modelcontextprotocol/ext-apps/server";
|
|
6267
|
-
async function
|
|
6525
|
+
async function safeCallWithToken2(client, _token, method, params = {}) {
|
|
6268
6526
|
try {
|
|
6269
6527
|
return { data: await client.call(method, params), error: null };
|
|
6270
|
-
} catch (
|
|
6528
|
+
} catch (err7) {
|
|
6271
6529
|
return {
|
|
6272
6530
|
data: null,
|
|
6273
|
-
error:
|
|
6531
|
+
error: err7 instanceof Error ? err7.message : String(err7)
|
|
6274
6532
|
};
|
|
6275
6533
|
}
|
|
6276
6534
|
}
|
|
@@ -6332,7 +6590,7 @@ function registerBalanceTopupApp(server2, ctx) {
|
|
|
6332
6590
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
6333
6591
|
const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
6334
6592
|
if (preview === true) {
|
|
6335
|
-
const result2 = await
|
|
6593
|
+
const result2 = await safeCallWithToken2(
|
|
6336
6594
|
client,
|
|
6337
6595
|
token2,
|
|
6338
6596
|
"getSingleSubscriber",
|
|
@@ -6357,7 +6615,7 @@ function registerBalanceTopupApp(server2, ctx) {
|
|
|
6357
6615
|
structuredContent: structured2
|
|
6358
6616
|
};
|
|
6359
6617
|
}
|
|
6360
|
-
const execResult = await
|
|
6618
|
+
const execResult = await safeCallWithToken2(
|
|
6361
6619
|
client,
|
|
6362
6620
|
token2,
|
|
6363
6621
|
// CAR-78: subscriber top-up uses modifySubscriberBalance with { subscriber, amount }
|
|
@@ -7931,8 +8189,8 @@ function buildPricingHandler(tool, ctx) {
|
|
|
7931
8189
|
}
|
|
7932
8190
|
]
|
|
7933
8191
|
};
|
|
7934
|
-
} catch (
|
|
7935
|
-
const message =
|
|
8192
|
+
} catch (err7) {
|
|
8193
|
+
const message = err7 instanceof Error ? err7.message : String(err7);
|
|
7936
8194
|
ctx.audit({
|
|
7937
8195
|
tool_name: tool.name,
|
|
7938
8196
|
ocs_method: "billing",
|
|
@@ -8252,8 +8510,8 @@ function writeUsageThresholdAudit(env, severity, remainingCredits, month, taskCo
|
|
|
8252
8510
|
doubles: [remainingCredits],
|
|
8253
8511
|
indexes: ["manus_usage"]
|
|
8254
8512
|
});
|
|
8255
|
-
} catch (
|
|
8256
|
-
console.error(`[manus-usage] threshold audit write failed: ${
|
|
8513
|
+
} catch (err7) {
|
|
8514
|
+
console.error(`[manus-usage] threshold audit write failed: ${err7.message}`);
|
|
8257
8515
|
}
|
|
8258
8516
|
}
|
|
8259
8517
|
|
|
@@ -8363,7 +8621,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8363
8621
|
}
|
|
8364
8622
|
]
|
|
8365
8623
|
};
|
|
8366
|
-
} catch (
|
|
8624
|
+
} catch (err7) {
|
|
8367
8625
|
ctx.audit({
|
|
8368
8626
|
tool_name: "ui_agent_schedule_create",
|
|
8369
8627
|
ocs_method: "[manus:schedule.create]",
|
|
@@ -8372,7 +8630,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8372
8630
|
duration_ms: Date.now() - start,
|
|
8373
8631
|
event_type: "ui_agent_dispatch"
|
|
8374
8632
|
});
|
|
8375
|
-
const msg =
|
|
8633
|
+
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
8376
8634
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8377
8635
|
}
|
|
8378
8636
|
}
|
|
@@ -8430,7 +8688,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8430
8688
|
}
|
|
8431
8689
|
]
|
|
8432
8690
|
};
|
|
8433
|
-
} catch (
|
|
8691
|
+
} catch (err7) {
|
|
8434
8692
|
ctx.audit({
|
|
8435
8693
|
tool_name: "ui_agent_schedule_list",
|
|
8436
8694
|
ocs_method: "[manus:schedule.list]",
|
|
@@ -8438,7 +8696,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8438
8696
|
dry_run: false,
|
|
8439
8697
|
duration_ms: Date.now() - start
|
|
8440
8698
|
});
|
|
8441
|
-
const msg =
|
|
8699
|
+
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
8442
8700
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8443
8701
|
}
|
|
8444
8702
|
}
|
|
@@ -8499,7 +8757,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8499
8757
|
}
|
|
8500
8758
|
]
|
|
8501
8759
|
};
|
|
8502
|
-
} catch (
|
|
8760
|
+
} catch (err7) {
|
|
8503
8761
|
ctx.audit({
|
|
8504
8762
|
tool_name: "ui_agent_schedule_delete",
|
|
8505
8763
|
ocs_method: "[manus:schedule.delete]",
|
|
@@ -8508,7 +8766,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8508
8766
|
duration_ms: Date.now() - start,
|
|
8509
8767
|
event_type: "ui_agent_dispatch"
|
|
8510
8768
|
});
|
|
8511
|
-
const msg =
|
|
8769
|
+
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
8512
8770
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8513
8771
|
}
|
|
8514
8772
|
}
|
|
@@ -8569,7 +8827,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8569
8827
|
}
|
|
8570
8828
|
]
|
|
8571
8829
|
};
|
|
8572
|
-
} catch (
|
|
8830
|
+
} catch (err7) {
|
|
8573
8831
|
ctx.audit({
|
|
8574
8832
|
tool_name: "ui_agent_schedule_pause",
|
|
8575
8833
|
ocs_method: "[manus:schedule.pause]",
|
|
@@ -8578,7 +8836,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8578
8836
|
duration_ms: Date.now() - start,
|
|
8579
8837
|
event_type: "ui_agent_dispatch"
|
|
8580
8838
|
});
|
|
8581
|
-
const msg =
|
|
8839
|
+
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
8582
8840
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8583
8841
|
}
|
|
8584
8842
|
}
|
|
@@ -8639,7 +8897,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8639
8897
|
}
|
|
8640
8898
|
]
|
|
8641
8899
|
};
|
|
8642
|
-
} catch (
|
|
8900
|
+
} catch (err7) {
|
|
8643
8901
|
ctx.audit({
|
|
8644
8902
|
tool_name: "ui_agent_schedule_resume",
|
|
8645
8903
|
ocs_method: "[manus:schedule.resume]",
|
|
@@ -8648,7 +8906,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8648
8906
|
duration_ms: Date.now() - start,
|
|
8649
8907
|
event_type: "ui_agent_dispatch"
|
|
8650
8908
|
});
|
|
8651
|
-
const msg =
|
|
8909
|
+
const msg = err7 instanceof ManusScheduleError ? `Manus API error (HTTP ${err7.statusCode}): ${err7.message}` : err7 instanceof Error ? err7.message : String(err7);
|
|
8652
8910
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8653
8911
|
}
|
|
8654
8912
|
}
|
|
@@ -8693,7 +8951,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8693
8951
|
}
|
|
8694
8952
|
]
|
|
8695
8953
|
};
|
|
8696
|
-
} catch (
|
|
8954
|
+
} catch (err7) {
|
|
8697
8955
|
ctx.audit({
|
|
8698
8956
|
tool_name: "ui_agent_usage",
|
|
8699
8957
|
ocs_method: "[manus:usage.get]",
|
|
@@ -8701,7 +8959,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8701
8959
|
dry_run: false,
|
|
8702
8960
|
duration_ms: Date.now() - start
|
|
8703
8961
|
});
|
|
8704
|
-
const msg =
|
|
8962
|
+
const msg = err7 instanceof Error ? err7.message : String(err7);
|
|
8705
8963
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8706
8964
|
}
|
|
8707
8965
|
}
|
|
@@ -8944,7 +9202,7 @@ Report success or failure with a clear summary.`;
|
|
|
8944
9202
|
}
|
|
8945
9203
|
]
|
|
8946
9204
|
};
|
|
8947
|
-
} catch (
|
|
9205
|
+
} catch (err7) {
|
|
8948
9206
|
ctx.audit({
|
|
8949
9207
|
tool_name: toolName,
|
|
8950
9208
|
ocs_method: `[ui-agent:${gapId}]`,
|
|
@@ -8958,7 +9216,7 @@ Report success or failure with a clear summary.`;
|
|
|
8958
9216
|
content: [
|
|
8959
9217
|
{
|
|
8960
9218
|
type: "text",
|
|
8961
|
-
text: `Error dispatching UI agent: ${
|
|
9219
|
+
text: `Error dispatching UI agent: ${err7 instanceof Error ? err7.message : String(err7)}`
|
|
8962
9220
|
}
|
|
8963
9221
|
]
|
|
8964
9222
|
};
|
|
@@ -9382,7 +9640,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
9382
9640
|
try {
|
|
9383
9641
|
const replyOutcome = await askReply(keys, task_id, reply);
|
|
9384
9642
|
result2 = replyOutcome.data;
|
|
9385
|
-
} catch (
|
|
9643
|
+
} catch (err7) {
|
|
9386
9644
|
ctx.audit({
|
|
9387
9645
|
tool_name: "ui_agent_reply",
|
|
9388
9646
|
ocs_method: "[ui-agent:ask-reply]",
|
|
@@ -9397,7 +9655,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
9397
9655
|
content: [
|
|
9398
9656
|
{
|
|
9399
9657
|
type: "text",
|
|
9400
|
-
text: `Error calling Manus task.reply: ${
|
|
9658
|
+
text: `Error calling Manus task.reply: ${err7 instanceof Error ? err7.message : String(err7)}`
|
|
9401
9659
|
}
|
|
9402
9660
|
]
|
|
9403
9661
|
};
|
|
@@ -9475,13 +9733,13 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
9475
9733
|
try {
|
|
9476
9734
|
const listing = await ctx.env.CARRIER_USERS.list({ prefix: PENDING_ASK_PREFIX });
|
|
9477
9735
|
keys = listing.keys;
|
|
9478
|
-
} catch (
|
|
9736
|
+
} catch (err7) {
|
|
9479
9737
|
return {
|
|
9480
9738
|
isError: true,
|
|
9481
9739
|
content: [
|
|
9482
9740
|
{
|
|
9483
9741
|
type: "text",
|
|
9484
|
-
text: `Error listing pending tasks: ${
|
|
9742
|
+
text: `Error listing pending tasks: ${err7 instanceof Error ? err7.message : String(err7)}`
|
|
9485
9743
|
}
|
|
9486
9744
|
]
|
|
9487
9745
|
};
|
|
@@ -10935,6 +11193,138 @@ function registerStorefrontLogoTools(server2, env = {}) {
|
|
|
10935
11193
|
);
|
|
10936
11194
|
}
|
|
10937
11195
|
|
|
11196
|
+
// src/tools-storefront-deploy.ts
|
|
11197
|
+
import { z as z22 } from "zod";
|
|
11198
|
+
var ok6 = (value) => ({
|
|
11199
|
+
content: [{ type: "text", text: JSON.stringify(value) }]
|
|
11200
|
+
});
|
|
11201
|
+
var err6 = (text) => ({ isError: true, content: [{ type: "text", text }] });
|
|
11202
|
+
function registerStorefrontDeployTools(server2) {
|
|
11203
|
+
server2.tool(
|
|
11204
|
+
"list_deploy_targets",
|
|
11205
|
+
"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.",
|
|
11206
|
+
{
|
|
11207
|
+
dir: z22.string().min(1).describe("Path to the scaffolded storefront directory")
|
|
11208
|
+
},
|
|
11209
|
+
async ({ dir }) => {
|
|
11210
|
+
try {
|
|
11211
|
+
const statuses = await probeAll(dir);
|
|
11212
|
+
const ranked = rankTargets(statuses);
|
|
11213
|
+
return ok6({
|
|
11214
|
+
targets: statuses,
|
|
11215
|
+
default: ranked[0]?.id ?? null,
|
|
11216
|
+
reason: ranked.length ? void 0 : "No host is installed and logged in."
|
|
11217
|
+
});
|
|
11218
|
+
} catch (e) {
|
|
11219
|
+
return err6(e instanceof Error ? e.message : "target probe failed");
|
|
11220
|
+
}
|
|
11221
|
+
}
|
|
11222
|
+
);
|
|
11223
|
+
server2.tool(
|
|
11224
|
+
"deploy_storefront",
|
|
11225
|
+
"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.",
|
|
11226
|
+
{
|
|
11227
|
+
dir: z22.string().min(1).describe("Path to the scaffolded storefront directory"),
|
|
11228
|
+
target: z22.enum(TARGET_IDS).optional().describe("Force a host instead of auto-detecting"),
|
|
11229
|
+
name: z22.string().optional().describe("Project/worker name (defaults from brand)"),
|
|
11230
|
+
skip_build: z22.boolean().optional().describe("Deploy the existing build without rebuilding"),
|
|
11231
|
+
push_secrets: z22.boolean().optional().describe("Stage runtime secrets on the host before deploy (default true)"),
|
|
11232
|
+
verify: z22.boolean().optional().describe("Probe the deployed URL and report whether it actually serves (default true)"),
|
|
11233
|
+
domain: z22.boolean().optional().describe("Route the brand domain to the deployment (Cloudflare provisions DNS + cert)")
|
|
11234
|
+
},
|
|
11235
|
+
async ({ dir, target, name, skip_build, push_secrets, verify, domain }) => {
|
|
11236
|
+
try {
|
|
11237
|
+
if (target && !isTargetId(target)) return err6(`Unknown target "${target}".`);
|
|
11238
|
+
const brand = await loadStorefrontBrand(dir, name ? { name } : void 0);
|
|
11239
|
+
const preferred = target;
|
|
11240
|
+
if (!skip_build) {
|
|
11241
|
+
const ranked = rankTargets(await probeAll(dir));
|
|
11242
|
+
const buildFor = preferred ?? ranked[0]?.id ?? "cloudflare";
|
|
11243
|
+
const built = await buildSite(dir, buildFor);
|
|
11244
|
+
if (!built) return err6(`Build failed for ${buildFor}. Run the build locally to see why.`);
|
|
11245
|
+
}
|
|
11246
|
+
const result2 = await deploySite(dir, brand, {
|
|
11247
|
+
preferred,
|
|
11248
|
+
pushSecrets: push_secrets !== false,
|
|
11249
|
+
customDomain: domain === true
|
|
11250
|
+
});
|
|
11251
|
+
if (!result2.ok) {
|
|
11252
|
+
return err6(result2.reason ?? "Deploy failed.");
|
|
11253
|
+
}
|
|
11254
|
+
const verification = verify !== false && result2.url ? await verifyStorefront(result2.url, dir, process.env) : void 0;
|
|
11255
|
+
return ok6({
|
|
11256
|
+
deployed: true,
|
|
11257
|
+
target: result2.target,
|
|
11258
|
+
project: result2.projectName,
|
|
11259
|
+
url: result2.url ?? null,
|
|
11260
|
+
secrets_staged: result2.secrets?.pushed ?? [],
|
|
11261
|
+
secrets_failed: result2.secrets?.failed ?? [],
|
|
11262
|
+
verified: verification ? verification.ok : null,
|
|
11263
|
+
diagnosis: verification?.ok === false ? verification.diagnosis : void 0,
|
|
11264
|
+
probes: verification ? formatProbes(verification.probes) : void 0,
|
|
11265
|
+
repair_hint: verification && !verification.ok ? repairPlanFor(verification.diagnosis)?.note ?? verification.summary : void 0,
|
|
11266
|
+
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."
|
|
11267
|
+
});
|
|
11268
|
+
} catch (e) {
|
|
11269
|
+
return err6(e instanceof Error ? e.message : "deploy failed");
|
|
11270
|
+
}
|
|
11271
|
+
}
|
|
11272
|
+
);
|
|
11273
|
+
server2.tool(
|
|
11274
|
+
"provision_storefront_clerk",
|
|
11275
|
+
"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.",
|
|
11276
|
+
{
|
|
11277
|
+
dir: z22.string().min(1).describe("Path to the scaffolded storefront directory"),
|
|
11278
|
+
name: z22.string().optional().describe("Clerk application name (defaults from brand)"),
|
|
11279
|
+
production: z22.boolean().optional().describe("Also create a production instance (Platform API only)"),
|
|
11280
|
+
no_create: z22.boolean().optional().describe("Never create a new application; only reuse existing keys"),
|
|
11281
|
+
url: z22.string().optional().describe("Deployed URL to whitelist as origin and redirect")
|
|
11282
|
+
},
|
|
11283
|
+
async ({ dir, name, production, no_create, url }) => {
|
|
11284
|
+
try {
|
|
11285
|
+
const brand = await loadStorefrontBrand(dir, name ? { name } : void 0);
|
|
11286
|
+
const result2 = await provisionClerk({
|
|
11287
|
+
name: name ?? brand.name,
|
|
11288
|
+
domain: brand.domain,
|
|
11289
|
+
production: production === true,
|
|
11290
|
+
noCreate: no_create === true,
|
|
11291
|
+
env: process.env,
|
|
11292
|
+
storefront: dir,
|
|
11293
|
+
cli: clerkCliDeps()
|
|
11294
|
+
});
|
|
11295
|
+
if (!result2.ok || !result2.credentials) {
|
|
11296
|
+
return ok6({
|
|
11297
|
+
provisioned: false,
|
|
11298
|
+
tier: result2.tier,
|
|
11299
|
+
reason: result2.reason,
|
|
11300
|
+
guidance: result2.guidance ?? []
|
|
11301
|
+
});
|
|
11302
|
+
}
|
|
11303
|
+
const written = await mergeEnvLocal(dir, {
|
|
11304
|
+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: result2.credentials.publishableKey,
|
|
11305
|
+
CLERK_SECRET_KEY: result2.credentials.secretKey
|
|
11306
|
+
});
|
|
11307
|
+
const { allowedOrigins, redirectUrls } = storefrontClerkUrls(url, brand.domain);
|
|
11308
|
+
const configured = await configureClerkInstance(result2.credentials.secretKey, {
|
|
11309
|
+
allowedOrigins,
|
|
11310
|
+
redirectUrls
|
|
11311
|
+
});
|
|
11312
|
+
return ok6({
|
|
11313
|
+
provisioned: true,
|
|
11314
|
+
tier: result2.tier,
|
|
11315
|
+
application_id: result2.credentials.applicationId ?? null,
|
|
11316
|
+
instance_id: result2.credentials.instanceId ?? null,
|
|
11317
|
+
env_written: written,
|
|
11318
|
+
configured: configured.applied,
|
|
11319
|
+
configure_failures: configured.failed
|
|
11320
|
+
});
|
|
11321
|
+
} catch (e) {
|
|
11322
|
+
return err6(e instanceof Error ? e.message : "clerk provisioning failed");
|
|
11323
|
+
}
|
|
11324
|
+
}
|
|
11325
|
+
);
|
|
11326
|
+
}
|
|
11327
|
+
|
|
10938
11328
|
// src/index.ts
|
|
10939
11329
|
var baseUrl = process.env.CARRIER_OCS_BASE_URL ?? process.env.ESIMVAULT_BASE_URL ?? "https://ocs.esimvault.cloud";
|
|
10940
11330
|
var token = process.env.CARRIER_OCS_API_TOKEN ?? process.env.ESIMVAULT_API_TOKEN;
|
|
@@ -11003,6 +11393,7 @@ registerWalletTools(server, { env: stdioEnv, props });
|
|
|
11003
11393
|
registerGreenzoneTools(server, toolCtx);
|
|
11004
11394
|
registerUiAgentGenericTools(server, toolCtx);
|
|
11005
11395
|
registerStorefrontLogoTools(server, stdioEnv);
|
|
11396
|
+
registerStorefrontDeployTools(server);
|
|
11006
11397
|
registerAllPrompts(server);
|
|
11007
11398
|
var transport = new StdioServerTransport();
|
|
11008
11399
|
await server.connect(transport);
|