@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/chunk-6OI56RSR.js +2452 -0
- package/dist/chunk-6OI56RSR.js.map +1 -0
- package/dist/chunk-SHKKVIIA.js +114 -0
- package/dist/chunk-SHKKVIIA.js.map +1 -0
- package/dist/cli.js +891 -369
- 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 +605 -216
- 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-IYCGWBQU.js +0 -461
- package/dist/chunk-IYCGWBQU.js.map +0 -1
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
|
-
|
|
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
|
-
|
|
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 (
|
|
547
|
+
} catch (err7) {
|
|
314
548
|
try {
|
|
315
|
-
Sentry.captureException(
|
|
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 =
|
|
325
|
-
const ocsCode =
|
|
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
|
-
|
|
469
|
-
|
|
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
|
|
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
|
|
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
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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().
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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().
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 (
|
|
3046
|
-
return { data: null, error:
|
|
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) =>
|
|
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
|
|
3304
|
-
const
|
|
3305
|
-
const
|
|
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(`-
|
|
3310
|
-
sections.push(`-
|
|
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
|
|
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
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
throw
|
|
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 (
|
|
4970
|
-
if (
|
|
4971
|
-
const 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
|
|
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 (
|
|
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:
|
|
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 (
|
|
5423
|
-
const message =
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
6027
|
+
FLEET_APP_RESOURCE_URI,
|
|
5718
6028
|
{
|
|
5719
|
-
description: "
|
|
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
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
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:
|
|
6051
|
+
uri: FLEET_APP_RESOURCE_URI,
|
|
5735
6052
|
mimeType: RESOURCE_MIME_TYPE,
|
|
5736
|
-
text: html
|
|
5737
|
-
|
|
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: "
|
|
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:
|
|
6072
|
+
resourceUri: FLEET_APP_RESOURCE_URI,
|
|
5762
6073
|
visibility: ["model", "app"]
|
|
5763
6074
|
}
|
|
5764
6075
|
}
|
|
5765
6076
|
},
|
|
5766
6077
|
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);
|
|
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
|
|
6160
|
+
async function safeCallWithToken(client, _token, method, params = {}) {
|
|
5905
6161
|
try {
|
|
5906
6162
|
return { data: await client.call(method, params), error: null };
|
|
5907
|
-
} catch (
|
|
6163
|
+
} catch (err7) {
|
|
5908
6164
|
return {
|
|
5909
6165
|
data: null,
|
|
5910
|
-
error:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6523
|
+
async function safeCallWithToken2(client, _token, method, params = {}) {
|
|
6268
6524
|
try {
|
|
6269
6525
|
return { data: await client.call(method, params), error: null };
|
|
6270
|
-
} catch (
|
|
6526
|
+
} catch (err7) {
|
|
6271
6527
|
return {
|
|
6272
6528
|
data: null,
|
|
6273
|
-
error:
|
|
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
|
|
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
|
|
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 (
|
|
7935
|
-
const message =
|
|
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 (
|
|
8256
|
-
console.error(`[manus-usage] threshold audit write failed: ${
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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: ${
|
|
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 (
|
|
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: ${
|
|
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 (
|
|
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: ${
|
|
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);
|