@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/tools.js
CHANGED
|
@@ -1,34 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Carrier MCP
|
|
2
|
+
* Carrier MCP — OCS tool registrations (43 tools).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* All 43 OCS v1 methods are exposed as MCP tools. Each is wrapped with:
|
|
5
|
+
* - Scope enforcement (read / write / admin)
|
|
6
|
+
* - Dry-run short-circuit for destructive tools (no OCS call)
|
|
7
|
+
* - Sentry error capture
|
|
8
|
+
* - Audit hook into Analytics Engine
|
|
9
|
+
* - Per-user token resolution via getUserToken(sub)
|
|
6
10
|
*
|
|
7
|
-
*
|
|
11
|
+
* Scope assignments live in TOOL_SCOPES below; must match packages/ocs-spec/ocs-methods.json.
|
|
12
|
+
*
|
|
13
|
+
* PR #10 schema fixes applied:
|
|
8
14
|
* Fix #1: getSimProviderStatus — ICCID → simId lookup → bare integer
|
|
9
15
|
* Fix #2: hlrGetBitrate — ICCID → IMSI lookup → { imsi }
|
|
10
|
-
* Fix #3: hlrSetBitrate — ICCID → IMSI + bitrate → limit rename
|
|
11
|
-
* Fix #4: subscriberUsageOverPeriod — { subscriber: { iccid }, period }
|
|
16
|
+
* Fix #3: hlrSetBitrate — ICCID → IMSI + bitrate → limit rename → { imsi, limit }
|
|
17
|
+
* Fix #4: subscriberUsageOverPeriod — { subscriber: { iccid }, period: { start, end } }
|
|
12
18
|
* Fix #5: subscriberNetworkEventsOverPeriod — same nested shape
|
|
13
19
|
* Fix #6: modifySubscriberStatus — { subscriber, newStatus }
|
|
14
|
-
* Fix #7: modifySubscriberBalance — { subscriber, amount
|
|
15
|
-
* Fix #8: changeSimStatus — ICCID → simId + newStatus
|
|
16
|
-
* Fix #9: modifySubscriberContactInfo — name
|
|
17
|
-
* Fix #10: setSubscriberTrafficRestrictions — typed booleans
|
|
20
|
+
* Fix #7: modifySubscriberBalance — { subscriber, amount } or { subscriber, setBalance }
|
|
21
|
+
* Fix #8: changeSimStatus — ICCID → simId + simStatus → newStatus
|
|
22
|
+
* Fix #9: modifySubscriberContactInfo — firstName+lastName → name, email→mail, phone→phone
|
|
23
|
+
* Fix #10: setSubscriberTrafficRestrictions — typed booleans, not JSON string
|
|
18
24
|
* Fix #11: sendMtSms — ICCID→IMSI, message→text, sender→senderId
|
|
19
25
|
* Fix #12: listSponsor — bare integer (resellerId)
|
|
20
26
|
* Fix #13: listSteeringList — bare integer (resellerId)
|
|
21
|
-
* Fix #14: getCustomerTariff — bare integer +
|
|
27
|
+
* Fix #14: getCustomerTariff — bare integer + response key listTariffRule
|
|
22
28
|
* Fix #15: listDetailedLocationZone — bare integer (resellerId)
|
|
23
29
|
* Fix #16: modifySubscriberSteeringList — { subscriber, steeringListId }
|
|
24
30
|
*/
|
|
25
31
|
import { z } from "zod";
|
|
26
|
-
import
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
import * as Sentry from "@sentry/cloudflare";
|
|
33
|
+
import { OcsClient, OcsApiError } from "./client.js";
|
|
34
|
+
import { checkCallQuota, recordUsage, UPGRADE_URL } from "./billing.js";
|
|
35
|
+
// Scope metadata per tool — all 43 tools declared here.
|
|
36
|
+
// Must stay in sync with ocs-methods.json from packages/ocs-spec.
|
|
37
|
+
export const TOOL_SCOPES = {
|
|
38
|
+
// --- read ---
|
|
39
|
+
list_reseller_accounts: "read",
|
|
40
|
+
get_reseller_info: "read",
|
|
41
|
+
esim_status_per_account: "read",
|
|
42
|
+
list_sponsors: "read",
|
|
43
|
+
list_steering_lists: "read",
|
|
44
|
+
get_subscriber: "read",
|
|
45
|
+
list_subscribers: "read",
|
|
46
|
+
get_sim_provider_status: "read",
|
|
47
|
+
get_subscriber_location: "read",
|
|
48
|
+
hlr_get_bitrate: "read",
|
|
49
|
+
list_subscriber_packages: "read",
|
|
50
|
+
list_package_templates: "read",
|
|
51
|
+
list_location_zones: "read",
|
|
52
|
+
list_detailed_location_zones: "read",
|
|
53
|
+
list_destination_prefixes: "read",
|
|
54
|
+
subscriber_usage: "read",
|
|
55
|
+
subscriber_network_events: "read",
|
|
56
|
+
subscriber_active_period: "read",
|
|
57
|
+
get_tariff: "read",
|
|
58
|
+
list_network_profiles: "read",
|
|
59
|
+
// --- write ---
|
|
60
|
+
modify_subscriber_balance: "write",
|
|
61
|
+
modify_subscriber_status: "write",
|
|
62
|
+
modify_subscriber_contact_info: "write",
|
|
63
|
+
set_subscriber_traffic_restrictions: "write",
|
|
64
|
+
modify_subscriber_steering_list: "write",
|
|
65
|
+
move_subscriber_range_to_account: "write",
|
|
66
|
+
hlr_set_bitrate: "write",
|
|
67
|
+
assign_package: "write",
|
|
68
|
+
assign_recurring_package: "write",
|
|
69
|
+
modify_package_limits: "write",
|
|
70
|
+
modify_package_expiry: "write",
|
|
71
|
+
modify_package_status: "write",
|
|
72
|
+
stop_resume_recurring_package: "write",
|
|
73
|
+
create_package_template: "write",
|
|
74
|
+
create_location_zone: "write",
|
|
75
|
+
// --- admin ---
|
|
76
|
+
modify_account_balance: "admin", // corrected per ocs-spec PR #6
|
|
77
|
+
change_sim_status: "admin",
|
|
78
|
+
delete_subscriber_package: "admin",
|
|
79
|
+
clean_all_packages: "admin",
|
|
80
|
+
modify_template_core: "admin",
|
|
81
|
+
modify_template_recurring: "admin",
|
|
82
|
+
modify_template_throttling: "admin",
|
|
83
|
+
send_sms: "admin",
|
|
32
84
|
};
|
|
33
85
|
export const DESTRUCTIVE_TOOLS = new Set([
|
|
34
86
|
"modify_account_balance",
|
|
@@ -54,159 +106,350 @@ export const DESTRUCTIVE_TOOLS = new Set([
|
|
|
54
106
|
"create_package_template",
|
|
55
107
|
"create_location_zone",
|
|
56
108
|
"send_sms",
|
|
109
|
+
// v1.1 backlog — same dry_run contract as v1 write/admin tools (tools-backlog.ts)
|
|
110
|
+
"affect_subscriber_phone_number",
|
|
111
|
+
"modify_subscriber_mobile_plan",
|
|
112
|
+
"modify_subscriber_package_active_period",
|
|
113
|
+
"modify_subscriber_voip_plan",
|
|
114
|
+
"push_steering_to_subscriber",
|
|
115
|
+
"reset_subscriber_gz_counter",
|
|
57
116
|
]);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const hit = cache.get(iccid);
|
|
64
|
-
if (hit)
|
|
65
|
-
return hit;
|
|
66
|
-
const record = await client.call("getSingleSubscriber", { iccid });
|
|
67
|
-
cache.set(iccid, record);
|
|
68
|
-
return record;
|
|
69
|
-
}
|
|
70
|
-
async function getDefaultResellerId(client) {
|
|
71
|
-
const info = await client.call("getResellerInfo", {});
|
|
72
|
-
const id = info?.id;
|
|
73
|
-
if (typeof id !== "number") {
|
|
74
|
-
throw new Error("Could not determine resellerId from getResellerInfo");
|
|
75
|
-
}
|
|
76
|
-
return id;
|
|
77
|
-
}
|
|
78
|
-
function makeHandler(toolName, _client, fn) {
|
|
117
|
+
/**
|
|
118
|
+
* Wraps a tool handler with scope enforcement, dry-run short-circuit,
|
|
119
|
+
* Sentry capture, and audit logging.
|
|
120
|
+
*/
|
|
121
|
+
export function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
79
122
|
return async (args) => {
|
|
80
|
-
|
|
123
|
+
const start = Date.now();
|
|
124
|
+
const isDryRun = args.dry_run === true;
|
|
125
|
+
// Scope enforcement
|
|
126
|
+
if (!ctx.props.scope.includes(requiredScope)) {
|
|
127
|
+
ctx.audit({
|
|
128
|
+
tool_name: toolName,
|
|
129
|
+
ocs_method: ocsMethod,
|
|
130
|
+
status: "scope_denied",
|
|
131
|
+
dry_run: false,
|
|
132
|
+
duration_ms: 0,
|
|
133
|
+
});
|
|
81
134
|
return {
|
|
135
|
+
isError: true,
|
|
82
136
|
content: [
|
|
83
137
|
{
|
|
84
138
|
type: "text",
|
|
85
|
-
text: `
|
|
139
|
+
text: `Scope denied: tool '${toolName}' requires '${requiredScope}' scope. Your token has: [${ctx.props.scope.join(", ")}].`,
|
|
86
140
|
},
|
|
87
141
|
],
|
|
88
142
|
};
|
|
89
143
|
}
|
|
144
|
+
// Billing quota check (Phase 7 — v1.1)
|
|
145
|
+
// Enterprise is unlimited; free/pro are gate-checked against KV counter.
|
|
146
|
+
const quota = await checkCallQuota(ctx.env, ctx.props.sub, ctx.props.tier);
|
|
147
|
+
if (!quota.allowed) {
|
|
148
|
+
ctx.audit({
|
|
149
|
+
tool_name: toolName,
|
|
150
|
+
ocs_method: ocsMethod,
|
|
151
|
+
status: "quota_exceeded",
|
|
152
|
+
dry_run: isDryRun,
|
|
153
|
+
duration_ms: 0,
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
isError: true,
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: [
|
|
161
|
+
`Quota exceeded: your ${quota.tier} plan allows ${quota.tier === "free" ? "1,000" : "50,000"} tool calls/month.`,
|
|
162
|
+
`Resets ${quota.resetAt}.`,
|
|
163
|
+
`Upgrade at ${UPGRADE_URL}`,
|
|
164
|
+
].join(" "),
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// Dry-run short-circuit for destructive tools
|
|
170
|
+
if (isDryRun && DESTRUCTIVE_TOOLS.has(toolName)) {
|
|
171
|
+
ctx.audit({
|
|
172
|
+
tool_name: toolName,
|
|
173
|
+
ocs_method: ocsMethod,
|
|
174
|
+
status: "dry_run",
|
|
175
|
+
dry_run: true,
|
|
176
|
+
duration_ms: 0,
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: "text",
|
|
182
|
+
text: `[dry_run=true] Would execute '${toolName}' (OCS method '${ocsMethod}') with args: ${JSON.stringify(args)}. No changes made.`,
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
let result;
|
|
90
188
|
try {
|
|
91
|
-
|
|
189
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
190
|
+
result = await handler(args, token);
|
|
92
191
|
}
|
|
93
192
|
catch (err) {
|
|
193
|
+
try {
|
|
194
|
+
Sentry.captureException(err, {
|
|
195
|
+
tags: {
|
|
196
|
+
tool: toolName,
|
|
197
|
+
feature: "mcp",
|
|
198
|
+
reseller_id: String(ctx.props.reseller_id),
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Sentry may not be initialised in tests — swallow.
|
|
204
|
+
}
|
|
94
205
|
const message = err instanceof Error ? err.message : String(err);
|
|
95
|
-
const
|
|
206
|
+
const ocsCode = err instanceof OcsApiError ? err.code : undefined;
|
|
207
|
+
ctx.audit({
|
|
208
|
+
tool_name: toolName,
|
|
209
|
+
ocs_method: ocsMethod,
|
|
210
|
+
status: "error",
|
|
211
|
+
dry_run: isDryRun,
|
|
212
|
+
duration_ms: Date.now() - start,
|
|
213
|
+
...(ocsCode !== undefined ? { ocs_status_code: ocsCode } : {}),
|
|
214
|
+
});
|
|
96
215
|
return {
|
|
97
216
|
isError: true,
|
|
98
|
-
content: [{ type: "text", text: `Error: ${
|
|
217
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
99
218
|
};
|
|
100
219
|
}
|
|
220
|
+
ctx.audit({
|
|
221
|
+
tool_name: toolName,
|
|
222
|
+
ocs_method: ocsMethod,
|
|
223
|
+
status: result.isError ? "error" : "ok",
|
|
224
|
+
dry_run: isDryRun,
|
|
225
|
+
duration_ms: Date.now() - start,
|
|
226
|
+
});
|
|
227
|
+
// Record usage on successful (non-error, non-dry-run) calls.
|
|
228
|
+
// Fire-and-forget — never blocks the response.
|
|
229
|
+
if (!result.isError && !isDryRun) {
|
|
230
|
+
recordUsage(ctx.env, ctx.props.sub, ctx.props.tier);
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// OCS call helper — accepts explicit token (no module-level singleton).
|
|
237
|
+
// Fix: params broadened to accept bare scalar for methods that expect it
|
|
238
|
+
// (listSponsor, listSteeringList, getCustomerTariff, listDetailedLocationZone,
|
|
239
|
+
// getSimProviderStatus).
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
async function ocsCall(env, token, method, params = {}) {
|
|
242
|
+
const client = new OcsClient(env.CARRIER_OCS_BASE_URL, token);
|
|
243
|
+
const result = await client.call(method, params);
|
|
244
|
+
return {
|
|
245
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
101
246
|
};
|
|
102
247
|
}
|
|
103
|
-
export function
|
|
248
|
+
export async function resolveSubscriberByIccid(env, token, iccid, cache) {
|
|
249
|
+
const hit = cache.get(iccid);
|
|
250
|
+
if (hit)
|
|
251
|
+
return hit;
|
|
252
|
+
const client = new OcsClient(env.CARRIER_OCS_BASE_URL, token);
|
|
253
|
+
const record = await client.call("getSingleSubscriber", { iccid });
|
|
254
|
+
cache.set(iccid, record);
|
|
255
|
+
return record;
|
|
256
|
+
}
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// Fetch the token owner's reseller ID via getResellerInfo.
|
|
259
|
+
// Used for bare-integer OCS methods: listSponsor, listSteeringList,
|
|
260
|
+
// getCustomerTariff, listDetailedLocationZone.
|
|
261
|
+
// Fix #12, #13, #14, #15.
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
export async function getDefaultResellerId(env, token) {
|
|
264
|
+
const client = new OcsClient(env.CARRIER_OCS_BASE_URL, token);
|
|
265
|
+
const info = await client.call("getResellerInfo", {});
|
|
266
|
+
const id = info?.id;
|
|
267
|
+
if (typeof id !== "number") {
|
|
268
|
+
throw new Error("Could not determine resellerId from getResellerInfo");
|
|
269
|
+
}
|
|
270
|
+
return id;
|
|
271
|
+
}
|
|
272
|
+
// Shorthand: build the destructive-tool annotation + dry_run schema field
|
|
273
|
+
const DRY_RUN_FIELD = {
|
|
274
|
+
dry_run: z
|
|
275
|
+
.boolean()
|
|
276
|
+
.optional()
|
|
277
|
+
.describe("If true, do not call OCS — return the would-be request for confirmation"),
|
|
278
|
+
};
|
|
279
|
+
export function registerAllTools(server, ctx) {
|
|
104
280
|
// =========================================================================
|
|
105
281
|
// 1. RESELLER TOOLS
|
|
106
282
|
// =========================================================================
|
|
107
283
|
server.registerTool("list_reseller_accounts", {
|
|
108
284
|
title: "List Reseller Accounts",
|
|
109
|
-
description: "
|
|
285
|
+
description: "Use this to enumerate all accounts (sub-resellers or customer accounts) under a reseller. " +
|
|
286
|
+
"Returns each account's name, ID, current balance, package-only flag, and account type. " +
|
|
287
|
+
"Params: `resellerId` (integer, optional — omit to list accounts under the token owner's reseller). " +
|
|
288
|
+
"Returns: array of account records, each containing `accountId`, `name`, `balance`, `type`. " +
|
|
289
|
+
"Do NOT use this to fetch a single subscriber's details — use `get_subscriber` instead. " +
|
|
290
|
+
"Do NOT use this to check eSIM activation counts — use `esim_status_per_account` for that.",
|
|
110
291
|
inputSchema: {
|
|
111
|
-
resellerId: z
|
|
292
|
+
resellerId: z
|
|
293
|
+
.number()
|
|
294
|
+
.optional()
|
|
295
|
+
.describe("Filter to a specific reseller by ID (omit for token owner's reseller)"),
|
|
112
296
|
},
|
|
113
297
|
annotations: { readOnlyHint: true },
|
|
114
|
-
},
|
|
298
|
+
}, wrapHandler("list_reseller_accounts", "listResellerAccount", TOOL_SCOPES["list_reseller_accounts"], ctx, async ({ resellerId }, token) => {
|
|
115
299
|
const params = {};
|
|
116
300
|
if (resellerId !== undefined)
|
|
117
301
|
params.resellerId = resellerId;
|
|
118
|
-
return ocsCall(
|
|
302
|
+
return ocsCall(ctx.env, token, "listResellerAccount", params);
|
|
119
303
|
}));
|
|
120
304
|
server.registerTool("modify_account_balance", {
|
|
121
305
|
title: "Modify Account Balance",
|
|
122
|
-
description: "
|
|
306
|
+
description: "Use this to adjust or set the monetary balance on a reseller account. " +
|
|
307
|
+
"'adapt' mode adds (positive amount) or subtracts (negative amount) from the current balance; " +
|
|
308
|
+
"'set' mode replaces the balance with the exact amount. Every change is logged as a transaction. " +
|
|
309
|
+
"Params: `accountId` (integer account ID from `list_reseller_accounts`), `amount` (number), " +
|
|
310
|
+
"`mode` ('adapt' | 'set'). " +
|
|
311
|
+
"Returns: updated account balance record with the transaction ID. " +
|
|
312
|
+
"Do NOT use this to modify a subscriber's personal balance — use `modify_subscriber_balance` instead. " +
|
|
313
|
+
"Always call `list_reseller_accounts` first to confirm the target accountId before executing.",
|
|
123
314
|
inputSchema: {
|
|
124
315
|
accountId: z.number().describe("The account ID to modify"),
|
|
125
316
|
amount: z.number().describe("Amount to add (adapt) or set to (set)"),
|
|
126
|
-
mode: z
|
|
317
|
+
mode: z
|
|
318
|
+
.enum(["adapt", "set"])
|
|
319
|
+
.describe("'adapt' adds/subtracts, 'set' replaces the balance"),
|
|
127
320
|
...DRY_RUN_FIELD,
|
|
128
321
|
},
|
|
129
322
|
annotations: { destructiveHint: true },
|
|
130
|
-
},
|
|
323
|
+
}, wrapHandler("modify_account_balance", "modifyAccountBalance", TOOL_SCOPES["modify_account_balance"], ctx, async ({ accountId, amount, mode }, token) => {
|
|
131
324
|
const params = { accountId };
|
|
132
325
|
if (mode === "adapt")
|
|
133
326
|
params.adaptBalance = amount;
|
|
134
327
|
else
|
|
135
328
|
params.setBalance = amount;
|
|
136
|
-
return ocsCall(
|
|
329
|
+
return ocsCall(ctx.env, token, "modifyAccountBalance", params);
|
|
137
330
|
}));
|
|
138
331
|
server.registerTool("get_reseller_info", {
|
|
139
332
|
title: "Get Reseller Info",
|
|
140
|
-
description: "
|
|
333
|
+
description: "Use this to retrieve full details for a reseller: main info, traffic configuration, " +
|
|
334
|
+
"charging info, contact info, and active pricing plans. " +
|
|
335
|
+
"Params: `resellerId` (integer, optional — omit to return the token owner's reseller). " +
|
|
336
|
+
"Returns: reseller object with `id`, `name`, `balance`, `pricingPlan`, `contactInfo`, and more. " +
|
|
337
|
+
"Do NOT use this to list all accounts under a reseller — use `list_reseller_accounts` for that.",
|
|
141
338
|
inputSchema: {
|
|
142
|
-
resellerId: z
|
|
339
|
+
resellerId: z
|
|
340
|
+
.number()
|
|
341
|
+
.optional()
|
|
342
|
+
.describe("Reseller ID (omit for token owner)"),
|
|
143
343
|
},
|
|
144
344
|
annotations: { readOnlyHint: true },
|
|
145
|
-
},
|
|
345
|
+
}, wrapHandler("get_reseller_info", "getResellerInfo", TOOL_SCOPES["get_reseller_info"], ctx, async ({ resellerId }, token) => {
|
|
146
346
|
const params = {};
|
|
147
347
|
if (resellerId !== undefined)
|
|
148
348
|
params.resellerId = resellerId;
|
|
149
|
-
return ocsCall(
|
|
349
|
+
return ocsCall(ctx.env, token, "getResellerInfo", params);
|
|
150
350
|
}));
|
|
151
351
|
server.registerTool("esim_status_per_account", {
|
|
152
352
|
title: "eSIM Status Per Account",
|
|
153
|
-
description: "
|
|
353
|
+
description: "Use this to get eSIM status counts broken down by account: active, suspended, inventory " +
|
|
354
|
+
"(not yet activated), and other states. Good for fleet health dashboards and capacity planning. " +
|
|
355
|
+
"Params: `accountId` (integer, optional — omit for all accounts under the reseller). " +
|
|
356
|
+
"Returns: array of per-account objects with `accountId`, `active`, `suspended`, `inventory`, `other`. " +
|
|
357
|
+
"Do NOT use this to check a single subscriber's status — use `get_subscriber` for that. " +
|
|
358
|
+
"Do NOT use this for billing or balance checks — use `list_reseller_accounts` for balances.",
|
|
154
359
|
inputSchema: {
|
|
155
|
-
accountId: z
|
|
360
|
+
accountId: z
|
|
361
|
+
.number()
|
|
362
|
+
.optional()
|
|
363
|
+
.describe("Filter to a specific account"),
|
|
156
364
|
},
|
|
157
365
|
annotations: { readOnlyHint: true },
|
|
158
|
-
},
|
|
366
|
+
}, wrapHandler("esim_status_per_account", "esimStatusPerAccount", TOOL_SCOPES["esim_status_per_account"], ctx, async ({ accountId }, token) => {
|
|
159
367
|
const params = {};
|
|
160
368
|
if (accountId !== undefined)
|
|
161
369
|
params.accountId = accountId;
|
|
162
|
-
return ocsCall(
|
|
370
|
+
return ocsCall(ctx.env, token, "esimStatusPerAccount", params);
|
|
163
371
|
}));
|
|
164
|
-
// Fix #12: bare integer
|
|
372
|
+
// Fix #12: OCS expects bare integer (resellerId), not {}
|
|
165
373
|
server.registerTool("list_sponsors", {
|
|
166
374
|
title: "List Sponsors",
|
|
167
|
-
description: "
|
|
375
|
+
description: "Use this to list all sponsor networks (eSIM sponsor carriers) available to this reseller. " +
|
|
376
|
+
"A sponsor defines which physical network infrastructure backs a given eSIM profile. " +
|
|
377
|
+
"Params: `resellerId` (integer, optional — omit to use the token owner's reseller). " +
|
|
378
|
+
"Returns: array of sponsor records with `sponsorId`, `name`, and coverage metadata. " +
|
|
379
|
+
"Do NOT use this to list steering lists or network profiles — those are separate concepts. " +
|
|
380
|
+
"Use `list_steering_lists` to see operator preference configurations.",
|
|
168
381
|
inputSchema: {
|
|
169
|
-
resellerId: z
|
|
382
|
+
resellerId: z
|
|
383
|
+
.number()
|
|
384
|
+
.optional()
|
|
385
|
+
.describe("Reseller ID (omit to use token owner's reseller)"),
|
|
170
386
|
},
|
|
171
387
|
annotations: { readOnlyHint: true },
|
|
172
|
-
},
|
|
173
|
-
const id = resellerId ?? (await getDefaultResellerId(
|
|
174
|
-
return ocsCall(
|
|
388
|
+
}, wrapHandler("list_sponsors", "listSponsor", TOOL_SCOPES["list_sponsors"], ctx, async ({ resellerId }, token) => {
|
|
389
|
+
const id = resellerId ?? (await getDefaultResellerId(ctx.env, token));
|
|
390
|
+
return ocsCall(ctx.env, token, "listSponsor", id);
|
|
175
391
|
}));
|
|
176
|
-
// Fix #13: bare integer
|
|
392
|
+
// Fix #13: OCS expects bare integer (resellerId), not {}
|
|
177
393
|
server.registerTool("list_steering_lists", {
|
|
178
394
|
title: "List Steering Lists",
|
|
179
|
-
description: "
|
|
395
|
+
description: "Use this to retrieve all network steering lists configured for this reseller. " +
|
|
396
|
+
"A steering list is a named configuration of excluded and priority mobile operators that " +
|
|
397
|
+
"controls which networks an eSIM prefers to roam onto — the primary mechanism for network " +
|
|
398
|
+
"quality optimisation and cost control. Call this before `modify_subscriber_steering_list` " +
|
|
399
|
+
"to obtain valid steering list IDs. " +
|
|
400
|
+
"Params: `resellerId` (integer, optional — omit to use the token owner's reseller). " +
|
|
401
|
+
"Returns: array of steering list records with `steeringListId`, `name`, and configured operators. " +
|
|
402
|
+
"Do NOT use this to assign a steering list to a subscriber — use `modify_subscriber_steering_list`. " +
|
|
403
|
+
"Do NOT use this to push a steering change to a device — use `push_steering_to_subscriber` after assignment.",
|
|
180
404
|
inputSchema: {
|
|
181
|
-
resellerId: z
|
|
405
|
+
resellerId: z
|
|
406
|
+
.number()
|
|
407
|
+
.optional()
|
|
408
|
+
.describe("Reseller ID (omit to use token owner's reseller)"),
|
|
182
409
|
},
|
|
183
410
|
annotations: { readOnlyHint: true },
|
|
184
|
-
},
|
|
185
|
-
const id = resellerId ?? (await getDefaultResellerId(
|
|
186
|
-
return ocsCall(
|
|
411
|
+
}, wrapHandler("list_steering_lists", "listSteeringList", TOOL_SCOPES["list_steering_lists"], ctx, async ({ resellerId }, token) => {
|
|
412
|
+
const id = resellerId ?? (await getDefaultResellerId(ctx.env, token));
|
|
413
|
+
return ocsCall(ctx.env, token, "listSteeringList", id);
|
|
187
414
|
}));
|
|
188
415
|
// =========================================================================
|
|
189
416
|
// 2. SUBSCRIBER TOOLS
|
|
190
417
|
// =========================================================================
|
|
191
418
|
server.registerTool("get_subscriber", {
|
|
192
419
|
title: "Get Single Subscriber",
|
|
193
|
-
description: "
|
|
420
|
+
description: "Use this as the primary lookup for a single subscriber by ICCID or MSISDN. " +
|
|
421
|
+
"Returns the complete subscriber record: status, balance, assigned account, contact info, " +
|
|
422
|
+
"IMSI, simId, steering list, active pricing plan, and traffic restriction flags. " +
|
|
423
|
+
"Params: `iccid` (20-digit ICC identifier, optional) OR `msisdn` (E.164 phone number, optional) — " +
|
|
424
|
+
"provide at least one. " +
|
|
425
|
+
"Returns: full subscriber object. Key fields: `status` (ACTIVE/SUSPENDED/TERMINATED), " +
|
|
426
|
+
"`balance`, `imsi`, `simId`, `steeringListId`. " +
|
|
427
|
+
"Do NOT use this for bulk lookups — use `list_subscribers` with filters for that.",
|
|
194
428
|
inputSchema: {
|
|
195
429
|
iccid: z.string().optional().describe("The ICCID of the subscriber"),
|
|
196
|
-
msisdn: z
|
|
430
|
+
msisdn: z
|
|
431
|
+
.string()
|
|
432
|
+
.optional()
|
|
433
|
+
.describe("The MSISDN (phone number) of the subscriber"),
|
|
197
434
|
},
|
|
198
435
|
annotations: { readOnlyHint: true },
|
|
199
|
-
},
|
|
436
|
+
}, wrapHandler("get_subscriber", "getSingleSubscriber", TOOL_SCOPES["get_subscriber"], ctx, async ({ iccid, msisdn }, token) => {
|
|
200
437
|
const params = {};
|
|
201
438
|
if (iccid)
|
|
202
439
|
params.iccid = iccid;
|
|
203
440
|
if (msisdn)
|
|
204
441
|
params.msisdn = msisdn;
|
|
205
|
-
return ocsCall(
|
|
442
|
+
return ocsCall(ctx.env, token, "getSingleSubscriber", params);
|
|
206
443
|
}));
|
|
207
444
|
server.registerTool("list_subscribers", {
|
|
208
445
|
title: "List Subscribers",
|
|
209
|
-
description: "
|
|
446
|
+
description: "Use this to list subscribers with optional filters and pagination. Good for fleet enumeration, " +
|
|
447
|
+
"bulk status checks, and finding subscribers by account or status. " +
|
|
448
|
+
"Params: `accountId` (integer, filter by account), `status` (string, e.g. 'ACTIVE'/'SUSPENDED'), " +
|
|
449
|
+
"`offset` (integer, for pagination — default 0), `limit` (integer, max results — always set to avoid " +
|
|
450
|
+
"unbounded fetches; recommended max 100 per call). " +
|
|
451
|
+
"Returns: array of subscriber summary records with ICCID, status, and account. " +
|
|
452
|
+
"Do NOT use this to fetch full details for a specific subscriber — use `get_subscriber` for that.",
|
|
210
453
|
inputSchema: {
|
|
211
454
|
accountId: z.number().optional().describe("Filter by account ID"),
|
|
212
455
|
status: z.string().optional().describe("Filter by status"),
|
|
@@ -214,7 +457,9 @@ export function registerAllTools(server, client) {
|
|
|
214
457
|
limit: z.number().optional().describe("Max results to return"),
|
|
215
458
|
},
|
|
216
459
|
annotations: { readOnlyHint: true },
|
|
217
|
-
},
|
|
460
|
+
}, wrapHandler("list_subscribers", "listSubscriber", TOOL_SCOPES["list_subscribers"], ctx, async (args, token) => {
|
|
461
|
+
// OCS listSubscriber does not accept `limit` — strip it before forwarding.
|
|
462
|
+
// We keep `limit` in the inputSchema as a UX hint so callers can express intent.
|
|
218
463
|
const params = {};
|
|
219
464
|
if (args.accountId !== undefined)
|
|
220
465
|
params.accountId = args.accountId;
|
|
@@ -222,90 +467,148 @@ export function registerAllTools(server, client) {
|
|
|
222
467
|
params.status = args.status;
|
|
223
468
|
if (args.offset !== undefined)
|
|
224
469
|
params.offset = args.offset;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
470
|
+
const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token);
|
|
471
|
+
const raw = await client.call("listSubscriber", params);
|
|
472
|
+
const limit = args.limit;
|
|
473
|
+
const payload = Array.isArray(raw) && typeof limit === "number" && limit >= 0
|
|
474
|
+
? raw.slice(0, limit)
|
|
475
|
+
: raw;
|
|
476
|
+
return {
|
|
477
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
478
|
+
};
|
|
228
479
|
}));
|
|
229
|
-
// Fix #7: { subscriber, amount } or { subscriber, setBalance }
|
|
480
|
+
// Fix #7: OCS expects { subscriber, amount } or { subscriber, setBalance } not { iccid, adaptBalance|setBalance }
|
|
230
481
|
server.registerTool("modify_subscriber_balance", {
|
|
231
482
|
title: "Modify Subscriber Balance",
|
|
232
|
-
description: "
|
|
483
|
+
description: "Use this to adjust or set the monetary balance for an individual subscriber. " +
|
|
484
|
+
"'adapt' mode adds (positive) or subtracts (negative) from the current balance; " +
|
|
485
|
+
"'set' mode replaces the balance with the exact amount provided. " +
|
|
486
|
+
"Params: `iccid` (subscriber identifier), `amount` (number), `mode` ('adapt' | 'set'). " +
|
|
487
|
+
"Returns: updated subscriber balance. " +
|
|
488
|
+
"Do NOT use this to modify an account-level balance — use `modify_account_balance` for that. " +
|
|
489
|
+
"Always call `get_subscriber` first to capture the current balance before adjusting.",
|
|
233
490
|
inputSchema: {
|
|
234
491
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
235
492
|
amount: z.number().describe("Amount to add (adapt) or set to (set)"),
|
|
236
|
-
mode: z
|
|
493
|
+
mode: z
|
|
494
|
+
.enum(["adapt", "set"])
|
|
495
|
+
.describe("'adapt' adds/subtracts, 'set' replaces"),
|
|
237
496
|
...DRY_RUN_FIELD,
|
|
238
497
|
},
|
|
239
498
|
annotations: { destructiveHint: true },
|
|
240
|
-
},
|
|
499
|
+
}, wrapHandler("modify_subscriber_balance", "modifySubscriberBalance", TOOL_SCOPES["modify_subscriber_balance"], ctx, async ({ iccid, amount, mode }, token) => {
|
|
241
500
|
const params = { subscriber: iccid };
|
|
242
501
|
if (mode === "adapt")
|
|
243
502
|
params.amount = amount;
|
|
244
503
|
else
|
|
245
504
|
params.setBalance = amount;
|
|
246
|
-
return ocsCall(
|
|
505
|
+
return ocsCall(ctx.env, token, "modifySubscriberBalance", params);
|
|
247
506
|
}));
|
|
248
|
-
// Fix #6: { subscriber, newStatus }
|
|
507
|
+
// Fix #6: OCS expects { subscriber, newStatus } not { iccid, status }
|
|
249
508
|
server.registerTool("modify_subscriber_status", {
|
|
250
509
|
title: "Modify Subscriber Status",
|
|
251
|
-
description: "
|
|
510
|
+
description: "Use this to change the OCS lifecycle status of a subscriber. " +
|
|
511
|
+
"Common transitions: ACTIVE → SUSPENDED (pause without losing packages), " +
|
|
512
|
+
"SUSPENDED → ACTIVE (reactivate), ACTIVE/SUSPENDED → TERMINATED (irreversible). " +
|
|
513
|
+
"WARNING: TERMINATED status is permanent — the subscriber record cannot be reactivated. " +
|
|
514
|
+
"Params: `iccid` (subscriber identifier), `status` (new status string, e.g. 'ACTIVE', " +
|
|
515
|
+
"'SUSPENDED', 'TERMINATED'). " +
|
|
516
|
+
"Returns: updated subscriber record with the new status. " +
|
|
517
|
+
"Do NOT use this to disable the SIM card at the network level — use `change_sim_status` for that. " +
|
|
518
|
+
"Always call `get_subscriber` first to confirm current status before modifying.",
|
|
252
519
|
inputSchema: {
|
|
253
520
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
254
521
|
status: z.string().describe("New status value"),
|
|
255
522
|
...DRY_RUN_FIELD,
|
|
256
523
|
},
|
|
257
524
|
annotations: { destructiveHint: true },
|
|
258
|
-
},
|
|
259
|
-
|
|
525
|
+
}, wrapHandler("modify_subscriber_status", "modifySubscriberStatus", TOOL_SCOPES["modify_subscriber_status"], ctx, async ({ iccid, status }, token) => ocsCall(ctx.env, token, "modifySubscriberStatus", {
|
|
526
|
+
subscriber: iccid,
|
|
527
|
+
newStatus: status,
|
|
528
|
+
})));
|
|
529
|
+
// Fix #8: OCS expects { simId, newStatus } not { iccid, simStatus }.
|
|
530
|
+
// Resolve ICCID → simId via getSingleSubscriber, then send { simId, newStatus }.
|
|
260
531
|
server.registerTool("change_sim_status", {
|
|
261
532
|
title: "Change SIM Status",
|
|
262
|
-
description: "
|
|
533
|
+
description: "Use this to change the physical SIM/eSIM card status at the SIM provider level, " +
|
|
534
|
+
"independent of the OCS subscriber lifecycle status. " +
|
|
535
|
+
"Statuses: ENABLED (normal operation), DISABLED (blocked at network level, subscriber cannot connect), " +
|
|
536
|
+
"DELETED (irrecoverably removes the SIM profile — use only to decommission). " +
|
|
537
|
+
"WARNING: DELETED is irreversible. Always use `dry_run=true` first. " +
|
|
538
|
+
"Internally resolves ICCID → numeric simId via a getSingleSubscriber call before forwarding to OCS. " +
|
|
539
|
+
"Params: `iccid` (subscriber identifier), `simStatus` ('ENABLED' | 'DISABLED' | 'DELETED'). " +
|
|
540
|
+
"Returns: updated SIM record with new status. " +
|
|
541
|
+
"Do NOT use this to change the subscriber's OCS lifecycle status — use `modify_subscriber_status`. " +
|
|
542
|
+
"Do NOT confuse DISABLED (reversible) with DELETED (irreversible).",
|
|
263
543
|
inputSchema: {
|
|
264
544
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
265
545
|
simStatus: z.string().describe("New SIM status (e.g. ENABLED, DISABLED, DELETED)"),
|
|
266
546
|
...DRY_RUN_FIELD,
|
|
267
547
|
},
|
|
268
548
|
annotations: { destructiveHint: true },
|
|
269
|
-
},
|
|
549
|
+
}, wrapHandler("change_sim_status", "changeSimStatus", TOOL_SCOPES["change_sim_status"], ctx, async ({ iccid, simStatus }, token) => {
|
|
270
550
|
const cache = new Map();
|
|
271
|
-
const sub = await resolveSubscriberByIccid(
|
|
272
|
-
const simId = sub.simId ?? sub.sim_id ?? sub.id;
|
|
551
|
+
const sub = await resolveSubscriberByIccid(ctx.env, token, iccid, cache);
|
|
552
|
+
const simId = Number(sub.simId ?? sub.sim_id ?? sub.id);
|
|
273
553
|
if (simId === undefined) {
|
|
274
554
|
return {
|
|
275
555
|
isError: true,
|
|
276
556
|
content: [{ type: "text", text: `Error: Could not resolve simId for ICCID ${iccid}` }],
|
|
277
557
|
};
|
|
278
558
|
}
|
|
279
|
-
return ocsCall(
|
|
559
|
+
return ocsCall(ctx.env, token, "changeSimStatus", {
|
|
560
|
+
simId: Number(simId),
|
|
561
|
+
newStatus: simStatus,
|
|
562
|
+
});
|
|
280
563
|
}));
|
|
281
|
-
// Fix #1:
|
|
564
|
+
// Fix #1: OCS expects bare Long (simId integer) not { iccid }.
|
|
565
|
+
// Resolve ICCID → simId via getSingleSubscriber, then send bare integer.
|
|
282
566
|
server.registerTool("get_sim_provider_status", {
|
|
283
567
|
title: "Get SIM Provider Status",
|
|
284
|
-
description: "
|
|
285
|
-
|
|
568
|
+
description: "Use this to check the physical SIM/eSIM card status at the SIM provider level " +
|
|
569
|
+
"(ENABLED, DISABLED, DELETED) — distinct from the OCS subscriber status. " +
|
|
570
|
+
"Useful when `get_subscriber` shows ACTIVE but connectivity is broken; the SIM may be " +
|
|
571
|
+
"DISABLED at the provider level. Internally resolves ICCID → numeric simId. " +
|
|
572
|
+
"Params: `iccid` (subscriber identifier). " +
|
|
573
|
+
"Returns: provider status object with `simStatus`, `activationDate`, `lastStatusChange`. " +
|
|
574
|
+
"Do NOT use this to change the SIM status — use `change_sim_status` for that.",
|
|
575
|
+
inputSchema: {
|
|
576
|
+
iccid: z.string().describe("The subscriber ICCID"),
|
|
577
|
+
},
|
|
286
578
|
annotations: { readOnlyHint: true },
|
|
287
|
-
},
|
|
579
|
+
}, wrapHandler("get_sim_provider_status", "getSimProviderStatus", TOOL_SCOPES["get_sim_provider_status"], ctx, async ({ iccid }, token) => {
|
|
288
580
|
const cache = new Map();
|
|
289
|
-
const sub = await resolveSubscriberByIccid(
|
|
290
|
-
const simId = sub.simId ?? sub.sim_id ?? sub.id;
|
|
581
|
+
const sub = await resolveSubscriberByIccid(ctx.env, token, iccid, cache);
|
|
582
|
+
const simId = Number(sub.simId ?? sub.sim_id ?? sub.id);
|
|
291
583
|
if (simId === undefined) {
|
|
292
584
|
return {
|
|
293
585
|
isError: true,
|
|
294
586
|
content: [{ type: "text", text: `Error: Could not resolve simId for ICCID ${iccid}` }],
|
|
295
587
|
};
|
|
296
588
|
}
|
|
297
|
-
return ocsCall(
|
|
589
|
+
return ocsCall(ctx.env, token, "getSimProviderStatus", Number(simId));
|
|
298
590
|
}));
|
|
299
591
|
server.registerTool("get_subscriber_location", {
|
|
300
592
|
title: "Get Subscriber Location",
|
|
301
|
-
description: "
|
|
593
|
+
description: "Use this to get the last known roaming location of a subscriber (country and operator). " +
|
|
594
|
+
"Returned on a best-effort basis — location is updated when the device re-registers on the network. " +
|
|
595
|
+
"Params: `iccid` (subscriber identifier). " +
|
|
596
|
+
"Returns: object with `country`, `countryCode`, `network`, `operator`, and `timestamp` of last update. " +
|
|
597
|
+
"Do NOT use this for cell-tower-level precision — use `get_subscriber_location_by_cell_id` instead. " +
|
|
598
|
+
"Do NOT use this for bulk fleet location analysis — use `audit_network_coverage` for that.",
|
|
302
599
|
inputSchema: { iccid: z.string().describe("The subscriber ICCID") },
|
|
303
600
|
annotations: { readOnlyHint: true },
|
|
304
|
-
},
|
|
305
|
-
// Fix #9: name
|
|
601
|
+
}, wrapHandler("get_subscriber_location", "getSubscriberLocation", TOOL_SCOPES["get_subscriber_location"], ctx, async ({ iccid }, token) => ocsCall(ctx.env, token, "getSubscriberLocation", { iccid })));
|
|
602
|
+
// Fix #9: OCS expects { subscriber, name, company, phone, mail }
|
|
603
|
+
// not { iccid, firstName, lastName, email, phoneNumber }
|
|
306
604
|
server.registerTool("modify_subscriber_contact_info", {
|
|
307
605
|
title: "Modify Subscriber Contact Info",
|
|
308
|
-
description: "
|
|
606
|
+
description: "Use this to update the contact details stored on a subscriber record in OCS. " +
|
|
607
|
+
"Only the fields you provide are updated — omitted fields are left unchanged. " +
|
|
608
|
+
"Params: `iccid` (subscriber identifier), `firstName` (optional), `lastName` (optional), " +
|
|
609
|
+
"`company` (optional), `email` (optional), `phoneNumber` (optional). " +
|
|
610
|
+
"Returns: updated subscriber contact record. " +
|
|
611
|
+
"Do NOT use this to change subscriber status, balance, or traffic flags — those have dedicated tools.",
|
|
309
612
|
inputSchema: {
|
|
310
613
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
311
614
|
firstName: z.string().optional().describe("First name"),
|
|
@@ -316,7 +619,7 @@ export function registerAllTools(server, client) {
|
|
|
316
619
|
...DRY_RUN_FIELD,
|
|
317
620
|
},
|
|
318
621
|
annotations: { destructiveHint: true },
|
|
319
|
-
},
|
|
622
|
+
}, wrapHandler("modify_subscriber_contact_info", "modifySubscriberContactInfo", TOOL_SCOPES["modify_subscriber_contact_info"], ctx, async ({ iccid, firstName, lastName, company, email, phoneNumber }, token) => {
|
|
320
623
|
const params = { subscriber: iccid };
|
|
321
624
|
const nameParts = [firstName, lastName].filter(Boolean);
|
|
322
625
|
if (nameParts.length > 0)
|
|
@@ -327,12 +630,21 @@ export function registerAllTools(server, client) {
|
|
|
327
630
|
params.phone = phoneNumber;
|
|
328
631
|
if (email !== undefined)
|
|
329
632
|
params.mail = email;
|
|
330
|
-
return ocsCall(
|
|
633
|
+
return ocsCall(ctx.env, token, "modifySubscriberContactInfo", params);
|
|
331
634
|
}));
|
|
332
|
-
// Fix #10:
|
|
635
|
+
// Fix #10: OCS expects { subscriber, mtcAllowed, smsMoAllowed, dataAllowed, mocAllowed }
|
|
636
|
+
// Drop JSON-string antipattern; use typed booleans directly.
|
|
333
637
|
server.registerTool("set_subscriber_traffic_restrictions", {
|
|
334
638
|
title: "Set Traffic Restrictions",
|
|
335
|
-
description: "
|
|
639
|
+
description: "Use this to enable or disable individual traffic types for a subscriber: mobile data, " +
|
|
640
|
+
"voice calls (mobile-originated and mobile-terminated), and SMS. Omit any flag to leave " +
|
|
641
|
+
"it unchanged. Changes take effect immediately at the OCS level. " +
|
|
642
|
+
"Params: `iccid` (subscriber identifier), `dataAllowed` (boolean, controls data traffic), " +
|
|
643
|
+
"`mocAllowed` (boolean, controls outbound calls), `mtcAllowed` (boolean, controls inbound calls), " +
|
|
644
|
+
"`smsMoAllowed` (boolean, controls outbound SMS). " +
|
|
645
|
+
"Returns: updated traffic restriction record for the subscriber. " +
|
|
646
|
+
"Do NOT use this to throttle bandwidth — use `hlr_set_bitrate` for speed limiting. " +
|
|
647
|
+
"Do NOT use this to suspend the subscriber entirely — use `modify_subscriber_status` (SUSPENDED) instead.",
|
|
336
648
|
inputSchema: {
|
|
337
649
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
338
650
|
mtcAllowed: z.boolean().optional().describe("Allow mobile-terminated calls"),
|
|
@@ -342,7 +654,7 @@ export function registerAllTools(server, client) {
|
|
|
342
654
|
...DRY_RUN_FIELD,
|
|
343
655
|
},
|
|
344
656
|
annotations: { destructiveHint: true },
|
|
345
|
-
},
|
|
657
|
+
}, wrapHandler("set_subscriber_traffic_restrictions", "setSubscriberTrafficRestrictions", TOOL_SCOPES["set_subscriber_traffic_restrictions"], ctx, async ({ iccid, mtcAllowed, smsMoAllowed, dataAllowed, mocAllowed }, token) => {
|
|
346
658
|
const params = { subscriber: iccid };
|
|
347
659
|
if (mtcAllowed !== undefined)
|
|
348
660
|
params.mtcAllowed = mtcAllowed;
|
|
@@ -352,22 +664,40 @@ export function registerAllTools(server, client) {
|
|
|
352
664
|
params.dataAllowed = dataAllowed;
|
|
353
665
|
if (mocAllowed !== undefined)
|
|
354
666
|
params.mocAllowed = mocAllowed;
|
|
355
|
-
return ocsCall(
|
|
667
|
+
return ocsCall(ctx.env, token, "setSubscriberTrafficRestrictions", params);
|
|
356
668
|
}));
|
|
357
|
-
// Fix #16: { subscriber, steeringListId }
|
|
669
|
+
// Fix #16: OCS expects { subscriber, steeringListId } not { iccid, steeringListId }
|
|
358
670
|
server.registerTool("modify_subscriber_steering_list", {
|
|
359
671
|
title: "Modify Subscriber Steering List",
|
|
360
|
-
description: "
|
|
672
|
+
description: "Use this to assign or remove a network steering list on a specific subscriber, controlling " +
|
|
673
|
+
"which mobile operators the subscriber's eSIM prefers to connect to. Steering lists are " +
|
|
674
|
+
"managed separately — call `list_steering_lists` to get valid IDs. " +
|
|
675
|
+
"This operates at the SUBSCRIBER level only. After assigning, call `push_steering_to_subscriber` " +
|
|
676
|
+
"to push the change to the physical device immediately; without that call the device continues " +
|
|
677
|
+
"using the old operator preference list until next re-registration. " +
|
|
678
|
+
"Params: `iccid` (subscriber identifier), `steeringListId` (integer from `list_steering_lists`, " +
|
|
679
|
+
"or null/0 to remove the current steering list). " +
|
|
680
|
+
"Returns: updated subscriber record confirming the new steeringListId. " +
|
|
681
|
+
"Do NOT use this for account-level steering (no MCP tool yet — gap G-03, awaiting eSIMVault input).",
|
|
361
682
|
inputSchema: {
|
|
362
683
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
363
684
|
steeringListId: z.number().describe("The steering list ID to assign"),
|
|
364
685
|
...DRY_RUN_FIELD,
|
|
365
686
|
},
|
|
366
687
|
annotations: { destructiveHint: true },
|
|
367
|
-
},
|
|
688
|
+
}, wrapHandler("modify_subscriber_steering_list", "modifySubscriberSteeringList", TOOL_SCOPES["modify_subscriber_steering_list"], ctx, async ({ iccid, steeringListId }, token) => ocsCall(ctx.env, token, "modifySubscriberSteeringList", {
|
|
689
|
+
subscriber: iccid,
|
|
690
|
+
steeringListId,
|
|
691
|
+
})));
|
|
368
692
|
server.registerTool("move_subscriber_range_to_account", {
|
|
369
693
|
title: "Move Subscribers to Account",
|
|
370
|
-
description: "
|
|
694
|
+
description: "Use this to move a contiguous ICCID range of subscribers to a different account. " +
|
|
695
|
+
"Useful for bulk subscriber migrations between accounts or during account restructuring. " +
|
|
696
|
+
"Params: `iccidFrom` (start ICCID of range, inclusive), `iccidTo` (end ICCID of range, inclusive), " +
|
|
697
|
+
"`accountId` (target account ID from `list_reseller_accounts`). " +
|
|
698
|
+
"Returns: OCS confirmation of the range move with affected subscriber count. " +
|
|
699
|
+
"Do NOT use this for a single subscriber move — provide identical iccidFrom and iccidTo. " +
|
|
700
|
+
"Always call `list_subscribers` on the range first to verify the correct subscribers are included.",
|
|
371
701
|
inputSchema: {
|
|
372
702
|
iccidFrom: z.string().describe("Start ICCID of range"),
|
|
373
703
|
iccidTo: z.string().describe("End ICCID of range"),
|
|
@@ -375,20 +705,33 @@ export function registerAllTools(server, client) {
|
|
|
375
705
|
...DRY_RUN_FIELD,
|
|
376
706
|
},
|
|
377
707
|
annotations: { destructiveHint: true },
|
|
378
|
-
},
|
|
379
|
-
|
|
708
|
+
}, wrapHandler("move_subscriber_range_to_account", "moveSubscriberRangeToAccount", TOOL_SCOPES["move_subscriber_range_to_account"], ctx, async ({ iccidFrom, iccidTo, accountId }, token) => ocsCall(ctx.env, token, "moveSubscriberRangeToAccount", {
|
|
709
|
+
iccidFrom,
|
|
710
|
+
iccidTo,
|
|
711
|
+
accountId,
|
|
712
|
+
})));
|
|
713
|
+
// Fix #3: OCS expects { imsi, limit } not { iccid, bitrate }.
|
|
714
|
+
// Resolve ICCID → IMSI via getSingleSubscriber; rename bitrate → limit.
|
|
380
715
|
server.registerTool("hlr_set_bitrate", {
|
|
381
716
|
title: "Set HLR Bitrate",
|
|
382
|
-
description: "
|
|
717
|
+
description: "Use this to set a hard bandwidth cap for a subscriber at the HLR (Home Location Register) level. " +
|
|
718
|
+
"This is a network-level throttle applied regardless of package allowance — use it to enforce " +
|
|
719
|
+
"fair-use speed limits or to throttle heavy users without suspending service. " +
|
|
720
|
+
"Common values: 256000 (256 kbps throttle), 1000000 (1 Mbps), 0 (remove limit). " +
|
|
721
|
+
"Internally resolves ICCID → IMSI via a getSingleSubscriber lookup. " +
|
|
722
|
+
"Params: `iccid` (subscriber identifier), `bitrate` (integer, bits-per-second; 0 removes the limit). " +
|
|
723
|
+
"Returns: HLR confirmation with the applied bitrate. " +
|
|
724
|
+
"Do NOT use this to block data entirely — use `set_subscriber_traffic_restrictions` with `dataAllowed=false`. " +
|
|
725
|
+
"Do NOT use this to change throttling thresholds on a package template — use `modify_template_throttling`.",
|
|
383
726
|
inputSchema: {
|
|
384
727
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
385
728
|
bitrate: z.number().describe("Max bitrate in bps"),
|
|
386
729
|
...DRY_RUN_FIELD,
|
|
387
730
|
},
|
|
388
731
|
annotations: { destructiveHint: true },
|
|
389
|
-
},
|
|
732
|
+
}, wrapHandler("hlr_set_bitrate", "hlrSetBitrate", TOOL_SCOPES["hlr_set_bitrate"], ctx, async ({ iccid, bitrate }, token) => {
|
|
390
733
|
const cache = new Map();
|
|
391
|
-
const sub = await resolveSubscriberByIccid(
|
|
734
|
+
const sub = await resolveSubscriberByIccid(ctx.env, token, iccid, cache);
|
|
392
735
|
const imsi = sub.imsi;
|
|
393
736
|
if (typeof imsi !== "string" || imsi.length === 0) {
|
|
394
737
|
return {
|
|
@@ -396,17 +739,24 @@ export function registerAllTools(server, client) {
|
|
|
396
739
|
content: [{ type: "text", text: `Error: Could not resolve IMSI for ICCID ${iccid}` }],
|
|
397
740
|
};
|
|
398
741
|
}
|
|
399
|
-
return ocsCall(
|
|
742
|
+
return ocsCall(ctx.env, token, "hlrSetBitrate", { imsi, limit: bitrate });
|
|
400
743
|
}));
|
|
401
|
-
// Fix #2:
|
|
744
|
+
// Fix #2: OCS expects { imsi } not { iccid }.
|
|
745
|
+
// Resolve ICCID → IMSI via getSingleSubscriber.
|
|
402
746
|
server.registerTool("hlr_get_bitrate", {
|
|
403
747
|
title: "Get HLR Bitrate",
|
|
404
|
-
description: "
|
|
748
|
+
description: "Use this to read the current HLR-level bandwidth cap applied to a subscriber. " +
|
|
749
|
+
"A non-zero value means the subscriber is throttled to that speed regardless of package allowance. " +
|
|
750
|
+
"A zero or null response means no HLR-level cap is in effect. " +
|
|
751
|
+
"Internally resolves ICCID → IMSI via a getSingleSubscriber lookup. " +
|
|
752
|
+
"Params: `iccid` (subscriber identifier). " +
|
|
753
|
+
"Returns: object with `bitrate` (integer, bits-per-second) or null if no limit is set. " +
|
|
754
|
+
"Do NOT use this to check package data allowance limits — use `list_subscriber_packages` for that.",
|
|
405
755
|
inputSchema: { iccid: z.string().describe("The subscriber ICCID") },
|
|
406
756
|
annotations: { readOnlyHint: true },
|
|
407
|
-
},
|
|
757
|
+
}, wrapHandler("hlr_get_bitrate", "hlrGetBitrate", TOOL_SCOPES["hlr_get_bitrate"], ctx, async ({ iccid }, token) => {
|
|
408
758
|
const cache = new Map();
|
|
409
|
-
const sub = await resolveSubscriberByIccid(
|
|
759
|
+
const sub = await resolveSubscriberByIccid(ctx.env, token, iccid, cache);
|
|
410
760
|
const imsi = sub.imsi;
|
|
411
761
|
if (typeof imsi !== "string" || imsi.length === 0) {
|
|
412
762
|
return {
|
|
@@ -414,40 +764,101 @@ export function registerAllTools(server, client) {
|
|
|
414
764
|
content: [{ type: "text", text: `Error: Could not resolve IMSI for ICCID ${iccid}` }],
|
|
415
765
|
};
|
|
416
766
|
}
|
|
417
|
-
return ocsCall(
|
|
767
|
+
return ocsCall(ctx.env, token, "hlrGetBitrate", { imsi });
|
|
418
768
|
}));
|
|
419
769
|
// =========================================================================
|
|
420
770
|
// 3. PACKAGE TOOLS
|
|
421
771
|
// =========================================================================
|
|
422
772
|
server.registerTool("list_subscriber_packages", {
|
|
423
773
|
title: "List Subscriber Packages",
|
|
424
|
-
description: "
|
|
774
|
+
description: "Use this to retrieve all prepaid packages currently assigned to a subscriber. " +
|
|
775
|
+
"Returns each package's allowance (data/voice/SMS), consumed usage, expiry date, status, and packageId. " +
|
|
776
|
+
"Always call this before any package modification tool (`modify_package_limits`, " +
|
|
777
|
+
"`modify_package_expiry`, `modify_package_status`, `delete_subscriber_package`) to confirm " +
|
|
778
|
+
"the correct packageId and current state. " +
|
|
779
|
+
"Params: `iccid` (subscriber identifier). " +
|
|
780
|
+
"Returns: array of package records with `packageId`, `name`, `status`, `dataLimit`, `dataUsed`, " +
|
|
781
|
+
"`expirationDate`, `recurring` flag. " +
|
|
782
|
+
"Do NOT use this to browse the product catalog — use `list_package_templates` for that.",
|
|
425
783
|
inputSchema: { iccid: z.string().describe("The subscriber ICCID") },
|
|
426
784
|
annotations: { readOnlyHint: true },
|
|
427
|
-
},
|
|
785
|
+
}, wrapHandler("list_subscriber_packages", "listSubscriberPrepaidPackages", TOOL_SCOPES["list_subscriber_packages"], ctx, async ({ iccid }, token) => ocsCall(ctx.env, token, "listSubscriberPrepaidPackages", { iccid })));
|
|
428
786
|
server.registerTool("assign_package", {
|
|
429
787
|
title: "Assign Package to Subscriber",
|
|
430
|
-
description: "
|
|
788
|
+
description: "Use this to assign a one-time prepaid data/voice package to a subscriber from an existing template. " +
|
|
789
|
+
"The package is active immediately (or at first usage, depending on template settings). " +
|
|
790
|
+
"Params: `iccid` (subscriber identifier), `packageTemplateId` (integer from `list_package_templates`). " +
|
|
791
|
+
"Returns: created package record with `packageId`, `startDate`, `endDate`, and allowances. " +
|
|
792
|
+
"Do NOT use this for packages that should auto-renew — use `assign_recurring_package` instead. " +
|
|
793
|
+
"Do NOT use this to provision a new subscriber end-to-end — consider `provision_esim_wizard` " +
|
|
794
|
+
"for a guided flow with dry-run preview.",
|
|
431
795
|
inputSchema: {
|
|
432
796
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
433
|
-
packageTemplateId: z
|
|
797
|
+
packageTemplateId: z
|
|
798
|
+
.number()
|
|
799
|
+
.describe("The package template ID to assign"),
|
|
434
800
|
...DRY_RUN_FIELD,
|
|
435
801
|
},
|
|
436
802
|
annotations: { destructiveHint: true },
|
|
437
|
-
},
|
|
803
|
+
}, wrapHandler("assign_package", "affectPackageToSubscriber", TOOL_SCOPES["assign_package"], ctx, async ({ iccid, packageTemplateId }, token) => {
|
|
804
|
+
// Fix #17: OCS affectPackageToSubscriber expects integer subscriberId, not ICCID string.
|
|
805
|
+
// Resolve ICCID → numeric id via getSingleSubscriber before calling OCS.
|
|
806
|
+
const cache = new Map();
|
|
807
|
+
const sub = await resolveSubscriberByIccid(ctx.env, token, iccid, cache);
|
|
808
|
+
const subscriberId = sub.id ?? sub.subscriberId;
|
|
809
|
+
if (subscriberId === undefined) {
|
|
810
|
+
return {
|
|
811
|
+
isError: true,
|
|
812
|
+
content: [{ type: "text", text: `Error: Could not resolve subscriberId for ICCID ${iccid}` }],
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return ocsCall(ctx.env, token, "affectPackageToSubscriber", {
|
|
816
|
+
subscriber: Number(subscriberId),
|
|
817
|
+
packageTemplateId,
|
|
818
|
+
});
|
|
819
|
+
}));
|
|
438
820
|
server.registerTool("assign_recurring_package", {
|
|
439
821
|
title: "Assign Recurring Package",
|
|
440
|
-
description: "
|
|
822
|
+
description: "Use this to assign an auto-renewing prepaid package to a subscriber. The package renews " +
|
|
823
|
+
"automatically based on the template's periodicity settings, reducing churn from manual renewal. " +
|
|
824
|
+
"Params: `iccid` (subscriber identifier), `packageTemplateId` (integer from `list_package_templates` " +
|
|
825
|
+
"— must be a template configured with recurring/periodicity settings). " +
|
|
826
|
+
"Returns: created recurring package record with `packageId` and renewal schedule. " +
|
|
827
|
+
"Do NOT use this for one-time packages — use `assign_package` instead. " +
|
|
828
|
+
"To pause or cancel auto-renewal without deleting the package, use `stop_resume_recurring_package`.",
|
|
441
829
|
inputSchema: {
|
|
442
830
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
443
831
|
packageTemplateId: z.number().describe("The package template ID"),
|
|
444
832
|
...DRY_RUN_FIELD,
|
|
445
833
|
},
|
|
446
834
|
annotations: { destructiveHint: true },
|
|
447
|
-
},
|
|
835
|
+
}, wrapHandler("assign_recurring_package", "affectRecurringPackageToSubscriber", TOOL_SCOPES["assign_recurring_package"], ctx, async ({ iccid, packageTemplateId }, token) => {
|
|
836
|
+
// Fix #18: OCS affectRecurringPackageToSubscriber expects integer subscriberId, not ICCID string.
|
|
837
|
+
// Resolve ICCID → numeric id via getSingleSubscriber before calling OCS.
|
|
838
|
+
const cache = new Map();
|
|
839
|
+
const sub = await resolveSubscriberByIccid(ctx.env, token, iccid, cache);
|
|
840
|
+
const subscriberId = sub.id ?? sub.subscriberId;
|
|
841
|
+
if (subscriberId === undefined) {
|
|
842
|
+
return {
|
|
843
|
+
isError: true,
|
|
844
|
+
content: [{ type: "text", text: `Error: Could not resolve subscriberId for ICCID ${iccid}` }],
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return ocsCall(ctx.env, token, "affectRecurringPackageToSubscriber", {
|
|
848
|
+
subscriber: Number(subscriberId),
|
|
849
|
+
packageTemplateId,
|
|
850
|
+
});
|
|
851
|
+
}));
|
|
448
852
|
server.registerTool("modify_package_limits", {
|
|
449
853
|
title: "Modify Package Limits",
|
|
450
|
-
description: "
|
|
854
|
+
description: "Use this to change the data, voice, or SMS allowance ceilings on an already-assigned subscriber package. " +
|
|
855
|
+
"Useful for mid-cycle top-ups or corrections without assigning a new package. " +
|
|
856
|
+
"Params: `iccid` (subscriber identifier), `packageId` (integer from `list_subscriber_packages`), " +
|
|
857
|
+
"`limits` (JSON string with the limit fields to change, e.g. {\"dataLimit\": 5368709120}). " +
|
|
858
|
+
"Returns: updated package record with new limits. " +
|
|
859
|
+
"Do NOT use this to change the package template (affecting future subscribers) — use `modify_template_core`. " +
|
|
860
|
+
"Do NOT use this to change expiry — use `modify_package_expiry`. " +
|
|
861
|
+
"Always call `list_subscriber_packages` first to confirm the correct packageId.",
|
|
451
862
|
inputSchema: {
|
|
452
863
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
453
864
|
packageId: z.number().describe("The active package ID"),
|
|
@@ -455,25 +866,43 @@ export function registerAllTools(server, client) {
|
|
|
455
866
|
...DRY_RUN_FIELD,
|
|
456
867
|
},
|
|
457
868
|
annotations: { destructiveHint: true },
|
|
458
|
-
},
|
|
869
|
+
}, wrapHandler("modify_package_limits", "modifySubscriberPrepaidPackageLimits", TOOL_SCOPES["modify_package_limits"], ctx, async ({ iccid, packageId, limits }, token) => ocsCall(ctx.env, token, "modifySubscriberPrepaidPackageLimits", {
|
|
459
870
|
iccid,
|
|
460
871
|
packageId,
|
|
461
872
|
...JSON.parse(limits),
|
|
462
873
|
})));
|
|
463
874
|
server.registerTool("modify_package_expiry", {
|
|
464
875
|
title: "Modify Package Expiry Date",
|
|
465
|
-
description: "
|
|
876
|
+
description: "Use this to extend or shorten the expiry date of an active prepaid package on a subscriber. " +
|
|
877
|
+
"Useful when a subscriber's trip is longer than expected or for promotional extensions. " +
|
|
878
|
+
"Params: `iccid` (subscriber identifier), `packageId` (integer from `list_subscriber_packages`), " +
|
|
879
|
+
"`expirationDate` (ISO 8601 date string, e.g. '2026-06-01' or '2026-06-01T23:59:59'). " +
|
|
880
|
+
"Returns: updated package record with the new expiry date. " +
|
|
881
|
+
"Do NOT use this to change when a package becomes active — use `modify_subscriber_package_active_period`. " +
|
|
882
|
+
"Do NOT use this to change data allowances — use `modify_package_limits`.",
|
|
466
883
|
inputSchema: {
|
|
467
884
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
468
885
|
packageId: z.number().describe("The active package ID"),
|
|
469
|
-
expirationDate: z
|
|
886
|
+
expirationDate: z
|
|
887
|
+
.string()
|
|
888
|
+
.describe("New expiry date (ISO 8601 format)"),
|
|
470
889
|
...DRY_RUN_FIELD,
|
|
471
890
|
},
|
|
472
891
|
annotations: { destructiveHint: true },
|
|
473
|
-
},
|
|
892
|
+
}, wrapHandler("modify_package_expiry", "modifySubscriberPrepaidPackageExpDate", TOOL_SCOPES["modify_package_expiry"], ctx, async ({ iccid, packageId, expirationDate }, token) => ocsCall(ctx.env, token, "modifySubscriberPrepaidPackageExpDate", {
|
|
893
|
+
iccid,
|
|
894
|
+
packageId,
|
|
895
|
+
expirationDate,
|
|
896
|
+
})));
|
|
474
897
|
server.registerTool("modify_package_status", {
|
|
475
898
|
title: "Modify Package Status",
|
|
476
|
-
description: "
|
|
899
|
+
description: "Use this to activate or deactivate a specific prepaid package on a subscriber without " +
|
|
900
|
+
"removing it. A deactivated package retains its allowances and can be reactivated later. " +
|
|
901
|
+
"Params: `iccid` (subscriber identifier), `packageId` (integer from `list_subscriber_packages`), " +
|
|
902
|
+
"`status` (new package status string, e.g. 'ACTIVE', 'INACTIVE'). " +
|
|
903
|
+
"Returns: updated package record with the new status. " +
|
|
904
|
+
"Do NOT use this to delete a package — use `delete_subscriber_package` for permanent removal. " +
|
|
905
|
+
"Do NOT use this to change the subscriber's overall account status — use `modify_subscriber_status`.",
|
|
477
906
|
inputSchema: {
|
|
478
907
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
479
908
|
packageId: z.number().describe("The active package ID"),
|
|
@@ -481,195 +910,354 @@ export function registerAllTools(server, client) {
|
|
|
481
910
|
...DRY_RUN_FIELD,
|
|
482
911
|
},
|
|
483
912
|
annotations: { destructiveHint: true },
|
|
484
|
-
},
|
|
913
|
+
}, wrapHandler("modify_package_status", "modifySubscriberPrepaidPackageStatus", TOOL_SCOPES["modify_package_status"], ctx, async ({ iccid, packageId, status }, token) => ocsCall(ctx.env, token, "modifySubscriberPrepaidPackageStatus", {
|
|
914
|
+
iccid,
|
|
915
|
+
packageId,
|
|
916
|
+
status,
|
|
917
|
+
})));
|
|
485
918
|
server.registerTool("stop_resume_recurring_package", {
|
|
486
919
|
title: "Stop/Resume Recurring Package",
|
|
487
|
-
description: "
|
|
920
|
+
description: "Use this to pause or restart the auto-renewal cycle of a recurring package without removing it. " +
|
|
921
|
+
"'stop' halts future renewals (subscriber keeps current period until expiry); " +
|
|
922
|
+
"'resume' re-enables auto-renewal from the next renewal date. " +
|
|
923
|
+
"Params: `iccid` (subscriber identifier), `packageId` (integer from `list_subscriber_packages`), " +
|
|
924
|
+
"`action` ('stop' | 'resume'). " +
|
|
925
|
+
"Returns: updated recurring package record with new renewal state. " +
|
|
926
|
+
"Do NOT use this to permanently delete a recurring package — use `delete_subscriber_package`. " +
|
|
927
|
+
"Do NOT confuse this with `modify_package_status` (which activates/deactivates a package for usage).",
|
|
488
928
|
inputSchema: {
|
|
489
929
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
490
930
|
packageId: z.number().describe("The recurring package ID"),
|
|
491
|
-
action: z
|
|
931
|
+
action: z
|
|
932
|
+
.enum(["stop", "resume"])
|
|
933
|
+
.describe("Whether to stop or resume"),
|
|
492
934
|
...DRY_RUN_FIELD,
|
|
493
935
|
},
|
|
494
936
|
annotations: { destructiveHint: true },
|
|
495
|
-
},
|
|
937
|
+
}, wrapHandler("stop_resume_recurring_package", "stopResumeSubsRecurringPackage", TOOL_SCOPES["stop_resume_recurring_package"], ctx, async ({ iccid, packageId, action }, token) => ocsCall(ctx.env, token, "stopResumeSubsRecurringPackage", {
|
|
938
|
+
iccid,
|
|
939
|
+
packageId,
|
|
940
|
+
action,
|
|
941
|
+
})));
|
|
496
942
|
server.registerTool("delete_subscriber_package", {
|
|
497
943
|
title: "Delete Subscriber Package",
|
|
498
|
-
description: "
|
|
944
|
+
description: "Use this to permanently remove a single prepaid package from a subscriber. " +
|
|
945
|
+
"This is irreversible — the package record and any unused allowance are deleted. " +
|
|
946
|
+
"Always call `list_subscriber_packages` first to confirm the correct packageId and snapshot " +
|
|
947
|
+
"the current state. Use `dry_run=true` on the first call. " +
|
|
948
|
+
"Params: `iccid` (subscriber identifier), `packageId` (integer from `list_subscriber_packages`). " +
|
|
949
|
+
"Returns: OCS confirmation of deletion. " +
|
|
950
|
+
"Do NOT use this to remove ALL packages at once — use `clean_all_packages` for that (requires separate confirm). " +
|
|
951
|
+
"Do NOT use this to pause a package — use `modify_package_status` to deactivate it instead.",
|
|
499
952
|
inputSchema: {
|
|
500
953
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
501
954
|
packageId: z.number().describe("The package ID to delete"),
|
|
502
955
|
...DRY_RUN_FIELD,
|
|
503
956
|
},
|
|
504
957
|
annotations: { destructiveHint: true },
|
|
505
|
-
},
|
|
958
|
+
}, wrapHandler("delete_subscriber_package", "deleteSubscriberPackage", TOOL_SCOPES["delete_subscriber_package"], ctx, async ({ iccid, packageId }, token) => ocsCall(ctx.env, token, "deleteSubscriberPackage", { iccid, packageId })));
|
|
506
959
|
server.registerTool("clean_all_packages", {
|
|
507
960
|
title: "Clean All Subscriber Packages",
|
|
508
|
-
description: "
|
|
961
|
+
description: "DANGEROUS: Removes ALL prepaid packages from a subscriber in a single irreversible operation. " +
|
|
962
|
+
"There is no undo. Typical use: resetting a subscriber to zero before re-provisioning a new package series. " +
|
|
963
|
+
"REQUIRED workflow: (1) call `list_subscriber_packages` to snapshot what will be deleted; " +
|
|
964
|
+
"(2) call this tool with `dry_run=true` to preview; (3) get explicit user confirmation; " +
|
|
965
|
+
"(4) call again with `dry_run=false`. " +
|
|
966
|
+
"Params: `iccid` (subscriber identifier), `dry_run` (boolean — MUST be true on first call). " +
|
|
967
|
+
"Returns: list of packages that were (or would be) deleted. " +
|
|
968
|
+
"Do NOT use this to remove a single package — use `delete_subscriber_package` instead.",
|
|
509
969
|
inputSchema: {
|
|
510
970
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
511
971
|
...DRY_RUN_FIELD,
|
|
512
972
|
},
|
|
513
973
|
annotations: { destructiveHint: true },
|
|
514
|
-
},
|
|
974
|
+
}, wrapHandler("clean_all_packages", "cleanSubscriberAllPackages", TOOL_SCOPES["clean_all_packages"], ctx, async ({ iccid }, token) => ocsCall(ctx.env, token, "cleanSubscriberAllPackages", { iccid })));
|
|
515
975
|
// =========================================================================
|
|
516
976
|
// 4. PACKAGE TEMPLATE TOOLS
|
|
517
977
|
// =========================================================================
|
|
518
978
|
server.registerTool("list_package_templates", {
|
|
519
979
|
title: "List Package Templates",
|
|
520
|
-
description: "
|
|
980
|
+
description: "Use this to browse the product catalog of prepaid package templates available for assignment. " +
|
|
981
|
+
"Returns each template's name, data/voice/SMS limits, pricing, validity period, location zone, " +
|
|
982
|
+
"and recurring configuration. Call this before `assign_package` or `assign_recurring_package` " +
|
|
983
|
+
"to obtain valid `packageTemplateId` values. " +
|
|
984
|
+
"Params: `accountId` (integer, optional — filter templates visible to a specific account). " +
|
|
985
|
+
"Returns: array of template records with `templateId`, `name`, `dataLimit`, `price`, " +
|
|
986
|
+
"`validityDays`, `locationZoneId`, `recurring`. " +
|
|
987
|
+
"Do NOT use this to list packages assigned to a specific subscriber — use `list_subscriber_packages`.",
|
|
521
988
|
inputSchema: {
|
|
522
|
-
accountId: z
|
|
989
|
+
accountId: z
|
|
990
|
+
.number()
|
|
991
|
+
.optional()
|
|
992
|
+
.describe("Filter templates by account ID"),
|
|
523
993
|
},
|
|
524
994
|
annotations: { readOnlyHint: true },
|
|
525
|
-
},
|
|
995
|
+
}, wrapHandler("list_package_templates", "listPrepaidPackageTemplate", TOOL_SCOPES["list_package_templates"], ctx, async ({ accountId }, token) => {
|
|
526
996
|
const params = {};
|
|
527
997
|
if (accountId !== undefined)
|
|
528
998
|
params.accountId = accountId;
|
|
529
|
-
return ocsCall(
|
|
999
|
+
return ocsCall(ctx.env, token, "listPrepaidPackageTemplate", params);
|
|
530
1000
|
}));
|
|
531
1001
|
server.registerTool("create_package_template", {
|
|
532
1002
|
title: "Create Package Template",
|
|
533
|
-
description: "
|
|
1003
|
+
description: "Use this to create a new prepaid package template in the product catalog. Templates define " +
|
|
1004
|
+
"allowances, pricing, location zones, validity, and throttling thresholds that are reused each " +
|
|
1005
|
+
"time the template is assigned to a subscriber. " +
|
|
1006
|
+
"Params: `template` (full template configuration as a JSON string — fields include `name`, " +
|
|
1007
|
+
"`dataLimit` in bytes, `price`, `validityDays`, `locationZoneId`, `recurring`, `throttlingActive`). " +
|
|
1008
|
+
"Returns: created template record with the new `templateId`. " +
|
|
1009
|
+
"Do NOT use this to modify an existing template — use `modify_template_core`. " +
|
|
1010
|
+
"After creation, call `list_package_templates` to confirm the template is visible.",
|
|
534
1011
|
inputSchema: {
|
|
535
|
-
template: z
|
|
1012
|
+
template: z
|
|
1013
|
+
.string()
|
|
1014
|
+
.describe("Full template configuration as JSON string"),
|
|
536
1015
|
...DRY_RUN_FIELD,
|
|
537
1016
|
},
|
|
538
1017
|
annotations: { destructiveHint: true },
|
|
539
|
-
},
|
|
1018
|
+
}, wrapHandler("create_package_template", "createPrepaidPackageTemplate", TOOL_SCOPES["create_package_template"], ctx, async ({ template }, token) => ocsCall(ctx.env, token, "createPrepaidPackageTemplate", JSON.parse(template))));
|
|
540
1019
|
server.registerTool("modify_template_core", {
|
|
541
1020
|
title: "Modify Template Core Settings",
|
|
542
|
-
description: "
|
|
1021
|
+
description: "Use this to change the core fields of an existing package template: name, data/voice/SMS limits, " +
|
|
1022
|
+
"pricing, validity period, and location zone. Changes affect future package assignments from this " +
|
|
1023
|
+
"template but do NOT retroactively change packages already assigned to subscribers. " +
|
|
1024
|
+
"Params: `templateId` (integer from `list_package_templates`), `changes` (JSON string with fields " +
|
|
1025
|
+
"to modify, e.g. {\"name\": \"Europe 5GB\", \"dataLimit\": 5368709120}). " +
|
|
1026
|
+
"Returns: updated template record. " +
|
|
1027
|
+
"Do NOT use this to modify throttling thresholds — use `modify_template_throttling`. " +
|
|
1028
|
+
"Do NOT use this to modify recurring/renewal settings — use `modify_template_recurring`.",
|
|
543
1029
|
inputSchema: {
|
|
544
1030
|
templateId: z.number().describe("The template ID"),
|
|
545
1031
|
changes: z.string().describe("Core fields to modify as JSON string"),
|
|
546
1032
|
...DRY_RUN_FIELD,
|
|
547
1033
|
},
|
|
548
1034
|
annotations: { destructiveHint: true },
|
|
549
|
-
},
|
|
1035
|
+
}, wrapHandler("modify_template_core", "modifyPPTCore", TOOL_SCOPES["modify_template_core"], ctx, async ({ templateId, changes }, token) => ocsCall(ctx.env, token, "modifyPPTCore", {
|
|
1036
|
+
templateId,
|
|
1037
|
+
...JSON.parse(changes),
|
|
1038
|
+
})));
|
|
550
1039
|
server.registerTool("modify_template_recurring", {
|
|
551
1040
|
title: "Modify Template Recurring Settings",
|
|
552
|
-
description: "
|
|
1041
|
+
description: "Use this to change the auto-renewal configuration of a package template: periodicity " +
|
|
1042
|
+
"(daily/weekly/monthly), occurrence count, and renewal trigger conditions. Changes affect " +
|
|
1043
|
+
"future assignments and existing recurring packages assigned from this template. " +
|
|
1044
|
+
"Params: `templateId` (integer from `list_package_templates`), `changes` (JSON string with " +
|
|
1045
|
+
"recurring fields, e.g. {\"periodicity\": \"monthly\", \"occurrences\": 12}). " +
|
|
1046
|
+
"Returns: updated template record with new recurring settings. " +
|
|
1047
|
+
"Do NOT use this to stop an individual subscriber's recurring renewal — use `stop_resume_recurring_package`. " +
|
|
1048
|
+
"Do NOT use this to change core template fields like data limits — use `modify_template_core`.",
|
|
553
1049
|
inputSchema: {
|
|
554
1050
|
templateId: z.number().describe("The template ID"),
|
|
555
|
-
changes: z
|
|
1051
|
+
changes: z
|
|
1052
|
+
.string()
|
|
1053
|
+
.describe("Recurring fields to modify as JSON string"),
|
|
556
1054
|
...DRY_RUN_FIELD,
|
|
557
1055
|
},
|
|
558
1056
|
annotations: { destructiveHint: true },
|
|
559
|
-
},
|
|
1057
|
+
}, wrapHandler("modify_template_recurring", "modifyPPTRecurring", TOOL_SCOPES["modify_template_recurring"], ctx, async ({ templateId, changes }, token) => ocsCall(ctx.env, token, "modifyPPTRecurring", {
|
|
1058
|
+
templateId,
|
|
1059
|
+
...JSON.parse(changes),
|
|
1060
|
+
})));
|
|
560
1061
|
server.registerTool("modify_template_throttling", {
|
|
561
1062
|
title: "Modify Template Throttling",
|
|
562
|
-
description: "
|
|
1063
|
+
description: "Use this to change the bandwidth throttling thresholds on a package template. " +
|
|
1064
|
+
"WARNING: changes apply immediately to ALL existing subscriber packages created from this template, " +
|
|
1065
|
+
"not just future ones. Setting a lower threshold will NOT retroactively throttle subscribers " +
|
|
1066
|
+
"already below the new threshold (the system does not re-check existing usage). " +
|
|
1067
|
+
"Params: `templateId` (integer), `changes` (JSON string with throttling fields, e.g. " +
|
|
1068
|
+
"{\"throttlingActive\": true, \"firstThresholdPercent\": 80, \"firstThresholdLimitKbps\": 1024, " +
|
|
1069
|
+
"\"errorAction\": \"continue_unthrottled\"}). " +
|
|
1070
|
+
"Returns: updated template record with new throttling configuration. " +
|
|
1071
|
+
"Do NOT use this to throttle a single subscriber — use `hlr_set_bitrate` instead. " +
|
|
1072
|
+
"Do NOT use this to change core package limits — use `modify_template_core`.",
|
|
563
1073
|
inputSchema: {
|
|
564
1074
|
templateId: z.number().describe("The template ID"),
|
|
565
|
-
changes: z
|
|
1075
|
+
changes: z
|
|
1076
|
+
.string()
|
|
1077
|
+
.describe("Throttling fields to modify as JSON string"),
|
|
566
1078
|
...DRY_RUN_FIELD,
|
|
567
1079
|
},
|
|
568
1080
|
annotations: { destructiveHint: true },
|
|
569
|
-
},
|
|
1081
|
+
}, wrapHandler("modify_template_throttling", "modifyPPTThrottling", TOOL_SCOPES["modify_template_throttling"], ctx, async ({ templateId, changes }, token) => ocsCall(ctx.env, token, "modifyPPTThrottling", {
|
|
1082
|
+
templateId,
|
|
1083
|
+
...JSON.parse(changes),
|
|
1084
|
+
})));
|
|
570
1085
|
server.registerTool("list_location_zones", {
|
|
571
1086
|
title: "List Location Zone Elements",
|
|
572
|
-
description: "
|
|
1087
|
+
description: "Use this to list countries and networks within a specific location zone. " +
|
|
1088
|
+
"WARNING: this method has a known Jackson deserialization bug in the upstream OCS API that " +
|
|
1089
|
+
"may return malformed responses. Prefer `list_detailed_location_zones` for reliable results. " +
|
|
1090
|
+
"Params: `locationZoneId` (integer, optional — filter to a specific zone). " +
|
|
1091
|
+
"Returns: array of zone element records with country and operator entries. " +
|
|
1092
|
+
"Do NOT use this for reliable zone data — use `list_detailed_location_zones` instead. " +
|
|
1093
|
+
"Do NOT use this to create zones — use `create_location_zone`.",
|
|
573
1094
|
inputSchema: {
|
|
574
1095
|
locationZoneId: z.number().optional().describe("Filter by zone ID"),
|
|
575
1096
|
},
|
|
576
1097
|
annotations: { readOnlyHint: true },
|
|
577
|
-
},
|
|
1098
|
+
}, wrapHandler("list_location_zones", "listLocationZoneElement", TOOL_SCOPES["list_location_zones"], ctx, async ({ locationZoneId }, token) => {
|
|
578
1099
|
const params = {};
|
|
579
1100
|
if (locationZoneId !== undefined)
|
|
580
1101
|
params.locationZoneId = locationZoneId;
|
|
581
|
-
return ocsCall(
|
|
1102
|
+
return ocsCall(ctx.env, token, "listLocationZoneElement", params);
|
|
582
1103
|
}));
|
|
583
|
-
// Fix #15: bare integer
|
|
1104
|
+
// Fix #15: OCS expects bare integer (resellerId), not {}
|
|
584
1105
|
server.registerTool("list_detailed_location_zones", {
|
|
585
1106
|
title: "List Detailed Location Zones",
|
|
586
|
-
description: "
|
|
1107
|
+
description: "Use this as the preferred way to list location zones with full detail: included countries, " +
|
|
1108
|
+
"operator networks, zone IDs, and names. This is the working alternative to `list_location_zones` " +
|
|
1109
|
+
"which has a known upstream deserialization bug. Use `locationZoneId` values from this response " +
|
|
1110
|
+
"when creating or editing package templates. " +
|
|
1111
|
+
"Params: `resellerId` (integer, optional — omit to use the token owner's reseller). " +
|
|
1112
|
+
"Returns: array of zone objects each containing `locationZoneId`, `name`, `countries`, and `operators`. " +
|
|
1113
|
+
"Do NOT use `list_location_zones` when you need reliable data — always use this tool instead.",
|
|
587
1114
|
inputSchema: {
|
|
588
|
-
resellerId: z
|
|
1115
|
+
resellerId: z
|
|
1116
|
+
.number()
|
|
1117
|
+
.optional()
|
|
1118
|
+
.describe("Reseller ID (omit to use token owner's reseller)"),
|
|
589
1119
|
},
|
|
590
1120
|
annotations: { readOnlyHint: true },
|
|
591
|
-
},
|
|
592
|
-
const id = resellerId ?? (await getDefaultResellerId(
|
|
593
|
-
return ocsCall(
|
|
1121
|
+
}, wrapHandler("list_detailed_location_zones", "listDetailedLocationZone", TOOL_SCOPES["list_detailed_location_zones"], ctx, async ({ resellerId }, token) => {
|
|
1122
|
+
const id = resellerId ?? (await getDefaultResellerId(ctx.env, token));
|
|
1123
|
+
return ocsCall(ctx.env, token, "listDetailedLocationZone", id);
|
|
594
1124
|
}));
|
|
595
1125
|
server.registerTool("list_destination_prefixes", {
|
|
596
1126
|
title: "List Destination List Prefixes",
|
|
597
|
-
description: "
|
|
1127
|
+
description: "Use this to list the phone number prefixes (country dialling codes) within a specific " +
|
|
1128
|
+
"named destination list. Destination lists control which countries a subscriber may call on " +
|
|
1129
|
+
"voice/SMS packages. You must already know the `destinationListId` to use this tool. " +
|
|
1130
|
+
"Params: `destinationListId` (integer, optional — omit to list all known prefixes). " +
|
|
1131
|
+
"Returns: array of prefix records with country code and E.164 prefix. " +
|
|
1132
|
+
"Do NOT use this to discover the destination list catalog — use `list_destination_lists` for that. " +
|
|
1133
|
+
"For data-only eSIM products without MOC voice, destination lists are irrelevant.",
|
|
598
1134
|
inputSchema: {
|
|
599
|
-
destinationListId: z
|
|
1135
|
+
destinationListId: z
|
|
1136
|
+
.number()
|
|
1137
|
+
.optional()
|
|
1138
|
+
.describe("Filter by destination list ID"),
|
|
600
1139
|
},
|
|
601
1140
|
annotations: { readOnlyHint: true },
|
|
602
|
-
},
|
|
1141
|
+
}, wrapHandler("list_destination_prefixes", "listDestinationListPrefix", TOOL_SCOPES["list_destination_prefixes"], ctx, async ({ destinationListId }, token) => {
|
|
603
1142
|
const params = {};
|
|
604
1143
|
if (destinationListId !== undefined)
|
|
605
1144
|
params.destinationListId = destinationListId;
|
|
606
|
-
return ocsCall(
|
|
1145
|
+
return ocsCall(ctx.env, token, "listDestinationListPrefix", params);
|
|
607
1146
|
}));
|
|
608
1147
|
server.registerTool("create_location_zone", {
|
|
609
1148
|
title: "Create Location Zone",
|
|
610
|
-
description: "
|
|
1149
|
+
description: "Use this to create a new location zone — a named collection of countries and operators " +
|
|
1150
|
+
"that defines where a package can be used. Location zones are required when creating package " +
|
|
1151
|
+
"templates. Use `list_network_profiles` to find valid operator identifiers to include. " +
|
|
1152
|
+
"Params: `zone` (full zone configuration as a JSON string — fields include `name`, `countries` " +
|
|
1153
|
+
"(array of ISO country codes), `operators` (array of MCC-MNC strings)). " +
|
|
1154
|
+
"Returns: created zone record with the new `locationZoneId`. " +
|
|
1155
|
+
"Do NOT use this to modify an existing zone — no edit tool exists yet (gap G-19, pending eSIMVault). " +
|
|
1156
|
+
"After creation, verify with `list_detailed_location_zones`.",
|
|
611
1157
|
inputSchema: {
|
|
612
1158
|
zone: z.string().describe("Zone configuration as JSON string"),
|
|
613
1159
|
...DRY_RUN_FIELD,
|
|
614
1160
|
},
|
|
615
1161
|
annotations: { destructiveHint: true },
|
|
616
|
-
},
|
|
1162
|
+
}, wrapHandler("create_location_zone", "createLocationZone", TOOL_SCOPES["create_location_zone"], ctx, async ({ zone }, token) => ocsCall(ctx.env, token, "createLocationZone", JSON.parse(zone))));
|
|
617
1163
|
// =========================================================================
|
|
618
1164
|
// 5. STATISTICS TOOLS
|
|
619
1165
|
// =========================================================================
|
|
620
|
-
// Fix #4: { subscriber: { iccid }, period: { start, end } }
|
|
1166
|
+
// Fix #4: OCS expects { subscriber: { iccid }, period: { start, end } }
|
|
1167
|
+
// not { iccid, startDate, endDate }
|
|
621
1168
|
server.registerTool("subscriber_usage", {
|
|
622
1169
|
title: "Subscriber Usage Over Period",
|
|
623
|
-
description: "
|
|
1170
|
+
description: "Use this to retrieve daily data, voice, and SMS usage for a subscriber over a date range. " +
|
|
1171
|
+
"Hard limit: maximum 7 days per query — do not exceed or OCS will return an error. " +
|
|
1172
|
+
"Params: `iccid` (subscriber identifier), `startDate` (YYYY-MM-DD, inclusive), " +
|
|
1173
|
+
"`endDate` (YYYY-MM-DD, inclusive, max 7 days from start). " +
|
|
1174
|
+
"Returns: array of daily usage records with `date`, `dataBytes`, `voiceSeconds`, `smsCount`. " +
|
|
1175
|
+
"Do NOT use this for event-level network activity — use `subscriber_network_events` for attach/detach events. " +
|
|
1176
|
+
"Do NOT use this to check current package allowances — use `list_subscriber_packages`.",
|
|
624
1177
|
inputSchema: {
|
|
625
1178
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
626
1179
|
startDate: z.string().describe("Start date (YYYY-MM-DD, inclusive)"),
|
|
627
|
-
endDate: z
|
|
1180
|
+
endDate: z
|
|
1181
|
+
.string()
|
|
1182
|
+
.describe("End date (YYYY-MM-DD, inclusive, max 7 days from start)"),
|
|
628
1183
|
},
|
|
629
1184
|
annotations: { readOnlyHint: true },
|
|
630
|
-
},
|
|
1185
|
+
}, wrapHandler("subscriber_usage", "subscriberUsageOverPeriod", TOOL_SCOPES["subscriber_usage"], ctx, async ({ iccid, startDate, endDate }, token) => ocsCall(ctx.env, token, "subscriberUsageOverPeriod", {
|
|
631
1186
|
subscriber: { iccid },
|
|
632
1187
|
period: { start: startDate, end: endDate },
|
|
633
1188
|
})));
|
|
634
|
-
// Fix #5: same nested shape
|
|
1189
|
+
// Fix #5: same nested shape as subscriberUsageOverPeriod
|
|
635
1190
|
server.registerTool("subscriber_network_events", {
|
|
636
1191
|
title: "Subscriber Network Events",
|
|
637
|
-
description: "
|
|
1192
|
+
description: "Use this to retrieve timestamped network events for a subscriber: attach, detach, location " +
|
|
1193
|
+
"updates, and handovers between operators. Useful for connectivity troubleshooting, roaming " +
|
|
1194
|
+
"activity verification, and fraud pattern detection. Max 7 days per query. " +
|
|
1195
|
+
"Params: `iccid` (subscriber identifier), `startDate` (YYYY-MM-DD, inclusive), " +
|
|
1196
|
+
"`endDate` (YYYY-MM-DD, inclusive, max 7 days from start). " +
|
|
1197
|
+
"Returns: array of event records with `timestamp`, `eventType`, `country`, `operator`, `mccMnc`. " +
|
|
1198
|
+
"Do NOT use this for daily usage volumes — use `subscriber_usage` for data/voice/SMS byte counts. " +
|
|
1199
|
+
"For real-time events (last 24h), prefer `list_recent_ocs_events` which reads from the ring buffer.",
|
|
638
1200
|
inputSchema: {
|
|
639
1201
|
iccid: z.string().describe("The subscriber ICCID"),
|
|
640
1202
|
startDate: z.string().describe("Start date (YYYY-MM-DD, inclusive)"),
|
|
641
1203
|
endDate: z.string().describe("End date (YYYY-MM-DD, inclusive)"),
|
|
642
1204
|
},
|
|
643
1205
|
annotations: { readOnlyHint: true },
|
|
644
|
-
},
|
|
1206
|
+
}, wrapHandler("subscriber_network_events", "subscriberNetworkEventsOverPeriod", TOOL_SCOPES["subscriber_network_events"], ctx, async ({ iccid, startDate, endDate }, token) => ocsCall(ctx.env, token, "subscriberNetworkEventsOverPeriod", {
|
|
645
1207
|
subscriber: { iccid },
|
|
646
1208
|
period: { start: startDate, end: endDate },
|
|
647
1209
|
})));
|
|
648
1210
|
server.registerTool("subscriber_active_period", {
|
|
649
1211
|
title: "Get Subscriber Active Period",
|
|
650
|
-
description: "
|
|
1212
|
+
description: "Use this to retrieve the lifetime activity window for a subscriber: the date of first usage " +
|
|
1213
|
+
"and the date of last usage. Useful for churn analysis, dormancy detection, and subscriber " +
|
|
1214
|
+
"lifetime value calculations. " +
|
|
1215
|
+
"Params: `iccid` (subscriber identifier). " +
|
|
1216
|
+
"Returns: object with `firstUseDate` and `lastUseDate` (ISO 8601 strings). " +
|
|
1217
|
+
"Do NOT use this to check current package status — use `list_subscriber_packages`. " +
|
|
1218
|
+
"Do NOT use this for detailed daily usage patterns — use `subscriber_usage`.",
|
|
651
1219
|
inputSchema: { iccid: z.string().describe("The subscriber ICCID") },
|
|
652
1220
|
annotations: { readOnlyHint: true },
|
|
653
|
-
},
|
|
1221
|
+
}, wrapHandler("subscriber_active_period", "getSubscriberActivePeriod", TOOL_SCOPES["subscriber_active_period"], ctx, async ({ iccid }, token) => ocsCall(ctx.env, token, "getSubscriberActivePeriod", { iccid })));
|
|
654
1222
|
// =========================================================================
|
|
655
|
-
// 6. MISC TOOLS
|
|
1223
|
+
// 6. MISC TOOLS (tariff, SMS, network profiles)
|
|
656
1224
|
// =========================================================================
|
|
657
|
-
// Fix #14: bare integer
|
|
1225
|
+
// Fix #14: OCS expects bare integer (resellerId); response key is listTariffRule.
|
|
658
1226
|
server.registerTool("get_tariff", {
|
|
659
1227
|
title: "Get Customer Tariff",
|
|
660
|
-
description: "
|
|
1228
|
+
description: "Use this to retrieve the complete tariff table for a reseller: per-country, per-traffic-type " +
|
|
1229
|
+
"(data/voice/SMS) wholesale rates. Useful for cost analysis, margin calculations, and identifying " +
|
|
1230
|
+
"expensive roaming countries before steering decisions. " +
|
|
1231
|
+
"Params: `resellerId` (integer, optional — omit to use the token owner's reseller). " +
|
|
1232
|
+
"Returns: array of tariff rules, each with `country`, `trafficType`, `rate`, and `currency`. " +
|
|
1233
|
+
"Response key in OCS is `listTariffRule`. " +
|
|
1234
|
+
"Do NOT use this to assign a pricing plan to a subscriber — use `modify_subscriber_mobile_plan`. " +
|
|
1235
|
+
"This shows the RESELLER's wholesale cost, not what end-users are charged.",
|
|
661
1236
|
inputSchema: {
|
|
662
|
-
resellerId: z
|
|
1237
|
+
resellerId: z
|
|
1238
|
+
.number()
|
|
1239
|
+
.optional()
|
|
1240
|
+
.describe("Reseller ID (omit to use token owner's reseller)"),
|
|
663
1241
|
},
|
|
664
1242
|
annotations: { readOnlyHint: true },
|
|
665
|
-
},
|
|
666
|
-
const id = resellerId ?? (await getDefaultResellerId(
|
|
667
|
-
return ocsCall(
|
|
1243
|
+
}, wrapHandler("get_tariff", "getCustomerTariff", TOOL_SCOPES["get_tariff"], ctx, async ({ resellerId }, token) => {
|
|
1244
|
+
const id = resellerId ?? (await getDefaultResellerId(ctx.env, token));
|
|
1245
|
+
return ocsCall(ctx.env, token, "getCustomerTariff", id);
|
|
668
1246
|
}));
|
|
669
|
-
// Fix #11:
|
|
1247
|
+
// Fix #11: OCS expects { imsi, msisdn, text, senderId? }
|
|
1248
|
+
// not { iccid, msisdn, message, sender }
|
|
1249
|
+
// Resolve ICCID → IMSI; rename message → text, sender → senderId.
|
|
670
1250
|
server.registerTool("send_sms", {
|
|
671
1251
|
title: "Send MT SMS",
|
|
672
|
-
description: "
|
|
1252
|
+
description: "Use this to send a mobile-terminated (MT) SMS to a subscriber. Useful for service notifications, " +
|
|
1253
|
+
"package expiry alerts, and support messages sent programmatically from the platform. " +
|
|
1254
|
+
"Internally resolves ICCID → IMSI via a getSingleSubscriber lookup before forwarding to OCS. " +
|
|
1255
|
+
"Params: `iccid` (subscriber identifier), `msisdn` (E.164 phone number of the subscriber), " +
|
|
1256
|
+
"`message` (SMS text content, max 160 chars for single SMS), " +
|
|
1257
|
+
"`sender` (optional sender ID or phone number displayed on the device). " +
|
|
1258
|
+
"Returns: OCS delivery confirmation. " +
|
|
1259
|
+
"Do NOT use this for bulk SMS campaigns — this sends one message per call and is rate-limited. " +
|
|
1260
|
+
"Requires admin scope.",
|
|
673
1261
|
inputSchema: {
|
|
674
1262
|
iccid: z.string().describe("The target subscriber ICCID"),
|
|
675
1263
|
msisdn: z.string().describe("The target MSISDN"),
|
|
@@ -678,9 +1266,9 @@ export function registerAllTools(server, client) {
|
|
|
678
1266
|
...DRY_RUN_FIELD,
|
|
679
1267
|
},
|
|
680
1268
|
annotations: { destructiveHint: true },
|
|
681
|
-
},
|
|
1269
|
+
}, wrapHandler("send_sms", "sendMtSms", TOOL_SCOPES["send_sms"], ctx, async ({ iccid, msisdn, message, sender }, token) => {
|
|
682
1270
|
const cache = new Map();
|
|
683
|
-
const sub = await resolveSubscriberByIccid(
|
|
1271
|
+
const sub = await resolveSubscriberByIccid(ctx.env, token, iccid, cache);
|
|
684
1272
|
const imsi = sub.imsi;
|
|
685
1273
|
if (typeof imsi !== "string" || imsi.length === 0) {
|
|
686
1274
|
return {
|
|
@@ -691,12 +1279,17 @@ export function registerAllTools(server, client) {
|
|
|
691
1279
|
const params = { imsi, msisdn, text: message };
|
|
692
1280
|
if (sender)
|
|
693
1281
|
params.senderId = sender;
|
|
694
|
-
return ocsCall(
|
|
1282
|
+
return ocsCall(ctx.env, token, "sendMtSms", params);
|
|
695
1283
|
}));
|
|
696
1284
|
server.registerTool("list_network_profiles", {
|
|
697
1285
|
title: "List Network Profiles",
|
|
698
|
-
description: "
|
|
1286
|
+
description: "Use this to list all network profiles available to this reseller. A network profile defines " +
|
|
1287
|
+
"the roaming configuration and operator partnerships for eSIM provisioning. Use profile IDs " +
|
|
1288
|
+
"when creating location zones or configuring steering lists. " +
|
|
1289
|
+
"Params: none. " +
|
|
1290
|
+
"Returns: array of profile records with `profileId`, `name`, and coverage metadata. " +
|
|
1291
|
+
"Do NOT use this to list operator steering configurations — use `list_steering_lists` for that.",
|
|
699
1292
|
annotations: { readOnlyHint: true },
|
|
700
|
-
},
|
|
1293
|
+
}, wrapHandler("list_network_profiles", "listNetworkProfile", TOOL_SCOPES["list_network_profiles"], ctx, async (_args, token) => ocsCall(ctx.env, token, "listNetworkProfile")));
|
|
701
1294
|
}
|
|
702
1295
|
//# sourceMappingURL=tools.js.map
|