@carrierllc/mcp 0.2.16 → 0.2.17

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 (79) hide show
  1. package/dist/cli.js +644 -0
  2. package/dist/cli.js.map +1 -0
  3. package/dist/index.js +1263 -233
  4. package/dist/index.js.map +1 -1
  5. package/package.json +21 -14
  6. package/plugin/.claude-plugin/marketplace.json +31 -0
  7. package/plugin/carrier/.claude-plugin/plugin.json +19 -0
  8. package/plugin/carrier/.mcp.json +8 -0
  9. package/plugin/carrier/README.md +48 -0
  10. package/plugin/carrier/agents/carrier-billing-auditor.md +16 -0
  11. package/plugin/carrier/agents/carrier-fleet-ops.md +15 -0
  12. package/plugin/carrier/agents/carrier-storefront-builder.md +17 -0
  13. package/plugin/carrier/commands/billing.md +16 -0
  14. package/plugin/carrier/commands/churn.md +15 -0
  15. package/plugin/carrier/commands/credits.md +15 -0
  16. package/plugin/carrier/commands/esim-status.md +15 -0
  17. package/plugin/carrier/commands/fleet.md +17 -0
  18. package/plugin/carrier/commands/greenzone.md +16 -0
  19. package/plugin/carrier/commands/onboard.md +17 -0
  20. package/plugin/carrier/commands/packages.md +18 -0
  21. package/plugin/carrier/commands/provision.md +20 -0
  22. package/plugin/carrier/commands/sms.md +18 -0
  23. package/plugin/carrier/commands/status.md +15 -0
  24. package/plugin/carrier/commands/storefront.md +21 -0
  25. package/plugin/carrier/commands/subscribers.md +18 -0
  26. package/plugin/carrier/commands/usage.md +16 -0
  27. package/plugin/carrier/skills/carrier-operations/SKILL.md +36 -0
  28. package/templates/storefront/eslint.config.mjs +15 -0
  29. package/templates/storefront/next.config.ts +22 -0
  30. package/templates/storefront/open-next.config.ts +3 -0
  31. package/templates/storefront/package.json +38 -0
  32. package/templates/storefront/pnpm-lock.yaml +4020 -0
  33. package/templates/storefront/postcss.config.mjs +7 -0
  34. package/templates/storefront/src/app/activate/[orderId]/page.tsx +28 -0
  35. package/templates/storefront/src/app/apple-icon.tsx +29 -0
  36. package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +52 -0
  37. package/templates/storefront/src/app/checkout/[templateId]/page.tsx +24 -0
  38. package/templates/storefront/src/app/contact/page.tsx +35 -0
  39. package/templates/storefront/src/app/dashboard/page.tsx +26 -0
  40. package/templates/storefront/src/app/globals.css +99 -0
  41. package/templates/storefront/src/app/help/page.tsx +25 -0
  42. package/templates/storefront/src/app/icon.tsx +29 -0
  43. package/templates/storefront/src/app/layout.tsx +56 -0
  44. package/templates/storefront/src/app/legal/acceptable-use/page.tsx +21 -0
  45. package/templates/storefront/src/app/legal/privacy/page.tsx +27 -0
  46. package/templates/storefront/src/app/legal/terms/page.tsx +25 -0
  47. package/templates/storefront/src/app/manifest.ts +18 -0
  48. package/templates/storefront/src/app/page.tsx +31 -0
  49. package/templates/storefront/src/app/robots.ts +9 -0
  50. package/templates/storefront/src/app/shop/ShopClient.tsx +27 -0
  51. package/templates/storefront/src/app/shop/page.tsx +9 -0
  52. package/templates/storefront/src/app/sign-in/[[...sign-in]]/page.tsx +24 -0
  53. package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +13 -0
  54. package/templates/storefront/src/app/sitemap.ts +14 -0
  55. package/templates/storefront/src/brand.config.ts +28 -0
  56. package/templates/storefront/src/components/Providers.tsx +16 -0
  57. package/templates/storefront/src/components/faq/FaqSection.tsx +80 -0
  58. package/templates/storefront/src/components/footer/StorefrontFooter.tsx +61 -0
  59. package/templates/storefront/src/components/landing/HeroSection.tsx +52 -0
  60. package/templates/storefront/src/components/landing/PlanCard.tsx +61 -0
  61. package/templates/storefront/src/components/nav/StorefrontNav.tsx +69 -0
  62. package/templates/storefront/src/components/theme/clerk-appearance.ts +51 -0
  63. package/templates/storefront/src/components/theme/theme-provider.tsx +87 -0
  64. package/templates/storefront/src/components/theme/theme-script.tsx +24 -0
  65. package/templates/storefront/src/lib/conversion-events.ts +36 -0
  66. package/templates/storefront/src/lib/sanitize-auth-redirect.ts +16 -0
  67. package/templates/storefront/src/middleware.ts +25 -0
  68. package/templates/storefront/src/vendor/carrier/client.ts +95 -0
  69. package/templates/storefront/src/vendor/carrier/index.ts +2 -0
  70. package/templates/storefront/src/vendor/carrier/types.ts +21 -0
  71. package/templates/storefront/src/vendor/config/index.ts +2 -0
  72. package/templates/storefront/src/vendor/geo/index.ts +32 -0
  73. package/templates/storefront/src/vendor/ui/brand.ts +68 -0
  74. package/templates/storefront/src/vendor/ui/cn.ts +7 -0
  75. package/templates/storefront/src/vendor/ui/countryImagery.ts +46 -0
  76. package/templates/storefront/src/vendor/ui/index.ts +3 -0
  77. package/templates/storefront/tsconfig.json +23 -0
  78. package/templates/storefront/wrangler.jsonc +11 -0
  79. package/dist/.metadata_never_index +0 -0
package/dist/index.js CHANGED
@@ -8,8 +8,169 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
8
8
  import { z } from "zod";
9
9
  import * as Sentry from "@sentry/cloudflare";
10
10
 
11
+ // ../../packages/ocs-client/dist/chunk-67QO5NVD.js
12
+ var ENDPOINT_LIMITS_PER_MIN = {
13
+ // Global sentinel (keyed as "__global__")
14
+ __global__: 600,
15
+ // 30/min — tightest endpoint
16
+ subscriberNetworkEventsOverPeriod: 30,
17
+ // 100/min
18
+ subscriberUsageOverPeriod: 100,
19
+ // 150/min
20
+ listLocationZoneElement: 150,
21
+ sendMtSms: 150,
22
+ // 300/min (explicit; also the fallback default)
23
+ affectPlanToSubscriber: 300,
24
+ affectRecurringPackageToSubscriber: 300,
25
+ affectSubscriberFakePhoneNumber: 300,
26
+ affectSubscriberRealPhoneNumber: 300,
27
+ esimStatusPerAccount: 300,
28
+ getSubscriberActivePeriod: 300,
29
+ hlrGetBitrate: 300,
30
+ hlrSetBitrate: 300,
31
+ listSubscriber: 300,
32
+ listSubscriberPrepaidPackages: 300,
33
+ moveSubscriberRangeToAccount: 300,
34
+ pushSteeringToSubs: 300,
35
+ setSubscriberTrafficRestrictions: 300,
36
+ getSingleSubscriber: 300,
37
+ getSubscriberLocationByCellId: 300
38
+ };
39
+ var DEFAULT_LIMIT_PER_MIN = 300;
40
+ var WINDOW_MS2 = 6e4;
41
+ var METRIC_EMIT_MIN_INTERVAL_MS = 1e4;
42
+ var BATCH_FRACTION = 0.2;
43
+ function resellerKeyHash(resellerKey) {
44
+ if (!resellerKey) return "00000000";
45
+ let h = 5381;
46
+ for (let i = 0; i < resellerKey.length; i++) {
47
+ h = ((h << 5) + h ^ resellerKey.charCodeAt(i)) >>> 0;
48
+ }
49
+ return h.toString(16).padStart(8, "0");
50
+ }
51
+ function emitMetric(endpoint, resellerKey, callsInWindow, limitPerMin) {
52
+ const pct = Math.round(callsInWindow / limitPerMin * 100);
53
+ const reseller_hash = resellerKeyHash(resellerKey);
54
+ console.log(
55
+ JSON.stringify({
56
+ metric: "carrier_ocs_calls_per_min",
57
+ endpoint,
58
+ reseller_hash,
59
+ calls_in_window: callsInWindow,
60
+ limit_per_min: limitPerMin,
61
+ utilisation_pct: pct,
62
+ alert: pct >= 80,
63
+ ts: Date.now()
64
+ })
65
+ );
66
+ if (pct >= 80) {
67
+ console.warn(
68
+ `[rate-governor] ALERT: ${endpoint} at ${pct}% capacity (${callsInWindow}/${limitPerMin} per min) reseller=${reseller_hash}`
69
+ );
70
+ }
71
+ }
72
+ var _endpointWindows = /* @__PURE__ */ new Map();
73
+ function _getEndpointState(key) {
74
+ let s = _endpointWindows.get(key);
75
+ if (!s) {
76
+ s = {
77
+ log: [],
78
+ batchLog: [],
79
+ gate: Promise.resolve(),
80
+ lastMetricEmitAt: 0,
81
+ metricBelowHighUtil: true
82
+ };
83
+ _endpointWindows.set(key, s);
84
+ }
85
+ return s;
86
+ }
87
+ function _prune(log, windowStart) {
88
+ let i = 0;
89
+ while (i < log.length && log[i] <= windowStart) i++;
90
+ return i > 0 ? log.slice(i) : log;
91
+ }
92
+ async function acquireEndpointSlot(resellerId, endpoint, priority = "interactive") {
93
+ const proc = globalThis["process"];
94
+ if (proc?.env?.["RATE_FLOOR_DISABLED"] === "true" || globalThis["RATE_FLOOR_DISABLED"] === "true") {
95
+ return;
96
+ }
97
+ await _acquireOneSlot(resellerId, "__global__", priority);
98
+ await _acquireOneSlot(resellerId, endpoint, priority);
99
+ }
100
+ async function _acquireOneSlot(resellerId, endpoint, priority) {
101
+ const bucketKey = `${resellerId}:${endpoint}`;
102
+ const limitPerMin = ENDPOINT_LIMITS_PER_MIN[endpoint] ?? DEFAULT_LIMIT_PER_MIN;
103
+ const batchCap = Math.floor(limitPerMin * BATCH_FRACTION);
104
+ const state = _getEndpointState(bucketKey);
105
+ const ticket = state.gate.then(async () => {
106
+ while (true) {
107
+ const now = Date.now();
108
+ const windowStart = now - WINDOW_MS2;
109
+ state.log = _prune(state.log, windowStart);
110
+ state.batchLog = _prune(state.batchLog, windowStart);
111
+ const totalInWindow = state.log.length;
112
+ const batchInWindow = state.batchLog.length;
113
+ if (totalInWindow > 0 && endpoint !== "__global__") {
114
+ const utilPct = Math.round(totalInWindow / limitPerMin * 100);
115
+ const intervalOk = now - state.lastMetricEmitAt >= METRIC_EMIT_MIN_INTERVAL_MS;
116
+ const enteredHighUtil = utilPct >= 80 && state.metricBelowHighUtil;
117
+ if (intervalOk || enteredHighUtil) {
118
+ emitMetric(endpoint, resellerId, totalInWindow, limitPerMin);
119
+ state.lastMetricEmitAt = now;
120
+ if (utilPct >= 80) {
121
+ state.metricBelowHighUtil = false;
122
+ }
123
+ }
124
+ if (utilPct < 80) {
125
+ state.metricBelowHighUtil = true;
126
+ }
127
+ }
128
+ const globalFull = totalInWindow >= limitPerMin;
129
+ const batchFull = priority === "batch" && batchInWindow >= batchCap;
130
+ if (!globalFull && !batchFull) {
131
+ state.log.push(now);
132
+ if (priority === "batch") {
133
+ state.batchLog.push(now);
134
+ }
135
+ return;
136
+ }
137
+ let waitMs;
138
+ if (globalFull) {
139
+ const oldest = state.log[0];
140
+ waitMs = WINDOW_MS2 - (now - oldest) + 1;
141
+ } else {
142
+ const oldestBatch = state.batchLog[0];
143
+ waitMs = WINDOW_MS2 - (now - oldestBatch) + 1;
144
+ }
145
+ await new Promise((r) => setTimeout(r, waitMs));
146
+ }
147
+ });
148
+ state.gate = ticket;
149
+ return ticket;
150
+ }
151
+ function getLimitForEndpoint(endpoint) {
152
+ return ENDPOINT_LIMITS_PER_MIN[endpoint] ?? DEFAULT_LIMIT_PER_MIN;
153
+ }
154
+ function getRateLimitWindowCounts(resellerKey, endpoint) {
155
+ const bucketKey = `${resellerKey}:${endpoint}`;
156
+ const state = _endpointWindows.get(bucketKey);
157
+ if (!state) {
158
+ return { calls_in_window: 0, batch_calls_in_window: 0 };
159
+ }
160
+ const windowStart = Date.now() - WINDOW_MS2;
161
+ let calls_in_window = 0;
162
+ for (const ts of state.log) {
163
+ if (ts > windowStart) calls_in_window++;
164
+ }
165
+ let batch_calls_in_window = 0;
166
+ for (const ts of state.batchLog) {
167
+ if (ts > windowStart) batch_calls_in_window++;
168
+ }
169
+ return { calls_in_window, batch_calls_in_window };
170
+ }
171
+
11
172
  // src/client.ts
