@carrierllc/mcp 0.1.0 → 0.2.1

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