@carrierllc/mcp 0.1.0 → 0.2.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/apps/app-state.d.ts +19 -0
- package/dist/apps/app-state.d.ts.map +1 -0
- package/dist/apps/app-state.js +28 -0
- package/dist/apps/app-state.js.map +1 -0
- package/dist/apps/balance-topup.d.ts +19 -0
- package/dist/apps/balance-topup.d.ts.map +1 -0
- package/dist/apps/balance-topup.js +121 -0
- package/dist/apps/balance-topup.js.map +1 -0
- package/dist/apps/fleet-health-app.d.ts +29 -0
- package/dist/apps/fleet-health-app.d.ts.map +1 -0
- package/dist/apps/fleet-health-app.js +152 -0
- package/dist/apps/fleet-health-app.js.map +1 -0
- package/dist/apps/index.d.ts +37 -0
- package/dist/apps/index.d.ts.map +1 -0
- package/dist/apps/index.js +42 -0
- package/dist/apps/index.js.map +1 -0
- package/dist/apps/provisioning-wizard.d.ts +11 -0
- package/dist/apps/provisioning-wizard.d.ts.map +1 -0
- package/dist/apps/provisioning-wizard.js +345 -0
- package/dist/apps/provisioning-wizard.js.map +1 -0
- package/dist/audit.d.ts +31 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +42 -0
- package/dist/audit.js.map +1 -0
- package/dist/billing-thresholds.d.ts +71 -0
- package/dist/billing-thresholds.d.ts.map +1 -0
- package/dist/billing-thresholds.js +238 -0
- package/dist/billing-thresholds.js.map +1 -0
- package/dist/billing.d.ts +95 -0
- package/dist/billing.d.ts.map +1 -0
- package/dist/billing.js +239 -0
- package/dist/billing.js.map +1 -0
- package/dist/clerk.d.ts +125 -0
- package/dist/clerk.d.ts.map +1 -0
- package/dist/clerk.js +389 -0
- package/dist/clerk.js.map +1 -0
- package/dist/client.d.ts +12 -15
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +48 -55
- package/dist/client.js.map +1 -1
- package/dist/credits.d.ts +165 -0
- package/dist/credits.d.ts.map +1 -0
- package/dist/credits.js +483 -0
- package/dist/credits.js.map +1 -0
- package/dist/index.d.ts +6 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -23
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +2 -8
- package/dist/intelligence.d.ts.map +1 -1
- package/dist/intelligence.js +321 -154
- package/dist/intelligence.js.map +1 -1
- package/dist/list-recent-ocs-events.d.ts +48 -0
- package/dist/list-recent-ocs-events.d.ts.map +1 -0
- package/dist/list-recent-ocs-events.js +160 -0
- package/dist/list-recent-ocs-events.js.map +1 -0
- package/dist/manus-client.d.ts +429 -0
- package/dist/manus-client.d.ts.map +1 -0
- package/dist/manus-client.js +401 -0
- package/dist/manus-client.js.map +1 -0
- package/dist/manus-common.d.ts +5 -0
- package/dist/manus-common.d.ts.map +1 -0
- package/dist/manus-common.js +5 -0
- package/dist/manus-common.js.map +1 -0
- package/dist/manus-schedule.d.ts +74 -0
- package/dist/manus-schedule.d.ts.map +1 -0
- package/dist/manus-schedule.js +213 -0
- package/dist/manus-schedule.js.map +1 -0
- package/dist/manus-usage.d.ts +32 -0
- package/dist/manus-usage.d.ts.map +1 -0
- package/dist/manus-usage.js +120 -0
- package/dist/manus-usage.js.map +1 -0
- package/dist/manus-webhook.d.ts +60 -0
- package/dist/manus-webhook.d.ts.map +1 -0
- package/dist/manus-webhook.js +361 -0
- package/dist/manus-webhook.js.map +1 -0
- package/dist/pricing-tools.d.ts +26 -0
- package/dist/pricing-tools.d.ts.map +1 -0
- package/dist/pricing-tools.js +428 -0
- package/dist/pricing-tools.js.map +1 -0
- package/dist/projects-tools.d.ts +39 -0
- package/dist/projects-tools.d.ts.map +1 -0
- package/dist/projects-tools.js +545 -0
- package/dist/projects-tools.js.map +1 -0
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +1 -0
- package/dist/prompts.js.map +1 -1
- package/dist/stripe-connect-tools.d.ts +33 -0
- package/dist/stripe-connect-tools.d.ts.map +1 -0
- package/dist/stripe-connect-tools.js +504 -0
- package/dist/stripe-connect-tools.js.map +1 -0
- package/dist/timing-safe-equal.d.ts +5 -0
- package/dist/timing-safe-equal.d.ts.map +1 -0
- package/dist/timing-safe-equal.js +13 -0
- package/dist/timing-safe-equal.js.map +1 -0
- package/dist/tools-backlog.d.ts +28 -0
- package/dist/tools-backlog.d.ts.map +1 -0
- package/dist/tools-backlog.js +315 -0
- package/dist/tools-backlog.js.map +1 -0
- package/dist/tools-carrier-ask.d.ts +138 -0
- package/dist/tools-carrier-ask.d.ts.map +1 -0
- package/dist/tools-carrier-ask.js +913 -0
- package/dist/tools-carrier-ask.js.map +1 -0
- package/dist/tools-pricing.d.ts +18 -0
- package/dist/tools-pricing.d.ts.map +1 -0
- package/dist/tools-pricing.js +172 -0
- package/dist/tools-pricing.js.map +1 -0
- package/dist/tools-ui-agent-ask.d.ts +29 -0
- package/dist/tools-ui-agent-ask.d.ts.map +1 -0
- package/dist/tools-ui-agent-ask.js +272 -0
- package/dist/tools-ui-agent-ask.js.map +1 -0
- package/dist/tools-ui-agent-schedule.d.ts +29 -0
- package/dist/tools-ui-agent-schedule.d.ts.map +1 -0
- package/dist/tools-ui-agent-schedule.js +517 -0
- package/dist/tools-ui-agent-schedule.js.map +1 -0
- package/dist/tools-ui-agent.d.ts +40 -0
- package/dist/tools-ui-agent.d.ts.map +1 -0
- package/dist/tools-ui-agent.js +508 -0
- package/dist/tools-ui-agent.js.map +1 -0
- package/dist/tools.d.ts +44 -13
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +794 -201
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +291 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +14 -8
package/dist/intelligence.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Carrier MCP stdio — Intelligence tools (8 composites).
|
|
3
|
-
*
|
|
4
|
-
* These tools chain multiple OCS read methods to produce structured insights.
|
|
5
|
-
* Stdio variant: no scope enforcement, no audit hook, no Sentry.
|
|
6
|
-
*/
|
|
7
1
|
import { z } from "zod";
|
|
8
|
-
|
|
2
|
+
import { OcsClient } from "./client.js";
|
|
3
|
+
import { getDefaultResellerId } from "./tools.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
async function safeCall(env, token, method, params = {}) {
|
|
9
8
|
try {
|
|
10
|
-
|
|
9
|
+
const client = new OcsClient(env.CARRIER_OCS_BASE_URL, token);
|
|
10
|
+
const data = await client.call(method, params);
|
|
11
|
+
return { data, error: null };
|
|
11
12
|
}
|
|
12
13
|
catch (err) {
|
|
13
14
|
return { data: null, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -21,7 +22,9 @@ function formatBytes(bytes) {
|
|
|
21
22
|
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
|
|
22
23
|
}
|
|
23
24
|
function daysUntil(dateStr) {
|
|
24
|
-
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const target = new Date(dateStr);
|
|
27
|
+
return Math.ceil((target.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
25
28
|
}
|
|
26
29
|
function toISODate(d) {
|
|
27
30
|
return d.toISOString().split("T")[0];
|
|
@@ -29,60 +32,60 @@ function toISODate(d) {
|
|
|
29
32
|
function result(text, isError = false) {
|
|
30
33
|
return { content: [{ type: "text", text }], ...(isError ? { isError: true } : {}) };
|
|
31
34
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
return result(`Error: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
export function registerAllIntelligenceTools(server, client) {
|
|
43
|
-
// -------------------------------------------------------------------------
|
|
44
|
-
// 1. DIAGNOSE SUBSCRIBER
|
|
45
|
-
// -------------------------------------------------------------------------
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// 1. DIAGNOSE SUBSCRIBER — "why is this subscriber offline?"
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
export function registerIntelligenceTools(server, ctx) {
|
|
46
39
|
server.registerTool("diagnose_subscriber", {
|
|
47
40
|
title: "Diagnose Subscriber Issues",
|
|
48
|
-
description: "Smart diagnostic that chains multiple API calls to
|
|
49
|
-
|
|
41
|
+
description: "Smart diagnostic that chains multiple API calls to analyze why a subscriber " +
|
|
42
|
+
"may be offline, throttled, or having connectivity issues. Returns a structured " +
|
|
43
|
+
"diagnosis with root cause analysis and recommended actions.",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
iccid: z.string().describe("The subscriber ICCID to diagnose"),
|
|
46
|
+
},
|
|
50
47
|
annotations: { readOnlyHint: true },
|
|
51
|
-
},
|
|
48
|
+
}, async ({ iccid }) => {
|
|
49
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
52
50
|
const findings = [];
|
|
53
51
|
const actions = [];
|
|
54
52
|
let severity = "healthy";
|
|
55
|
-
|
|
53
|
+
// 1. Get subscriber details
|
|
54
|
+
const sub = await safeCall(ctx.env, token, "getSingleSubscriber", { iccid });
|
|
56
55
|
if (sub.error)
|
|
57
56
|
return result(`Failed to fetch subscriber: ${sub.error}`, true);
|
|
58
57
|
if (!sub.data)
|
|
59
58
|
return result("Subscriber not found", true);
|
|
60
59
|
const status = String(sub.data.status ?? "").toUpperCase();
|
|
61
60
|
const balance = Number(sub.data.balance ?? 0);
|
|
61
|
+
// Check OCS status
|
|
62
62
|
if (status !== "ACTIVE") {
|
|
63
63
|
findings.push(`OCS status is ${status} (not ACTIVE)`);
|
|
64
|
-
actions.push(
|
|
64
|
+
actions.push(`Reactivate subscriber via modify_subscriber_status`);
|
|
65
65
|
severity = "critical";
|
|
66
66
|
}
|
|
67
|
+
// Check balance
|
|
67
68
|
if (balance <= 0) {
|
|
68
69
|
findings.push(`Balance is ${balance} — subscriber may be blocked from usage`);
|
|
69
|
-
actions.push(
|
|
70
|
+
actions.push(`Top up balance via modify_subscriber_balance`);
|
|
70
71
|
if (severity !== "critical")
|
|
71
72
|
severity = "warning";
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
74
|
+
// 2. Check SIM provider status — OCS expects bare simId (Long), resolve from subscriber record
|
|
75
|
+
const simId = sub.data.simId ?? sub.data.sim_id ?? sub.data.id;
|
|
76
|
+
const sim = simId !== undefined
|
|
77
|
+
? await safeCall(ctx.env, token, "getSimProviderStatus", Number(simId))
|
|
76
78
|
: { data: null, error: null };
|
|
77
79
|
if (sim.data) {
|
|
78
80
|
const simStatus = String(sim.data.simStatus ?? sim.data.status ?? "").toUpperCase();
|
|
79
81
|
if (simStatus && !["ENABLED", "ACTIVE", "ACTIVATED"].includes(simStatus)) {
|
|
80
82
|
findings.push(`SIM provider status is ${simStatus} — SIM may be disabled at network level`);
|
|
81
|
-
actions.push(
|
|
83
|
+
actions.push(`Enable SIM via change_sim_status`);
|
|
82
84
|
severity = "critical";
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
|
-
|
|
87
|
+
// 3. Check packages
|
|
88
|
+
const pkgs = await safeCall(ctx.env, token, "listSubscriberPrepaidPackages", { iccid });
|
|
86
89
|
if (pkgs.data && Array.isArray(pkgs.data)) {
|
|
87
90
|
const activePkgs = pkgs.data.filter((p) => String(p.status ?? "").toUpperCase() === "ACTIVE");
|
|
88
91
|
if (activePkgs.length === 0) {
|
|
@@ -91,15 +94,18 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
91
94
|
severity = "critical";
|
|
92
95
|
}
|
|
93
96
|
else {
|
|
97
|
+
// Check for depleted packages
|
|
94
98
|
for (const pkg of activePkgs) {
|
|
95
99
|
const dataUsed = Number(pkg.dataUsed ?? pkg.dataConsumed ?? 0);
|
|
96
100
|
const dataLimit = Number(pkg.dataLimit ?? pkg.dataAllowance ?? 0);
|
|
97
101
|
if (dataLimit > 0 && dataUsed >= dataLimit) {
|
|
98
|
-
findings.push(`Package "${pkg.name ?? pkg.packageTemplateId}" data depleted:
|
|
102
|
+
findings.push(`Package "${pkg.name ?? pkg.packageTemplateId}" data depleted: ` +
|
|
103
|
+
`${formatBytes(dataUsed)} / ${formatBytes(dataLimit)}`);
|
|
99
104
|
actions.push("Assign additional package or increase limits via modify_package_limits");
|
|
100
105
|
if (severity !== "critical")
|
|
101
106
|
severity = "warning";
|
|
102
107
|
}
|
|
108
|
+
// Check expiry
|
|
103
109
|
const expiry = String(pkg.expirationDate ?? pkg.endDate ?? "");
|
|
104
110
|
if (expiry) {
|
|
105
111
|
const days = daysUntil(expiry);
|
|
@@ -119,9 +125,13 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
}
|
|
128
|
+
// 4. Check recent network events (last 2 days)
|
|
122
129
|
const now = new Date();
|
|
123
130
|
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
|
|
124
|
-
const events = await safeCall(
|
|
131
|
+
const events = await safeCall(ctx.env, token, "subscriberNetworkEventsOverPeriod", {
|
|
132
|
+
subscriber: { iccid },
|
|
133
|
+
period: { start: toISODate(twoDaysAgo), end: toISODate(now) },
|
|
134
|
+
});
|
|
125
135
|
if (events.data && Array.isArray(events.data)) {
|
|
126
136
|
if (events.data.length === 0) {
|
|
127
137
|
findings.push("No network events in last 48 hours — device may be powered off or out of coverage");
|
|
@@ -134,9 +144,10 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
134
144
|
findings.push(`Last network event: ${lastType} at ${lastEvent.timestamp ?? lastEvent.date ?? "unknown"}`);
|
|
135
145
|
}
|
|
136
146
|
}
|
|
147
|
+
// 5. Check HLR bitrate — OCS expects { imsi }, resolve from subscriber record
|
|
137
148
|
const imsiForBitrate = typeof sub.data.imsi === "string" ? sub.data.imsi : null;
|
|
138
149
|
const bitrate = imsiForBitrate
|
|
139
|
-
? await safeCall(
|
|
150
|
+
? await safeCall(ctx.env, token, "hlrGetBitrate", { imsi: imsiForBitrate })
|
|
140
151
|
: { data: null, error: null };
|
|
141
152
|
if (bitrate.data) {
|
|
142
153
|
const rate = Number(bitrate.data.bitrate ?? bitrate.data.maxBitrate ?? 0);
|
|
@@ -147,12 +158,11 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
147
158
|
severity = "info";
|
|
148
159
|
}
|
|
149
160
|
}
|
|
150
|
-
|
|
161
|
+
// Build report
|
|
162
|
+
if (findings.length === 0) {
|
|
151
163
|
findings.push("No issues detected — subscriber appears healthy");
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
: "unknown";
|
|
155
|
-
return result([
|
|
164
|
+
}
|
|
165
|
+
const report = [
|
|
156
166
|
`# Subscriber Diagnosis: ${iccid}`,
|
|
157
167
|
``,
|
|
158
168
|
`## Severity: ${severity.toUpperCase()}`,
|
|
@@ -160,30 +170,39 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
160
170
|
`## Findings`,
|
|
161
171
|
...findings.map((f, i) => `${i + 1}. ${f}`),
|
|
162
172
|
``,
|
|
163
|
-
...(actions.length > 0 ? [
|
|
173
|
+
...(actions.length > 0 ? [
|
|
174
|
+
`## Recommended Actions`,
|
|
175
|
+
...actions.map((a, i) => `${i + 1}. ${a}`),
|
|
176
|
+
] : []),
|
|
164
177
|
``,
|
|
165
178
|
`## Raw Status`,
|
|
166
179
|
`- OCS Status: ${status}`,
|
|
167
180
|
`- Balance: ${balance}`,
|
|
168
|
-
`- Active Packages: ${
|
|
169
|
-
].join("\n")
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
//
|
|
181
|
+
`- Active Packages: ${pkgs.data && Array.isArray(pkgs.data) ? pkgs.data.filter((p) => String(p.status ?? "").toUpperCase() === "ACTIVE").length : "unknown"}`,
|
|
182
|
+
].join("\n");
|
|
183
|
+
return result(report);
|
|
184
|
+
});
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// 2. FLEET HEALTH — single-call fleet overview
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
174
188
|
server.registerTool("fleet_health", {
|
|
175
189
|
title: "Fleet Health Dashboard",
|
|
176
|
-
description: "Aggregates eSIM status counts, low-balance accounts, and provides a
|
|
190
|
+
description: "Aggregates eSIM status counts, low-balance accounts, and provides a " +
|
|
191
|
+
"fleet-wide health summary in a single call. Identifies accounts that " +
|
|
192
|
+
"need attention.",
|
|
177
193
|
inputSchema: {
|
|
178
194
|
accountId: z.number().optional().describe("Filter to a specific account (omit for all)"),
|
|
179
195
|
},
|
|
180
196
|
annotations: { readOnlyHint: true },
|
|
181
|
-
},
|
|
197
|
+
}, async ({ accountId }) => {
|
|
198
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
199
|
+
// Parallel fetch
|
|
182
200
|
const [statusResult, accountsResult] = await Promise.all([
|
|
183
|
-
safeCall(
|
|
184
|
-
safeCall(
|
|
201
|
+
safeCall(ctx.env, token, "esimStatusPerAccount", accountId !== undefined ? { accountId } : {}),
|
|
202
|
+
safeCall(ctx.env, token, "listResellerAccount", {}),
|
|
185
203
|
]);
|
|
186
204
|
const sections = ["# Fleet Health Dashboard\n"];
|
|
205
|
+
// eSIM status breakdown
|
|
187
206
|
if (statusResult.data && Array.isArray(statusResult.data)) {
|
|
188
207
|
let totalActive = 0, totalSuspended = 0, totalInventory = 0, totalOther = 0;
|
|
189
208
|
for (const account of statusResult.data) {
|
|
@@ -204,9 +223,10 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
204
223
|
sections.push(`| **Total** | **${total}** | |`);
|
|
205
224
|
sections.push(`\n**Fleet Utilization: ${utilization}%**`);
|
|
206
225
|
if (totalSuspended > totalActive * 0.1) {
|
|
207
|
-
sections.push(`\
|
|
226
|
+
sections.push(`\n⚠ High suspension rate (${totalSuspended} suspended vs ${totalActive} active)`);
|
|
208
227
|
}
|
|
209
228
|
}
|
|
229
|
+
// Account balances
|
|
210
230
|
if (accountsResult.data && Array.isArray(accountsResult.data)) {
|
|
211
231
|
const lowBalance = accountsResult.data.filter((a) => Number(a.balance ?? 0) < 10);
|
|
212
232
|
if (lowBalance.length > 0) {
|
|
@@ -222,41 +242,54 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
222
242
|
sections.push(`- Low balance (< 10): ${lowBalance.length}`);
|
|
223
243
|
}
|
|
224
244
|
return result(sections.join("\n"));
|
|
225
|
-
})
|
|
226
|
-
//
|
|
227
|
-
// 3.
|
|
228
|
-
//
|
|
245
|
+
});
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// 3. USAGE ANOMALY DETECTION
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
229
249
|
server.registerTool("detect_usage_anomalies", {
|
|
230
250
|
title: "Detect Usage Anomalies",
|
|
231
|
-
description: "
|
|
232
|
-
|
|
251
|
+
description: "Analyzes a subscriber's recent usage patterns over the last 7 days to detect " +
|
|
252
|
+
"anomalies: sudden spikes, unusual off-hours activity, or consumption rates that " +
|
|
253
|
+
"would exhaust the package before expiry.",
|
|
254
|
+
inputSchema: {
|
|
255
|
+
iccid: z.string().describe("The subscriber ICCID to analyze"),
|
|
256
|
+
},
|
|
233
257
|
annotations: { readOnlyHint: true },
|
|
234
|
-
},
|
|
258
|
+
}, async ({ iccid }) => {
|
|
259
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
235
260
|
const now = new Date();
|
|
236
261
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
237
262
|
const [usageResult, pkgResult] = await Promise.all([
|
|
238
|
-
safeCall(
|
|
239
|
-
|
|
263
|
+
safeCall(ctx.env, token, "subscriberUsageOverPeriod", {
|
|
264
|
+
subscriber: { iccid },
|
|
265
|
+
period: { start: toISODate(weekAgo), end: toISODate(now) },
|
|
266
|
+
}),
|
|
267
|
+
safeCall(ctx.env, token, "listSubscriberPrepaidPackages", { iccid }),
|
|
240
268
|
]);
|
|
241
269
|
if (usageResult.error)
|
|
242
270
|
return result(`Failed to fetch usage: ${usageResult.error}`, true);
|
|
243
271
|
const sections = [`# Usage Anomaly Report: ${iccid}\n`];
|
|
244
272
|
const anomalies = [];
|
|
245
273
|
if (usageResult.data && Array.isArray(usageResult.data) && usageResult.data.length > 0) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
274
|
+
// Extract daily data volumes
|
|
275
|
+
const dailyData = [];
|
|
276
|
+
for (const entry of usageResult.data) {
|
|
277
|
+
const bytes = Number(entry.dataBytes ?? entry.dataVolume ?? entry.totalData ?? 0);
|
|
278
|
+
const date = String(entry.date ?? entry.day ?? "?");
|
|
279
|
+
dailyData.push({ date, bytes });
|
|
280
|
+
}
|
|
250
281
|
if (dailyData.length >= 2) {
|
|
251
|
-
|
|
282
|
+
// Calculate stats
|
|
283
|
+
const volumes = dailyData.map(d => d.bytes);
|
|
252
284
|
const mean = volumes.reduce((a, b) => a + b, 0) / volumes.length;
|
|
253
285
|
const stdDev = Math.sqrt(volumes.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / volumes.length);
|
|
254
286
|
sections.push(`## Daily Usage (Last 7 Days)`);
|
|
255
287
|
sections.push(`| Date | Data | vs Average |`);
|
|
256
288
|
sections.push(`|------|------|-----------|`);
|
|
257
289
|
for (const d of dailyData) {
|
|
258
|
-
const deviation = mean > 0 ? ((
|
|
259
|
-
const flag = d.bytes > mean + 2 * stdDev ? " SPIKE" :
|
|
290
|
+
const deviation = mean > 0 ? ((d.bytes - mean) / mean * 100).toFixed(0) : "0";
|
|
291
|
+
const flag = d.bytes > mean + 2 * stdDev ? " SPIKE" :
|
|
292
|
+
d.bytes > mean + stdDev ? " HIGH" : "";
|
|
260
293
|
sections.push(`| ${d.date} | ${formatBytes(d.bytes)} | ${deviation}%${flag} |`);
|
|
261
294
|
if (d.bytes > mean + 2 * stdDev) {
|
|
262
295
|
anomalies.push(`Spike on ${d.date}: ${formatBytes(d.bytes)} (${deviation}% above average)`);
|
|
@@ -264,6 +297,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
264
297
|
}
|
|
265
298
|
sections.push(`\n**Average daily usage: ${formatBytes(mean)}**`);
|
|
266
299
|
sections.push(`**Std deviation: ${formatBytes(stdDev)}**`);
|
|
300
|
+
// Burn rate analysis against active packages
|
|
267
301
|
if (pkgResult.data && Array.isArray(pkgResult.data)) {
|
|
268
302
|
const activePkgs = pkgResult.data.filter((p) => String(p.status ?? "").toUpperCase() === "ACTIVE");
|
|
269
303
|
for (const pkg of activePkgs) {
|
|
@@ -279,7 +313,8 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
279
313
|
sections.push(`- Days until expiry: ${daysLeft}`);
|
|
280
314
|
sections.push(`- At current rate, data exhausts in: ${daysToExhaust.toFixed(1)} days`);
|
|
281
315
|
if (daysToExhaust < daysLeft * 0.5) {
|
|
282
|
-
anomalies.push(`Package "${pkg.name ?? pkg.packageTemplateId}" will run out
|
|
316
|
+
anomalies.push(`Package "${pkg.name ?? pkg.packageTemplateId}" will run out ` +
|
|
317
|
+
`${(daysLeft - daysToExhaust).toFixed(0)} days before expiry at current consumption`);
|
|
283
318
|
}
|
|
284
319
|
}
|
|
285
320
|
}
|
|
@@ -297,24 +332,33 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
297
332
|
sections.push(`\n## No anomalies detected — usage appears normal.`);
|
|
298
333
|
}
|
|
299
334
|
return result(sections.join("\n"));
|
|
300
|
-
})
|
|
301
|
-
//
|
|
302
|
-
// 4.
|
|
303
|
-
//
|
|
335
|
+
});
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
// 4. PACKAGE OPTIMIZER — recommend better-fit packages
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
304
339
|
server.registerTool("optimize_package", {
|
|
305
340
|
title: "Package Optimization Advisor",
|
|
306
|
-
description: "Compares a subscriber's usage against their current package and all
|
|
307
|
-
|
|
341
|
+
description: "Compares a subscriber's actual usage against their current package and all " +
|
|
342
|
+
"available templates. Recommends better-fit packages to reduce waste or prevent " +
|
|
343
|
+
"overages. Calculates potential savings.",
|
|
344
|
+
inputSchema: {
|
|
345
|
+
iccid: z.string().describe("The subscriber ICCID to optimize"),
|
|
346
|
+
},
|
|
308
347
|
annotations: { readOnlyHint: true },
|
|
309
|
-
},
|
|
348
|
+
}, async ({ iccid }) => {
|
|
349
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
310
350
|
const now = new Date();
|
|
311
351
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
312
352
|
const [usageResult, pkgResult, templatesResult] = await Promise.all([
|
|
313
|
-
safeCall(
|
|
314
|
-
|
|
315
|
-
|
|
353
|
+
safeCall(ctx.env, token, "subscriberUsageOverPeriod", {
|
|
354
|
+
subscriber: { iccid },
|
|
355
|
+
period: { start: toISODate(weekAgo), end: toISODate(now) },
|
|
356
|
+
}),
|
|
357
|
+
safeCall(ctx.env, token, "listSubscriberPrepaidPackages", { iccid }),
|
|
358
|
+
safeCall(ctx.env, token, "listPrepaidPackageTemplate", {}),
|
|
316
359
|
]);
|
|
317
360
|
const sections = [`# Package Optimization: ${iccid}\n`];
|
|
361
|
+
// Calculate average daily usage
|
|
318
362
|
let avgDailyData = 0;
|
|
319
363
|
if (usageResult.data && Array.isArray(usageResult.data) && usageResult.data.length > 0) {
|
|
320
364
|
const totalData = usageResult.data.reduce((sum, e) => sum + Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0), 0);
|
|
@@ -323,6 +367,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
323
367
|
sections.push(`- Average daily data: ${formatBytes(avgDailyData)}`);
|
|
324
368
|
sections.push(`- Projected monthly: ${formatBytes(avgDailyData * 30)}`);
|
|
325
369
|
}
|
|
370
|
+
// Current packages
|
|
326
371
|
if (pkgResult.data && Array.isArray(pkgResult.data)) {
|
|
327
372
|
const activePkgs = pkgResult.data.filter((p) => String(p.status ?? "").toUpperCase() === "ACTIVE");
|
|
328
373
|
if (activePkgs.length > 0) {
|
|
@@ -339,6 +384,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
339
384
|
const expiry = String(pkg.expirationDate ?? pkg.endDate ?? "");
|
|
340
385
|
if (expiry)
|
|
341
386
|
sections.push(`- Expires: ${expiry} (${daysUntil(expiry)} days)`);
|
|
387
|
+
// Flag waste
|
|
342
388
|
if (dataLimit > 0 && Number(utilization) < 30) {
|
|
343
389
|
sections.push(`- LOW UTILIZATION — subscriber is using less than 30% of allowance`);
|
|
344
390
|
}
|
|
@@ -348,16 +394,21 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
348
394
|
}
|
|
349
395
|
}
|
|
350
396
|
}
|
|
397
|
+
// Recommend templates
|
|
351
398
|
if (templatesResult.data && Array.isArray(templatesResult.data) && avgDailyData > 0) {
|
|
399
|
+
// Score templates by fit
|
|
352
400
|
const scored = templatesResult.data
|
|
353
401
|
.map((t) => {
|
|
354
402
|
const limit = Number(t.dataLimit ?? t.dataAllowance ?? 0);
|
|
355
403
|
const validity = Number(t.validityDays ?? t.duration ?? 30);
|
|
356
404
|
const price = Number(t.price ?? t.cost ?? 0);
|
|
357
405
|
const projectedUsage = avgDailyData * validity;
|
|
406
|
+
// Fit score: penalize both waste (too much data) and shortage (too little)
|
|
358
407
|
const ratio = limit > 0 ? projectedUsage / limit : 0;
|
|
359
|
-
const fitScore = 1 - Math.abs(1 - ratio);
|
|
360
|
-
|
|
408
|
+
const fitScore = 1 - Math.abs(1 - ratio); // 1.0 = perfect fit, 0 = terrible
|
|
409
|
+
const costPerGB = limit > 0 && price > 0 ? price / (limit / (1024 * 1024 * 1024)) : Infinity;
|
|
410
|
+
const name = String(t.name ?? t.templateId ?? "?");
|
|
411
|
+
return { name, limit, validity, price, projectedUsage, fitScore, costPerGB, ratio };
|
|
361
412
|
})
|
|
362
413
|
.filter((t) => t.fitScore > 0.3 && t.limit > 0)
|
|
363
414
|
.sort((a, b) => b.fitScore - a.fitScore)
|
|
@@ -368,7 +419,9 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
368
419
|
sections.push(`|----------|------|----------|-------|-----|--------------|`);
|
|
369
420
|
for (const t of scored) {
|
|
370
421
|
const fitLabel = t.fitScore > 0.8 ? "GREAT" : t.fitScore > 0.6 ? "GOOD" : "OK";
|
|
371
|
-
sections.push(`| ${t.name} | ${formatBytes(t.limit)} |
|
|
422
|
+
sections.push(`| ${t.name} | ${formatBytes(t.limit)} | ` +
|
|
423
|
+
`${t.validity}d | ${t.price > 0 ? t.price.toFixed(2) : "?"} | ` +
|
|
424
|
+
`${fitLabel} (${(t.fitScore * 100).toFixed(0)}%) | ${formatBytes(t.projectedUsage)} |`);
|
|
372
425
|
}
|
|
373
426
|
}
|
|
374
427
|
}
|
|
@@ -376,26 +429,35 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
376
429
|
sections.push(`\n*No usage data available — cannot recommend packages without usage history.*`);
|
|
377
430
|
}
|
|
378
431
|
return result(sections.join("\n"));
|
|
379
|
-
})
|
|
380
|
-
//
|
|
381
|
-
// 5. CHURN RISK
|
|
382
|
-
//
|
|
432
|
+
});
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// 5. CHURN RISK SCORING
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
383
436
|
server.registerTool("churn_risk", {
|
|
384
437
|
title: "Churn Risk Assessment",
|
|
385
|
-
description: "
|
|
386
|
-
|
|
438
|
+
description: "Analyzes a subscriber's usage trends, package status, balance, and activity " +
|
|
439
|
+
"to produce a churn risk score (0-100) with contributing factors and retention " +
|
|
440
|
+
"recommendations.",
|
|
441
|
+
inputSchema: {
|
|
442
|
+
iccid: z.string().describe("The subscriber ICCID to assess"),
|
|
443
|
+
},
|
|
387
444
|
annotations: { readOnlyHint: true },
|
|
388
|
-
},
|
|
445
|
+
}, async ({ iccid }) => {
|
|
446
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
389
447
|
const now = new Date();
|
|
390
448
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
391
449
|
const [subResult, usageResult, pkgResult, activeResult] = await Promise.all([
|
|
392
|
-
safeCall(
|
|
393
|
-
safeCall(
|
|
394
|
-
|
|
395
|
-
|
|
450
|
+
safeCall(ctx.env, token, "getSingleSubscriber", { iccid }),
|
|
451
|
+
safeCall(ctx.env, token, "subscriberUsageOverPeriod", {
|
|
452
|
+
subscriber: { iccid },
|
|
453
|
+
period: { start: toISODate(weekAgo), end: toISODate(now) },
|
|
454
|
+
}),
|
|
455
|
+
safeCall(ctx.env, token, "listSubscriberPrepaidPackages", { iccid }),
|
|
456
|
+
safeCall(ctx.env, token, "getSubscriberActivePeriod", { iccid }),
|
|
396
457
|
]);
|
|
397
458
|
let riskScore = 0;
|
|
398
459
|
const factors = [];
|
|
460
|
+
// Factor 1: Usage trend (declining usage = higher risk)
|
|
399
461
|
if (usageResult.data && Array.isArray(usageResult.data) && usageResult.data.length >= 3) {
|
|
400
462
|
const volumes = usageResult.data.map((e) => Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0));
|
|
401
463
|
const firstHalf = volumes.slice(0, Math.floor(volumes.length / 2));
|
|
@@ -405,12 +467,14 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
405
467
|
if (avgFirst > 0) {
|
|
406
468
|
const trend = (avgSecond - avgFirst) / avgFirst;
|
|
407
469
|
if (trend < -0.5) {
|
|
408
|
-
|
|
409
|
-
|
|
470
|
+
const impact = 30;
|
|
471
|
+
riskScore += impact;
|
|
472
|
+
factors.push({ factor: "Declining usage", impact, detail: `Usage dropped ${Math.abs(trend * 100).toFixed(0)}% week-over-week` });
|
|
410
473
|
}
|
|
411
474
|
else if (trend < -0.2) {
|
|
412
|
-
|
|
413
|
-
|
|
475
|
+
const impact = 15;
|
|
476
|
+
riskScore += impact;
|
|
477
|
+
factors.push({ factor: "Moderately declining usage", impact, detail: `Usage dropped ${Math.abs(trend * 100).toFixed(0)}%` });
|
|
414
478
|
}
|
|
415
479
|
}
|
|
416
480
|
}
|
|
@@ -418,6 +482,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
418
482
|
riskScore += 25;
|
|
419
483
|
factors.push({ factor: "No recent usage", impact: 25, detail: "Zero data activity in last 7 days" });
|
|
420
484
|
}
|
|
485
|
+
// Factor 2: Package status
|
|
421
486
|
if (pkgResult.data && Array.isArray(pkgResult.data)) {
|
|
422
487
|
const activePkgs = pkgResult.data.filter((p) => String(p.status ?? "").toUpperCase() === "ACTIVE");
|
|
423
488
|
if (activePkgs.length === 0) {
|
|
@@ -425,6 +490,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
425
490
|
factors.push({ factor: "No active packages", impact: 20, detail: "Subscriber has no active data packages" });
|
|
426
491
|
}
|
|
427
492
|
else {
|
|
493
|
+
// Check if all packages are near expiry
|
|
428
494
|
const allExpiringSoon = activePkgs.every((p) => {
|
|
429
495
|
const expiry = String(p.expirationDate ?? p.endDate ?? "");
|
|
430
496
|
return expiry && daysUntil(expiry) <= 5;
|
|
@@ -433,6 +499,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
433
499
|
riskScore += 15;
|
|
434
500
|
factors.push({ factor: "All packages expiring soon", impact: 15, detail: "No package renewal in sight" });
|
|
435
501
|
}
|
|
502
|
+
// Check if no recurring packages
|
|
436
503
|
const hasRecurring = activePkgs.some((p) => p.recurring === true || p.isRecurring === true);
|
|
437
504
|
if (!hasRecurring) {
|
|
438
505
|
riskScore += 10;
|
|
@@ -440,6 +507,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
440
507
|
}
|
|
441
508
|
}
|
|
442
509
|
}
|
|
510
|
+
// Factor 3: Balance
|
|
443
511
|
if (subResult.data) {
|
|
444
512
|
const balance = Number(subResult.data.balance ?? 0);
|
|
445
513
|
if (balance <= 0) {
|
|
@@ -447,6 +515,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
447
515
|
factors.push({ factor: "Zero balance", impact: 15, detail: "Cannot purchase new packages" });
|
|
448
516
|
}
|
|
449
517
|
}
|
|
518
|
+
// Factor 4: Subscriber age (newer = higher risk)
|
|
450
519
|
if (activeResult.data) {
|
|
451
520
|
const firstUse = String(activeResult.data.firstUseDate ?? activeResult.data.activationDate ?? "");
|
|
452
521
|
if (firstUse) {
|
|
@@ -457,14 +526,17 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
457
526
|
}
|
|
458
527
|
}
|
|
459
528
|
}
|
|
529
|
+
// Cap at 100
|
|
460
530
|
riskScore = Math.min(riskScore, 100);
|
|
531
|
+
// Risk level
|
|
461
532
|
const level = riskScore >= 70 ? "HIGH" : riskScore >= 40 ? "MEDIUM" : "LOW";
|
|
533
|
+
// Build report
|
|
462
534
|
const sections = [
|
|
463
535
|
`# Churn Risk Assessment: ${iccid}`,
|
|
464
536
|
``,
|
|
465
537
|
`## Risk Score: ${riskScore}/100 (${level})`,
|
|
466
538
|
``,
|
|
467
|
-
`${"
|
|
539
|
+
`${"█".repeat(Math.floor(riskScore / 5))}${"░".repeat(20 - Math.floor(riskScore / 5))}`,
|
|
468
540
|
``,
|
|
469
541
|
];
|
|
470
542
|
if (factors.length > 0) {
|
|
@@ -476,9 +548,10 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
476
548
|
sections.push(`| ${f.factor} | +${f.impact} | ${f.detail} |`);
|
|
477
549
|
}
|
|
478
550
|
}
|
|
551
|
+
// Retention recommendations
|
|
479
552
|
sections.push(`\n## Retention Recommendations`);
|
|
480
553
|
if (riskScore >= 70) {
|
|
481
|
-
sections.push("1. Immediate outreach — contact subscriber with special offer");
|
|
554
|
+
sections.push("1. **Immediate outreach** — contact subscriber with special offer");
|
|
482
555
|
sections.push("2. Assign a complimentary small data package to re-engage");
|
|
483
556
|
sections.push("3. Set up a recurring package to reduce renewal friction");
|
|
484
557
|
}
|
|
@@ -492,56 +565,74 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
492
565
|
sections.push("2. Continue monitoring via regular fleet_health checks");
|
|
493
566
|
}
|
|
494
567
|
return result(sections.join("\n"));
|
|
495
|
-
})
|
|
496
|
-
//
|
|
497
|
-
// 6. AUDIT
|
|
498
|
-
//
|
|
568
|
+
});
|
|
569
|
+
// ---------------------------------------------------------------------------
|
|
570
|
+
// 6. NETWORK COVERAGE AUDIT — "am I on the right networks?"
|
|
571
|
+
// ---------------------------------------------------------------------------
|
|
499
572
|
server.registerTool("audit_network_coverage", {
|
|
500
573
|
title: "Network Coverage Audit",
|
|
501
|
-
description: "
|
|
574
|
+
description: "Analyzes which networks your subscribers are actually connecting to in a given " +
|
|
575
|
+
"country or across all countries. Compares against your steering lists to identify " +
|
|
576
|
+
"mismatches — subscribers roaming on expensive or non-preferred networks. " +
|
|
577
|
+
"Use this to answer: 'Am I using the right networks in country X?'",
|
|
502
578
|
inputSchema: {
|
|
503
579
|
accountId: z.number().optional().describe("Filter to a specific account"),
|
|
504
580
|
limit: z.number().optional().describe("Max subscribers to sample (default 50)"),
|
|
505
581
|
},
|
|
506
582
|
annotations: { readOnlyHint: true },
|
|
507
|
-
},
|
|
583
|
+
}, async ({ accountId, limit: sampleLimit }) => {
|
|
584
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
508
585
|
const maxSample = sampleLimit ?? 50;
|
|
509
|
-
|
|
586
|
+
// Fetch subscribers and steering lists in parallel
|
|
587
|
+
const subParams = { status: "ACTIVE" };
|
|
510
588
|
if (accountId !== undefined)
|
|
511
589
|
subParams.accountId = accountId;
|
|
512
|
-
const [subsResult,
|
|
513
|
-
safeCall(
|
|
514
|
-
|
|
590
|
+
const [subsResult, resellerId] = await Promise.all([
|
|
591
|
+
safeCall(ctx.env, token, "listSubscriber", subParams),
|
|
592
|
+
getDefaultResellerId(ctx.env, token),
|
|
515
593
|
]);
|
|
594
|
+
const steeringResult = await safeCall(ctx.env, token, "listSteeringList", resellerId);
|
|
516
595
|
if (subsResult.error)
|
|
517
596
|
return result(`Failed to fetch subscribers: ${subsResult.error}`, true);
|
|
518
597
|
const sections = [`# Network Coverage Audit\n`];
|
|
598
|
+
// Build steering list lookup
|
|
599
|
+
const steeringMap = new Map();
|
|
519
600
|
if (steeringResult.data && Array.isArray(steeringResult.data)) {
|
|
520
|
-
|
|
601
|
+
for (const sl of steeringResult.data) {
|
|
602
|
+
steeringMap.set(Number(sl.steeringListId ?? sl.id), sl);
|
|
603
|
+
}
|
|
604
|
+
sections.push(`## Steering Lists: ${steeringMap.size} configured`);
|
|
521
605
|
}
|
|
606
|
+
// Sample subscriber locations and networks
|
|
522
607
|
const countryStats = new Map();
|
|
523
608
|
if (subsResult.data && Array.isArray(subsResult.data)) {
|
|
524
|
-
sections.push(`## Sampling ${subsResult.data.length} active subscribers\n`);
|
|
525
609
|
const subs = subsResult.data.slice(0, maxSample);
|
|
610
|
+
sections.push(`## Sampling ${subs.length} active subscribers\n`);
|
|
611
|
+
// Fetch locations in batches of 10
|
|
526
612
|
const batchSize = 10;
|
|
527
613
|
for (let i = 0; i < subs.length; i += batchSize) {
|
|
528
614
|
const batch = subs.slice(i, i + batchSize);
|
|
529
|
-
const locations = await Promise.all(batch.map((s) => safeCall(
|
|
615
|
+
const locations = await Promise.all(batch.map((s) => safeCall(ctx.env, token, "getSubscriberLocation", {
|
|
616
|
+
iccid: String(s.iccid ?? ""),
|
|
617
|
+
})));
|
|
530
618
|
for (let j = 0; j < batch.length; j++) {
|
|
531
619
|
const loc = locations[j];
|
|
532
620
|
if (loc.data) {
|
|
533
621
|
const country = String(loc.data.country ?? loc.data.countryCode ?? "Unknown");
|
|
534
622
|
const network = String(loc.data.network ?? loc.data.operator ?? loc.data.mccMnc ?? "Unknown");
|
|
623
|
+
const iccid = String(batch[j].iccid ?? "");
|
|
535
624
|
if (!countryStats.has(country)) {
|
|
536
|
-
countryStats.set(country, { count: 0, networks: new Map() });
|
|
625
|
+
countryStats.set(country, { count: 0, networks: new Map(), subscribers: [] });
|
|
537
626
|
}
|
|
538
627
|
const stat = countryStats.get(country);
|
|
539
628
|
stat.count++;
|
|
540
629
|
stat.networks.set(network, (stat.networks.get(network) ?? 0) + 1);
|
|
630
|
+
stat.subscribers.push(iccid);
|
|
541
631
|
}
|
|
542
632
|
}
|
|
543
633
|
}
|
|
544
634
|
}
|
|
635
|
+
// Report by country
|
|
545
636
|
if (countryStats.size > 0) {
|
|
546
637
|
sections.push(`## Network Distribution by Country`);
|
|
547
638
|
const sorted = [...countryStats.entries()].sort((a, b) => b[1].count - a[1].count);
|
|
@@ -562,44 +653,56 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
562
653
|
sections.push("No location data available for sampled subscribers.");
|
|
563
654
|
}
|
|
564
655
|
return result(sections.join("\n"));
|
|
565
|
-
})
|
|
566
|
-
//
|
|
567
|
-
// 7. MARKETING INTELLIGENCE
|
|
568
|
-
//
|
|
656
|
+
});
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
// 7. MARKETING INTELLIGENCE — "which countries should I target?"
|
|
659
|
+
// ---------------------------------------------------------------------------
|
|
569
660
|
server.registerTool("marketing_intelligence", {
|
|
570
661
|
title: "Marketing Intelligence Report",
|
|
571
|
-
description: "
|
|
662
|
+
description: "Analyzes your subscriber base to identify high-growth markets, underserved regions, " +
|
|
663
|
+
"and revenue concentration. Answers: 'Which countries should I target with marketing?' " +
|
|
664
|
+
"and 'Where are my most valuable subscribers?'",
|
|
572
665
|
inputSchema: {
|
|
573
666
|
accountId: z.number().optional().describe("Filter to a specific account"),
|
|
574
667
|
},
|
|
575
668
|
annotations: { readOnlyHint: true },
|
|
576
|
-
},
|
|
577
|
-
const
|
|
669
|
+
}, async ({ accountId }) => {
|
|
670
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
671
|
+
const params = {};
|
|
578
672
|
if (accountId !== undefined)
|
|
579
673
|
params.accountId = accountId;
|
|
580
|
-
const [subsResult, templatesResult] = await Promise.all([
|
|
581
|
-
safeCall(
|
|
582
|
-
safeCall(
|
|
674
|
+
const [subsResult, templatesResult, resellerId] = await Promise.all([
|
|
675
|
+
safeCall(ctx.env, token, "listSubscriber", params),
|
|
676
|
+
safeCall(ctx.env, token, "listPrepaidPackageTemplate", {}),
|
|
677
|
+
getDefaultResellerId(ctx.env, token),
|
|
583
678
|
]);
|
|
679
|
+
const zonesResult = await safeCall(ctx.env, token, "listDetailedLocationZone", resellerId);
|
|
584
680
|
const sections = [`# Marketing Intelligence Report\n`];
|
|
681
|
+
// Subscriber distribution by location
|
|
585
682
|
if (subsResult.data && Array.isArray(subsResult.data)) {
|
|
586
683
|
const total = subsResult.data.length;
|
|
587
684
|
sections.push(`## Fleet Size: ${total} subscribers sampled\n`);
|
|
685
|
+
// Get locations for a sample
|
|
588
686
|
const sample = subsResult.data.slice(0, 100);
|
|
589
687
|
const countryData = new Map();
|
|
590
688
|
const batchSize = 10;
|
|
591
689
|
for (let i = 0; i < sample.length; i += batchSize) {
|
|
592
690
|
const batch = sample.slice(i, i + batchSize);
|
|
593
|
-
const locations = await Promise.all(batch.map((s) => safeCall(
|
|
691
|
+
const locations = await Promise.all(batch.map((s) => safeCall(ctx.env, token, "getSubscriberLocation", {
|
|
692
|
+
iccid: String(s.iccid ?? ""),
|
|
693
|
+
})));
|
|
594
694
|
for (let j = 0; j < batch.length; j++) {
|
|
595
695
|
const loc = locations[j];
|
|
596
|
-
const
|
|
597
|
-
const country = loc.data
|
|
598
|
-
|
|
599
|
-
|
|
696
|
+
const sub = batch[j];
|
|
697
|
+
const country = loc.data
|
|
698
|
+
? String(loc.data.country ?? loc.data.countryCode ?? "Unknown")
|
|
699
|
+
: "Unknown";
|
|
700
|
+
if (!countryData.has(country)) {
|
|
701
|
+
countryData.set(country, { subscribers: 0, activePackages: 0, totalBalance: 0 });
|
|
702
|
+
}
|
|
600
703
|
const cd = countryData.get(country);
|
|
601
704
|
cd.subscribers++;
|
|
602
|
-
cd.totalBalance += Number(
|
|
705
|
+
cd.totalBalance += Number(sub.balance ?? 0);
|
|
603
706
|
}
|
|
604
707
|
}
|
|
605
708
|
if (countryData.size > 0) {
|
|
@@ -612,29 +715,55 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
612
715
|
const avgBal = (data.totalBalance / data.subscribers).toFixed(2);
|
|
613
716
|
sections.push(`| ${country} | ${data.subscribers} | ${pct}% | ${avgBal} |`);
|
|
614
717
|
}
|
|
718
|
+
// Insights
|
|
615
719
|
sections.push(`\n## Market Insights`);
|
|
720
|
+
// Top market
|
|
616
721
|
const topMarket = sorted[0];
|
|
617
722
|
if (topMarket) {
|
|
618
|
-
sections.push(`- Strongest market
|
|
723
|
+
sections.push(`- **Strongest market**: ${topMarket[0]} (${topMarket[1].subscribers} subscribers)`);
|
|
619
724
|
if (topMarket[1].subscribers / sample.length > 0.5) {
|
|
620
725
|
sections.push(` Revenue concentration risk — >50% of fleet in one market`);
|
|
621
726
|
}
|
|
622
727
|
}
|
|
728
|
+
// High-value markets (high avg balance)
|
|
729
|
+
const highValue = sorted
|
|
730
|
+
.filter(([, d]) => d.subscribers >= 3)
|
|
731
|
+
.sort((a, b) => (b[1].totalBalance / b[1].subscribers) - (a[1].totalBalance / a[1].subscribers))
|
|
732
|
+
.slice(0, 3);
|
|
733
|
+
if (highValue.length > 0) {
|
|
734
|
+
sections.push(`\n### High-Value Markets (by avg balance)`);
|
|
735
|
+
for (const [country, data] of highValue) {
|
|
736
|
+
sections.push(`- **${country}**: avg balance ${(data.totalBalance / data.subscribers).toFixed(2)} (${data.subscribers} subs)`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// Underserved (1-2 subscribers — early traction)
|
|
623
740
|
const emerging = sorted.filter(([, d]) => d.subscribers >= 1 && d.subscribers <= 3);
|
|
624
741
|
if (emerging.length > 0) {
|
|
625
|
-
sections.push(`\n### Emerging Markets (1-3 subscribers)`);
|
|
742
|
+
sections.push(`\n### Emerging Markets (early traction, 1-3 subscribers)`);
|
|
743
|
+
sections.push(`These markets show initial demand — consider targeted campaigns:`);
|
|
626
744
|
for (const [country, data] of emerging) {
|
|
627
745
|
sections.push(`- ${country}: ${data.subscribers} subscriber(s)`);
|
|
628
746
|
}
|
|
629
747
|
}
|
|
630
748
|
}
|
|
631
749
|
}
|
|
750
|
+
// Available coverage vs actual usage
|
|
751
|
+
if (zonesResult.data && Array.isArray(zonesResult.data)) {
|
|
752
|
+
sections.push(`\n## Coverage Catalog`);
|
|
753
|
+
sections.push(`- Location zones available: ${zonesResult.data.length}`);
|
|
754
|
+
}
|
|
632
755
|
if (templatesResult.data && Array.isArray(templatesResult.data)) {
|
|
633
|
-
sections.push(
|
|
634
|
-
|
|
756
|
+
sections.push(`- Package templates available: ${templatesResult.data.length}`);
|
|
757
|
+
// Price analysis
|
|
758
|
+
const prices = templatesResult.data
|
|
759
|
+
.map((t) => Number(t.price ?? t.cost ?? 0))
|
|
760
|
+
.filter((p) => p > 0);
|
|
635
761
|
if (prices.length > 0) {
|
|
636
762
|
const avgPrice = prices.reduce((a, b) => a + b, 0) / prices.length;
|
|
637
|
-
|
|
763
|
+
const minPrice = Math.min(...prices);
|
|
764
|
+
const maxPrice = Math.max(...prices);
|
|
765
|
+
sections.push(`\n### Pricing Range`);
|
|
766
|
+
sections.push(`- Min: ${minPrice.toFixed(2)} | Avg: ${avgPrice.toFixed(2)} | Max: ${maxPrice.toFixed(2)}`);
|
|
638
767
|
}
|
|
639
768
|
}
|
|
640
769
|
sections.push(`\n## Recommended Actions`);
|
|
@@ -642,26 +771,29 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
642
771
|
sections.push(`2. Run \`high_cost_subscribers\` to identify margin pressure`);
|
|
643
772
|
sections.push(`3. Consider creating regional package templates for emerging markets`);
|
|
644
773
|
return result(sections.join("\n"));
|
|
645
|
-
})
|
|
646
|
-
//
|
|
647
|
-
// 8. HIGH COST SUBSCRIBERS
|
|
648
|
-
//
|
|
774
|
+
});
|
|
775
|
+
// ---------------------------------------------------------------------------
|
|
776
|
+
// 8. HIGH COST SUBSCRIBERS — "who's costing me money?"
|
|
777
|
+
// ---------------------------------------------------------------------------
|
|
649
778
|
server.registerTool("high_cost_subscribers", {
|
|
650
779
|
title: "High Cost Subscriber Report",
|
|
651
|
-
description: "Identifies subscribers with disproportionately high data consumption relative
|
|
780
|
+
description: "Identifies subscribers with disproportionately high data consumption relative " +
|
|
781
|
+
"to their package value. Finds subscribers burning through data at rates that " +
|
|
782
|
+
"erode margins. Answers: 'Which subscribers are costing me money?'",
|
|
652
783
|
inputSchema: {
|
|
653
784
|
accountId: z.number().optional().describe("Filter to a specific account"),
|
|
654
|
-
limit: z.number().optional().describe("Max subscribers to
|
|
785
|
+
limit: z.number().optional().describe("Max subscribers to analyze (default 100)"),
|
|
655
786
|
thresholdPct: z.number().optional().describe("Usage % threshold to flag (default 80)"),
|
|
656
787
|
},
|
|
657
788
|
annotations: { readOnlyHint: true },
|
|
658
|
-
},
|
|
789
|
+
}, async ({ accountId, limit: maxLimit, thresholdPct }) => {
|
|
790
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
659
791
|
const sampleSize = maxLimit ?? 100;
|
|
660
792
|
const threshold = thresholdPct ?? 80;
|
|
661
|
-
const subParams = {
|
|
793
|
+
const subParams = { status: "ACTIVE" };
|
|
662
794
|
if (accountId !== undefined)
|
|
663
795
|
subParams.accountId = accountId;
|
|
664
|
-
const subsResult = await safeCall(
|
|
796
|
+
const subsResult = await safeCall(ctx.env, token, "listSubscriber", subParams);
|
|
665
797
|
if (subsResult.error)
|
|
666
798
|
return result(`Failed to fetch subscribers: ${subsResult.error}`, true);
|
|
667
799
|
if (!subsResult.data || !Array.isArray(subsResult.data))
|
|
@@ -670,6 +802,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
670
802
|
const now = new Date();
|
|
671
803
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
672
804
|
const highCostSubs = [];
|
|
805
|
+
// Analyze in batches
|
|
673
806
|
const batchSize = 5;
|
|
674
807
|
const subs = subsResult.data.slice(0, sampleSize);
|
|
675
808
|
for (let i = 0; i < subs.length; i += batchSize) {
|
|
@@ -677,15 +810,20 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
677
810
|
await Promise.all(batch.map(async (sub) => {
|
|
678
811
|
const iccid = String(sub.iccid ?? "");
|
|
679
812
|
const [usage, pkgs, loc] = await Promise.all([
|
|
680
|
-
safeCall(
|
|
681
|
-
|
|
682
|
-
|
|
813
|
+
safeCall(ctx.env, token, "subscriberUsageOverPeriod", {
|
|
814
|
+
subscriber: { iccid },
|
|
815
|
+
period: { start: toISODate(weekAgo), end: toISODate(now) },
|
|
816
|
+
}),
|
|
817
|
+
safeCall(ctx.env, token, "listSubscriberPrepaidPackages", { iccid }),
|
|
818
|
+
safeCall(ctx.env, token, "getSubscriberLocation", { iccid }),
|
|
683
819
|
]);
|
|
820
|
+
// Calculate daily avg
|
|
684
821
|
let dailyAvgBytes = 0;
|
|
685
822
|
if (usage.data && Array.isArray(usage.data) && usage.data.length > 0) {
|
|
686
823
|
const totalBytes = usage.data.reduce((sum, e) => sum + Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0), 0);
|
|
687
824
|
dailyAvgBytes = totalBytes / usage.data.length;
|
|
688
825
|
}
|
|
826
|
+
// Get active package info
|
|
689
827
|
if (pkgs.data && Array.isArray(pkgs.data)) {
|
|
690
828
|
const activePkg = pkgs.data.find((p) => String(p.status ?? "").toUpperCase() === "ACTIVE");
|
|
691
829
|
if (activePkg && dailyAvgBytes > 0) {
|
|
@@ -695,17 +833,31 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
695
833
|
const utilizationPct = dataLimit > 0 ? (dataUsed / dataLimit) * 100 : 0;
|
|
696
834
|
const remaining = dataLimit - dataUsed;
|
|
697
835
|
const daysToExhaust = dailyAvgBytes > 0 ? remaining / dailyAvgBytes : Infinity;
|
|
698
|
-
const costPerGB = price > 0 && dataUsed > 0
|
|
699
|
-
|
|
836
|
+
const costPerGB = price > 0 && dataUsed > 0
|
|
837
|
+
? price / (dataUsed / (1024 * 1024 * 1024))
|
|
838
|
+
: 0;
|
|
839
|
+
const country = loc.data
|
|
840
|
+
? String(loc.data.country ?? loc.data.countryCode ?? "?")
|
|
841
|
+
: "?";
|
|
700
842
|
if (utilizationPct >= threshold || daysToExhaust < 3) {
|
|
701
|
-
highCostSubs.push({
|
|
843
|
+
highCostSubs.push({
|
|
844
|
+
iccid,
|
|
845
|
+
dailyAvgBytes,
|
|
846
|
+
packageDataLimit: dataLimit,
|
|
847
|
+
packagePrice: price,
|
|
848
|
+
utilizationPct,
|
|
849
|
+
costPerGB,
|
|
850
|
+
daysToExhaust,
|
|
851
|
+
country,
|
|
852
|
+
});
|
|
702
853
|
}
|
|
703
854
|
}
|
|
704
855
|
}
|
|
705
856
|
}));
|
|
706
857
|
}
|
|
858
|
+
// Sort by utilization (highest first)
|
|
707
859
|
highCostSubs.sort((a, b) => b.utilizationPct - a.utilizationPct);
|
|
708
|
-
sections.push(`
|
|
860
|
+
sections.push(`Analyzed ${subs.length} active subscribers (threshold: ${threshold}% usage)\n`);
|
|
709
861
|
if (highCostSubs.length === 0) {
|
|
710
862
|
sections.push(`No subscribers above ${threshold}% package utilization. Fleet margins look healthy.`);
|
|
711
863
|
}
|
|
@@ -716,17 +868,32 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
716
868
|
let totalDailyBytes = 0;
|
|
717
869
|
for (const s of highCostSubs) {
|
|
718
870
|
totalDailyBytes += s.dailyAvgBytes;
|
|
719
|
-
sections.push(`| ${s.iccid.slice(-8)}... | ${s.country} | ${formatBytes(s.dailyAvgBytes)} |
|
|
871
|
+
sections.push(`| ${s.iccid.slice(-8)}... | ${s.country} | ${formatBytes(s.dailyAvgBytes)} | ` +
|
|
872
|
+
`${s.utilizationPct.toFixed(0)}% | ${s.daysToExhaust === Infinity ? "inf" : s.daysToExhaust.toFixed(1)} | ` +
|
|
873
|
+
`${s.costPerGB > 0 ? s.costPerGB.toFixed(2) : "?"} |`);
|
|
720
874
|
}
|
|
721
875
|
sections.push(`\n## Summary`);
|
|
722
876
|
sections.push(`- High-cost subscribers: ${highCostSubs.length} / ${subs.length} (${((highCostSubs.length / subs.length) * 100).toFixed(1)}%)`);
|
|
723
877
|
sections.push(`- Combined daily data burn: ${formatBytes(totalDailyBytes)}`);
|
|
878
|
+
// Country breakdown
|
|
879
|
+
const byCountry = new Map();
|
|
880
|
+
for (const s of highCostSubs) {
|
|
881
|
+
byCountry.set(s.country, (byCountry.get(s.country) ?? 0) + 1);
|
|
882
|
+
}
|
|
883
|
+
const countrySorted = [...byCountry.entries()].sort((a, b) => b[1] - a[1]);
|
|
884
|
+
if (countrySorted.length > 0) {
|
|
885
|
+
sections.push(`\n### By Country`);
|
|
886
|
+
for (const [country, count] of countrySorted) {
|
|
887
|
+
sections.push(`- ${country}: ${count} high-cost subscriber(s)`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
724
890
|
sections.push(`\n## Recommended Actions`);
|
|
725
891
|
sections.push(`1. Review tariff rates for top countries via \`get_tariff\``);
|
|
726
892
|
sections.push(`2. Consider throttling heavy users via \`hlr_set_bitrate\``);
|
|
727
893
|
sections.push(`3. Run \`optimize_package\` on flagged ICCIDs to find better-fit plans`);
|
|
894
|
+
sections.push(`4. Negotiate better wholesale rates for high-volume countries`);
|
|
728
895
|
}
|
|
729
896
|
return result(sections.join("\n"));
|
|
730
|
-
})
|
|
897
|
+
});
|
|
731
898
|
}
|
|
732
899
|
//# sourceMappingURL=intelligence.js.map
|