12
- var OcsApiError = class extends Error {
173
+ var OcsApiError2 = class extends Error {
13
174
  constructor(code, message, method) {
14
175
  super(`[${method}] OCS error ${code}: ${message}`);
15
176
  this.code = code;
@@ -19,7 +180,7 @@ var OcsApiError = class extends Error {
19
180
  code;
20
181
  method;
21
182
  };
22
- var OcsClient = class {
183
+ var OcsClient2 = class {
23
184
  baseUrl;
24
185
  token;
25
186
  constructor(baseUrl2, token2) {
@@ -27,6 +188,7 @@ var OcsClient = class {
27
188
  this.token = token2;
28
189
  }
29
190
  async call(method, params = {}) {
191
+ await acquireEndpointSlot(this.token, method, "interactive");
30
192
  const url = `${this.baseUrl}/v1?token=${this.token}`;
31
193
  const body = JSON.stringify({ [method]: params });
32
194
  const res = await fetch(url, {
@@ -35,11 +197,11 @@ var OcsClient = class {
35
197
  body
36
198
  });
37
199
  if (!res.ok) {
38
- throw new OcsApiError(res.status, `HTTP ${res.status} ${res.statusText}`, method);
200
+ throw new OcsApiError2(res.status, `HTTP ${res.status} ${res.statusText}`, method);
39
201
  }
40
202
  const json = await res.json();
41
203
  if (json.status?.code !== 0) {
42
- throw new OcsApiError(json.status?.code ?? -1, json.status?.msg ?? "Unknown error", method);
204
+ throw new OcsApiError2(json.status?.code ?? -1, json.status?.msg ?? "Unknown error", method);
43
205
  }
44
206
  if (method === "getCustomerTariff" && json["listTariffRule"] !== void 0) {
45
207
  return json["listTariffRule"];
@@ -295,9 +457,9 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
295
457
  try {
296
458
  const token2 = await ctx.getUserToken(ctx.props.sub);
297
459
  result2 = await handler(args, token2);
298
- } catch (err2) {
460
+ } catch (err4) {
299
461
  try {
300
- Sentry.captureException(err2, {
462
+ Sentry.captureException(err4, {
301
463
  tags: {
302
464
  tool: toolName,
303
465
  feature: "mcp",
@@ -306,8 +468,8 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
306
468
  });
307
469
  } catch {
308
470
  }
309
- const message = err2 instanceof Error ? err2.message : String(err2);
310
- const ocsCode = err2 instanceof OcsApiError ? err2.code : void 0;
471
+ const message = err4 instanceof Error ? err4.message : String(err4);
472
+ const ocsCode = err4 instanceof OcsApiError2 ? err4.code : void 0;
311
473
  ctx.audit({
312
474
  tool_name: toolName,
313
475
  ocs_method: ocsMethod,
@@ -335,7 +497,7 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
335
497
  };
336
498
  }
337
499
  async function ocsCall(env, token2, method, params = {}) {
338
- const client = new OcsClient(env.CARRIER_OCS_BASE_URL, token2);
500
+ const client = new OcsClient2(env.CARRIER_OCS_BASE_URL, token2);
339
501
  const result2 = await client.call(method, params);
340
502
  return {
341
503
  content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
@@ -344,13 +506,13 @@ async function ocsCall(env, token2, method, params = {}) {
344
506
  async function resolveSubscriberByIccid(env, token2, iccid, cache) {
345
507
  const hit = cache.get(iccid);
346
508
  if (hit) return hit;
347
- const client = new OcsClient(env.CARRIER_OCS_BASE_URL, token2);
509
+ const client = new OcsClient2(env.CARRIER_OCS_BASE_URL, token2);
348
510
  const record = await client.call("getSingleSubscriber", { iccid });
349
511
  cache.set(iccid, record);
350
512
  return record;
351
513
  }
352
514
  async function getDefaultResellerId(env, token2) {
353
- const client = new OcsClient(env.CARRIER_OCS_BASE_URL, token2);
515
+ const client = new OcsClient2(env.CARRIER_OCS_BASE_URL, token2);
354
516
  const info = await client.call("getResellerInfo", {});
355
517
  const id = info?.id;
356
518
  if (typeof id !== "number") {
@@ -567,7 +729,7 @@ function registerAllTools(server2, ctx) {
567
729
  if (args.msisdn) params.msisdn = args.msisdn;
568
730
  if (args.status) params.status = args.status;
569
731
  if (args.offset !== void 0) params.offset = args.offset;
570
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
732
+ const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
571
733
  const raw = await client.call("listSubscriber", params);
572
734
  const limit = args.limit;
573
735
  const payload = Array.isArray(raw) && typeof limit === "number" && limit >= 0 ? raw.slice(0, limit) : raw;
@@ -2968,11 +3130,11 @@ var ocsAppMethods = ocsSpec.v1_app_methods;
2968
3130
  // src/intelligence.ts
2969
3131
  async function safeCall(env, token2, method, params = {}) {
2970
3132
  try {
2971
- const client = new OcsClient(env.CARRIER_OCS_BASE_URL, token2);
3133
+ const client = new OcsClient2(env.CARRIER_OCS_BASE_URL, token2);
2972
3134
  const data = await client.call(method, params);
2973
3135
  return { data, error: null };
2974
- } catch (err2) {
2975
- return { data: null, error: err2 instanceof Error ? err2.message : String(err2) };
3136
+ } catch (err4) {
3137
+ return { data: null, error: err4 instanceof Error ? err4.message : String(err4) };
2976
3138
  }
2977
3139
  }
2978
3140
  async function fetchActiveSubscribers(env, token2, accountId, resellerId) {
@@ -3972,6 +4134,11 @@ var DRY_RUN_FIELD2 = {
3972
4134
  "If true, do not call OCS \u2014 return the would-be request for confirmation"
3973
4135
  )
3974
4136
  };
4137
+ function normalizeRadioType(value) {
4138
+ if (/^nb-?iot$/i.test(value)) return "NB-IoT";
4139
+ return value;
4140
+ }
4141
+ var VALID_RADIO_TYPES = /* @__PURE__ */ new Set(["2G", "3G", "4G", "5G", "NB-IoT"]);
3975
4142
  var BACKLOG_TOOL_SCOPES = {
3976
4143
  affect_subscriber_phone_number: "write",
3977
4144
  carrier_webhook_config: "read",
@@ -4006,7 +4173,7 @@ function registerAllBacklogTools(server2, ctx) {
4006
4173
  ctx,
4007
4174
  async ({ iccid, phone_number, phone_type }, token2) => {
4008
4175
  const ocsMethod = phone_type === "fake" ? "affectSubscriberFakePhoneNumber" : "affectSubscriberRealPhoneNumber";
4009
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
4176
+ const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
4010
4177
  const result2 = await client.call(ocsMethod, { subscriber: iccid, phoneNumber: phone_number });
4011
4178
  return {
4012
4179
  content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
@@ -4018,9 +4185,11 @@ function registerAllBacklogTools(server2, ctx) {
4018
4185
  "get_subscriber_location_by_cell_id",
4019
4186
  {
4020
4187
  title: "Get Location by Cell ID (GeoSense)",
4021
- description: "Powered by Bridge4IP GeoSense \u2014 cell-level location resolution with sub-cell accuracy where available. Use this to resolve a cell tower tuple (radio type + MCC + MNC + LAC + optional cellId) to a latitude/longitude estimate. No subscriber identifier required \u2014 caller supplies cell parameters directly. Useful for fraud detection, roaming cost attribution, and network troubleshooting when you have raw cell info from an external source (e.g. a Relay LU webhook event). Params: `radio_type` ('2G'|'3G'|'4G'|'5G'|'NB-IoT'), `mcc` (int), `mnc` (int), `lac` (int), `cell_id` (int, optional but strongly recommended for accuracy), `signal_strength` (number dBm, optional). Returns: { latitude, longitude, accuracy } \u2014 accuracy is median error in meters at 50% confidence. Without cell_id accuracy degrades significantly (>10 km). Do NOT use this for bulk fleet location sweeps \u2014 one OCS call per cell tower; use `audit_network_coverage` for fleet-level analysis instead.",
4188
+ description: "Powered by Bridge4IP GeoSense \u2014 cell-level location resolution with sub-cell accuracy where available. Use this to resolve a cell tower tuple (radio type + MCC + MNC + LAC + optional cellId) to a latitude/longitude estimate. No subscriber identifier required \u2014 caller supplies cell parameters directly. Useful for fraud detection, roaming cost attribution, and network troubleshooting when you have raw cell info from an external source (e.g. a Relay LU webhook event). Params: `radio_type` ('2G'|'3G'|'4G'|'5G'|'NB-IoT'; also accepts 'NBIOT'|'nb-iot'|'nbiot' \u2014 normalised to 'NB-IoT'), `mcc` (int), `mnc` (int), `lac` (int), `cell_id` (int, optional but strongly recommended for accuracy), `signal_strength` (number dBm, optional). Returns: { latitude, longitude, accuracy } \u2014 accuracy is median error in meters at 50% confidence. Without cell_id accuracy degrades significantly (>10 km). Do NOT use this for bulk fleet location sweeps \u2014 one OCS call per cell tower; use `audit_network_coverage` for fleet-level analysis instead.",
4022
4189
  inputSchema: {
4023
- radio_type: z3.enum(["2G", "3G", "4G", "5G", "NB-IoT"]).describe("Radio access technology type"),
4190
+ radio_type: z3.string().transform(normalizeRadioType).refine((v) => VALID_RADIO_TYPES.has(v), {
4191
+ message: "radio_type must be one of: 2G, 3G, 4G, 5G, NB-IoT (also accepts NBIOT, nb-iot, nbiot)"
4192
+ }).describe("Radio access technology type: '2G'|'3G'|'4G'|'5G'|'NB-IoT'. Also accepts 'NBIOT'|'nb-iot'|'nbiot' \u2014 normalised to 'NB-IoT'."),
4024
4193
  mcc: z3.number().int().describe("Mobile Country Code (e.g. 250 for Russia, 234 for UK)"),
4025
4194
  mnc: z3.number().int().describe("Mobile Network Code"),
4026
4195
  lac: z3.number().int().describe("Location Area Code"),
@@ -4042,7 +4211,7 @@ function registerAllBacklogTools(server2, ctx) {
4042
4211
  cell_id,
4043
4212
  signal_strength
4044
4213
  }, token2) => {
4045
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
4214
+ const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
4046
4215
  const params = { radioType: radio_type, mcc, mnc, lac };
4047
4216
  if (cell_id !== void 0) params.cellId = cell_id;
4048
4217
  if (signal_strength !== void 0) params.signalStrength = signal_strength;
@@ -4070,7 +4239,7 @@ function registerAllBacklogTools(server2, ctx) {
4070
4239
  ctx,
4071
4240
  async ({ resellerId }, token2) => {
4072
4241
  const id = resellerId ?? await (async () => {
4073
- const client2 = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
4242
+ const client2 = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
4074
4243
  const info = await client2.call("getResellerInfo", {});
4075
4244
  const resolvedId = info?.id;
4076
4245
  if (typeof resolvedId !== "number") {
@@ -4078,7 +4247,7 @@ function registerAllBacklogTools(server2, ctx) {
4078
4247
  }
4079
4248
  return resolvedId;
4080
4249
  })();
4081
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
4250
+ const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
4082
4251
  const result2 = await client.call("listDetailedDestinationList", id);
4083
4252
  return {
4084
4253
  content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
@@ -4104,7 +4273,7 @@ function registerAllBacklogTools(server2, ctx) {
4104
4273
  BACKLOG_TOOL_SCOPES["modify_subscriber_mobile_plan"],
4105
4274
  ctx,
4106
4275
  async ({ iccid, mobile_plan_id }, token2) => {
4107
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
4276
+ const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
4108
4277
  const result2 = await client.call(
4109
4278
  "modifySubscriberMobilePlan",
4110
4279
  { subscriber: iccid, mobilePlanId: mobile_plan_id }
@@ -4138,7 +4307,7 @@ function registerAllBacklogTools(server2, ctx) {
4138
4307
  const params = { subscriber: iccid, packageId: package_id };
4139
4308
  if (start_date !== void 0) params.startDate = start_date;
4140
4309
  if (end_date !== void 0) params.endDate = end_date;
4141
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
4310
+ const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
4142
4311
  const result2 = await client.call(
4143
4312
  "modifySubscriberPrepaidPackageActivePeriod",
4144
4313
  params
@@ -4167,7 +4336,7 @@ function registerAllBacklogTools(server2, ctx) {
4167
4336
  BACKLOG_TOOL_SCOPES["modify_subscriber_voip_plan"],
4168
4337
  ctx,
4169
4338
  async ({ iccid, voip_plan_id }, token2) => {
4170
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
4339
+ const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
4171
4340
  const result2 = await client.call(
4172
4341
  "modifySubscriberVoipPlan",
4173
4342
  { subscriber: iccid, voipPlanId: voip_plan_id }
@@ -4198,7 +4367,7 @@ function registerAllBacklogTools(server2, ctx) {
4198
4367
  const cache = /* @__PURE__ */ new Map();
4199
4368
  const sub = await resolveSubscriberByIccid(ctx.env, token2, iccid, cache);
4200
4369
  const subscriberId = sub.id ?? sub.subscriberId ?? iccid;
4201
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
4370
+ const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
4202
4371
  const result2 = await client.call(
4203
4372
  "pushSteeringToSubs",
4204
4373
  { subscriber: subscriberId }
@@ -4226,7 +4395,7 @@ function registerAllBacklogTools(server2, ctx) {
4226
4395
  BACKLOG_TOOL_SCOPES["reset_subscriber_gz_counter"],
4227
4396
  ctx,
4228
4397
  async ({ iccid }, token2) => {
4229
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
4398
+ const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
4230
4399
  const result2 = await client.call("resetSubsGzCounter", { subscriber: iccid });
4231
4400
  return {
4232
4401
  content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
@@ -4250,7 +4419,7 @@ function registerAllBacklogTools(server2, ctx) {
4250
4419
  BACKLOG_TOOL_SCOPES["carrier_webhook_config"],
4251
4420
  ctx,
4252
4421
  async ({ reseller_id }, token2) => {
4253
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
4422
+ const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
4254
4423
  let resellerId = reseller_id;
4255
4424
  if (resellerId === void 0) {
4256
4425
  resellerId = await getDefaultResellerId(ctx.env, token2);
@@ -4299,10 +4468,20 @@ var TOOL_REGISTRY = /* @__PURE__ */ new Set([
4299
4468
  "high_cost_subscribers",
4300
4469
  // event ring buffer (1)
4301
4470
  "list_recent_ocs_events",
4471
+ // CAR-39: rate-limit governor (1)
4472
+ "rate_limit_status",
4473
+ // CAR-38: Relay LU country history (1)
4474
+ "subscriber_country_history",
4475
+ // bundle depletion events (1) — CAR-42 wave-2
4476
+ "subscriber_depletion_events",
4302
4477
  // MCP App tools (3)
4303
4478
  "fleet_health_app",
4304
4479
  "provision_esim_wizard",
4305
4480
  "balance_topup_form",
4481
+ // greenzone whitelist tools (3) — portal-only, executed via Kapture browser automation
4482
+ "greenzone_whitelist_add",
4483
+ "greenzone_whitelist_remove",
4484
+ "greenzone_whitelist_list",
4306
4485
  // router tools (self-reference)
4307
4486
  "carrier_ask",
4308
4487
  "carrier_ask_describe"
@@ -4517,8 +4696,22 @@ var ROUTER_TOOLS = [
4517
4696
  },
4518
4697
  {
4519
4698
  name: "get_subscriber_location_by_cell_id",
4520
- description: "Get granular subscriber location by cell tower. Intent: 'cell-level location', 'cell tower for subscriber', 'exact location'.",
4521
- input_schema: { type: "object", properties: { iccid: { type: "string" } } }
4699
+ description: "Get granular cell-tower location via GeoSense using radio_type, mcc, mnc, lac, and optional cell_id. Use radio_type '4G' when the user means LTE \u2014 the literal 'LTE' is not valid for the underlying API. NB-IoT aliases NBIOT, nbiot, nb-iot normalise to NB-IoT. Intent: 'cell-level location', 'cell tower for subscriber', 'exact location', 'find subscriber by cell id', 'locate SIM by cell tower', 'NB-IoT subscriber location', 'which cell is this SIM on'.",
4700
+ input_schema: {
4701
+ type: "object",
4702
+ required: ["radio_type", "mcc", "mnc", "lac"],
4703
+ properties: {
4704
+ iccid: { type: "string" },
4705
+ radio_type: {
4706
+ type: "string",
4707
+ description: "One of 2G, 3G, 4G, 5G, NB-IoT. Use 4G for LTE networks. NB-IoT also accepts NBIOT, nbiot, nb-iot."
4708
+ },
4709
+ mcc: { type: "number", description: "Mobile Country Code" },
4710
+ mnc: { type: "number", description: "Mobile Network Code" },
4711
+ lac: { type: "number", description: "Location Area Code" },
4712
+ cell_id: { type: "number", description: "Cell tower ID (optional but recommended)" }
4713
+ }
4714
+ }
4522
4715
  },
4523
4716
  {
4524
4717
  name: "list_steering_lists",
@@ -4595,6 +4788,26 @@ var ROUTER_TOOLS = [
4595
4788
  description: "List recent OCS events for a subscriber (last 50, 24h window). Intent: 'recent events', 'event history', 'OCS log'.",
4596
4789
  input_schema: { type: "object", properties: { iccid: { type: "string" } } }
4597
4790
  },
4791
+ {
4792
+ name: "rate_limit_status",
4793
+ description: "Inspect Bridge4IP OCS rate-limit governor state \u2014 bucket fill levels and reseller quota per endpoint. Intent: 'are we rate limited', 'ocs quota', 'rate limit status', 'how full is the api bucket', 'check rate limit', 'api capacity'.",
4794
+ input_schema: {
4795
+ type: "object",
4796
+ properties: { endpoint: { type: "string" } }
4797
+ }
4798
+ },
4799
+ {
4800
+ name: "subscriber_country_history",
4801
+ description: "Return cross-border movement history for a subscriber from Relay LU events. Intent: 'where has subscriber X traveled', 'country history for ICCID', 'border crossings', 'which countries did this SIM visit', 'travel history'.",
4802
+ input_schema: {
4803
+ type: "object",
4804
+ properties: {
4805
+ subscriberId: { type: "string" },
4806
+ limit: { type: "number" }
4807
+ },
4808
+ required: ["subscriberId"]
4809
+ }
4810
+ },
4598
4811
  {
4599
4812
  name: "affect_subscriber_phone_number",
4600
4813
  description: "Assign or unassign a phone number to a subscriber. Intent: 'assign phone number', 'give SIM a number', 'remove number'.",
@@ -4679,6 +4892,44 @@ var ROUTER_TOOLS = [
4679
4892
  name: "carrier_ask_describe",
4680
4893
  description: "Get documentation for a specific carrier tool. Intent: 'describe tool X', 'how does assign_package work', 'tool documentation'.",
4681
4894
  input_schema: { type: "object", properties: { tool_name: { type: "string" } } }
4895
+ },
4896
+ {
4897
+ name: "greenzone_whitelist_add",
4898
+ description: "Add a host and/or IP to the GreenZone whitelist. Operates via portal-only Kapture browser automation \u2014 no direct REST API. Intent: 'add to greenzone whitelist', 'whitelist this host in greenzone', 'allow host in greenzone', 'greenzone add host', 'add IP to greenzone'.",
4899
+ input_schema: {
4900
+ type: "object",
4901
+ properties: {
4902
+ host: { type: "string", description: "Hostname to whitelist (e.g. api.example.com)" },
4903
+ ip: { type: "string", description: "IP address to whitelist (optional)" }
4904
+ }
4905
+ }
4906
+ },
4907
+ {
4908
+ name: "greenzone_whitelist_remove",
4909
+ description: "Remove a host and/or IP from the GreenZone whitelist. Operates via portal-only Kapture browser automation \u2014 no direct REST API. Intent: 'remove from greenzone whitelist', 'de-whitelist host in greenzone', 'block host in greenzone', 'greenzone remove host'.",
4910
+ input_schema: {
4911
+ type: "object",
4912
+ properties: {
4913
+ host: { type: "string", description: "Hostname to remove" },
4914
+ ip: { type: "string", description: "IP address to remove (optional)" }
4915
+ }
4916
+ }
4917
+ },
4918
+ {
4919
+ name: "greenzone_whitelist_list",
4920
+ description: "List all hosts and IPs in the GreenZone whitelist (reads from KV cache populated by last portal sync). Intent: 'show greenzone whitelist', 'list greenzone hosts', 'what hosts are in greenzone', 'greenzone whitelist', 'show greenzone entries', 'which IPs are whitelisted in greenzone'.",
4921
+ input_schema: { type: "object", properties: {} }
4922
+ },
4923
+ {
4924
+ name: "subscriber_depletion_events",
4925
+ description: "Look up bundle depletion events from prepaid-usage webhooks (last_slice notifications, 7-day retention). Intent: 'did subscriber X run out of data', 'depletion history for ICCID', 'bundle depleted events', 'which subscribers ran out of data', 'recent depletions', 'show data exhaustion events', 'has this SIM hit its data cap', 'list depleted subscribers'.",
4926
+ input_schema: {
4927
+ type: "object",
4928
+ properties: {
4929
+ subscriberId: { type: "string", description: "OCS subscriber ID \u2014 omit to list fleet-wide depletions" },
4930
+ since: { type: "string", description: "ISO-8601 timestamp \u2014 filter to events at or after this time" }
4931
+ }
4932
+ }
4682
4933
  }
4683
4934
  ];
4684
4935
  var ROUTER_SYSTEM_PROMPT = `You are a carrier fleet operations router. Your job is to map a user's natural-language intent to exactly ONE tool from the Carrier MCP tool registry.
@@ -4711,10 +4962,10 @@ async function callBedrock(env, payload) {
4711
4962
  const body = await resp.text();
4712
4963
  if (resp.status === 429) {
4713
4964
  const retryAfter = resp.headers.get("retry-after");
4714
- const err2 = new Error(`Bedrock rate limit: ${body}`);
4715
- err2.isRateLimit = true;
4716
- err2.retryAfter = retryAfter;
4717
- throw err2;
4965
+ const err4 = new Error(`Bedrock rate limit: ${body}`);
4966
+ err4.isRateLimit = true;
4967
+ err4.retryAfter = retryAfter;
4968
+ throw err4;
4718
4969
  }
4719
4970
  throw new Error(`Bedrock invoke failed: ${resp.status} ${body}`);
4720
4971
  }
@@ -4752,9 +5003,9 @@ Pre-resolved context: ${JSON.stringify(definedContext)}` : "";
4752
5003
  AWS_REGION: env.AWS_REGION,
4753
5004
  BEDROCK_MODEL_ID: env.BEDROCK_MODEL_ID
4754
5005
  }, payload);
4755
- } catch (err2) {
4756
- if (err2 instanceof Error && err2.isRateLimit) {
4757
- const retryAfter = err2.retryAfter;
5006
+ } catch (err4) {
5007
+ if (err4 instanceof Error && err4.isRateLimit) {
5008
+ const retryAfter = err4.retryAfter;
4758
5009
  const parsedSeconds = retryAfter ? parseInt(retryAfter, 10) : 60;
4759
5010
  return {
4760
5011
  match: "rate_limited",
@@ -4762,7 +5013,7 @@ Pre-resolved context: ${JSON.stringify(definedContext)}` : "";
4762
5013
  suggestion: "Bedrock rate limit reached. Please retry after the indicated delay."
4763
5014
  };
4764
5015
  }
4765
- throw err2;
5016
+ throw err4;
4766
5017
  }
4767
5018
  const toolUseBlock = response.content.find(
4768
5019
  (block) => block.type === "tool_use"
@@ -4936,7 +5187,7 @@ function registerAllCarrierAskTools(server2, ctx) {
4936
5187
  let route;
4937
5188
  try {
4938
5189
  route = await _routeIntent(intent, context, ctx.env);
4939
- } catch (err2) {
5190
+ } catch (err4) {
4940
5191
  writeCarrierAskAudit(ctx.env, {
4941
5192
  intent_hash: intentHash,
4942
5193
  match: "error",
@@ -4953,7 +5204,7 @@ function registerAllCarrierAskTools(server2, ctx) {
4953
5204
  type: "text",
4954
5205
  text: JSON.stringify({
4955
5206
  error: "routing_error",
4956
- message: err2 instanceof Error ? err2.message : "An unexpected error occurred during routing."
5207
+ message: err4 instanceof Error ? err4.message : "An unexpected error occurred during routing."
4957
5208
  })
4958
5209
  }
4959
5210
  ],
@@ -5095,24 +5346,10 @@ function buildExamples(toolName) {
5095
5346
 
5096
5347
  // src/list-recent-ocs-events.ts
5097
5348
  import { z as z5 } from "zod";
5098
- var ALLOWED_EVENT_TYPES = [
5099
- "esim.activated",
5100
- "esim.disabled",
5101
- "package.expiry_warning",
5102
- "location.changed",
5103
- "balance.low"
5104
- ];
5105
- var listRecentOcsEventsSchema = {
5106
- iccid: z5.string().regex(/^\d{19,20}$/).describe("ICCID, 19 or 20 digits, ITU-T E.118 format"),
5107
- limit: z5.number().int().min(1).max(50).default(20).describe("Max events to return, newest first"),
5108
- event_types: z5.array(z5.enum(ALLOWED_EVENT_TYPES)).optional().describe("Filter to specific event types"),
5109
- since: z5.string().datetime().optional().describe("ISO-8601 timestamp; only events after this point")
5110
- };
5349
+
5350
+ // ../../packages/ocs-spec/src/ocs-event-buffer-read.ts
5111
5351
  async function listRecentOcsEvents(iccid, limit, eventTypes, since, kv) {
5112
- const routingRaw = await kv.get(
5113
- `iccid:${iccid}`,
5114
- "json"
5115
- );
5352
+ const routingRaw = await kv.get(`iccid:${iccid}`, "json");
5116
5353
  const resellerId = routingRaw?.reseller_id ?? 0;
5117
5354
  const ringKey = `events:${resellerId}:${iccid}`;
5118
5355
  const itemPrefix = `${ringKey}:evt:`;
@@ -5148,8 +5385,7 @@ async function listRecentOcsEvents(iccid, limit, eventTypes, since, kv) {
5148
5385
  const sinceMs = since ? new Date(since).getTime() : null;
5149
5386
  const filtered = allSorted.filter((e) => {
5150
5387
  if (sinceMs !== null && e.occurred_at * 1e3 <= sinceMs) return false;
5151
- if (eventTypes && eventTypes.length > 0 && !eventTypes.includes(e.event_type))
5152
- return false;
5388
+ if (eventTypes && eventTypes.length > 0 && !eventTypes.includes(e.event_type)) return false;
5153
5389
  return true;
5154
5390
  });
5155
5391
  const filteredCount = filtered.length;
@@ -5175,6 +5411,25 @@ async function listRecentOcsEvents(iccid, limit, eventTypes, since, kv) {
5175
5411
  buffer_newest_event_timestamp: bufferNewest
5176
5412
  };
5177
5413
  }
5414
+
5415
+ // src/list-recent-ocs-events.ts
5416
+ var ALLOWED_EVENT_TYPES = [
5417
+ "esim.activated",
5418
+ "esim.disabled",
5419
+ "package.expiry_warning",
5420
+ "location.changed",
5421
+ "country.entered",
5422
+ "balance.low"
5423
+ ];
5424
+ var listRecentOcsEventsSchema = {
5425
+ iccid: z5.string().regex(/^\d{19,20}$/).describe("ICCID, 19 or 20 digits, ITU-T E.118 format"),
5426
+ limit: z5.number().int().min(1).max(50).default(20).describe("Max events to return, newest first"),
5427
+ event_types: z5.array(z5.enum(ALLOWED_EVENT_TYPES)).optional().describe("Filter to specific event types"),
5428
+ since: z5.string().datetime().optional().describe("ISO-8601 timestamp; only events after this point")
5429
+ };
5430
+ async function listRecentOcsEvents2(iccid, limit, eventTypes, since, kv) {
5431
+ return listRecentOcsEvents(iccid, limit, eventTypes, since, kv);
5432
+ }
5178
5433
  function registerListRecentOcsEventsTool(server2, env) {
5179
5434
  server2.registerTool(
5180
5435
  "list_recent_ocs_events",
@@ -5191,7 +5446,7 @@ function registerListRecentOcsEventsTool(server2, env) {
5191
5446
  async (args) => {
5192
5447
  const { iccid, limit, event_types, since } = args;
5193
5448
  try {
5194
- const result2 = await listRecentOcsEvents(
5449
+ const result2 = await listRecentOcsEvents2(
5195
5450
  iccid,
5196
5451
  limit,
5197
5452
  event_types,
@@ -5201,8 +5456,8 @@ function registerListRecentOcsEventsTool(server2, env) {
5201
5456
  return {
5202
5457
  content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
5203
5458
  };
5204
- } catch (err2) {
5205
- const message = err2 instanceof Error ? err2.message : String(err2);
5459
+ } catch (err4) {
5460
+ const message = err4 instanceof Error ? err4.message : String(err4);
5206
5461
  return {
5207
5462
  isError: true,
5208
5463
  content: [{ type: "text", text: `Error reading OCS event buffer: ${message}` }]
@@ -5212,8 +5467,271 @@ function registerListRecentOcsEventsTool(server2, env) {
5212
5467
  );
5213
5468
  }
5214
5469
 
5215
- // src/apps/fleet-health-app.ts
5470
+ // src/tools-rate-governor.ts
5216
5471
  import { z as z6 } from "zod";
5472
+ var MONITORED_ENDPOINTS = [
5473
+ "subscriberNetworkEventsOverPeriod",
5474
+ // 30/min
5475
+ "subscriberUsageOverPeriod",
5476
+ // 100/min
5477
+ "listLocationZoneElement",
5478
+ // 150/min
5479
+ "sendMtSms",
5480
+ // 150/min
5481
+ "affectPlanToSubscriber",
5482
+ // 300/min
5483
+ "affectRecurringPackageToSubscriber",
5484
+ // 300/min
5485
+ "listSubscriber",
5486
+ // 300/min
5487
+ "listSubscriberPrepaidPackages",
5488
+ // 300/min
5489
+ "getSingleSubscriber",
5490
+ // 300/min
5491
+ "getSubscriberLocationByCellId",
5492
+ // 300/min
5493
+ "__global__"
5494
+ // 600/min
5495
+ ];
5496
+ function registerRateLimitStatusTool(server2, ctx) {
5497
+ server2.registerTool(
5498
+ "rate_limit_status",
5499
+ {
5500
+ title: "API Rate-Limit Governor Status",
5501
+ description: "Inspect Bridge4IP OCS rate-limit governor state \u2014 bucket fill levels, per-endpoint limits, and 80% alert thresholds. Use this to answer: 'are we rate limited', 'check ocs quota', 'how full is the rate limit bucket', 'rate limit status'. Params: `endpoint` (optional \u2014 OCS camelCase method name to inspect a single bucket, e.g. 'subscriberUsageOverPeriod'; omit for all monitored endpoints). Returns: per-endpoint { limit_per_min, batch_cap_per_min, calls_in_window, batch_calls_in_window, utilisation_pct, alert (true when \u226580% of limit) }. Note: counts come from the in-process sliding-window log (same key as OCS calls: API token + endpoint) \u2014 they reset when the Worker isolate recycles. Use as a relative signal, not an exact counter. Do NOT use this to adjust limits \u2014 limits are set by Bridge4IP contract and cannot be changed via MCP.",
5502
+ inputSchema: {
5503
+ endpoint: z6.string().optional().describe(
5504
+ "OCS method name in camelCase (e.g. 'subscriberUsageOverPeriod'). Omit to return status for all monitored endpoints."
5505
+ )
5506
+ },
5507
+ annotations: { readOnlyHint: true, idempotentHint: true }
5508
+ },
5509
+ wrapHandler(
5510
+ "rate_limit_status",
5511
+ "[rate-governor:in-process]",
5512
+ "read",
5513
+ ctx,
5514
+ async ({ endpoint }, token2) => {
5515
+ const targets = endpoint ? [endpoint] : MONITORED_ENDPOINTS;
5516
+ const rows = targets.map((ep) => {
5517
+ const limit = getLimitForEndpoint(ep);
5518
+ const batch_cap_per_min = Math.floor(limit * 0.2);
5519
+ const { calls_in_window, batch_calls_in_window } = getRateLimitWindowCounts(token2, ep);
5520
+ const utilisation_pct = limit > 0 ? Math.round(calls_in_window / limit * 100) : 0;
5521
+ const alert = utilisation_pct >= 80;
5522
+ return {
5523
+ endpoint: ep,
5524
+ limit_per_min: limit,
5525
+ batch_cap_per_min,
5526
+ calls_in_window,
5527
+ batch_calls_in_window,
5528
+ utilisation_pct,
5529
+ alert
5530
+ };
5531
+ });
5532
+ const resellerId = String(ctx.props.reseller_id);
5533
+ return {
5534
+ content: [
5535
+ {
5536
+ type: "text",
5537
+ text: JSON.stringify(
5538
+ {
5539
+ reseller_id: resellerId,
5540
+ window_seconds: 60,
5541
+ batch_fraction: 0.2,
5542
+ endpoints: rows,
5543
+ alert_threshold_pct: 80,
5544
+ metric_name: "carrier_ocs_calls_per_min",
5545
+ metric_location: "CF Workers Tail / BetterStack \u2014 structured JSON log lines with metric='carrier_ocs_calls_per_min'"
5546
+ },
5547
+ null,
5548
+ 2
5549
+ )
5550
+ }
5551
+ ]
5552
+ };
5553
+ }
5554
+ )
5555
+ );
5556
+ }
5557
+
5558
+ // src/tools-depletion-events.ts
5559
+ import { z as z7 } from "zod";
5560
+ function ok(payload) {
5561
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
5562
+ }
5563
+ function err(message) {
5564
+ return { isError: true, content: [{ type: "text", text: message }] };
5565
+ }
5566
+ function registerDepletionEventsTool(server2, ctx) {
5567
+ server2.registerTool(
5568
+ "subscriber_depletion_events",
5569
+ {
5570
+ title: "Subscriber Bundle Depletion Events",
5571
+ description: "Look up bundle depletion events for a subscriber (or list recent fleet-wide depletions). Returns the most recent bundle.depleted event stored when the prepaid usage webhook received a last_slice notification. Events are retained for 7 days. Use subscriberId to check a specific subscriber, or omit to list the 20 most recent fleet-wide depletions. Requires 'read' scope.",
5572
+ inputSchema: {
5573
+ subscriberId: z7.string().optional().describe(
5574
+ "OCS subscriber ID to look up. Omit to list the 20 most recent fleet-wide depletions."
5575
+ ),
5576
+ since: z7.string().optional().describe(
5577
+ "ISO-8601 timestamp \u2014 only return events at or after this time (for single-subscriber lookup)."
5578
+ )
5579
+ },
5580
+ annotations: { readOnlyHint: true }
5581
+ },
5582
+ async (args) => {
5583
+ if (!ctx.props.scope.includes("read")) {
5584
+ return err(
5585
+ `Scope denied: subscriber_depletion_events requires 'read' scope. Your token has: [${ctx.props.scope.join(", ")}].`
5586
+ );
5587
+ }
5588
+ const kv = ctx.env.RATE_LIMIT_KV;
5589
+ if (!kv) {
5590
+ return err(
5591
+ "RATE_LIMIT_KV binding is not available in this environment. Add the binding to wrangler.jsonc and redeploy."
5592
+ );
5593
+ }
5594
+ const { subscriberId, since } = args;
5595
+ if (subscriberId) {
5596
+ const raw = await kv.get(`bundle-depleted:${subscriberId}`, "text");
5597
+ if (!raw) {
5598
+ return ok({
5599
+ subscriber_id: subscriberId,
5600
+ depleted: false,
5601
+ event: null,
5602
+ note: "No bundle depletion event found in the last 7 days."
5603
+ });
5604
+ }
5605
+ let event;
5606
+ try {
5607
+ event = JSON.parse(raw);
5608
+ } catch {
5609
+ return ok({
5610
+ subscriber_id: subscriberId,
5611
+ depleted: false,
5612
+ event: null,
5613
+ note: "Stored depletion entry was invalid JSON and was ignored."
5614
+ });
5615
+ }
5616
+ if (since && event.depletedAt < since) {
5617
+ return ok({
5618
+ subscriber_id: subscriberId,
5619
+ depleted: false,
5620
+ event: null,
5621
+ note: `No bundle depletion after ${since}.`
5622
+ });
5623
+ }
5624
+ return ok({
5625
+ subscriber_id: subscriberId,
5626
+ depleted: true,
5627
+ event: {
5628
+ subscriber_id: event.subscriberId,
5629
+ iccid: event.iccid,
5630
+ msisdn: event.msisdn,
5631
+ plan_id: event.planId,
5632
+ depleted_at: event.depletedAt
5633
+ }
5634
+ });
5635
+ }
5636
+ const listResult = await kv.list({ prefix: "bundle-depleted:", limit: 20 });
5637
+ if (listResult.keys.length === 0) {
5638
+ return ok({
5639
+ depletions: [],
5640
+ total: 0,
5641
+ note: "No bundle depletion events in the last 7 days."
5642
+ });
5643
+ }
5644
+ const events = [];
5645
+ await Promise.all(
5646
+ listResult.keys.map(async (k) => {
5647
+ const raw = await kv.get(k.name, "text");
5648
+ if (!raw) return;
5649
+ try {
5650
+ const ev = JSON.parse(raw);
5651
+ if (since && ev.depletedAt < since) return;
5652
+ events.push({
5653
+ subscriber_id: ev.subscriberId,
5654
+ iccid: ev.iccid,
5655
+ msisdn: ev.msisdn,
5656
+ plan_id: ev.planId,
5657
+ depleted_at: ev.depletedAt
5658
+ });
5659
+ } catch {
5660
+ }
5661
+ })
5662
+ );
5663
+ events.sort((a, b) => b.depleted_at.localeCompare(a.depleted_at));
5664
+ return ok({
5665
+ depletions: events,
5666
+ total: events.length,
5667
+ list_truncated: !listResult.list_complete
5668
+ });
5669
+ }
5670
+ );
5671
+ }
5672
+
5673
+ // src/tools-country-history.ts
5674
+ import { z as z8 } from "zod";
5675
+ function registerCountryHistoryTool(server2, ctx) {
5676
+ server2.registerTool(
5677
+ "subscriber_country_history",
5678
+ {
5679
+ title: "Subscriber Country History",
5680
+ description: "Return cross-border movement history for a subscriber from Relay LU events. Reads country.entered events from the OCS ring-buffer (24h window, up to `limit` entries per request) written by the Bridge4IP Relay LU webhook. Each entry includes the new country, previous country, MCC/MNC, and timestamp. Use this to answer: 'where has subscriber X traveled', 'show country history for ICCID', 'border crossings for this SIM'. Params: `subscriberId` (ICCID, 19\u201320 digits), `limit` (1\u201350, default 20, newest first). Returns: ordered list of country.entered events with { new_country, previous_country, mcc, mnc, timestamp, source }. Empty list when no cross-border events recorded. Do NOT use this for raw location events \u2014 use `list_recent_ocs_events` with event_types=['location.changed'] for finer-grained location data.",
5681
+ inputSchema: {
5682
+ subscriberId: z8.string().regex(/^\d{19,20}$/, "subscriberId must be a 19\u201320 digit ICCID").describe("Subscriber ICCID (19\u201320 digits, ITU-T E.118)"),
5683
+ limit: z8.number().int().min(1).max(50).default(20).optional().describe("Maximum number of events to return, newest first (default 20, max 50)")
5684
+ },
5685
+ annotations: { readOnlyHint: true, idempotentHint: true }
5686
+ },
5687
+ wrapHandler(
5688
+ "subscriber_country_history",
5689
+ "[country-history:kv]",
5690
+ "read",
5691
+ ctx,
5692
+ async ({ subscriberId, limit }) => {
5693
+ const effectiveLimit = limit ?? 20;
5694
+ const result2 = await listRecentOcsEvents2(
5695
+ subscriberId,
5696
+ effectiveLimit,
5697
+ ["country.entered"],
5698
+ void 0,
5699
+ ctx.env.OCS_EVENT_ROUTING
5700
+ );
5701
+ const countryEvents = result2.events;
5702
+ return {
5703
+ content: [
5704
+ {
5705
+ type: "text",
5706
+ text: JSON.stringify(
5707
+ {
5708
+ subscriber_id: subscriberId,
5709
+ country_events: countryEvents.map((e) => ({
5710
+ event_id: e.event_id,
5711
+ new_country: e.data["new_country"] ?? null,
5712
+ previous_country: e.data["previous_country"] ?? null,
5713
+ mcc: e.data["mcc"] ?? null,
5714
+ mnc: e.data["mnc"] ?? null,
5715
+ timestamp: e.timestamp,
5716
+ source: e.data["source"] ?? "relay-lu"
5717
+ })),
5718
+ total_country_events: countryEvents.length,
5719
+ total_in_buffer: result2.total_in_buffer,
5720
+ note: countryEvents.length === 0 ? "No cross-border events recorded in the 24h ring-buffer for this subscriber." : void 0
5721
+ },
5722
+ null,
5723
+ 2
5724
+ )
5725
+ }
5726
+ ]
5727
+ };
5728
+ }
5729
+ )
5730
+ );
5731
+ }
5732
+
5733
+ // src/apps/fleet-health-app.ts
5734
+ import { z as z9 } from "zod";
5217
5735
  import {
5218
5736
  registerAppTool,
5219
5737
  registerAppResource,
@@ -5222,10 +5740,10 @@ import {
5222
5740
  async function safeCallWithToken(client, _token, method, params = {}) {
5223
5741
  try {
5224
5742
  return { data: await client.call(method, params), error: null };
5225
- } catch (err2) {
5743
+ } catch (err4) {
5226
5744
  return {
5227
5745
  data: null,
5228
- error: err2 instanceof Error ? err2.message : String(err2)
5746
+ error: err4 instanceof Error ? err4.message : String(err4)
5229
5747
  };
5230
5748
  }
5231
5749
  }
@@ -5272,7 +5790,7 @@ function registerFleetHealthApp(server2, ctx) {
5272
5790
  title: "Fleet Health Dashboard",
5273
5791
  description: "Renders an interactive Fleet Health Dashboard with eSIM status charts, top-10 account breakdown, and low-balance alerts. Returns structuredContent for the chart panel.",
5274
5792
  inputSchema: {
5275
- accountId: z6.number().optional().describe("Filter to a specific account (omit for all)")
5793
+ accountId: z9.number().optional().describe("Filter to a specific account (omit for all)")
5276
5794
  },
5277
5795
  annotations: { readOnlyHint: true },
5278
5796
  _meta: {
@@ -5284,7 +5802,7 @@ function registerFleetHealthApp(server2, ctx) {
5284
5802
  },
5285
5803
  async ({ accountId }) => {
5286
5804
  const token2 = await ctx.getUserToken(ctx.props.sub);
5287
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
5805
+ const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
5288
5806
  const [statusResult, accountsResult] = await Promise.all([
5289
5807
  safeCallWithToken(
5290
5808
  client,
@@ -5370,7 +5888,7 @@ function registerFleetHealthApp(server2, ctx) {
5370
5888
  }
5371
5889
 
5372
5890
  // src/apps/provisioning-wizard.ts
5373
- import { z as z7 } from "zod";
5891
+ import { z as z10 } from "zod";
5374
5892
  import {
5375
5893
  registerAppTool as registerAppTool2,
5376
5894
  registerAppResource as registerAppResource2,
@@ -5423,10 +5941,10 @@ async function deleteWizardSession(env, sub, wizardId) {
5423
5941
  async function safeCallWithToken2(client, _token, method, params = {}) {
5424
5942
  try {
5425
5943
  return { data: await client.call(method, params), error: null };
5426
- } catch (err2) {
5944
+ } catch (err4) {
5427
5945
  return {
5428
5946
  data: null,
5429
- error: err2 instanceof Error ? err2.message : String(err2)
5947
+ error: err4 instanceof Error ? err4.message : String(err4)
5430
5948
  };
5431
5949
  }
5432
5950
  }
@@ -5475,10 +5993,10 @@ function registerProvisioningWizard(server2, ctx) {
5475
5993
  title: "eSIM Provisioning Wizard",
5476
5994
  description: "Interactive 3-step wizard to provision an eSIM: select subscriber, choose package template, preview (dry_run) and confirm execution.",
5477
5995
  inputSchema: {
5478
- step: z7.enum(["init", "select-package", "preview", "confirm"]).describe("Current wizard step"),
5479
- wizardId: z7.string().optional().describe("Wizard session ID (absent on init)"),
5480
- subscriber_iccid: z7.string().optional().describe("Subscriber ICCID (required for select-package)"),
5481
- package_template_id: z7.number().optional().describe("Package template ID (required for preview)")
5996
+ step: z10.enum(["init", "select-package", "preview", "confirm"]).describe("Current wizard step"),
5997
+ wizardId: z10.string().optional().describe("Wizard session ID (absent on init)"),
5998
+ subscriber_iccid: z10.string().optional().describe("Subscriber ICCID (required for select-package)"),
5999
+ package_template_id: z10.number().optional().describe("Package template ID (required for preview)")
5482
6000
  },
5483
6001
  _meta: {
5484
6002
  ui: {
@@ -5489,7 +6007,7 @@ function registerProvisioningWizard(server2, ctx) {
5489
6007
  },
5490
6008
  async ({ step, wizardId, subscriber_iccid, package_template_id }) => {
5491
6009
  const token2 = await ctx.getUserToken(ctx.props.sub);
5492
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
6010
+ const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
5493
6011
  if (step === "init") {
5494
6012
  const newWizardId = generateWizardId();
5495
6013
  const subscribersResult = await safeCallWithToken2(
@@ -5777,7 +6295,7 @@ function registerProvisioningWizard(server2, ctx) {
5777
6295
  }
5778
6296
 
5779
6297
  // src/apps/balance-topup.ts
5780
- import { z as z8 } from "zod";
6298
+ import { z as z11 } from "zod";
5781
6299
  import {
5782
6300
  registerAppTool as registerAppTool3,
5783
6301
  registerAppResource as registerAppResource3,
@@ -5786,10 +6304,10 @@ import {
5786
6304
  async function safeCallWithToken3(client, _token, method, params = {}) {
5787
6305
  try {
5788
6306
  return { data: await client.call(method, params), error: null };
5789
- } catch (err2) {
6307
+ } catch (err4) {
5790
6308
  return {
5791
6309
  data: null,
5792
- error: err2 instanceof Error ? err2.message : String(err2)
6310
+ error: err4 instanceof Error ? err4.message : String(err4)
5793
6311
  };
5794
6312
  }
5795
6313
  }
@@ -5830,9 +6348,9 @@ function registerBalanceTopupApp(server2, ctx) {
5830
6348
  title: "Balance Top-up Form",
5831
6349
  description: "Enterprise admin tool: preview or commit an account balance adjustment. Set preview=true to fetch projection (no OCS write); preview=false (default) to execute. Requires admin scope + recent MFA.",
5832
6350
  inputSchema: {
5833
- iccid: z8.string().describe("The subscriber ICCID for the account lookup"),
5834
- delta: z8.number().describe("Amount to add (positive) or deduct (negative) from the account balance"),
5835
- preview: z8.boolean().default(true).describe("If true, return preview without writing. If false, execute the balance change.")
6351
+ iccid: z11.string().describe("The subscriber ICCID for the account lookup"),
6352
+ delta: z11.number().describe("Amount to add (positive) or deduct (negative) from the account balance"),
6353
+ preview: z11.boolean().default(true).describe("If true, return preview without writing. If false, execute the balance change.")
5836
6354
  },
5837
6355
  annotations: { destructiveHint: true },
5838
6356
  _meta: {
@@ -5849,7 +6367,7 @@ function registerBalanceTopupApp(server2, ctx) {
5849
6367
  ctx,
5850
6368
  async ({ iccid, delta, preview }) => {
5851
6369
  const token2 = await ctx.getUserToken(ctx.props.sub);
5852
- const client = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
6370
+ const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
5853
6371
  if (preview === true) {
5854
6372
  const result2 = await safeCallWithToken3(
5855
6373
  client,
@@ -5913,7 +6431,7 @@ function registerAllApps(server2, ctx) {
5913
6431
  }
5914
6432
 
5915
6433
  // src/prompts.ts
5916
- import { z as z9 } from "zod";
6434
+ import { z as z12 } from "zod";
5917
6435
  function registerAllPrompts(server2) {
5918
6436
  server2.registerPrompt(
5919
6437
  "fleet_health_report",
@@ -5949,7 +6467,7 @@ Format as a structured report with sections, tables, and actionable recommendati
5949
6467
  {
5950
6468
  title: "Subscriber Deep Dive",
5951
6469
  description: "Comprehensive analysis of a single subscriber: profile, packages, usage patterns, location history, recommendations.",
5952
- argsSchema: { iccid: z9.string().describe("The subscriber ICCID to analyse") }
6470
+ argsSchema: { iccid: z12.string().describe("The subscriber ICCID to analyse") }
5953
6471
  },
5954
6472
  async ({ iccid }) => ({
5955
6473
  messages: [
@@ -6048,7 +6566,7 @@ Analyze and present:
6048
6566
  {
6049
6567
  title: "Bulk Operations Planner",
6050
6568
  description: "Plan bulk operations safely: mass package assignments, account migrations, balance adjustments, or status changes.",
6051
- argsSchema: { operation: z9.string().describe("Describe the bulk operation you want to perform") }
6569
+ argsSchema: { operation: z12.string().describe("Describe the bulk operation you want to perform") }
6052
6570
  },
6053
6571
  async ({ operation }) => ({
6054
6572
  messages: [
@@ -6081,7 +6599,7 @@ NEVER execute destructive operations without confirmation.`
6081
6599
  }
6082
6600
 
6083
6601
  // src/tools-pricing.ts
6084
- import { z as z10 } from "zod";
6602
+ import { z as z13 } from "zod";
6085
6603
 
6086
6604
  // src/credits.ts
6087
6605
  var TIER_CREDIT_ALLOTMENTS = {
@@ -7448,8 +7966,8 @@ function buildPricingHandler(tool, ctx) {
7448
7966
  }
7449
7967
  ]
7450
7968
  };
7451
- } catch (err2) {
7452
- const message = err2 instanceof Error ? err2.message : String(err2);
7969
+ } catch (err4) {
7970
+ const message = err4 instanceof Error ? err4.message : String(err4);
7453
7971
  ctx.audit({
7454
7972
  tool_name: tool.name,
7455
7973
  ocs_method: "billing",
@@ -7473,19 +7991,19 @@ function buildZodSchema(schema) {
7473
7991
  for (const [key, prop] of Object.entries(properties)) {
7474
7992
  let field;
7475
7993
  if (prop.enum) {
7476
- field = z10.enum(prop.enum);
7994
+ field = z13.enum(prop.enum);
7477
7995
  } else if (prop.type === "number") {
7478
- field = z10.number();
7996
+ field = z13.number();
7479
7997
  } else if (prop.type === "boolean") {
7480
- field = z10.boolean();
7998
+ field = z13.boolean();
7481
7999
  } else if (prop.type === "array") {
7482
8000
  if (prop.items?.type === "number") {
7483
- field = z10.array(z10.number());
8001
+ field = z13.array(z13.number());
7484
8002
  } else {
7485
- field = z10.array(z10.string());
8003
+ field = z13.array(z13.string());
7486
8004
  }
7487
8005
  } else {
7488
- field = z10.string();
8006
+ field = z13.string();
7489
8007
  }
7490
8008
  const required = schema.required ?? [];
7491
8009
  if (!required.includes(key)) {
@@ -7500,7 +8018,7 @@ function buildZodSchema(schema) {
7500
8018
  }
7501
8019
 
7502
8020
  // src/tools-ui-agent-schedule.ts
7503
- import { z as z11 } from "zod";
8021
+ import { z as z14 } from "zod";
7504
8022
 
7505
8023
  // src/manus-common.ts
7506
8024
  var MANUS_API_BASE = "https://api.manus.ai/v2";
@@ -7769,8 +8287,8 @@ function writeUsageThresholdAudit(env, severity, remainingCredits, month, taskCo
7769
8287
  doubles: [remainingCredits],
7770
8288
  indexes: ["manus_usage"]
7771
8289
  });
7772
- } catch (err2) {
7773
- console.error(`[manus-usage] threshold audit write failed: ${err2.message}`);
8290
+ } catch (err4) {
8291
+ console.error(`[manus-usage] threshold audit write failed: ${err4.message}`);
7774
8292
  }
7775
8293
  }
7776
8294
 
@@ -7807,12 +8325,12 @@ function registerScheduleAndUsageTools(server2, ctx) {
7807
8325
  title: "Create Manus Schedule (UI Agent)",
7808
8326
  description: "Creates a recurring Manus agent run on a cron schedule. Use this to automate periodic OCS audits, fleet health checks, or any recurring browser-automation task. Minimum interval: 5 minutes (*/1, */2, */3, */4 and '* * * * *' are rejected). Requires admin scope. Returns schedule_id.",
7809
8327
  inputSchema: {
7810
- name: z11.string().describe("Human-readable name for this schedule"),
7811
- cron: z11.string().describe(
8328
+ name: z14.string().describe("Human-readable name for this schedule"),
8329
+ cron: z14.string().describe(
7812
8330
  "Standard 5-field cron expression (minute hour day month weekday). Minimum interval: 5 minutes. Example: '0 */6 * * *' = every 6 hours."
7813
8331
  ),
7814
- prompt_template: z11.string().describe("Agent prompt/task template the Manus agent will execute on each run"),
7815
- profile: z11.string().optional().describe("Manus agent profile to use. Defaults to 'manus-1.6-lite'.")
8332
+ prompt_template: z14.string().describe("Agent prompt/task template the Manus agent will execute on each run"),
8333
+ profile: z14.string().optional().describe("Manus agent profile to use. Defaults to 'manus-1.6-lite'.")
7816
8334
  }
7817
8335
  },
7818
8336
  async (args) => {
@@ -7880,7 +8398,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
7880
8398
  }
7881
8399
  ]
7882
8400
  };
7883
- } catch (err2) {
8401
+ } catch (err4) {
7884
8402
  ctx.audit({
7885
8403
  tool_name: "ui_agent_schedule_create",
7886
8404
  ocs_method: "[manus:schedule.create]",
@@ -7889,7 +8407,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
7889
8407
  duration_ms: Date.now() - start,
7890
8408
  event_type: "ui_agent_dispatch"
7891
8409
  });
7892
- const msg = err2 instanceof ManusScheduleError ? `Manus API error (HTTP ${err2.statusCode}): ${err2.message}` : err2 instanceof Error ? err2.message : String(err2);
8410
+ const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
7893
8411
  return { isError: true, content: [{ type: "text", text: msg }] };
7894
8412
  }
7895
8413
  }
@@ -7947,7 +8465,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
7947
8465
  }
7948
8466
  ]
7949
8467
  };
7950
- } catch (err2) {
8468
+ } catch (err4) {
7951
8469
  ctx.audit({
7952
8470
  tool_name: "ui_agent_schedule_list",
7953
8471
  ocs_method: "[manus:schedule.list]",
@@ -7955,7 +8473,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
7955
8473
  dry_run: false,
7956
8474
  duration_ms: Date.now() - start
7957
8475
  });
7958
- const msg = err2 instanceof ManusScheduleError ? `Manus API error (HTTP ${err2.statusCode}): ${err2.message}` : err2 instanceof Error ? err2.message : String(err2);
8476
+ const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
7959
8477
  return { isError: true, content: [{ type: "text", text: msg }] };
7960
8478
  }
7961
8479
  }
@@ -7966,7 +8484,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
7966
8484
  title: "Delete Manus Schedule",
7967
8485
  description: "Permanently deletes a Manus recurring schedule. This cannot be undone. Use ui_agent_schedule_pause to temporarily suspend instead. Requires admin scope.",
7968
8486
  inputSchema: {
7969
- schedule_id: z11.string().describe("ID of the schedule to delete")
8487
+ schedule_id: z14.string().describe("ID of the schedule to delete")
7970
8488
  }
7971
8489
  },
7972
8490
  async (args) => {
@@ -8016,7 +8534,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8016
8534
  }
8017
8535
  ]
8018
8536
  };
8019
- } catch (err2) {
8537
+ } catch (err4) {
8020
8538
  ctx.audit({
8021
8539
  tool_name: "ui_agent_schedule_delete",
8022
8540
  ocs_method: "[manus:schedule.delete]",
@@ -8025,7 +8543,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8025
8543
  duration_ms: Date.now() - start,
8026
8544
  event_type: "ui_agent_dispatch"
8027
8545
  });
8028
- const msg = err2 instanceof ManusScheduleError ? `Manus API error (HTTP ${err2.statusCode}): ${err2.message}` : err2 instanceof Error ? err2.message : String(err2);
8546
+ const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
8029
8547
  return { isError: true, content: [{ type: "text", text: msg }] };
8030
8548
  }
8031
8549
  }
@@ -8036,7 +8554,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8036
8554
  title: "Pause Manus Schedule",
8037
8555
  description: "Pauses an active Manus recurring schedule. The schedule is preserved and can be resumed later with ui_agent_schedule_resume. Requires admin scope.",
8038
8556
  inputSchema: {
8039
- schedule_id: z11.string().describe("ID of the schedule to pause")
8557
+ schedule_id: z14.string().describe("ID of the schedule to pause")
8040
8558
  }
8041
8559
  },
8042
8560
  async (args) => {
@@ -8086,7 +8604,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8086
8604
  }
8087
8605
  ]
8088
8606
  };
8089
- } catch (err2) {
8607
+ } catch (err4) {
8090
8608
  ctx.audit({
8091
8609
  tool_name: "ui_agent_schedule_pause",
8092
8610
  ocs_method: "[manus:schedule.pause]",
@@ -8095,7 +8613,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8095
8613
  duration_ms: Date.now() - start,
8096
8614
  event_type: "ui_agent_dispatch"
8097
8615
  });
8098
- const msg = err2 instanceof ManusScheduleError ? `Manus API error (HTTP ${err2.statusCode}): ${err2.message}` : err2 instanceof Error ? err2.message : String(err2);
8616
+ const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
8099
8617
  return { isError: true, content: [{ type: "text", text: msg }] };
8100
8618
  }
8101
8619
  }
@@ -8106,7 +8624,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8106
8624
  title: "Resume Manus Schedule",
8107
8625
  description: "Resumes a paused Manus recurring schedule. Requires admin scope.",
8108
8626
  inputSchema: {
8109
- schedule_id: z11.string().describe("ID of the schedule to resume")
8627
+ schedule_id: z14.string().describe("ID of the schedule to resume")
8110
8628
  }
8111
8629
  },
8112
8630
  async (args) => {
@@ -8156,7 +8674,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8156
8674
  }
8157
8675
  ]
8158
8676
  };
8159
- } catch (err2) {
8677
+ } catch (err4) {
8160
8678
  ctx.audit({
8161
8679
  tool_name: "ui_agent_schedule_resume",
8162
8680
  ocs_method: "[manus:schedule.resume]",
@@ -8165,7 +8683,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8165
8683
  duration_ms: Date.now() - start,
8166
8684
  event_type: "ui_agent_dispatch"
8167
8685
  });
8168
- const msg = err2 instanceof ManusScheduleError ? `Manus API error (HTTP ${err2.statusCode}): ${err2.message}` : err2 instanceof Error ? err2.message : String(err2);
8686
+ const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
8169
8687
  return { isError: true, content: [{ type: "text", text: msg }] };
8170
8688
  }
8171
8689
  }
@@ -8210,7 +8728,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8210
8728
  }
8211
8729
  ]
8212
8730
  };
8213
- } catch (err2) {
8731
+ } catch (err4) {
8214
8732
  ctx.audit({
8215
8733
  tool_name: "ui_agent_usage",
8216
8734
  ocs_method: "[manus:usage.get]",
@@ -8218,7 +8736,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8218
8736
  dry_run: false,
8219
8737
  duration_ms: Date.now() - start
8220
8738
  });
8221
- const msg = err2 instanceof Error ? err2.message : String(err2);
8739
+ const msg = err4 instanceof Error ? err4.message : String(err4);
8222
8740
  return { isError: true, content: [{ type: "text", text: msg }] };
8223
8741
  }
8224
8742
  }
@@ -8226,7 +8744,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
8226
8744
  }
8227
8745
 
8228
8746
  // src/tools-ui-agent.ts
8229
- import { z as z12 } from "zod";
8747
+ import { z as z15 } from "zod";
8230
8748
 
8231
8749
  // src/clerk.ts
8232
8750
  import { createClerkClient } from "@clerk/backend";
@@ -8461,7 +8979,7 @@ Report success or failure with a clear summary.`;
8461
8979
  }
8462
8980
  ]
8463
8981
  };
8464
- } catch (err2) {
8982
+ } catch (err4) {
8465
8983
  ctx.audit({
8466
8984
  tool_name: toolName,
8467
8985
  ocs_method: `[ui-agent:${gapId}]`,
@@ -8475,7 +8993,7 @@ Report success or failure with a clear summary.`;
8475
8993
  content: [
8476
8994
  {
8477
8995
  type: "text",
8478
- text: `Error dispatching UI agent: ${err2 instanceof Error ? err2.message : String(err2)}`
8996
+ text: `Error dispatching UI agent: ${err4 instanceof Error ? err4.message : String(err4)}`
8479
8997
  }
8480
8998
  ]
8481
8999
  };
@@ -8489,9 +9007,9 @@ function registerAllUiAgentTools(server2, ctx) {
8489
9007
  title: "Create Steering List (UI Agent)",
8490
9008
  description: "Creates a new network steering list (OPLMN preference configuration) via the OCS web dashboard. This operation is not available via the OCS REST API. A Manus browser agent will be dispatched to perform the operation. Params: `name` (steering list name), `description` (optional). Returns: dispatch confirmation with Manus task ID for tracking.",
8491
9009
  inputSchema: {
8492
- name: z12.string().describe("Name for the new steering list"),
8493
- description: z12.string().optional().describe("Optional description for the steering list"),
8494
- dry_run: z12.boolean().optional().describe("Preview the agent prompt without dispatching")
9010
+ name: z15.string().describe("Name for the new steering list"),
9011
+ description: z15.string().optional().describe("Optional description for the steering list"),
9012
+ dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
8495
9013
  }
8496
9014
  },
8497
9015
  wrapUiAgentHandler(
@@ -8514,11 +9032,11 @@ After creation, note the new steering list ID from the dashboard.
8514
9032
  title: "Build Steering List (UI Agent)",
8515
9033
  description: "Adds or removes operators (MCC-MNC) from an existing steering list via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `steering_list_id`, `add_operators` (array of MCC-MNC to add), `remove_operators` (array to remove), `operator_type` ('priority' or 'excluded').",
8516
9034
  inputSchema: {
8517
- steering_list_id: z12.number().describe("ID of the steering list to modify"),
8518
- add_operators: z12.array(z12.string()).optional().describe("MCC-MNC codes to add (e.g. ['20801', '26201'])"),
8519
- remove_operators: z12.array(z12.string()).optional().describe("MCC-MNC codes to remove"),
8520
- operator_type: z12.enum(["priority", "excluded"]).default("priority").describe("Whether operators are priority or excluded"),
8521
- dry_run: z12.boolean().optional().describe("Preview the agent prompt without dispatching")
9035
+ steering_list_id: z15.number().describe("ID of the steering list to modify"),
9036
+ add_operators: z15.array(z15.string()).optional().describe("MCC-MNC codes to add (e.g. ['20801', '26201'])"),
9037
+ remove_operators: z15.array(z15.string()).optional().describe("MCC-MNC codes to remove"),
9038
+ operator_type: z15.enum(["priority", "excluded"]).default("priority").describe("Whether operators are priority or excluded"),
9039
+ dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
8522
9040
  }
8523
9041
  },
8524
9042
  wrapUiAgentHandler(
@@ -8541,9 +9059,9 @@ Operator type: ${args.operator_type ?? "priority"}
8541
9059
  title: "Set Account Steering List (UI Agent)",
8542
9060
  description: "Assigns or removes a steering list at the account level via the OCS web dashboard. The subscriber-level counterpart `modify_subscriber_steering_list` is available via API; this account-level operation is UI-only. Params: `account_id`, `steering_list_id` (0 to remove).",
8543
9061
  inputSchema: {
8544
- account_id: z12.number().describe("Account ID to assign the steering list to"),
8545
- steering_list_id: z12.number().describe("Steering list ID to assign (0 to remove/unset)"),
8546
- dry_run: z12.boolean().optional().describe("Preview the agent prompt without dispatching")
9062
+ account_id: z15.number().describe("Account ID to assign the steering list to"),
9063
+ steering_list_id: z15.number().describe("Steering list ID to assign (0 to remove/unset)"),
9064
+ dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
8547
9065
  }
8548
9066
  },
8549
9067
  wrapUiAgentHandler(
@@ -8565,10 +9083,10 @@ Open account ID ${args.account_id}.
8565
9083
  title: "Create Account (UI Agent)",
8566
9084
  description: "Creates a new sub-account under the reseller via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `name` (account name), `description` (optional), `initial_balance` (optional, default 0).",
8567
9085
  inputSchema: {
8568
- name: z12.string().describe("Name for the new account"),
8569
- description: z12.string().optional().describe("Optional description"),
8570
- initial_balance: z12.number().optional().describe("Initial balance in account currency (default 0)"),
8571
- dry_run: z12.boolean().optional().describe("Preview the agent prompt without dispatching")
9086
+ name: z15.string().describe("Name for the new account"),
9087
+ description: z15.string().optional().describe("Optional description"),
9088
+ initial_balance: z15.number().optional().describe("Initial balance in account currency (default 0)"),
9089
+ dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
8572
9090
  }
8573
9091
  },
8574
9092
  wrapUiAgentHandler(
@@ -8592,10 +9110,10 @@ After creation, note the new account ID from the dashboard.
8592
9110
  title: "Create Destination List (UI Agent)",
8593
9111
  description: "Creates a new destination list (named set of phone number prefixes for MOC call permissions) via the OCS web dashboard. Params: `name`, `prefixes` (array of prefix strings), `description`.",
8594
9112
  inputSchema: {
8595
- name: z12.string().describe("Name for the new destination list"),
8596
- prefixes: z12.array(z12.string()).optional().describe("Phone number prefixes to include (e.g. ['+31', '+49'])"),
8597
- description: z12.string().optional().describe("Optional description"),
8598
- dry_run: z12.boolean().optional().describe("Preview the agent prompt without dispatching")
9113
+ name: z15.string().describe("Name for the new destination list"),
9114
+ prefixes: z15.array(z15.string()).optional().describe("Phone number prefixes to include (e.g. ['+31', '+49'])"),
9115
+ description: z15.string().optional().describe("Optional description"),
9116
+ dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
8599
9117
  }
8600
9118
  },
8601
9119
  wrapUiAgentHandler(
@@ -8618,11 +9136,11 @@ Create a new destination list with the following details:
8618
9136
  title: "Edit Destination List (UI Agent)",
8619
9137
  description: "Edits an existing destination list via the OCS web dashboard. Params: `destination_list_id`, `add_prefixes`, `remove_prefixes`, `new_name`.",
8620
9138
  inputSchema: {
8621
- destination_list_id: z12.number().describe("ID of the destination list to edit"),
8622
- add_prefixes: z12.array(z12.string()).optional().describe("Prefixes to add"),
8623
- remove_prefixes: z12.array(z12.string()).optional().describe("Prefixes to remove"),
8624
- new_name: z12.string().optional().describe("Rename the destination list"),
8625
- dry_run: z12.boolean().optional().describe("Preview the agent prompt without dispatching")
9139
+ destination_list_id: z15.number().describe("ID of the destination list to edit"),
9140
+ add_prefixes: z15.array(z15.string()).optional().describe("Prefixes to add"),
9141
+ remove_prefixes: z15.array(z15.string()).optional().describe("Prefixes to remove"),
9142
+ new_name: z15.string().optional().describe("Rename the destination list"),
9143
+ dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
8626
9144
  }
8627
9145
  },
8628
9146
  wrapUiAgentHandler(
@@ -8645,8 +9163,8 @@ Open destination list ID ${args.destination_list_id} for editing.
8645
9163
  title: "Delete Destination List (UI Agent)",
8646
9164
  description: "Deletes a destination list via the OCS web dashboard. Params: `destination_list_id`. WARNING: This is destructive and cannot be undone.",
8647
9165
  inputSchema: {
8648
- destination_list_id: z12.number().describe("ID of the destination list to delete"),
8649
- dry_run: z12.boolean().optional().describe("Preview the agent prompt without dispatching")
9166
+ destination_list_id: z15.number().describe("ID of the destination list to delete"),
9167
+ dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
8650
9168
  }
8651
9169
  },
8652
9170
  wrapUiAgentHandler(
@@ -8667,8 +9185,8 @@ Verify the list no longer appears in the dashboard.
8667
9185
  title: "Delete Package Template (UI Agent)",
8668
9186
  description: "Deletes a package template from the product catalog via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `template_id`. WARNING: This is destructive.",
8669
9187
  inputSchema: {
8670
- template_id: z12.number().describe("ID of the package template to delete"),
8671
- dry_run: z12.boolean().optional().describe("Preview the agent prompt without dispatching")
9188
+ template_id: z15.number().describe("ID of the package template to delete"),
9189
+ dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
8672
9190
  }
8673
9191
  },
8674
9192
  wrapUiAgentHandler(
@@ -8689,11 +9207,11 @@ Verify the template no longer appears in the template list.
8689
9207
  title: "Edit Location Zone (UI Agent)",
8690
9208
  description: "Edits an existing location zone via the OCS web dashboard. `create_location_zone` is available via API; edit is UI-only. Params: `zone_id`, `new_name`, `add_countries`, `remove_countries`.",
8691
9209
  inputSchema: {
8692
- zone_id: z12.number().describe("ID of the location zone to edit"),
8693
- new_name: z12.string().optional().describe("Rename the location zone"),
8694
- add_countries: z12.array(z12.string()).optional().describe("ISO country codes to add (e.g. ['NL', 'DE'])"),
8695
- remove_countries: z12.array(z12.string()).optional().describe("ISO country codes to remove"),
8696
- dry_run: z12.boolean().optional().describe("Preview the agent prompt without dispatching")
9210
+ zone_id: z15.number().describe("ID of the location zone to edit"),
9211
+ new_name: z15.string().optional().describe("Rename the location zone"),
9212
+ add_countries: z15.array(z15.string()).optional().describe("ISO country codes to add (e.g. ['NL', 'DE'])"),
9213
+ remove_countries: z15.array(z15.string()).optional().describe("ISO country codes to remove"),
9214
+ dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
8697
9215
  }
8698
9216
  },
8699
9217
  wrapUiAgentHandler(
@@ -8716,8 +9234,8 @@ Open location zone ID ${args.zone_id} for editing.
8716
9234
  title: "Delete Location Zone (UI Agent)",
8717
9235
  description: "Deletes a location zone via the OCS web dashboard. `create_location_zone` is available via API; delete is UI-only. Params: `zone_id`. WARNING: Zones in use by active templates may not be deletable.",
8718
9236
  inputSchema: {
8719
- zone_id: z12.number().describe("ID of the location zone to delete"),
8720
- dry_run: z12.boolean().optional().describe("Preview the agent prompt without dispatching")
9237
+ zone_id: z15.number().describe("ID of the location zone to delete"),
9238
+ dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
8721
9239
  }
8722
9240
  },
8723
9241
  wrapUiAgentHandler(
@@ -8736,7 +9254,7 @@ Verify the zone no longer appears in the zone list.
8736
9254
  }
8737
9255
 
8738
9256
  // src/tools-ui-agent-ask.ts
8739
- import { z as z13 } from "zod";
9257
+ import { z as z16 } from "zod";
8740
9258
 
8741
9259
  // src/manus-webhook.ts
8742
9260
  var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
@@ -8839,8 +9357,8 @@ function registerUiAgentAskTools(server2, ctx) {
8839
9357
  title: "Reply to Paused UI Agent Task",
8840
9358
  description: "Resumes a Manus browser automation task that paused with stop_reason 'ask'. Use ui_agent_list_pending to find tasks waiting for input. Provide the task_id and your reply (e.g. a 2FA code, a field value, or a yes/no answer). The reply content is never recorded in audit logs \u2014 only its length is logged.",
8841
9359
  inputSchema: {
8842
- task_id: z13.string().describe("Manus task ID to resume (from ui_agent_list_pending)"),
8843
- reply: z13.string().describe("Your answer to the agent's question (e.g. a 2FA code or confirmation)")
9360
+ task_id: z16.string().describe("Manus task ID to resume (from ui_agent_list_pending)"),
9361
+ reply: z16.string().describe("Your answer to the agent's question (e.g. a 2FA code or confirmation)")
8844
9362
  }
8845
9363
  },
8846
9364
  async (args) => {
@@ -8899,7 +9417,7 @@ function registerUiAgentAskTools(server2, ctx) {
8899
9417
  try {
8900
9418
  const replyOutcome = await askReply(keys, task_id, reply);
8901
9419
  result2 = replyOutcome.data;
8902
- } catch (err2) {
9420
+ } catch (err4) {
8903
9421
  ctx.audit({
8904
9422
  tool_name: "ui_agent_reply",
8905
9423
  ocs_method: "[ui-agent:ask-reply]",
@@ -8914,7 +9432,7 @@ function registerUiAgentAskTools(server2, ctx) {
8914
9432
  content: [
8915
9433
  {
8916
9434
  type: "text",
8917
- text: `Error calling Manus task.reply: ${err2 instanceof Error ? err2.message : String(err2)}`
9435
+ text: `Error calling Manus task.reply: ${err4 instanceof Error ? err4.message : String(err4)}`
8918
9436
  }
8919
9437
  ]
8920
9438
  };
@@ -8992,13 +9510,13 @@ function registerUiAgentAskTools(server2, ctx) {
8992
9510
  try {
8993
9511
  const listing = await ctx.env.CARRIER_USERS.list({ prefix: PENDING_ASK_PREFIX });
8994
9512
  keys = listing.keys;
8995
- } catch (err2) {
9513
+ } catch (err4) {
8996
9514
  return {
8997
9515
  isError: true,
8998
9516
  content: [
8999
9517
  {
9000
9518
  type: "text",
9001
- text: `Error listing pending tasks: ${err2 instanceof Error ? err2.message : String(err2)}`
9519
+ text: `Error listing pending tasks: ${err4 instanceof Error ? err4.message : String(err4)}`
9002
9520
  }
9003
9521
  ]
9004
9522
  };
@@ -9049,7 +9567,7 @@ function registerUiAgentAskTools(server2, ctx) {
9049
9567
  }
9050
9568
 
9051
9569
  // src/stripe-connect-tools.ts
9052
- import { z as z14 } from "zod";
9570
+ import { z as z17 } from "zod";
9053
9571
  import * as Sentry2 from "@sentry/cloudflare";
9054
9572
 
9055
9573
  // src/audit.ts
@@ -9085,10 +9603,10 @@ async function issueConfirmToken(env, sub, toolName) {
9085
9603
  await env.OAUTH_KV.put(key, token2, { expirationTtl: 300 });
9086
9604
  return token2;
9087
9605
  }
9088
- function ok(text) {
9606
+ function ok2(text) {
9089
9607
  return { content: [{ type: "text", text }] };
9090
9608
  }
9091
- function err(text) {
9609
+ function err2(text) {
9092
9610
  return { isError: true, content: [{ type: "text", text }] };
9093
9611
  }
9094
9612
  async function stripeGet(stripeKey, path, connectedAccountId) {
@@ -9123,7 +9641,7 @@ function registerStripeConnectTools(server2, ctx) {
9123
9641
  "stripe_connect_status",
9124
9642
  "Read-only: returns the Stripe Connect account status, capabilities, and requirements for the authenticated operator.",
9125
9643
  {
9126
- operator_id: z14.string().optional().describe("Override operator_id (admin use). Defaults to caller's org/user.")
9644
+ operator_id: z17.string().optional().describe("Override operator_id (admin use). Defaults to caller's org/user.")
9127
9645
  },
9128
9646
  async ({ operator_id }) => {
9129
9647
  const opId = operator_id ?? operatorId;
@@ -9131,13 +9649,13 @@ function registerStripeConnectTools(server2, ctx) {
9131
9649
  const start = Date.now();
9132
9650
  try {
9133
9651
  const stripeKey = env.STRIPE_SECRET_KEY;
9134
- if (!stripeKey) return err("Stripe not configured");
9652
+ if (!stripeKey) return err2("Stripe not configured");
9135
9653
  const accountId = await env.CARRIER_USERS.get(opKvKey);
9136
9654
  if (!accountId) {
9137
- return ok(JSON.stringify({ status: "not_connected", operator_id: opId }));
9655
+ return ok2(JSON.stringify({ status: "not_connected", operator_id: opId }));
9138
9656
  }
9139
9657
  const result2 = await stripeGet(stripeKey, `/v1/accounts/${accountId}`);
9140
- if (!result2.ok) return err(`Stripe error: ${JSON.stringify(result2.data)}`);
9658
+ if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
9141
9659
  const a = result2.data;
9142
9660
  const status = a.charges_enabled && a.details_submitted ? "active" : "pending";
9143
9661
  writeAudit(env, {
@@ -9149,7 +9667,7 @@ function registerStripeConnectTools(server2, ctx) {
9149
9667
  sub: props2.sub,
9150
9668
  reseller_id: props2.reseller_id
9151
9669
  });
9152
- return ok(JSON.stringify({
9670
+ return ok2(JSON.stringify({
9153
9671
  status,
9154
9672
  operator_id: opId,
9155
9673
  account_id: accountId,
@@ -9163,7 +9681,7 @@ function registerStripeConnectTools(server2, ctx) {
9163
9681
  }));
9164
9682
  } catch (e) {
9165
9683
  Sentry2.captureException(e);
9166
- return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9684
+ return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9167
9685
  }
9168
9686
  }
9169
9687
  );
@@ -9171,20 +9689,20 @@ function registerStripeConnectTools(server2, ctx) {
9171
9689
  "stripe_connect_payouts",
9172
9690
  "Read-only: list recent payouts for the operator's connected Stripe account.",
9173
9691
  {
9174
- limit: z14.number().int().min(1).max(50).default(10).describe("Number of payouts to return."),
9175
- status: z14.enum(["pending", "paid", "failed", "canceled", "in_transit"]).optional().describe("Filter by payout status.")
9692
+ limit: z17.number().int().min(1).max(50).default(10).describe("Number of payouts to return."),
9693
+ status: z17.enum(["pending", "paid", "failed", "canceled", "in_transit"]).optional().describe("Filter by payout status.")
9176
9694
  },
9177
9695
  async ({ limit, status }) => {
9178
9696
  const start = Date.now();
9179
9697
  try {
9180
9698
  const stripeKey = env.STRIPE_SECRET_KEY;
9181
- if (!stripeKey) return err("Stripe not configured");
9699
+ if (!stripeKey) return err2("Stripe not configured");
9182
9700
  const accountId = await getAccountId();
9183
- if (!accountId) return err("No connected Stripe account found.");
9701
+ if (!accountId) return err2("No connected Stripe account found.");
9184
9702
  const params = new URLSearchParams({ limit: String(limit) });
9185
9703
  if (status) params.set("status", status);
9186
9704
  const result2 = await stripeGet(stripeKey, `/v1/payouts?${params.toString()}`, accountId);
9187
- if (!result2.ok) return err(`Stripe error: ${JSON.stringify(result2.data)}`);
9705
+ if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
9188
9706
  writeAudit(env, {
9189
9707
  tool_name: "stripe_connect_payouts",
9190
9708
  ocs_method: "stripe.payouts.list",
@@ -9194,10 +9712,10 @@ function registerStripeConnectTools(server2, ctx) {
9194
9712
  sub: props2.sub,
9195
9713
  reseller_id: props2.reseller_id
9196
9714
  });
9197
- return ok(JSON.stringify({ payouts: result2.data.data, has_more: result2.data.has_more }));
9715
+ return ok2(JSON.stringify({ payouts: result2.data.data, has_more: result2.data.has_more }));
9198
9716
  } catch (e) {
9199
9717
  Sentry2.captureException(e);
9200
- return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9718
+ return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9201
9719
  }
9202
9720
  }
9203
9721
  );
@@ -9209,11 +9727,11 @@ function registerStripeConnectTools(server2, ctx) {
9209
9727
  const start = Date.now();
9210
9728
  try {
9211
9729
  const stripeKey = env.STRIPE_SECRET_KEY;
9212
- if (!stripeKey) return err("Stripe not configured");
9730
+ if (!stripeKey) return err2("Stripe not configured");
9213
9731
  const accountId = await getAccountId();
9214
- if (!accountId) return err("No connected Stripe account found.");
9732
+ if (!accountId) return err2("No connected Stripe account found.");
9215
9733
  const result2 = await stripeGet(stripeKey, `/v1/balance`, accountId);
9216
- if (!result2.ok) return err(`Stripe error: ${JSON.stringify(result2.data)}`);
9734
+ if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
9217
9735
  writeAudit(env, {
9218
9736
  tool_name: "stripe_connect_balance",
9219
9737
  ocs_method: "stripe.balance.read",
@@ -9223,14 +9741,14 @@ function registerStripeConnectTools(server2, ctx) {
9223
9741
  sub: props2.sub,
9224
9742
  reseller_id: props2.reseller_id
9225
9743
  });
9226
- return ok(JSON.stringify({
9744
+ return ok2(JSON.stringify({
9227
9745
  account_id: accountId,
9228
9746
  available: result2.data.available ?? [],
9229
9747
  pending: result2.data.pending ?? []
9230
9748
  }));
9231
9749
  } catch (e) {
9232
9750
  Sentry2.captureException(e);
9233
- return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9751
+ return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9234
9752
  }
9235
9753
  }
9236
9754
  );
@@ -9238,17 +9756,17 @@ function registerStripeConnectTools(server2, ctx) {
9238
9756
  "stripe_connect_refund",
9239
9757
  "Admin: issue a refund on a charge via the operator's connected Stripe account. Requires confirm_token (call without token first to get one).",
9240
9758
  {
9241
- charge_id: z14.string().min(1).describe("Stripe charge ID (ch_...)."),
9242
- amount_cents: z14.number().int().min(1).optional().describe("Partial refund amount in cents. Omit for full refund."),
9243
- reason: z14.enum(["duplicate", "fraudulent", "requested_by_customer"]).optional(),
9244
- confirm_token: z14.string().optional().describe("Confirmation token from previous call. Required to execute.")
9759
+ charge_id: z17.string().min(1).describe("Stripe charge ID (ch_...)."),
9760
+ amount_cents: z17.number().int().min(1).optional().describe("Partial refund amount in cents. Omit for full refund."),
9761
+ reason: z17.enum(["duplicate", "fraudulent", "requested_by_customer"]).optional(),
9762
+ confirm_token: z17.string().optional().describe("Confirmation token from previous call. Required to execute.")
9245
9763
  },
9246
9764
  async ({ charge_id, amount_cents, reason, confirm_token }) => {
9247
9765
  const start = Date.now();
9248
9766
  const toolName = "stripe_connect_refund";
9249
9767
  if (!confirm_token) {
9250
9768
  const token2 = await issueConfirmToken(env, props2.sub, toolName);
9251
- return ok(
9769
+ return ok2(
9252
9770
  `HARD_BLOCK: Refund ${amount_cents ? `${amount_cents} cents on` : "(full) on"} charge ${charge_id} requires confirmation.
9253
9771
  confirm_token: ${token2}
9254
9772
  Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes.`
@@ -9256,18 +9774,18 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
9256
9774
  }
9257
9775
  const valid = await verifyConfirmToken(env, props2.sub, toolName, confirm_token);
9258
9776
  if (!valid) {
9259
- return err("Invalid or expired confirm_token. Call without token to get a new one.");
9777
+ return err2("Invalid or expired confirm_token. Call without token to get a new one.");
9260
9778
  }
9261
9779
  try {
9262
9780
  const stripeKey = env.STRIPE_SECRET_KEY;
9263
- if (!stripeKey) return err("Stripe not configured");
9781
+ if (!stripeKey) return err2("Stripe not configured");
9264
9782
  const accountId = await getAccountId();
9265
- if (!accountId) return err("No connected Stripe account found.");
9783
+ if (!accountId) return err2("No connected Stripe account found.");
9266
9784
  const params = { charge: charge_id };
9267
9785
  if (amount_cents) params.amount = String(amount_cents);
9268
9786
  if (reason) params.reason = reason;
9269
9787
  const result2 = await stripePost(stripeKey, "/v1/refunds", params, accountId);
9270
- if (!result2.ok) return err(`Stripe error: ${JSON.stringify(result2.data)}`);
9788
+ if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
9271
9789
  writeAudit(env, {
9272
9790
  tool_name: toolName,
9273
9791
  ocs_method: "stripe.refund.create",
@@ -9277,10 +9795,10 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
9277
9795
  sub: props2.sub,
9278
9796
  reseller_id: props2.reseller_id
9279
9797
  });
9280
- return ok(`Refund issued: ${result2.data.id} \u2014 status: ${result2.data.status}`);
9798
+ return ok2(`Refund issued: ${result2.data.id} \u2014 status: ${result2.data.status}`);
9281
9799
  } catch (e) {
9282
9800
  Sentry2.captureException(e);
9283
- return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9801
+ return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9284
9802
  }
9285
9803
  }
9286
9804
  );
@@ -9288,20 +9806,20 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
9288
9806
  "stripe_connect_dispute_list",
9289
9807
  "Read-only: list active disputes for the operator's connected Stripe account.",
9290
9808
  {
9291
- limit: z14.number().int().min(1).max(50).default(10),
9292
- status: z14.string().optional().describe("Filter by dispute status (e.g. needs_response, under_review).")
9809
+ limit: z17.number().int().min(1).max(50).default(10),
9810
+ status: z17.string().optional().describe("Filter by dispute status (e.g. needs_response, under_review).")
9293
9811
  },
9294
9812
  async ({ limit, status }) => {
9295
9813
  const start = Date.now();
9296
9814
  try {
9297
9815
  const stripeKey = env.STRIPE_SECRET_KEY;
9298
- if (!stripeKey) return err("Stripe not configured");
9816
+ if (!stripeKey) return err2("Stripe not configured");
9299
9817
  const accountId = await getAccountId();
9300
- if (!accountId) return err("No connected Stripe account found.");
9818
+ if (!accountId) return err2("No connected Stripe account found.");
9301
9819
  const params = new URLSearchParams({ limit: String(limit) });
9302
9820
  if (status) params.set("status", status);
9303
9821
  const result2 = await stripeGet(stripeKey, `/v1/disputes?${params.toString()}`, accountId);
9304
- if (!result2.ok) return err(`Stripe error: ${JSON.stringify(result2.data)}`);
9822
+ if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
9305
9823
  writeAudit(env, {
9306
9824
  tool_name: "stripe_connect_dispute_list",
9307
9825
  ocs_method: "stripe.disputes.list",
@@ -9311,10 +9829,10 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
9311
9829
  sub: props2.sub,
9312
9830
  reseller_id: props2.reseller_id
9313
9831
  });
9314
- return ok(JSON.stringify({ disputes: result2.data.data, has_more: result2.data.has_more }));
9832
+ return ok2(JSON.stringify({ disputes: result2.data.data, has_more: result2.data.has_more }));
9315
9833
  } catch (e) {
9316
9834
  Sentry2.captureException(e);
9317
- return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9835
+ return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9318
9836
  }
9319
9837
  }
9320
9838
  );
@@ -9322,18 +9840,18 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
9322
9840
  "radar_review_list",
9323
9841
  "Read-only: list pending Radar reviews requiring manual platform decision.",
9324
9842
  {
9325
- open_only: z14.boolean().default(true).describe("If true, only returns open (undecided) reviews."),
9326
- limit: z14.number().int().min(1).max(50).default(10)
9843
+ open_only: z17.boolean().default(true).describe("If true, only returns open (undecided) reviews."),
9844
+ limit: z17.number().int().min(1).max(50).default(10)
9327
9845
  },
9328
9846
  async ({ open_only, limit }) => {
9329
9847
  const start = Date.now();
9330
9848
  try {
9331
9849
  const stripeKey = env.STRIPE_SECRET_KEY;
9332
- if (!stripeKey) return err("Stripe not configured");
9850
+ if (!stripeKey) return err2("Stripe not configured");
9333
9851
  const params = new URLSearchParams({ limit: String(limit) });
9334
9852
  if (open_only) params.set("open", "true");
9335
9853
  const result2 = await stripeGet(stripeKey, `/v1/radar/reviews?${params.toString()}`);
9336
- if (!result2.ok) return err(`Stripe error: ${JSON.stringify(result2.data)}`);
9854
+ if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
9337
9855
  writeAudit(env, {
9338
9856
  tool_name: "radar_review_list",
9339
9857
  ocs_method: "stripe.radar.reviews.list",
@@ -9343,10 +9861,10 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
9343
9861
  sub: props2.sub,
9344
9862
  reseller_id: props2.reseller_id
9345
9863
  });
9346
- return ok(JSON.stringify({ reviews: result2.data.data, has_more: result2.data.has_more }));
9864
+ return ok2(JSON.stringify({ reviews: result2.data.data, has_more: result2.data.has_more }));
9347
9865
  } catch (e) {
9348
9866
  Sentry2.captureException(e);
9349
- return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9867
+ return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9350
9868
  }
9351
9869
  }
9352
9870
  );
@@ -9354,31 +9872,31 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
9354
9872
  "radar_review_approve",
9355
9873
  "Admin: approve a Radar review, allowing the charge to proceed. Requires confirm_token.",
9356
9874
  {
9357
- review_id: z14.string().min(1).describe("Stripe Radar review ID (prv_...)."),
9358
- confirm_token: z14.string().optional()
9875
+ review_id: z17.string().min(1).describe("Stripe Radar review ID (prv_...)."),
9876
+ confirm_token: z17.string().optional()
9359
9877
  },
9360
9878
  async ({ review_id, confirm_token }) => {
9361
9879
  const toolName = "radar_review_approve";
9362
9880
  const start = Date.now();
9363
9881
  if (!confirm_token) {
9364
9882
  const token2 = await issueConfirmToken(env, props2.sub, toolName);
9365
- return ok(
9883
+ return ok2(
9366
9884
  `HARD_BLOCK: Approving review ${review_id} allows the charge to proceed.
9367
9885
  confirm_token: ${token2}
9368
9886
  Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
9369
9887
  );
9370
9888
  }
9371
9889
  const valid = await verifyConfirmToken(env, props2.sub, toolName, confirm_token);
9372
- if (!valid) return err("Invalid or expired confirm_token.");
9890
+ if (!valid) return err2("Invalid or expired confirm_token.");
9373
9891
  try {
9374
9892
  const stripeKey = env.STRIPE_SECRET_KEY;
9375
- if (!stripeKey) return err("Stripe not configured");
9893
+ if (!stripeKey) return err2("Stripe not configured");
9376
9894
  const result2 = await stripePost(
9377
9895
  stripeKey,
9378
9896
  `/v1/radar/reviews/${review_id}/approve`,
9379
9897
  {}
9380
9898
  );
9381
- if (!result2.ok) return err(`Stripe error: ${JSON.stringify(result2.data)}`);
9899
+ if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
9382
9900
  writeAudit(env, {
9383
9901
  tool_name: toolName,
9384
9902
  ocs_method: "stripe.radar.review.approve",
@@ -9388,10 +9906,10 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
9388
9906
  sub: props2.sub,
9389
9907
  reseller_id: props2.reseller_id
9390
9908
  });
9391
- return ok(`Review ${review_id} approved. Charge will proceed.`);
9909
+ return ok2(`Review ${review_id} approved. Charge will proceed.`);
9392
9910
  } catch (e) {
9393
9911
  Sentry2.captureException(e);
9394
- return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9912
+ return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9395
9913
  }
9396
9914
  }
9397
9915
  );
@@ -9399,31 +9917,31 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
9399
9917
  "radar_review_decline",
9400
9918
  "Admin: decline a Radar review, blocking/closing the charge. Requires confirm_token.",
9401
9919
  {
9402
- review_id: z14.string().min(1),
9403
- confirm_token: z14.string().optional()
9920
+ review_id: z17.string().min(1),
9921
+ confirm_token: z17.string().optional()
9404
9922
  },
9405
9923
  async ({ review_id, confirm_token }) => {
9406
9924
  const toolName = "radar_review_decline";
9407
9925
  const start = Date.now();
9408
9926
  if (!confirm_token) {
9409
9927
  const token2 = await issueConfirmToken(env, props2.sub, toolName);
9410
- return ok(
9928
+ return ok2(
9411
9929
  `HARD_BLOCK: Declining review ${review_id} will close/block the charge.
9412
9930
  confirm_token: ${token2}
9413
9931
  Expires in 5 minutes.`
9414
9932
  );
9415
9933
  }
9416
9934
  const valid = await verifyConfirmToken(env, props2.sub, toolName, confirm_token);
9417
- if (!valid) return err("Invalid or expired confirm_token.");
9935
+ if (!valid) return err2("Invalid or expired confirm_token.");
9418
9936
  try {
9419
9937
  const stripeKey = env.STRIPE_SECRET_KEY;
9420
- if (!stripeKey) return err("Stripe not configured");
9938
+ if (!stripeKey) return err2("Stripe not configured");
9421
9939
  const result2 = await stripePost(
9422
9940
  stripeKey,
9423
9941
  `/v1/radar/reviews/${review_id}/approve`,
9424
9942
  { reason: "fraudulent" }
9425
9943
  );
9426
- if (!result2.ok) return err(`Stripe error: ${JSON.stringify(result2.data)}`);
9944
+ if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
9427
9945
  writeAudit(env, {
9428
9946
  tool_name: toolName,
9429
9947
  ocs_method: "stripe.radar.review.decline",
@@ -9433,10 +9951,10 @@ Expires in 5 minutes.`
9433
9951
  sub: props2.sub,
9434
9952
  reseller_id: props2.reseller_id
9435
9953
  });
9436
- return ok(`Review ${review_id} declined.`);
9954
+ return ok2(`Review ${review_id} declined.`);
9437
9955
  } catch (e) {
9438
9956
  Sentry2.captureException(e);
9439
- return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9957
+ return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9440
9958
  }
9441
9959
  }
9442
9960
  );
@@ -9444,32 +9962,32 @@ Expires in 5 minutes.`
9444
9962
  "radar_value_list_add",
9445
9963
  "Admin: add an item (email, IP, card fingerprint, country code) to a Stripe Radar block/allow list. Requires confirm_token.",
9446
9964
  {
9447
- value_list_id: z14.string().min(1).describe("Stripe Radar value list ID (rsl_...)."),
9448
- value: z14.string().min(1).describe("The value to add (email, IP address, country code, etc.)."),
9449
- confirm_token: z14.string().optional()
9965
+ value_list_id: z17.string().min(1).describe("Stripe Radar value list ID (rsl_...)."),
9966
+ value: z17.string().min(1).describe("The value to add (email, IP address, country code, etc.)."),
9967
+ confirm_token: z17.string().optional()
9450
9968
  },
9451
9969
  async ({ value_list_id, value, confirm_token }) => {
9452
9970
  const toolName = "radar_value_list_add";
9453
9971
  const start = Date.now();
9454
9972
  if (!confirm_token) {
9455
9973
  const token2 = await issueConfirmToken(env, props2.sub, toolName);
9456
- return ok(
9974
+ return ok2(
9457
9975
  `HARD_BLOCK: Adding "${value}" to list ${value_list_id} will affect future charge decisions.
9458
9976
  confirm_token: ${token2}
9459
9977
  Expires in 5 minutes.`
9460
9978
  );
9461
9979
  }
9462
9980
  const valid = await verifyConfirmToken(env, props2.sub, toolName, confirm_token);
9463
- if (!valid) return err("Invalid or expired confirm_token.");
9981
+ if (!valid) return err2("Invalid or expired confirm_token.");
9464
9982
  try {
9465
9983
  const stripeKey = env.STRIPE_SECRET_KEY;
9466
- if (!stripeKey) return err("Stripe not configured");
9984
+ if (!stripeKey) return err2("Stripe not configured");
9467
9985
  const result2 = await stripePost(
9468
9986
  stripeKey,
9469
9987
  "/v1/radar/value_list_items",
9470
9988
  { value_list: value_list_id, value }
9471
9989
  );
9472
- if (!result2.ok) return err(`Stripe error: ${JSON.stringify(result2.data)}`);
9990
+ if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
9473
9991
  writeAudit(env, {
9474
9992
  tool_name: toolName,
9475
9993
  ocs_method: "stripe.radar.value_list.add",
@@ -9479,10 +9997,10 @@ Expires in 5 minutes.`
9479
9997
  sub: props2.sub,
9480
9998
  reseller_id: props2.reseller_id
9481
9999
  });
9482
- return ok(`Added "${value}" to Radar list ${value_list_id}.`);
10000
+ return ok2(`Added "${value}" to Radar list ${value_list_id}.`);
9483
10001
  } catch (e) {
9484
10002
  Sentry2.captureException(e);
9485
- return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
10003
+ return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
9486
10004
  }
9487
10005
  }
9488
10006
  );
@@ -9490,11 +10008,11 @@ Expires in 5 minutes.`
9490
10008
  "radar_rule_toggle",
9491
10009
  "Admin: enable or disable a Stripe Radar rule. NOTE: Stripe does not expose rule CRUD via the public API \u2014 this tool returns Dashboard instructions.",
9492
10010
  {
9493
- rule_id: z14.string().min(1).describe("Stripe Radar rule ID."),
9494
- enabled: z14.boolean().describe("true = enable, false = disable.")
10011
+ rule_id: z17.string().min(1).describe("Stripe Radar rule ID."),
10012
+ enabled: z17.boolean().describe("true = enable, false = disable.")
9495
10013
  },
9496
10014
  async ({ rule_id, enabled }) => {
9497
- return ok(
10015
+ return ok2(
9498
10016
  `Stripe Radar does not expose rule enable/disable via the public API.
9499
10017
  To ${enabled ? "enable" : "disable"} rule ${rule_id}:
9500
10018
  1. Open https://dashboard.stripe.com/radar/rules
@@ -9506,6 +10024,513 @@ Note: If you need this automated, use the Radar for Platforms beta \u2014 contac
9506
10024
  );
9507
10025
  }
9508
10026
 
10027
+ // src/tools-greenzone.ts
10028
+ import { z as z18 } from "zod";
10029
+
10030
+ // src/greenzone-whitelist.ts
10031
+ var STATE_KEY = "greenzone:state";
10032
+ var GREENZONE_PORTAL_PATH = "/greenzone";
10033
+ var MANAGE_HOSTS_PATH = "/greenzone/hosts";
10034
+ var MANAGE_IPS_PATH = "/greenzone/ips";
10035
+ async function readState(kv) {
10036
+ const raw = await kv.get(STATE_KEY, "json");
10037
+ if (!raw) {
10038
+ return {
10039
+ hosts: [],
10040
+ ips: [],
10041
+ last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
10042
+ };
10043
+ }
10044
+ return raw;
10045
+ }
10046
+ async function writeState(kv, state) {
10047
+ await kv.put(STATE_KEY, JSON.stringify(state), {
10048
+ expirationTtl: 60 * 60 * 24 * 7
10049
+ // 7 days
10050
+ });
10051
+ }
10052
+ function stateAddHost(state, host) {
10053
+ const normalised = host.toLowerCase().trim();
10054
+ if (state.hosts.includes(normalised)) return state;
10055
+ return {
10056
+ ...state,
10057
+ hosts: [...state.hosts, normalised].sort(),
10058
+ last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
10059
+ };
10060
+ }
10061
+ function stateRemoveHost(state, host) {
10062
+ const normalised = host.toLowerCase().trim();
10063
+ return {
10064
+ ...state,
10065
+ hosts: state.hosts.filter((h) => h !== normalised),
10066
+ last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
10067
+ };
10068
+ }
10069
+ function stateAddIp(state, ip) {
10070
+ const normalised = ip.trim();
10071
+ if (state.ips.includes(normalised)) return state;
10072
+ return {
10073
+ ...state,
10074
+ ips: [...state.ips, normalised].sort(),
10075
+ last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
10076
+ };
10077
+ }
10078
+ function stateRemoveIp(state, ip) {
10079
+ const normalised = ip.trim();
10080
+ return {
10081
+ ...state,
10082
+ ips: state.ips.filter((i) => i !== normalised),
10083
+ last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
10084
+ };
10085
+ }
10086
+ function escapeForHasTextDoubleQuoted(fragment) {
10087
+ return fragment.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
10088
+ }
10089
+ function buildAddHostSteps(portalBaseUrl, host) {
10090
+ return [
10091
+ {
10092
+ tool: "new_tab",
10093
+ description: "Open a dedicated tab for Greenzone host whitelist operation"
10094
+ },
10095
+ {
10096
+ tool: "navigate",
10097
+ args: { url: `${portalBaseUrl}${MANAGE_HOSTS_PATH}` },
10098
+ description: `Navigate to Manage hosts page at ${portalBaseUrl}${MANAGE_HOSTS_PATH}`
10099
+ },
10100
+ {
10101
+ tool: "find",
10102
+ args: { query: "Add host button or input field" },
10103
+ description: "Find the 'Add host' button or input in Manage hosts menu"
10104
+ },
10105
+ {
10106
+ tool: "click",
10107
+ args: { selector: "[data-action='add-host'], button:has-text('Add'), a:has-text('Add host')" },
10108
+ description: "Click Add host to open the entry form"
10109
+ },
10110
+ {
10111
+ tool: "fill",
10112
+ args: { selector: "input[name='host'], input[placeholder*='host'], input[type='text']", value: host },
10113
+ description: `Fill in host value: ${host}`
10114
+ },
10115
+ {
10116
+ tool: "click",
10117
+ args: { selector: "button[type='submit'], button:has-text('Save'), button:has-text('Confirm')" },
10118
+ description: "Submit the form to save the host"
10119
+ },
10120
+ {
10121
+ tool: "screenshot",
10122
+ description: "Screenshot to verify host was added successfully"
10123
+ },
10124
+ {
10125
+ tool: "close",
10126
+ description: "Close the tab (always runs \u2014 success or error)"
10127
+ }
10128
+ ];
10129
+ }
10130
+ function buildRemoveHostSteps(portalBaseUrl, host) {
10131
+ const hostKey = host.toLowerCase().trim();
10132
+ const hostText = escapeForHasTextDoubleQuoted(hostKey);
10133
+ return [
10134
+ {
10135
+ tool: "new_tab",
10136
+ description: "Open a dedicated tab for Greenzone host removal operation"
10137
+ },
10138
+ {
10139
+ tool: "navigate",
10140
+ args: { url: `${portalBaseUrl}${MANAGE_HOSTS_PATH}` },
10141
+ description: `Navigate to Manage hosts page at ${portalBaseUrl}${MANAGE_HOSTS_PATH}`
10142
+ },
10143
+ {
10144
+ tool: "find",
10145
+ args: { query: `row containing host ${hostKey}` },
10146
+ description: `Find the row for host: ${hostKey}`
10147
+ },
10148
+ {
10149
+ tool: "click",
10150
+ args: {
10151
+ selector: `tr:has-text("${hostText}") [data-action='delete'], tr:has-text("${hostText}") button:has-text('Remove'), tr:has-text("${hostText}") button:has-text('Delete')`
10152
+ },
10153
+ description: `Click Remove/Delete for host: ${hostKey}`
10154
+ },
10155
+ {
10156
+ tool: "click",
10157
+ args: { selector: "button:has-text('Confirm'), button:has-text('Yes'), [data-action='confirm']" },
10158
+ description: "Confirm the deletion in the confirmation dialog"
10159
+ },
10160
+ {
10161
+ tool: "screenshot",
10162
+ description: "Screenshot to verify host was removed"
10163
+ },
10164
+ {
10165
+ tool: "close",
10166
+ description: "Close the tab (always runs \u2014 success or error)"
10167
+ }
10168
+ ];
10169
+ }
10170
+ function buildAddIpSteps(portalBaseUrl, ip) {
10171
+ return [
10172
+ {
10173
+ tool: "new_tab",
10174
+ description: "Open a dedicated tab for Greenzone IP whitelist operation"
10175
+ },
10176
+ {
10177
+ tool: "navigate",
10178
+ args: { url: `${portalBaseUrl}${MANAGE_IPS_PATH}` },
10179
+ description: `Navigate to Manage IPs page at ${portalBaseUrl}${MANAGE_IPS_PATH}`
10180
+ },
10181
+ {
10182
+ tool: "find",
10183
+ args: { query: "Add IP button or input field" },
10184
+ description: "Find the 'Add IP' button or input in Manage IPs menu"
10185
+ },
10186
+ {
10187
+ tool: "click",
10188
+ args: { selector: "[data-action='add-ip'], button:has-text('Add'), a:has-text('Add IP')" },
10189
+ description: "Click Add IP to open the entry form"
10190
+ },
10191
+ {
10192
+ tool: "fill",
10193
+ args: { selector: "input[name='ip'], input[placeholder*='IP'], input[placeholder*='ip'], input[type='text']", value: ip },
10194
+ description: `Fill in IP value: ${ip}`
10195
+ },
10196
+ {
10197
+ tool: "click",
10198
+ args: { selector: "button[type='submit'], button:has-text('Save'), button:has-text('Confirm')" },
10199
+ description: "Submit the form to save the IP"
10200
+ },
10201
+ {
10202
+ tool: "screenshot",
10203
+ description: "Screenshot to verify IP was added successfully"
10204
+ },
10205
+ {
10206
+ tool: "close",
10207
+ description: "Close the tab (always runs \u2014 success or error)"
10208
+ }
10209
+ ];
10210
+ }
10211
+ function buildRemoveIpSteps(portalBaseUrl, ip) {
10212
+ const ipKey = ip.trim();
10213
+ const ipText = escapeForHasTextDoubleQuoted(ipKey);
10214
+ return [
10215
+ {
10216
+ tool: "new_tab",
10217
+ description: "Open a dedicated tab for Greenzone IP removal operation"
10218
+ },
10219
+ {
10220
+ tool: "navigate",
10221
+ args: { url: `${portalBaseUrl}${MANAGE_IPS_PATH}` },
10222
+ description: `Navigate to Manage IPs page at ${portalBaseUrl}${MANAGE_IPS_PATH}`
10223
+ },
10224
+ {
10225
+ tool: "find",
10226
+ args: { query: `row containing IP ${ipKey}` },
10227
+ description: `Find the row for IP: ${ipKey}`
10228
+ },
10229
+ {
10230
+ tool: "click",
10231
+ args: {
10232
+ selector: `tr:has-text("${ipText}") [data-action='delete'], tr:has-text("${ipText}") button:has-text('Remove'), tr:has-text("${ipText}") button:has-text('Delete')`
10233
+ },
10234
+ description: `Click Remove/Delete for IP: ${ipKey}`
10235
+ },
10236
+ {
10237
+ tool: "click",
10238
+ args: { selector: "button:has-text('Confirm'), button:has-text('Yes'), [data-action='confirm']" },
10239
+ description: "Confirm the deletion in the confirmation dialog"
10240
+ },
10241
+ {
10242
+ tool: "screenshot",
10243
+ description: "Screenshot to verify IP was removed"
10244
+ },
10245
+ {
10246
+ tool: "close",
10247
+ description: "Close the tab (always runs \u2014 success or error)"
10248
+ }
10249
+ ];
10250
+ }
10251
+ function buildListSteps(portalBaseUrl) {
10252
+ return [
10253
+ {
10254
+ tool: "new_tab",
10255
+ description: "Open a dedicated tab for Greenzone whitelist read"
10256
+ },
10257
+ {
10258
+ tool: "navigate",
10259
+ args: { url: `${portalBaseUrl}${GREENZONE_PORTAL_PATH}` },
10260
+ description: `Navigate to Greenzone overview at ${portalBaseUrl}${GREENZONE_PORTAL_PATH}`
10261
+ },
10262
+ {
10263
+ tool: "find",
10264
+ args: { query: "Manage hosts and Manage IPs sections or links" },
10265
+ description: "Find Manage hosts and Manage IPs menu items"
10266
+ },
10267
+ {
10268
+ tool: "screenshot",
10269
+ description: "Screenshot of Greenzone overview for audit"
10270
+ },
10271
+ {
10272
+ tool: "navigate",
10273
+ args: { url: `${portalBaseUrl}${MANAGE_HOSTS_PATH}` },
10274
+ description: "Navigate to Manage hosts to read current host list"
10275
+ },
10276
+ {
10277
+ tool: "find",
10278
+ args: { query: "host entries table or list" },
10279
+ description: "Read all host entries from the page"
10280
+ },
10281
+ {
10282
+ tool: "navigate",
10283
+ args: { url: `${portalBaseUrl}${MANAGE_IPS_PATH}` },
10284
+ description: "Navigate to Manage IPs to read current IP list"
10285
+ },
10286
+ {
10287
+ tool: "find",
10288
+ args: { query: "IP entries table or list" },
10289
+ description: "Read all IP entries from the page"
10290
+ },
10291
+ {
10292
+ tool: "close",
10293
+ description: "Close the tab (always runs)"
10294
+ }
10295
+ ];
10296
+ }
10297
+ function resolvePortalBaseUrl(env) {
10298
+ return env.GREENZONE_PORTAL_URL ?? env.CARRIER_OCS_PORTAL_URL ?? env.CARRIER_OCS_BASE_URL ?? "https://ocs.esimvault.cloud";
10299
+ }
10300
+
10301
+ // src/tools-greenzone.ts
10302
+ function getGreenzoneKv(env) {
10303
+ return env.GREENZONE_STATE_KV ?? null;
10304
+ }
10305
+ function ok3(payload) {
10306
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
10307
+ }
10308
+ function err3(message) {
10309
+ return { isError: true, content: [{ type: "text", text: message }] };
10310
+ }
10311
+ var KAPTURE_UNAVAILABLE_MSG = "Execute each Kapture step using mcp__kapture__* tools in sequence. Ensure the Kapture extension is active in Chrome before invoking.";
10312
+ function registerGreenzoneTools(server2, ctx) {
10313
+ server2.registerTool(
10314
+ "greenzone_whitelist_add",
10315
+ {
10316
+ title: "Greenzone Whitelist \u2014 Add",
10317
+ description: "Add a hostname and/or IP address to the Bridge4IP Greenzone whitelist. The Greenzone whitelist controls which hosts/IPs are exempt from data charging after a subscriber's bundle is depleted. This operates via browser automation (Kapture) against the OCS portal \u2014 no REST API exists for this surface. Provide `host` (e.g. 'example.com') and/or `ip` (e.g. '203.0.113.0/24'). At least one of host or ip must be provided. Returns Kapture step sequence for Claude Code to execute. Requires 'write' scope and Kapture running in Sam's Chrome.",
10318
+ inputSchema: {
10319
+ host: z18.string().optional().describe("Hostname to whitelist (e.g. 'example.com')"),
10320
+ ip: z18.string().optional().describe("IP address or CIDR to whitelist (e.g. '203.0.113.0/24')"),
10321
+ dry_run: z18.boolean().optional().describe("If true, return the Kapture steps without executing portal automation")
10322
+ },
10323
+ annotations: { destructiveHint: true }
10324
+ },
10325
+ async (args) => {
10326
+ if (!ctx.props.scope.includes("write")) {
10327
+ return err3(
10328
+ `Scope denied: greenzone_whitelist_add requires 'write' scope. Your token has: [${ctx.props.scope.join(", ")}].`
10329
+ );
10330
+ }
10331
+ const { host, ip, dry_run } = args;
10332
+ if (!host && !ip) {
10333
+ return err3("At least one of 'host' or 'ip' must be provided.");
10334
+ }
10335
+ const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
10336
+ const steps = [
10337
+ ...host ? buildAddHostSteps(portalBaseUrl, host) : [],
10338
+ ...ip ? buildAddIpSteps(portalBaseUrl, ip) : []
10339
+ ];
10340
+ if (dry_run) {
10341
+ return ok3({
10342
+ dry_run: true,
10343
+ message: "Would execute the following Kapture steps against the OCS portal:",
10344
+ kapture_steps: steps,
10345
+ host,
10346
+ ip
10347
+ });
10348
+ }
10349
+ const kv = getGreenzoneKv(ctx.env);
10350
+ let state;
10351
+ let kvWarning;
10352
+ if (kv) {
10353
+ try {
10354
+ state = await readState(kv);
10355
+ if (host) state = stateAddHost(state, host);
10356
+ if (ip) state = stateAddIp(state, ip);
10357
+ await writeState(kv, state);
10358
+ } catch (e) {
10359
+ kvWarning = `KV write failed: ${e instanceof Error ? e.message : String(e)}`;
10360
+ }
10361
+ } else {
10362
+ kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
10363
+ }
10364
+ return ok3({
10365
+ requires_kapture: true,
10366
+ message: "Execute the Kapture steps below to apply the change in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
10367
+ kapture_steps: steps,
10368
+ kv_state: state ?? null,
10369
+ kv_warning: kvWarning,
10370
+ portal_base_url: portalBaseUrl
10371
+ });
10372
+ }
10373
+ );
10374
+ server2.registerTool(
10375
+ "greenzone_whitelist_remove",
10376
+ {
10377
+ title: "Greenzone Whitelist \u2014 Remove",
10378
+ description: "Remove a hostname and/or IP address from the Bridge4IP Greenzone whitelist. This operates via browser automation (Kapture) against the OCS portal. Provide `host` and/or `ip` to remove \u2014 at least one required. Returns Kapture step sequence for Claude Code to execute. Requires 'write' scope and Kapture running in Sam's Chrome.",
10379
+ inputSchema: {
10380
+ host: z18.string().optional().describe("Hostname to remove from whitelist"),
10381
+ ip: z18.string().optional().describe("IP address or CIDR to remove from whitelist"),
10382
+ dry_run: z18.boolean().optional().describe("If true, return the Kapture steps without executing portal automation")
10383
+ },
10384
+ annotations: { destructiveHint: true }
10385
+ },
10386
+ async (args) => {
10387
+ if (!ctx.props.scope.includes("write")) {
10388
+ return err3(
10389
+ `Scope denied: greenzone_whitelist_remove requires 'write' scope. Your token has: [${ctx.props.scope.join(", ")}].`
10390
+ );
10391
+ }
10392
+ const { host, ip, dry_run } = args;
10393
+ if (!host && !ip) {
10394
+ return err3("At least one of 'host' or 'ip' must be provided.");
10395
+ }
10396
+ const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
10397
+ const steps = [
10398
+ ...host ? buildRemoveHostSteps(portalBaseUrl, host) : [],
10399
+ ...ip ? buildRemoveIpSteps(portalBaseUrl, ip) : []
10400
+ ];
10401
+ if (dry_run) {
10402
+ return ok3({
10403
+ dry_run: true,
10404
+ message: "Would execute the following Kapture steps to remove from OCS portal:",
10405
+ kapture_steps: steps,
10406
+ host,
10407
+ ip
10408
+ });
10409
+ }
10410
+ const kv = getGreenzoneKv(ctx.env);
10411
+ let state;
10412
+ let kvWarning;
10413
+ if (kv) {
10414
+ try {
10415
+ state = await readState(kv);
10416
+ if (host) state = stateRemoveHost(state, host);
10417
+ if (ip) state = stateRemoveIp(state, ip);
10418
+ await writeState(kv, state);
10419
+ } catch (e) {
10420
+ kvWarning = `KV write failed: ${e instanceof Error ? e.message : String(e)}`;
10421
+ }
10422
+ } else {
10423
+ kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
10424
+ }
10425
+ return ok3({
10426
+ requires_kapture: true,
10427
+ message: "Execute the Kapture steps below to apply the removal in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
10428
+ kapture_steps: steps,
10429
+ kv_state: state ?? null,
10430
+ kv_warning: kvWarning,
10431
+ portal_base_url: portalBaseUrl
10432
+ });
10433
+ }
10434
+ );
10435
+ server2.registerTool(
10436
+ "greenzone_whitelist_list",
10437
+ {
10438
+ title: "Greenzone Whitelist \u2014 List",
10439
+ description: "List current Greenzone whitelist entries from the KV cache. Returns all whitelisted hosts and IPs, when the state was last synced, and whether the last hourly reconciliation detected drift from the portal. This is a fast read from KV \u2014 no browser automation required. Use `from_portal: true` to trigger a live portal read via Kapture instead of KV. Requires 'read' scope.",
10440
+ inputSchema: {
10441
+ from_portal: z18.boolean().optional().describe(
10442
+ "When true, return Kapture steps to read live portal state instead of KV cache"
10443
+ )
10444
+ },
10445
+ annotations: { readOnlyHint: true }
10446
+ },
10447
+ async (args) => {
10448
+ if (!ctx.props.scope.includes("read")) {
10449
+ return err3(
10450
+ `Scope denied: greenzone_whitelist_list requires 'read' scope. Your token has: [${ctx.props.scope.join(", ")}].`
10451
+ );
10452
+ }
10453
+ const { from_portal } = args;
10454
+ if (from_portal) {
10455
+ const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
10456
+ return ok3({
10457
+ requires_kapture: true,
10458
+ message: "Execute the Kapture steps below to read live Greenzone whitelist state from the OCS portal.",
10459
+ kapture_steps: buildListSteps(portalBaseUrl),
10460
+ portal_base_url: portalBaseUrl
10461
+ });
10462
+ }
10463
+ const kv = getGreenzoneKv(ctx.env);
10464
+ if (!kv) {
10465
+ return ok3({
10466
+ source: "kv_cache",
10467
+ hosts: [],
10468
+ ips: [],
10469
+ last_synced_at: null,
10470
+ last_reconciled_at: null,
10471
+ reconcile_drift: false,
10472
+ warning: "GREENZONE_STATE_KV not bound in stdio mode. Use from_portal: true to read live portal state."
10473
+ });
10474
+ }
10475
+ try {
10476
+ const state = await readState(kv);
10477
+ return ok3({
10478
+ source: "kv_cache",
10479
+ hosts: state.hosts,
10480
+ ips: state.ips,
10481
+ last_synced_at: state.last_synced_at,
10482
+ last_reconciled_at: state.last_reconciled_at ?? null,
10483
+ reconcile_drift: state.reconcile_drift ?? false
10484
+ });
10485
+ } catch (e) {
10486
+ return err3(`Failed to read Greenzone state from KV: ${e instanceof Error ? e.message : String(e)}`);
10487
+ }
10488
+ }
10489
+ );
10490
+ }
10491
+
10492
+ // src/tools-ui-agent-generic.ts
10493
+ import { z as z19 } from "zod";
10494
+ var STDIO_ERROR = {
10495
+ isError: true,
10496
+ content: [
10497
+ {
10498
+ type: "text",
10499
+ text: JSON.stringify({
10500
+ error: "requires_worker_runtime",
10501
+ message: "ui_agent_ask and ui_agent_status require the remote Carrier MCP Worker deployment (mcp.carrier.llc/mcp) \u2014 they cannot run in stdio/local mode because Steel browser sessions and KV task state require Cloudflare Workers runtime bindings. Connect to the remote MCP endpoint to use these tools.",
10502
+ remote_url: "https://mcp.carrier.llc/mcp"
10503
+ })
10504
+ }
10505
+ ]
10506
+ };
10507
+ function registerUiAgentGenericTools(server2, _ctx) {
10508
+ server2.registerTool(
10509
+ "ui_agent_ask",
10510
+ {
10511
+ title: "Dispatch Generic Steel Browsing Agent",
10512
+ description: "Dispatch a Steel browsing agent task with a natural-language prompt. The agent will navigate the web, use Tavily web search if needed, and return the result. Use for ad-hoc research, page extraction, form filling outside the OCS portal. Pro/Enterprise scope required. Cost ~$0.20-1.00 per task depending on complexity. Returns task_id immediately \u2014 poll with ui_agent_status for completion. Set dry_run=true to preview cost estimate without executing. NOTE: requires remote Worker deployment (mcp.carrier.llc/mcp) \u2014 not available in stdio mode.",
10513
+ inputSchema: {
10514
+ prompt: z19.string().min(1).max(4e3).describe("Natural-language task description for the Steel browsing agent."),
10515
+ max_steps: z19.number().int().min(1).max(30).optional().describe("Maximum agent steps (1\u201330, default 15)."),
10516
+ dry_run: z19.boolean().optional().describe("Preview cost estimate without dispatching.")
10517
+ }
10518
+ },
10519
+ async (_args) => STDIO_ERROR
10520
+ );
10521
+ server2.registerTool(
10522
+ "ui_agent_status",
10523
+ {
10524
+ title: "Get Steel Agent Task Status",
10525
+ description: "Poll the status of a Steel browser agent task dispatched by ui_agent_ask or any ui_* tool. Returns current status (pending | running | completed | failed) and result when available. NOTE: requires remote Worker deployment (mcp.carrier.llc/mcp) \u2014 not available in stdio mode.",
10526
+ inputSchema: {
10527
+ task_id: z19.string().describe("Steel task ID returned by ui_agent_ask or any ui_* tool dispatch")
10528
+ }
10529
+ },
10530
+ async (_args) => STDIO_ERROR
10531
+ );
10532
+ }
10533
+
9509
10534
  // src/index.ts
9510
10535
  var baseUrl = process.env.CARRIER_OCS_BASE_URL ?? process.env.ESIMVAULT_BASE_URL ?? "https://ocs.esimvault.cloud";
9511
10536
  var token = process.env.CARRIER_OCS_API_TOKEN ?? process.env.ESIMVAULT_API_TOKEN;
@@ -9553,12 +10578,17 @@ registerIntelligenceTools(server, toolCtx);
9553
10578
  registerAllBacklogTools(server, toolCtx);
9554
10579
  registerAllCarrierAskTools(server, toolCtx);
9555
10580
  registerListRecentOcsEventsTool(server, stdioEnv);
10581
+ registerRateLimitStatusTool(server, toolCtx);
10582
+ registerCountryHistoryTool(server, toolCtx);
10583
+ registerDepletionEventsTool(server, toolCtx);
9556
10584
  registerAllApps(server, toolCtx);
9557
10585
  registerAllPricingTools(server, toolCtx);
9558
10586
  registerScheduleAndUsageTools(server, toolCtx);
9559
10587
  registerAllUiAgentTools(server, toolCtx);
9560
10588
  registerUiAgentAskTools(server, toolCtx);
9561
10589
  registerStripeConnectTools(server, { env: stdioEnv, props });
10590
+ registerGreenzoneTools(server, toolCtx);
10591
+ registerUiAgentGenericTools(server, toolCtx);
9562
10592
  registerAllPrompts(server);
9563
10593
  var transport = new StdioServerTransport();
9564
10594
  await server.connect(transport);