@carrierllc/mcp 0.2.16 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/dist/cli.js +1445 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +1437 -252
- package/dist/index.js.map +1 -1
- package/package.json +23 -15
- package/plugin/.claude-plugin/marketplace.json +31 -0
- package/plugin/carrier/.claude-plugin/plugin.json +19 -0
- package/plugin/carrier/.mcp.json +8 -0
- package/plugin/carrier/README.md +75 -0
- package/plugin/carrier/agents/carrier-billing-auditor.md +16 -0
- package/plugin/carrier/agents/carrier-fleet-ops.md +15 -0
- package/plugin/carrier/agents/carrier-storefront-builder.md +17 -0
- package/plugin/carrier/commands/billing.md +23 -0
- package/plugin/carrier/commands/churn.md +15 -0
- package/plugin/carrier/commands/credits.md +15 -0
- package/plugin/carrier/commands/esim-status.md +15 -0
- package/plugin/carrier/commands/fleet.md +17 -0
- package/plugin/carrier/commands/greenzone.md +16 -0
- package/plugin/carrier/commands/onboard.md +17 -0
- package/plugin/carrier/commands/packages.md +18 -0
- package/plugin/carrier/commands/provision.md +30 -0
- package/plugin/carrier/commands/sms.md +18 -0
- package/plugin/carrier/commands/status.md +15 -0
- package/plugin/carrier/commands/storefront.md +21 -0
- package/plugin/carrier/commands/subscribers.md +18 -0
- package/plugin/carrier/commands/usage.md +16 -0
- package/plugin/carrier/commands/wallet.md +31 -0
- package/plugin/carrier/skills/carrier-operations/SKILL.md +43 -0
- package/templates/storefront/eslint.config.mjs +15 -0
- package/templates/storefront/next.config.ts +22 -0
- package/templates/storefront/open-next.config.ts +3 -0
- package/templates/storefront/package.json +38 -0
- package/templates/storefront/pnpm-lock.yaml +4020 -0
- package/templates/storefront/postcss.config.mjs +7 -0
- package/templates/storefront/src/app/activate/[orderId]/ActivateClient.tsx +125 -0
- package/templates/storefront/src/app/activate/[orderId]/page.tsx +32 -0
- package/templates/storefront/src/app/api/checkout/claim/route.ts +30 -0
- package/templates/storefront/src/app/api/checkout/guest/route.ts +91 -0
- package/templates/storefront/src/app/api/profile/phone/route.ts +40 -0
- package/templates/storefront/src/app/apple-icon.tsx +29 -0
- package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +110 -0
- package/templates/storefront/src/app/checkout/[templateId]/page.tsx +24 -0
- package/templates/storefront/src/app/checkout/success/CheckoutSuccessClient.tsx +415 -0
- package/templates/storefront/src/app/checkout/success/page.tsx +59 -0
- package/templates/storefront/src/app/contact/page.tsx +35 -0
- package/templates/storefront/src/app/dashboard/page.tsx +26 -0
- package/templates/storefront/src/app/globals.css +99 -0
- package/templates/storefront/src/app/help/page.tsx +25 -0
- package/templates/storefront/src/app/icon.tsx +29 -0
- package/templates/storefront/src/app/layout.tsx +56 -0
- package/templates/storefront/src/app/legal/acceptable-use/page.tsx +21 -0
- package/templates/storefront/src/app/legal/privacy/page.tsx +27 -0
- package/templates/storefront/src/app/legal/terms/page.tsx +25 -0
- package/templates/storefront/src/app/manifest.ts +18 -0
- package/templates/storefront/src/app/page.tsx +31 -0
- package/templates/storefront/src/app/robots.ts +9 -0
- package/templates/storefront/src/app/shop/ShopClient.tsx +27 -0
- package/templates/storefront/src/app/shop/page.tsx +9 -0
- package/templates/storefront/src/app/sign-in/[[...sign-in]]/page.tsx +24 -0
- package/templates/storefront/src/app/sign-up/[[...sign-up]]/StorefrontSignUpClient.tsx +199 -0
- package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +28 -0
- package/templates/storefront/src/app/sitemap.ts +14 -0
- package/templates/storefront/src/brand.config.ts +28 -0
- package/templates/storefront/src/components/Providers.tsx +16 -0
- package/templates/storefront/src/components/faq/FaqSection.tsx +80 -0
- package/templates/storefront/src/components/footer/StorefrontFooter.tsx +61 -0
- package/templates/storefront/src/components/landing/HeroSection.tsx +52 -0
- package/templates/storefront/src/components/landing/PlanCard.tsx +61 -0
- package/templates/storefront/src/components/nav/StorefrontNav.tsx +69 -0
- package/templates/storefront/src/components/theme/clerk-appearance.ts +51 -0
- package/templates/storefront/src/components/theme/theme-provider.tsx +87 -0
- package/templates/storefront/src/components/theme/theme-script.tsx +24 -0
- package/templates/storefront/src/lib/checkout-order-claim.ts +127 -0
- package/templates/storefront/src/lib/complete-email-sign-up.ts +50 -0
- package/templates/storefront/src/lib/conversion-events.ts +36 -0
- package/templates/storefront/src/lib/sanitize-auth-redirect.ts +16 -0
- package/templates/storefront/src/lib/verify-checkout-session.ts +53 -0
- package/templates/storefront/src/middleware.ts +31 -0
- package/templates/storefront/src/vendor/carrier/client.ts +95 -0
- package/templates/storefront/src/vendor/carrier/index.ts +2 -0
- package/templates/storefront/src/vendor/carrier/types.ts +21 -0
- package/templates/storefront/src/vendor/config/index.ts +2 -0
- package/templates/storefront/src/vendor/geo/index.ts +32 -0
- package/templates/storefront/src/vendor/ui/brand.ts +68 -0
- package/templates/storefront/src/vendor/ui/cn.ts +7 -0
- package/templates/storefront/src/vendor/ui/countryImagery.ts +46 -0
- package/templates/storefront/src/vendor/ui/index.ts +3 -0
- package/templates/storefront/tsconfig.json +23 -0
- package/templates/storefront/wrangler.jsonc +11 -0
- 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-PMFMORPK.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
|
+
modifySubscriberMobilePlan: 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
|
|
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
|
|
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
|
|
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
|
|
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"];
|
|
@@ -227,7 +389,9 @@ var DESTRUCTIVE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
227
389
|
"modify_subscriber_package_active_period",
|
|
228
390
|
"modify_subscriber_voip_plan",
|
|
229
391
|
"push_steering_to_subscriber",
|
|
230
|
-
"reset_subscriber_gz_counter"
|
|
392
|
+
"reset_subscriber_gz_counter",
|
|
393
|
+
// S6 live-smoke gap fill (CAR-105)
|
|
394
|
+
"change_network_profile_of_location_zone"
|
|
231
395
|
]);
|
|
232
396
|
function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
233
397
|
return async (args) => {
|
|
@@ -295,9 +459,9 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
|
295
459
|
try {
|
|
296
460
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
297
461
|
result2 = await handler(args, token2);
|
|
298
|
-
} catch (
|
|
462
|
+
} catch (err4) {
|
|
299
463
|
try {
|
|
300
|
-
Sentry.captureException(
|
|
464
|
+
Sentry.captureException(err4, {
|
|
301
465
|
tags: {
|
|
302
466
|
tool: toolName,
|
|
303
467
|
feature: "mcp",
|
|
@@ -306,8 +470,8 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
|
306
470
|
});
|
|
307
471
|
} catch {
|
|
308
472
|
}
|
|
309
|
-
const message =
|
|
310
|
-
const ocsCode =
|
|
473
|
+
const message = err4 instanceof Error ? err4.message : String(err4);
|
|
474
|
+
const ocsCode = err4 instanceof OcsApiError2 ? err4.code : void 0;
|
|
311
475
|
ctx.audit({
|
|
312
476
|
tool_name: toolName,
|
|
313
477
|
ocs_method: ocsMethod,
|
|
@@ -335,7 +499,7 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
|
335
499
|
};
|
|
336
500
|
}
|
|
337
501
|
async function ocsCall(env, token2, method, params = {}) {
|
|
338
|
-
const client = new
|
|
502
|
+
const client = new OcsClient2(env.CARRIER_OCS_BASE_URL, token2);
|
|
339
503
|
const result2 = await client.call(method, params);
|
|
340
504
|
return {
|
|
341
505
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
@@ -344,13 +508,13 @@ async function ocsCall(env, token2, method, params = {}) {
|
|
|
344
508
|
async function resolveSubscriberByIccid(env, token2, iccid, cache) {
|
|
345
509
|
const hit = cache.get(iccid);
|
|
346
510
|
if (hit) return hit;
|
|
347
|
-
const client = new
|
|
511
|
+
const client = new OcsClient2(env.CARRIER_OCS_BASE_URL, token2);
|
|
348
512
|
const record = await client.call("getSingleSubscriber", { iccid });
|
|
349
513
|
cache.set(iccid, record);
|
|
350
514
|
return record;
|
|
351
515
|
}
|
|
352
516
|
async function getDefaultResellerId(env, token2) {
|
|
353
|
-
const client = new
|
|
517
|
+
const client = new OcsClient2(env.CARRIER_OCS_BASE_URL, token2);
|
|
354
518
|
const info = await client.call("getResellerInfo", {});
|
|
355
519
|
const id = info?.id;
|
|
356
520
|
if (typeof id !== "number") {
|
|
@@ -405,9 +569,7 @@ function registerAllTools(server2, ctx) {
|
|
|
405
569
|
TOOL_SCOPES["modify_account_balance"],
|
|
406
570
|
ctx,
|
|
407
571
|
async ({ accountId, amount, mode }, token2) => {
|
|
408
|
-
const params = { accountId };
|
|
409
|
-
if (mode === "adapt") params.adaptBalance = amount;
|
|
410
|
-
else params.setBalance = amount;
|
|
572
|
+
const params = { accountId, amount, mode };
|
|
411
573
|
return ocsCall(ctx.env, token2, "modifyAccountBalance", params);
|
|
412
574
|
}
|
|
413
575
|
)
|
|
@@ -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
|
|
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;
|
|
@@ -2233,12 +2395,27 @@ var ocs_methods_default = {
|
|
|
2233
2395
|
name: "high_cost_subscribers",
|
|
2234
2396
|
category: "intelligence",
|
|
2235
2397
|
scope: "read",
|
|
2398
|
+
http_method: "POST",
|
|
2399
|
+
url_path: "/v1/intelligence/high-cost-subscribers",
|
|
2236
2400
|
description: "Identify highest-cost subscribers for cost optimization",
|
|
2237
2401
|
wraps: ["listSubscriber", "subscriberUsageOverPeriod", "getCustomerTariff"],
|
|
2238
2402
|
params: {
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2403
|
+
start: { type: "string(YYYY-MM-DD)", required: false, default: "first day of current month" },
|
|
2404
|
+
end: { type: "string(YYYY-MM-DD)", required: false, default: "today" },
|
|
2405
|
+
limit: { type: "number", required: false, default: 20, max: 200 }
|
|
2406
|
+
},
|
|
2407
|
+
verified_against_server: true,
|
|
2408
|
+
verified_against_live_docs: false
|
|
2409
|
+
},
|
|
2410
|
+
{
|
|
2411
|
+
name: "detect_country_entry",
|
|
2412
|
+
category: "intelligence",
|
|
2413
|
+
scope: "read",
|
|
2414
|
+
description: "Detect subscriber country entry via getSingleSubscriber networkInfo.lastMcc (one OCS call). Resolves MCC to ISO 3166-1 alpha-2 and optionally diffs against expectedCountry.",
|
|
2415
|
+
wraps: ["getSingleSubscriber"],
|
|
2416
|
+
params: {
|
|
2417
|
+
subscriber: { type: "object(subscriberId|imsi|iccid|msisdn|multiImsi|activationCode)", required: true },
|
|
2418
|
+
expectedCountry: { type: "string(ISO3166-1 alpha-2)", required: false }
|
|
2242
2419
|
},
|
|
2243
2420
|
verified_against_server: true,
|
|
2244
2421
|
verified_against_live_docs: false
|
|
@@ -2377,10 +2554,10 @@ var ocs_methods_default = {
|
|
|
2377
2554
|
ocs_method: "getSubscriberLocationByCellId",
|
|
2378
2555
|
category: "subscriber",
|
|
2379
2556
|
scope: "read",
|
|
2380
|
-
status: "
|
|
2557
|
+
status: "obsolete",
|
|
2381
2558
|
audit_pr: "feat/ocs-feature-audit",
|
|
2382
2559
|
audit_stub_id: "S-03",
|
|
2383
|
-
description: "
|
|
2560
|
+
description: "OBSOLETE \u2014 duplicate of implemented get_subscriber_location_by_cell_id (same OCS method + cell tuple). Do not register. Use get_subscriber_location_by_cell_id for GeoSense cell-tower resolution.",
|
|
2384
2561
|
params: {
|
|
2385
2562
|
radio_type: { type: "enum[2G,3G,4G,5G,NB-IoT]", required: true },
|
|
2386
2563
|
mcc: { type: "integer", required: true },
|
|
@@ -2395,7 +2572,7 @@ var ocs_methods_default = {
|
|
|
2395
2572
|
accuracy: { type: "integer", notes: "median error in meters at 50% confidence" }
|
|
2396
2573
|
},
|
|
2397
2574
|
annotations: "readOnlyHint",
|
|
2398
|
-
|
|
2575
|
+
superseded_by: "get_subscriber_location_by_cell_id",
|
|
2399
2576
|
verified_against_server: false,
|
|
2400
2577
|
verified_against_live_docs: true
|
|
2401
2578
|
},
|
|
@@ -2404,7 +2581,7 @@ var ocs_methods_default = {
|
|
|
2404
2581
|
ocs_method: "getResellerInfo",
|
|
2405
2582
|
category: "reseller",
|
|
2406
2583
|
scope: "read",
|
|
2407
|
-
status: "
|
|
2584
|
+
status: "implemented",
|
|
2408
2585
|
audit_pr: "feat/ocs-feature-audit",
|
|
2409
2586
|
audit_stub_id: "S-04",
|
|
2410
2587
|
description: "Read-only view of Bridge4IP webhook and relay flag state from getResellerInfo.trafficInfo. Surfaces relayLU, relayGy, relayCallSms, relayVoIP booleans + notification webhook types. Relay LU is the key flag for event-driven country-change detection (vs polling). Relay endpoint config is OCS portal UI-only.",
|
|
@@ -2419,8 +2596,57 @@ var ocs_methods_default = {
|
|
|
2419
2596
|
notification_webhooks: { type: "array", notes: "Active notification types: prepaid_usage, low_credit, esim_status, recurring_packages" }
|
|
2420
2597
|
},
|
|
2421
2598
|
annotations: "readOnlyHint",
|
|
2422
|
-
verified_against_server:
|
|
2599
|
+
verified_against_server: true,
|
|
2423
2600
|
verified_against_live_docs: true
|
|
2601
|
+
},
|
|
2602
|
+
{
|
|
2603
|
+
name: "list_subscriber_voip_tariff",
|
|
2604
|
+
ocs_method: "listSubscriberVoipTariff",
|
|
2605
|
+
category: "tariff",
|
|
2606
|
+
scope: "read",
|
|
2607
|
+
status: "implemented",
|
|
2608
|
+
description: "List VoIP tariff definitions available to a reseller (smoke-verified OCS method; not on public docs SPA). Optional resellerId filter; omit for token owner's default catalog. Companion to list_voip_tariff_rule for rule detail.",
|
|
2609
|
+
params: {
|
|
2610
|
+
reseller_id: { type: "number", required: false }
|
|
2611
|
+
},
|
|
2612
|
+
annotations: "readOnlyHint",
|
|
2613
|
+
verified_against_server: true,
|
|
2614
|
+
verified_against_live_docs: false,
|
|
2615
|
+
verified_against_live_smoke: true,
|
|
2616
|
+
notes: "Live probe 2026-07-10: only known request property is resellerId (object). Empty object accepted (code 0)."
|
|
2617
|
+
},
|
|
2618
|
+
{
|
|
2619
|
+
name: "list_voip_tariff_rule",
|
|
2620
|
+
ocs_method: "listVoipTariffRule",
|
|
2621
|
+
category: "tariff",
|
|
2622
|
+
scope: "read",
|
|
2623
|
+
status: "implemented",
|
|
2624
|
+
description: "List rate rules for a VoIP plan/tariff by bare integer id (smoke-verified; not on public docs SPA). OCS expects a primitive integer body (same pattern as getCustomerTariff), not an object. Use plan ids from get_reseller_info chargingInfo.voipPlan or list_subscriber_voip_tariff.",
|
|
2625
|
+
params: {
|
|
2626
|
+
voip_plan_id: { type: "number", required: true }
|
|
2627
|
+
},
|
|
2628
|
+
annotations: "readOnlyHint",
|
|
2629
|
+
verified_against_server: true,
|
|
2630
|
+
verified_against_live_docs: false,
|
|
2631
|
+
verified_against_live_smoke: true,
|
|
2632
|
+
notes: "Live probe 2026-07-10: object body rejected (expects java.lang.Integer); unknown id \u2192 OCS 6 No VoIP plan found."
|
|
2633
|
+
},
|
|
2634
|
+
{
|
|
2635
|
+
name: "change_network_profile_of_location_zone",
|
|
2636
|
+
ocs_method: "changeNetworkProfileOfLocationZone",
|
|
2637
|
+
category: "network",
|
|
2638
|
+
scope: "write",
|
|
2639
|
+
status: "implemented",
|
|
2640
|
+
description: "Attach or change the network profile on an existing location zone (smoke-verified; not on public docs SPA). Required fields: locationZoneId + networkProfileId. Use list_network_profiles and list_detailed_location_zones to resolve ids.",
|
|
2641
|
+
params: {
|
|
2642
|
+
location_zone_id: { type: "number", required: true },
|
|
2643
|
+
network_profile_id: { type: "number", required: true }
|
|
2644
|
+
},
|
|
2645
|
+
annotations: "destructiveHint",
|
|
2646
|
+
verified_against_server: true,
|
|
2647
|
+
verified_against_live_docs: false,
|
|
2648
|
+
verified_against_live_smoke: true,
|
|
2649
|
+
notes: "Live probe 2026-07-10: known properties locationZoneId, networkProfileId; missing networkProfileId \u2192 code 2 Missing 'networkProfileId'."
|
|
2424
2650
|
}
|
|
2425
2651
|
],
|
|
2426
2652
|
v1_app_methods: [
|
|
@@ -2968,11 +3194,11 @@ var ocsAppMethods = ocsSpec.v1_app_methods;
|
|
|
2968
3194
|
// src/intelligence.ts
|
|
2969
3195
|
async function safeCall(env, token2, method, params = {}) {
|
|
2970
3196
|
try {
|
|
2971
|
-
const client = new
|
|
3197
|
+
const client = new OcsClient2(env.CARRIER_OCS_BASE_URL, token2);
|
|
2972
3198
|
const data = await client.call(method, params);
|
|
2973
3199
|
return { data, error: null };
|
|
2974
|
-
} catch (
|
|
2975
|
-
return { data: null, error:
|
|
3200
|
+
} catch (err4) {
|
|
3201
|
+
return { data: null, error: err4 instanceof Error ? err4.message : String(err4) };
|
|
2976
3202
|
}
|
|
2977
3203
|
}
|
|
2978
3204
|
async function fetchActiveSubscribers(env, token2, accountId, resellerId) {
|
|
@@ -3972,11 +4198,19 @@ var DRY_RUN_FIELD2 = {
|
|
|
3972
4198
|
"If true, do not call OCS \u2014 return the would-be request for confirmation"
|
|
3973
4199
|
)
|
|
3974
4200
|
};
|
|
4201
|
+
function normalizeRadioType(value) {
|
|
4202
|
+
if (/^nb-?iot$/i.test(value)) return "NB-IoT";
|
|
4203
|
+
return value;
|
|
4204
|
+
}
|
|
4205
|
+
var VALID_RADIO_TYPES = /* @__PURE__ */ new Set(["2G", "3G", "4G", "5G", "NB-IoT"]);
|
|
3975
4206
|
var BACKLOG_TOOL_SCOPES = {
|
|
3976
4207
|
affect_subscriber_phone_number: "write",
|
|
3977
4208
|
carrier_webhook_config: "read",
|
|
4209
|
+
change_network_profile_of_location_zone: "write",
|
|
3978
4210
|
get_subscriber_location_by_cell_id: "read",
|
|
3979
4211
|
list_destination_lists: "read",
|
|
4212
|
+
list_subscriber_voip_tariff: "read",
|
|
4213
|
+
list_voip_tariff_rule: "read",
|
|
3980
4214
|
modify_subscriber_mobile_plan: "write",
|
|
3981
4215
|
modify_subscriber_package_active_period: "write",
|
|
3982
4216
|
modify_subscriber_voip_plan: "write",
|
|
@@ -4006,7 +4240,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4006
4240
|
ctx,
|
|
4007
4241
|
async ({ iccid, phone_number, phone_type }, token2) => {
|
|
4008
4242
|
const ocsMethod = phone_type === "fake" ? "affectSubscriberFakePhoneNumber" : "affectSubscriberRealPhoneNumber";
|
|
4009
|
-
const client = new
|
|
4243
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4010
4244
|
const result2 = await client.call(ocsMethod, { subscriber: iccid, phoneNumber: phone_number });
|
|
4011
4245
|
return {
|
|
4012
4246
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
@@ -4018,9 +4252,11 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4018
4252
|
"get_subscriber_location_by_cell_id",
|
|
4019
4253
|
{
|
|
4020
4254
|
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.",
|
|
4255
|
+
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
4256
|
inputSchema: {
|
|
4023
|
-
radio_type: z3.
|
|
4257
|
+
radio_type: z3.string().transform(normalizeRadioType).refine((v) => VALID_RADIO_TYPES.has(v), {
|
|
4258
|
+
message: "radio_type must be one of: 2G, 3G, 4G, 5G, NB-IoT (also accepts NBIOT, nb-iot, nbiot)"
|
|
4259
|
+
}).describe("Radio access technology type: '2G'|'3G'|'4G'|'5G'|'NB-IoT'. Also accepts 'NBIOT'|'nb-iot'|'nbiot' \u2014 normalised to 'NB-IoT'."),
|
|
4024
4260
|
mcc: z3.number().int().describe("Mobile Country Code (e.g. 250 for Russia, 234 for UK)"),
|
|
4025
4261
|
mnc: z3.number().int().describe("Mobile Network Code"),
|
|
4026
4262
|
lac: z3.number().int().describe("Location Area Code"),
|
|
@@ -4042,7 +4278,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4042
4278
|
cell_id,
|
|
4043
4279
|
signal_strength
|
|
4044
4280
|
}, token2) => {
|
|
4045
|
-
const client = new
|
|
4281
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4046
4282
|
const params = { radioType: radio_type, mcc, mnc, lac };
|
|
4047
4283
|
if (cell_id !== void 0) params.cellId = cell_id;
|
|
4048
4284
|
if (signal_strength !== void 0) params.signalStrength = signal_strength;
|
|
@@ -4070,7 +4306,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4070
4306
|
ctx,
|
|
4071
4307
|
async ({ resellerId }, token2) => {
|
|
4072
4308
|
const id = resellerId ?? await (async () => {
|
|
4073
|
-
const client2 = new
|
|
4309
|
+
const client2 = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4074
4310
|
const info = await client2.call("getResellerInfo", {});
|
|
4075
4311
|
const resolvedId = info?.id;
|
|
4076
4312
|
if (typeof resolvedId !== "number") {
|
|
@@ -4078,7 +4314,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4078
4314
|
}
|
|
4079
4315
|
return resolvedId;
|
|
4080
4316
|
})();
|
|
4081
|
-
const client = new
|
|
4317
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4082
4318
|
const result2 = await client.call("listDetailedDestinationList", id);
|
|
4083
4319
|
return {
|
|
4084
4320
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
@@ -4104,7 +4340,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4104
4340
|
BACKLOG_TOOL_SCOPES["modify_subscriber_mobile_plan"],
|
|
4105
4341
|
ctx,
|
|
4106
4342
|
async ({ iccid, mobile_plan_id }, token2) => {
|
|
4107
|
-
const client = new
|
|
4343
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4108
4344
|
const result2 = await client.call(
|
|
4109
4345
|
"modifySubscriberMobilePlan",
|
|
4110
4346
|
{ subscriber: iccid, mobilePlanId: mobile_plan_id }
|
|
@@ -4138,7 +4374,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4138
4374
|
const params = { subscriber: iccid, packageId: package_id };
|
|
4139
4375
|
if (start_date !== void 0) params.startDate = start_date;
|
|
4140
4376
|
if (end_date !== void 0) params.endDate = end_date;
|
|
4141
|
-
const client = new
|
|
4377
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4142
4378
|
const result2 = await client.call(
|
|
4143
4379
|
"modifySubscriberPrepaidPackageActivePeriod",
|
|
4144
4380
|
params
|
|
@@ -4167,7 +4403,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4167
4403
|
BACKLOG_TOOL_SCOPES["modify_subscriber_voip_plan"],
|
|
4168
4404
|
ctx,
|
|
4169
4405
|
async ({ iccid, voip_plan_id }, token2) => {
|
|
4170
|
-
const client = new
|
|
4406
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4171
4407
|
const result2 = await client.call(
|
|
4172
4408
|
"modifySubscriberVoipPlan",
|
|
4173
4409
|
{ subscriber: iccid, voipPlanId: voip_plan_id }
|
|
@@ -4198,7 +4434,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4198
4434
|
const cache = /* @__PURE__ */ new Map();
|
|
4199
4435
|
const sub = await resolveSubscriberByIccid(ctx.env, token2, iccid, cache);
|
|
4200
4436
|
const subscriberId = sub.id ?? sub.subscriberId ?? iccid;
|
|
4201
|
-
const client = new
|
|
4437
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4202
4438
|
const result2 = await client.call(
|
|
4203
4439
|
"pushSteeringToSubs",
|
|
4204
4440
|
{ subscriber: subscriberId }
|
|
@@ -4226,7 +4462,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4226
4462
|
BACKLOG_TOOL_SCOPES["reset_subscriber_gz_counter"],
|
|
4227
4463
|
ctx,
|
|
4228
4464
|
async ({ iccid }, token2) => {
|
|
4229
|
-
const client = new
|
|
4465
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4230
4466
|
const result2 = await client.call("resetSubsGzCounter", { subscriber: iccid });
|
|
4231
4467
|
return {
|
|
4232
4468
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
@@ -4250,7 +4486,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4250
4486
|
BACKLOG_TOOL_SCOPES["carrier_webhook_config"],
|
|
4251
4487
|
ctx,
|
|
4252
4488
|
async ({ reseller_id }, token2) => {
|
|
4253
|
-
const client = new
|
|
4489
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4254
4490
|
let resellerId = reseller_id;
|
|
4255
4491
|
if (resellerId === void 0) {
|
|
4256
4492
|
resellerId = await getDefaultResellerId(ctx.env, token2);
|
|
@@ -4276,6 +4512,92 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4276
4512
|
}
|
|
4277
4513
|
)
|
|
4278
4514
|
);
|
|
4515
|
+
server2.registerTool(
|
|
4516
|
+
"list_subscriber_voip_tariff",
|
|
4517
|
+
{
|
|
4518
|
+
title: "List VoIP Tariffs",
|
|
4519
|
+
description: "Use this to list VoIP tariff definitions available to a reseller. Despite the OCS method name (listSubscriberVoipTariff), this is a reseller-level catalog listing \u2014 not a per-ICCID lookup. Use results (or get_reseller_info chargingInfo.voipPlan.id) to feed `list_voip_tariff_rule` for rate detail. Params: `reseller_id` (integer, optional \u2014 omit to use the token owner's reseller). Returns: OCS VoIP tariff records (may be empty when no VoIP catalog is configured). Do NOT use this for mobile wholesale rates \u2014 use `get_tariff` (getCustomerTariff). Do NOT use this to assign a VoIP plan \u2014 use `modify_subscriber_voip_plan`.",
|
|
4520
|
+
inputSchema: {
|
|
4521
|
+
reseller_id: z3.number().int().positive().optional().describe("Reseller ID (omit to list tariffs for the token owner's reseller)")
|
|
4522
|
+
},
|
|
4523
|
+
annotations: { readOnlyHint: true }
|
|
4524
|
+
},
|
|
4525
|
+
wrapHandler(
|
|
4526
|
+
"list_subscriber_voip_tariff",
|
|
4527
|
+
"listSubscriberVoipTariff",
|
|
4528
|
+
BACKLOG_TOOL_SCOPES["list_subscriber_voip_tariff"],
|
|
4529
|
+
ctx,
|
|
4530
|
+
async ({ reseller_id }, token2) => {
|
|
4531
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4532
|
+
const params = {};
|
|
4533
|
+
if (reseller_id !== void 0) {
|
|
4534
|
+
params.resellerId = reseller_id;
|
|
4535
|
+
}
|
|
4536
|
+
const result2 = await client.call("listSubscriberVoipTariff", params);
|
|
4537
|
+
return {
|
|
4538
|
+
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
)
|
|
4542
|
+
);
|
|
4543
|
+
server2.registerTool(
|
|
4544
|
+
"list_voip_tariff_rule",
|
|
4545
|
+
{
|
|
4546
|
+
title: "List VoIP Tariff Rules",
|
|
4547
|
+
description: "Use this to list the rate rules (costs) for a specific VoIP plan/tariff. OCS expects a bare integer plan id in the request body (not an object). Obtain plan ids from `get_reseller_info` \u2192 chargingInfo.voipPlan.id, or from `list_subscriber_voip_tariff`. Params: `voip_plan_id` (integer, required \u2014 VoIP plan/tariff id). Returns: VoIP tariff rule rows for that plan, or an OCS error when the id is unknown. Do NOT use this for mobile (non-VoIP) wholesale rates \u2014 use `get_tariff`. Do NOT use this to change a subscriber's VoIP plan \u2014 use `modify_subscriber_voip_plan`.",
|
|
4548
|
+
inputSchema: {
|
|
4549
|
+
voip_plan_id: z3.number().int().positive().describe(
|
|
4550
|
+
"VoIP plan/tariff ID (bare integer to OCS listVoipTariffRule; from get_reseller_info chargingInfo.voipPlan.id or list_subscriber_voip_tariff)"
|
|
4551
|
+
)
|
|
4552
|
+
},
|
|
4553
|
+
annotations: { readOnlyHint: true }
|
|
4554
|
+
},
|
|
4555
|
+
wrapHandler(
|
|
4556
|
+
"list_voip_tariff_rule",
|
|
4557
|
+
"listVoipTariffRule",
|
|
4558
|
+
BACKLOG_TOOL_SCOPES["list_voip_tariff_rule"],
|
|
4559
|
+
ctx,
|
|
4560
|
+
async ({ voip_plan_id }, token2) => {
|
|
4561
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4562
|
+
const result2 = await client.call("listVoipTariffRule", voip_plan_id);
|
|
4563
|
+
return {
|
|
4564
|
+
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
4565
|
+
};
|
|
4566
|
+
}
|
|
4567
|
+
)
|
|
4568
|
+
);
|
|
4569
|
+
server2.registerTool(
|
|
4570
|
+
"change_network_profile_of_location_zone",
|
|
4571
|
+
{
|
|
4572
|
+
title: "Change Network Profile of Location Zone",
|
|
4573
|
+
description: "Use this to attach or change the network profile on an existing location zone. A network profile defines sponsor/roaming configuration used when the zone is referenced by package templates. Params: `location_zone_id` (integer from list_detailed_location_zones), `network_profile_id` (integer from list_network_profiles). Returns: OCS confirmation of the updated zone/profile binding. Do NOT use this to create a new location zone \u2014 use `create_location_zone` (which requires networkProfileId at creation time). Do NOT use this for edit/delete of zone TADIG lists \u2014 those remain UI-agent-only (G-19).",
|
|
4574
|
+
inputSchema: {
|
|
4575
|
+
location_zone_id: z3.number().int().positive().describe("Location zone ID (locationZoneId / zoneId from list_detailed_location_zones)"),
|
|
4576
|
+
network_profile_id: z3.number().int().positive().describe("Network profile ID (from list_network_profiles)"),
|
|
4577
|
+
...DRY_RUN_FIELD2
|
|
4578
|
+
},
|
|
4579
|
+
annotations: { destructiveHint: true }
|
|
4580
|
+
},
|
|
4581
|
+
wrapHandler(
|
|
4582
|
+
"change_network_profile_of_location_zone",
|
|
4583
|
+
"changeNetworkProfileOfLocationZone",
|
|
4584
|
+
BACKLOG_TOOL_SCOPES["change_network_profile_of_location_zone"],
|
|
4585
|
+
ctx,
|
|
4586
|
+
async ({
|
|
4587
|
+
location_zone_id,
|
|
4588
|
+
network_profile_id
|
|
4589
|
+
}, token2) => {
|
|
4590
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4591
|
+
const result2 = await client.call("changeNetworkProfileOfLocationZone", {
|
|
4592
|
+
locationZoneId: location_zone_id,
|
|
4593
|
+
networkProfileId: network_profile_id
|
|
4594
|
+
});
|
|
4595
|
+
return {
|
|
4596
|
+
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
4597
|
+
};
|
|
4598
|
+
}
|
|
4599
|
+
)
|
|
4600
|
+
);
|
|
4279
4601
|
}
|
|
4280
4602
|
|
|
4281
4603
|
// src/tools-carrier-ask.ts
|
|
@@ -4299,10 +4621,20 @@ var TOOL_REGISTRY = /* @__PURE__ */ new Set([
|
|
|
4299
4621
|
"high_cost_subscribers",
|
|
4300
4622
|
// event ring buffer (1)
|
|
4301
4623
|
"list_recent_ocs_events",
|
|
4624
|
+
// CAR-39: rate-limit governor (1)
|
|
4625
|
+
"rate_limit_status",
|
|
4626
|
+
// CAR-38: Relay LU country history (1)
|
|
4627
|
+
"subscriber_country_history",
|
|
4628
|
+
// bundle depletion events (1) — CAR-42 wave-2
|
|
4629
|
+
"subscriber_depletion_events",
|
|
4302
4630
|
// MCP App tools (3)
|
|
4303
4631
|
"fleet_health_app",
|
|
4304
4632
|
"provision_esim_wizard",
|
|
4305
4633
|
"balance_topup_form",
|
|
4634
|
+
// greenzone whitelist tools (3) — portal-only, executed via Kapture browser automation
|
|
4635
|
+
"greenzone_whitelist_add",
|
|
4636
|
+
"greenzone_whitelist_remove",
|
|
4637
|
+
"greenzone_whitelist_list",
|
|
4306
4638
|
// router tools (self-reference)
|
|
4307
4639
|
"carrier_ask",
|
|
4308
4640
|
"carrier_ask_describe"
|
|
@@ -4517,8 +4849,22 @@ var ROUTER_TOOLS = [
|
|
|
4517
4849
|
},
|
|
4518
4850
|
{
|
|
4519
4851
|
name: "get_subscriber_location_by_cell_id",
|
|
4520
|
-
description: "Get granular
|
|
4521
|
-
input_schema: {
|
|
4852
|
+
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'.",
|
|
4853
|
+
input_schema: {
|
|
4854
|
+
type: "object",
|
|
4855
|
+
required: ["radio_type", "mcc", "mnc", "lac"],
|
|
4856
|
+
properties: {
|
|
4857
|
+
iccid: { type: "string" },
|
|
4858
|
+
radio_type: {
|
|
4859
|
+
type: "string",
|
|
4860
|
+
description: "One of 2G, 3G, 4G, 5G, NB-IoT. Use 4G for LTE networks. NB-IoT also accepts NBIOT, nbiot, nb-iot."
|
|
4861
|
+
},
|
|
4862
|
+
mcc: { type: "number", description: "Mobile Country Code" },
|
|
4863
|
+
mnc: { type: "number", description: "Mobile Network Code" },
|
|
4864
|
+
lac: { type: "number", description: "Location Area Code" },
|
|
4865
|
+
cell_id: { type: "number", description: "Cell tower ID (optional but recommended)" }
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4522
4868
|
},
|
|
4523
4869
|
{
|
|
4524
4870
|
name: "list_steering_lists",
|
|
@@ -4595,6 +4941,26 @@ var ROUTER_TOOLS = [
|
|
|
4595
4941
|
description: "List recent OCS events for a subscriber (last 50, 24h window). Intent: 'recent events', 'event history', 'OCS log'.",
|
|
4596
4942
|
input_schema: { type: "object", properties: { iccid: { type: "string" } } }
|
|
4597
4943
|
},
|
|
4944
|
+
{
|
|
4945
|
+
name: "rate_limit_status",
|
|
4946
|
+
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'.",
|
|
4947
|
+
input_schema: {
|
|
4948
|
+
type: "object",
|
|
4949
|
+
properties: { endpoint: { type: "string" } }
|
|
4950
|
+
}
|
|
4951
|
+
},
|
|
4952
|
+
{
|
|
4953
|
+
name: "subscriber_country_history",
|
|
4954
|
+
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'.",
|
|
4955
|
+
input_schema: {
|
|
4956
|
+
type: "object",
|
|
4957
|
+
properties: {
|
|
4958
|
+
subscriberId: { type: "string" },
|
|
4959
|
+
limit: { type: "number" }
|
|
4960
|
+
},
|
|
4961
|
+
required: ["subscriberId"]
|
|
4962
|
+
}
|
|
4963
|
+
},
|
|
4598
4964
|
{
|
|
4599
4965
|
name: "affect_subscriber_phone_number",
|
|
4600
4966
|
description: "Assign or unassign a phone number to a subscriber. Intent: 'assign phone number', 'give SIM a number', 'remove number'.",
|
|
@@ -4679,6 +5045,44 @@ var ROUTER_TOOLS = [
|
|
|
4679
5045
|
name: "carrier_ask_describe",
|
|
4680
5046
|
description: "Get documentation for a specific carrier tool. Intent: 'describe tool X', 'how does assign_package work', 'tool documentation'.",
|
|
4681
5047
|
input_schema: { type: "object", properties: { tool_name: { type: "string" } } }
|
|
5048
|
+
},
|
|
5049
|
+
{
|
|
5050
|
+
name: "greenzone_whitelist_add",
|
|
5051
|
+
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'.",
|
|
5052
|
+
input_schema: {
|
|
5053
|
+
type: "object",
|
|
5054
|
+
properties: {
|
|
5055
|
+
host: { type: "string", description: "Hostname to whitelist (e.g. api.example.com)" },
|
|
5056
|
+
ip: { type: "string", description: "IP address to whitelist (optional)" }
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
},
|
|
5060
|
+
{
|
|
5061
|
+
name: "greenzone_whitelist_remove",
|
|
5062
|
+
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'.",
|
|
5063
|
+
input_schema: {
|
|
5064
|
+
type: "object",
|
|
5065
|
+
properties: {
|
|
5066
|
+
host: { type: "string", description: "Hostname to remove" },
|
|
5067
|
+
ip: { type: "string", description: "IP address to remove (optional)" }
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
},
|
|
5071
|
+
{
|
|
5072
|
+
name: "greenzone_whitelist_list",
|
|
5073
|
+
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'.",
|
|
5074
|
+
input_schema: { type: "object", properties: {} }
|
|
5075
|
+
},
|
|
5076
|
+
{
|
|
5077
|
+
name: "subscriber_depletion_events",
|
|
5078
|
+
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'.",
|
|
5079
|
+
input_schema: {
|
|
5080
|
+
type: "object",
|
|
5081
|
+
properties: {
|
|
5082
|
+
subscriberId: { type: "string", description: "OCS subscriber ID \u2014 omit to list fleet-wide depletions" },
|
|
5083
|
+
since: { type: "string", description: "ISO-8601 timestamp \u2014 filter to events at or after this time" }
|
|
5084
|
+
}
|
|
5085
|
+
}
|
|
4682
5086
|
}
|
|
4683
5087
|
];
|
|
4684
5088
|
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 +5115,10 @@ async function callBedrock(env, payload) {
|
|
|
4711
5115
|
const body = await resp.text();
|
|
4712
5116
|
if (resp.status === 429) {
|
|
4713
5117
|
const retryAfter = resp.headers.get("retry-after");
|
|
4714
|
-
const
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
throw
|
|
5118
|
+
const err4 = new Error(`Bedrock rate limit: ${body}`);
|
|
5119
|
+
err4.isRateLimit = true;
|
|
5120
|
+
err4.retryAfter = retryAfter;
|
|
5121
|
+
throw err4;
|
|
4718
5122
|
}
|
|
4719
5123
|
throw new Error(`Bedrock invoke failed: ${resp.status} ${body}`);
|
|
4720
5124
|
}
|
|
@@ -4752,9 +5156,9 @@ Pre-resolved context: ${JSON.stringify(definedContext)}` : "";
|
|
|
4752
5156
|
AWS_REGION: env.AWS_REGION,
|
|
4753
5157
|
BEDROCK_MODEL_ID: env.BEDROCK_MODEL_ID
|
|
4754
5158
|
}, payload);
|
|
4755
|
-
} catch (
|
|
4756
|
-
if (
|
|
4757
|
-
const retryAfter =
|
|
5159
|
+
} catch (err4) {
|
|
5160
|
+
if (err4 instanceof Error && err4.isRateLimit) {
|
|
5161
|
+
const retryAfter = err4.retryAfter;
|
|
4758
5162
|
const parsedSeconds = retryAfter ? parseInt(retryAfter, 10) : 60;
|
|
4759
5163
|
return {
|
|
4760
5164
|
match: "rate_limited",
|
|
@@ -4762,7 +5166,7 @@ Pre-resolved context: ${JSON.stringify(definedContext)}` : "";
|
|
|
4762
5166
|
suggestion: "Bedrock rate limit reached. Please retry after the indicated delay."
|
|
4763
5167
|
};
|
|
4764
5168
|
}
|
|
4765
|
-
throw
|
|
5169
|
+
throw err4;
|
|
4766
5170
|
}
|
|
4767
5171
|
const toolUseBlock = response.content.find(
|
|
4768
5172
|
(block) => block.type === "tool_use"
|
|
@@ -4936,7 +5340,7 @@ function registerAllCarrierAskTools(server2, ctx) {
|
|
|
4936
5340
|
let route;
|
|
4937
5341
|
try {
|
|
4938
5342
|
route = await _routeIntent(intent, context, ctx.env);
|
|
4939
|
-
} catch (
|
|
5343
|
+
} catch (err4) {
|
|
4940
5344
|
writeCarrierAskAudit(ctx.env, {
|
|
4941
5345
|
intent_hash: intentHash,
|
|
4942
5346
|
match: "error",
|
|
@@ -4953,7 +5357,7 @@ function registerAllCarrierAskTools(server2, ctx) {
|
|
|
4953
5357
|
type: "text",
|
|
4954
5358
|
text: JSON.stringify({
|
|
4955
5359
|
error: "routing_error",
|
|
4956
|
-
message:
|
|
5360
|
+
message: err4 instanceof Error ? err4.message : "An unexpected error occurred during routing."
|
|
4957
5361
|
})
|
|
4958
5362
|
}
|
|
4959
5363
|
],
|
|
@@ -5095,24 +5499,10 @@ function buildExamples(toolName) {
|
|
|
5095
5499
|
|
|
5096
5500
|
// src/list-recent-ocs-events.ts
|
|
5097
5501
|
import { z as z5 } from "zod";
|
|
5098
|
-
|
|
5099
|
-
|
|
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
|
-
};
|
|
5502
|
+
|
|
5503
|
+
// ../../packages/ocs-spec/src/ocs-event-buffer-read.ts
|
|
5111
5504
|
async function listRecentOcsEvents(iccid, limit, eventTypes, since, kv) {
|
|
5112
|
-
const routingRaw = await kv.get(
|
|
5113
|
-
`iccid:${iccid}`,
|
|
5114
|
-
"json"
|
|
5115
|
-
);
|
|
5505
|
+
const routingRaw = await kv.get(`iccid:${iccid}`, "json");
|
|
5116
5506
|
const resellerId = routingRaw?.reseller_id ?? 0;
|
|
5117
5507
|
const ringKey = `events:${resellerId}:${iccid}`;
|
|
5118
5508
|
const itemPrefix = `${ringKey}:evt:`;
|
|
@@ -5148,8 +5538,7 @@ async function listRecentOcsEvents(iccid, limit, eventTypes, since, kv) {
|
|
|
5148
5538
|
const sinceMs = since ? new Date(since).getTime() : null;
|
|
5149
5539
|
const filtered = allSorted.filter((e) => {
|
|
5150
5540
|
if (sinceMs !== null && e.occurred_at * 1e3 <= sinceMs) return false;
|
|
5151
|
-
if (eventTypes && eventTypes.length > 0 && !eventTypes.includes(e.event_type))
|
|
5152
|
-
return false;
|
|
5541
|
+
if (eventTypes && eventTypes.length > 0 && !eventTypes.includes(e.event_type)) return false;
|
|
5153
5542
|
return true;
|
|
5154
5543
|
});
|
|
5155
5544
|
const filteredCount = filtered.length;
|
|
@@ -5175,6 +5564,25 @@ async function listRecentOcsEvents(iccid, limit, eventTypes, since, kv) {
|
|
|
5175
5564
|
buffer_newest_event_timestamp: bufferNewest
|
|
5176
5565
|
};
|
|
5177
5566
|
}
|
|
5567
|
+
|
|
5568
|
+
// src/list-recent-ocs-events.ts
|
|
5569
|
+
var ALLOWED_EVENT_TYPES = [
|
|
5570
|
+
"esim.activated",
|
|
5571
|
+
"esim.disabled",
|
|
5572
|
+
"package.expiry_warning",
|
|
5573
|
+
"location.changed",
|
|
5574
|
+
"country.entered",
|
|
5575
|
+
"balance.low"
|
|
5576
|
+
];
|
|
5577
|
+
var listRecentOcsEventsSchema = {
|
|
5578
|
+
iccid: z5.string().regex(/^\d{19,20}$/).describe("ICCID, 19 or 20 digits, ITU-T E.118 format"),
|
|
5579
|
+
limit: z5.number().int().min(1).max(50).default(20).describe("Max events to return, newest first"),
|
|
5580
|
+
event_types: z5.array(z5.enum(ALLOWED_EVENT_TYPES)).optional().describe("Filter to specific event types"),
|
|
5581
|
+
since: z5.string().datetime().optional().describe("ISO-8601 timestamp; only events after this point")
|
|
5582
|
+
};
|
|
5583
|
+
async function listRecentOcsEvents2(iccid, limit, eventTypes, since, kv) {
|
|
5584
|
+
return listRecentOcsEvents(iccid, limit, eventTypes, since, kv);
|
|
5585
|
+
}
|
|
5178
5586
|
function registerListRecentOcsEventsTool(server2, env) {
|
|
5179
5587
|
server2.registerTool(
|
|
5180
5588
|
"list_recent_ocs_events",
|
|
@@ -5191,7 +5599,7 @@ function registerListRecentOcsEventsTool(server2, env) {
|
|
|
5191
5599
|
async (args) => {
|
|
5192
5600
|
const { iccid, limit, event_types, since } = args;
|
|
5193
5601
|
try {
|
|
5194
|
-
const result2 = await
|
|
5602
|
+
const result2 = await listRecentOcsEvents2(
|
|
5195
5603
|
iccid,
|
|
5196
5604
|
limit,
|
|
5197
5605
|
event_types,
|
|
@@ -5201,8 +5609,8 @@ function registerListRecentOcsEventsTool(server2, env) {
|
|
|
5201
5609
|
return {
|
|
5202
5610
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
5203
5611
|
};
|
|
5204
|
-
} catch (
|
|
5205
|
-
const message =
|
|
5612
|
+
} catch (err4) {
|
|
5613
|
+
const message = err4 instanceof Error ? err4.message : String(err4);
|
|
5206
5614
|
return {
|
|
5207
5615
|
isError: true,
|
|
5208
5616
|
content: [{ type: "text", text: `Error reading OCS event buffer: ${message}` }]
|
|
@@ -5212,8 +5620,271 @@ function registerListRecentOcsEventsTool(server2, env) {
|
|
|
5212
5620
|
);
|
|
5213
5621
|
}
|
|
5214
5622
|
|
|
5215
|
-
// src/
|
|
5623
|
+
// src/tools-rate-governor.ts
|
|
5216
5624
|
import { z as z6 } from "zod";
|
|
5625
|
+
var MONITORED_ENDPOINTS = [
|
|
5626
|
+
"subscriberNetworkEventsOverPeriod",
|
|
5627
|
+
// 30/min
|
|
5628
|
+
"subscriberUsageOverPeriod",
|
|
5629
|
+
// 100/min
|
|
5630
|
+
"listLocationZoneElement",
|
|
5631
|
+
// 150/min
|
|
5632
|
+
"sendMtSms",
|
|
5633
|
+
// 150/min
|
|
5634
|
+
"modifySubscriberMobilePlan",
|
|
5635
|
+
// 300/min
|
|
5636
|
+
"affectRecurringPackageToSubscriber",
|
|
5637
|
+
// 300/min
|
|
5638
|
+
"listSubscriber",
|
|
5639
|
+
// 300/min
|
|
5640
|
+
"listSubscriberPrepaidPackages",
|
|
5641
|
+
// 300/min
|
|
5642
|
+
"getSingleSubscriber",
|
|
5643
|
+
// 300/min
|
|
5644
|
+
"getSubscriberLocationByCellId",
|
|
5645
|
+
// 300/min
|
|
5646
|
+
"__global__"
|
|
5647
|
+
// 600/min
|
|
5648
|
+
];
|
|
5649
|
+
function registerRateLimitStatusTool(server2, ctx) {
|
|
5650
|
+
server2.registerTool(
|
|
5651
|
+
"rate_limit_status",
|
|
5652
|
+
{
|
|
5653
|
+
title: "API Rate-Limit Governor Status",
|
|
5654
|
+
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.",
|
|
5655
|
+
inputSchema: {
|
|
5656
|
+
endpoint: z6.string().optional().describe(
|
|
5657
|
+
"OCS method name in camelCase (e.g. 'subscriberUsageOverPeriod'). Omit to return status for all monitored endpoints."
|
|
5658
|
+
)
|
|
5659
|
+
},
|
|
5660
|
+
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
5661
|
+
},
|
|
5662
|
+
wrapHandler(
|
|
5663
|
+
"rate_limit_status",
|
|
5664
|
+
"[rate-governor:in-process]",
|
|
5665
|
+
"read",
|
|
5666
|
+
ctx,
|
|
5667
|
+
async ({ endpoint }, token2) => {
|
|
5668
|
+
const targets = endpoint ? [endpoint] : MONITORED_ENDPOINTS;
|
|
5669
|
+
const rows = targets.map((ep) => {
|
|
5670
|
+
const limit = getLimitForEndpoint(ep);
|
|
5671
|
+
const batch_cap_per_min = Math.floor(limit * 0.2);
|
|
5672
|
+
const { calls_in_window, batch_calls_in_window } = getRateLimitWindowCounts(token2, ep);
|
|
5673
|
+
const utilisation_pct = limit > 0 ? Math.round(calls_in_window / limit * 100) : 0;
|
|
5674
|
+
const alert = utilisation_pct >= 80;
|
|
5675
|
+
return {
|
|
5676
|
+
endpoint: ep,
|
|
5677
|
+
limit_per_min: limit,
|
|
5678
|
+
batch_cap_per_min,
|
|
5679
|
+
calls_in_window,
|
|
5680
|
+
batch_calls_in_window,
|
|
5681
|
+
utilisation_pct,
|
|
5682
|
+
alert
|
|
5683
|
+
};
|
|
5684
|
+
});
|
|
5685
|
+
const resellerId = String(ctx.props.reseller_id);
|
|
5686
|
+
return {
|
|
5687
|
+
content: [
|
|
5688
|
+
{
|
|
5689
|
+
type: "text",
|
|
5690
|
+
text: JSON.stringify(
|
|
5691
|
+
{
|
|
5692
|
+
reseller_id: resellerId,
|
|
5693
|
+
window_seconds: 60,
|
|
5694
|
+
batch_fraction: 0.2,
|
|
5695
|
+
endpoints: rows,
|
|
5696
|
+
alert_threshold_pct: 80,
|
|
5697
|
+
metric_name: "carrier_ocs_calls_per_min",
|
|
5698
|
+
metric_location: "CF Workers Tail / BetterStack \u2014 structured JSON log lines with metric='carrier_ocs_calls_per_min'"
|
|
5699
|
+
},
|
|
5700
|
+
null,
|
|
5701
|
+
2
|
|
5702
|
+
)
|
|
5703
|
+
}
|
|
5704
|
+
]
|
|
5705
|
+
};
|
|
5706
|
+
}
|
|
5707
|
+
)
|
|
5708
|
+
);
|
|
5709
|
+
}
|
|
5710
|
+
|
|
5711
|
+
// src/tools-depletion-events.ts
|
|
5712
|
+
import { z as z7 } from "zod";
|
|
5713
|
+
function ok(payload) {
|
|
5714
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
5715
|
+
}
|
|
5716
|
+
function err(message) {
|
|
5717
|
+
return { isError: true, content: [{ type: "text", text: message }] };
|
|
5718
|
+
}
|
|
5719
|
+
function registerDepletionEventsTool(server2, ctx) {
|
|
5720
|
+
server2.registerTool(
|
|
5721
|
+
"subscriber_depletion_events",
|
|
5722
|
+
{
|
|
5723
|
+
title: "Subscriber Bundle Depletion Events",
|
|
5724
|
+
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.",
|
|
5725
|
+
inputSchema: {
|
|
5726
|
+
subscriberId: z7.string().optional().describe(
|
|
5727
|
+
"OCS subscriber ID to look up. Omit to list the 20 most recent fleet-wide depletions."
|
|
5728
|
+
),
|
|
5729
|
+
since: z7.string().optional().describe(
|
|
5730
|
+
"ISO-8601 timestamp \u2014 only return events at or after this time (for single-subscriber lookup)."
|
|
5731
|
+
)
|
|
5732
|
+
},
|
|
5733
|
+
annotations: { readOnlyHint: true }
|
|
5734
|
+
},
|
|
5735
|
+
async (args) => {
|
|
5736
|
+
if (!ctx.props.scope.includes("read")) {
|
|
5737
|
+
return err(
|
|
5738
|
+
`Scope denied: subscriber_depletion_events requires 'read' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
5739
|
+
);
|
|
5740
|
+
}
|
|
5741
|
+
const kv = ctx.env.RATE_LIMIT_KV;
|
|
5742
|
+
if (!kv) {
|
|
5743
|
+
return err(
|
|
5744
|
+
"RATE_LIMIT_KV binding is not available in this environment. Add the binding to wrangler.jsonc and redeploy."
|
|
5745
|
+
);
|
|
5746
|
+
}
|
|
5747
|
+
const { subscriberId, since } = args;
|
|
5748
|
+
if (subscriberId) {
|
|
5749
|
+
const raw = await kv.get(`bundle-depleted:${subscriberId}`, "text");
|
|
5750
|
+
if (!raw) {
|
|
5751
|
+
return ok({
|
|
5752
|
+
subscriber_id: subscriberId,
|
|
5753
|
+
depleted: false,
|
|
5754
|
+
event: null,
|
|
5755
|
+
note: "No bundle depletion event found in the last 7 days."
|
|
5756
|
+
});
|
|
5757
|
+
}
|
|
5758
|
+
let event;
|
|
5759
|
+
try {
|
|
5760
|
+
event = JSON.parse(raw);
|
|
5761
|
+
} catch {
|
|
5762
|
+
return ok({
|
|
5763
|
+
subscriber_id: subscriberId,
|
|
5764
|
+
depleted: false,
|
|
5765
|
+
event: null,
|
|
5766
|
+
note: "Stored depletion entry was invalid JSON and was ignored."
|
|
5767
|
+
});
|
|
5768
|
+
}
|
|
5769
|
+
if (since && event.depletedAt < since) {
|
|
5770
|
+
return ok({
|
|
5771
|
+
subscriber_id: subscriberId,
|
|
5772
|
+
depleted: false,
|
|
5773
|
+
event: null,
|
|
5774
|
+
note: `No bundle depletion after ${since}.`
|
|
5775
|
+
});
|
|
5776
|
+
}
|
|
5777
|
+
return ok({
|
|
5778
|
+
subscriber_id: subscriberId,
|
|
5779
|
+
depleted: true,
|
|
5780
|
+
event: {
|
|
5781
|
+
subscriber_id: event.subscriberId,
|
|
5782
|
+
iccid: event.iccid,
|
|
5783
|
+
msisdn: event.msisdn,
|
|
5784
|
+
plan_id: event.planId,
|
|
5785
|
+
depleted_at: event.depletedAt
|
|
5786
|
+
}
|
|
5787
|
+
});
|
|
5788
|
+
}
|
|
5789
|
+
const listResult = await kv.list({ prefix: "bundle-depleted:", limit: 20 });
|
|
5790
|
+
if (listResult.keys.length === 0) {
|
|
5791
|
+
return ok({
|
|
5792
|
+
depletions: [],
|
|
5793
|
+
total: 0,
|
|
5794
|
+
note: "No bundle depletion events in the last 7 days."
|
|
5795
|
+
});
|
|
5796
|
+
}
|
|
5797
|
+
const events = [];
|
|
5798
|
+
await Promise.all(
|
|
5799
|
+
listResult.keys.map(async (k) => {
|
|
5800
|
+
const raw = await kv.get(k.name, "text");
|
|
5801
|
+
if (!raw) return;
|
|
5802
|
+
try {
|
|
5803
|
+
const ev = JSON.parse(raw);
|
|
5804
|
+
if (since && ev.depletedAt < since) return;
|
|
5805
|
+
events.push({
|
|
5806
|
+
subscriber_id: ev.subscriberId,
|
|
5807
|
+
iccid: ev.iccid,
|
|
5808
|
+
msisdn: ev.msisdn,
|
|
5809
|
+
plan_id: ev.planId,
|
|
5810
|
+
depleted_at: ev.depletedAt
|
|
5811
|
+
});
|
|
5812
|
+
} catch {
|
|
5813
|
+
}
|
|
5814
|
+
})
|
|
5815
|
+
);
|
|
5816
|
+
events.sort((a, b) => b.depleted_at.localeCompare(a.depleted_at));
|
|
5817
|
+
return ok({
|
|
5818
|
+
depletions: events,
|
|
5819
|
+
total: events.length,
|
|
5820
|
+
list_truncated: !listResult.list_complete
|
|
5821
|
+
});
|
|
5822
|
+
}
|
|
5823
|
+
);
|
|
5824
|
+
}
|
|
5825
|
+
|
|
5826
|
+
// src/tools-country-history.ts
|
|
5827
|
+
import { z as z8 } from "zod";
|
|
5828
|
+
function registerCountryHistoryTool(server2, ctx) {
|
|
5829
|
+
server2.registerTool(
|
|
5830
|
+
"subscriber_country_history",
|
|
5831
|
+
{
|
|
5832
|
+
title: "Subscriber Country History",
|
|
5833
|
+
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.",
|
|
5834
|
+
inputSchema: {
|
|
5835
|
+
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)"),
|
|
5836
|
+
limit: z8.number().int().min(1).max(50).default(20).optional().describe("Maximum number of events to return, newest first (default 20, max 50)")
|
|
5837
|
+
},
|
|
5838
|
+
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
5839
|
+
},
|
|
5840
|
+
wrapHandler(
|
|
5841
|
+
"subscriber_country_history",
|
|
5842
|
+
"[country-history:kv]",
|
|
5843
|
+
"read",
|
|
5844
|
+
ctx,
|
|
5845
|
+
async ({ subscriberId, limit }) => {
|
|
5846
|
+
const effectiveLimit = limit ?? 20;
|
|
5847
|
+
const result2 = await listRecentOcsEvents2(
|
|
5848
|
+
subscriberId,
|
|
5849
|
+
effectiveLimit,
|
|
5850
|
+
["country.entered"],
|
|
5851
|
+
void 0,
|
|
5852
|
+
ctx.env.OCS_EVENT_ROUTING
|
|
5853
|
+
);
|
|
5854
|
+
const countryEvents = result2.events;
|
|
5855
|
+
return {
|
|
5856
|
+
content: [
|
|
5857
|
+
{
|
|
5858
|
+
type: "text",
|
|
5859
|
+
text: JSON.stringify(
|
|
5860
|
+
{
|
|
5861
|
+
subscriber_id: subscriberId,
|
|
5862
|
+
country_events: countryEvents.map((e) => ({
|
|
5863
|
+
event_id: e.event_id,
|
|
5864
|
+
new_country: e.data["new_country"] ?? null,
|
|
5865
|
+
previous_country: e.data["previous_country"] ?? null,
|
|
5866
|
+
mcc: e.data["mcc"] ?? null,
|
|
5867
|
+
mnc: e.data["mnc"] ?? null,
|
|
5868
|
+
timestamp: e.timestamp,
|
|
5869
|
+
source: e.data["source"] ?? "relay-lu"
|
|
5870
|
+
})),
|
|
5871
|
+
total_country_events: countryEvents.length,
|
|
5872
|
+
total_in_buffer: result2.total_in_buffer,
|
|
5873
|
+
note: countryEvents.length === 0 ? "No cross-border events recorded in the 24h ring-buffer for this subscriber." : void 0
|
|
5874
|
+
},
|
|
5875
|
+
null,
|
|
5876
|
+
2
|
|
5877
|
+
)
|
|
5878
|
+
}
|
|
5879
|
+
]
|
|
5880
|
+
};
|
|
5881
|
+
}
|
|
5882
|
+
)
|
|
5883
|
+
);
|
|
5884
|
+
}
|
|
5885
|
+
|
|
5886
|
+
// src/apps/fleet-health-app.ts
|
|
5887
|
+
import { z as z9 } from "zod";
|
|
5217
5888
|
import {
|
|
5218
5889
|
registerAppTool,
|
|
5219
5890
|
registerAppResource,
|
|
@@ -5222,10 +5893,10 @@ import {
|
|
|
5222
5893
|
async function safeCallWithToken(client, _token, method, params = {}) {
|
|
5223
5894
|
try {
|
|
5224
5895
|
return { data: await client.call(method, params), error: null };
|
|
5225
|
-
} catch (
|
|
5896
|
+
} catch (err4) {
|
|
5226
5897
|
return {
|
|
5227
5898
|
data: null,
|
|
5228
|
-
error:
|
|
5899
|
+
error: err4 instanceof Error ? err4.message : String(err4)
|
|
5229
5900
|
};
|
|
5230
5901
|
}
|
|
5231
5902
|
}
|
|
@@ -5272,7 +5943,7 @@ function registerFleetHealthApp(server2, ctx) {
|
|
|
5272
5943
|
title: "Fleet Health Dashboard",
|
|
5273
5944
|
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
5945
|
inputSchema: {
|
|
5275
|
-
accountId:
|
|
5946
|
+
accountId: z9.number().optional().describe("Filter to a specific account (omit for all)")
|
|
5276
5947
|
},
|
|
5277
5948
|
annotations: { readOnlyHint: true },
|
|
5278
5949
|
_meta: {
|
|
@@ -5284,7 +5955,7 @@ function registerFleetHealthApp(server2, ctx) {
|
|
|
5284
5955
|
},
|
|
5285
5956
|
async ({ accountId }) => {
|
|
5286
5957
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
5287
|
-
const client = new
|
|
5958
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
5288
5959
|
const [statusResult, accountsResult] = await Promise.all([
|
|
5289
5960
|
safeCallWithToken(
|
|
5290
5961
|
client,
|
|
@@ -5370,7 +6041,7 @@ function registerFleetHealthApp(server2, ctx) {
|
|
|
5370
6041
|
}
|
|
5371
6042
|
|
|
5372
6043
|
// src/apps/provisioning-wizard.ts
|
|
5373
|
-
import { z as
|
|
6044
|
+
import { z as z10 } from "zod";
|
|
5374
6045
|
import {
|
|
5375
6046
|
registerAppTool as registerAppTool2,
|
|
5376
6047
|
registerAppResource as registerAppResource2,
|
|
@@ -5423,10 +6094,10 @@ async function deleteWizardSession(env, sub, wizardId) {
|
|
|
5423
6094
|
async function safeCallWithToken2(client, _token, method, params = {}) {
|
|
5424
6095
|
try {
|
|
5425
6096
|
return { data: await client.call(method, params), error: null };
|
|
5426
|
-
} catch (
|
|
6097
|
+
} catch (err4) {
|
|
5427
6098
|
return {
|
|
5428
6099
|
data: null,
|
|
5429
|
-
error:
|
|
6100
|
+
error: err4 instanceof Error ? err4.message : String(err4)
|
|
5430
6101
|
};
|
|
5431
6102
|
}
|
|
5432
6103
|
}
|
|
@@ -5475,10 +6146,10 @@ function registerProvisioningWizard(server2, ctx) {
|
|
|
5475
6146
|
title: "eSIM Provisioning Wizard",
|
|
5476
6147
|
description: "Interactive 3-step wizard to provision an eSIM: select subscriber, choose package template, preview (dry_run) and confirm execution.",
|
|
5477
6148
|
inputSchema: {
|
|
5478
|
-
step:
|
|
5479
|
-
wizardId:
|
|
5480
|
-
subscriber_iccid:
|
|
5481
|
-
package_template_id:
|
|
6149
|
+
step: z10.enum(["init", "select-package", "preview", "confirm"]).describe("Current wizard step"),
|
|
6150
|
+
wizardId: z10.string().optional().describe("Wizard session ID (absent on init)"),
|
|
6151
|
+
subscriber_iccid: z10.string().optional().describe("Subscriber ICCID (required for select-package)"),
|
|
6152
|
+
package_template_id: z10.number().optional().describe("Package template ID (required for preview)")
|
|
5482
6153
|
},
|
|
5483
6154
|
_meta: {
|
|
5484
6155
|
ui: {
|
|
@@ -5489,7 +6160,7 @@ function registerProvisioningWizard(server2, ctx) {
|
|
|
5489
6160
|
},
|
|
5490
6161
|
async ({ step, wizardId, subscriber_iccid, package_template_id }) => {
|
|
5491
6162
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
5492
|
-
const client = new
|
|
6163
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
5493
6164
|
if (step === "init") {
|
|
5494
6165
|
const newWizardId = generateWizardId();
|
|
5495
6166
|
const subscribersResult = await safeCallWithToken2(
|
|
@@ -5777,7 +6448,7 @@ function registerProvisioningWizard(server2, ctx) {
|
|
|
5777
6448
|
}
|
|
5778
6449
|
|
|
5779
6450
|
// src/apps/balance-topup.ts
|
|
5780
|
-
import { z as
|
|
6451
|
+
import { z as z11 } from "zod";
|
|
5781
6452
|
import {
|
|
5782
6453
|
registerAppTool as registerAppTool3,
|
|
5783
6454
|
registerAppResource as registerAppResource3,
|
|
@@ -5786,10 +6457,10 @@ import {
|
|
|
5786
6457
|
async function safeCallWithToken3(client, _token, method, params = {}) {
|
|
5787
6458
|
try {
|
|
5788
6459
|
return { data: await client.call(method, params), error: null };
|
|
5789
|
-
} catch (
|
|
6460
|
+
} catch (err4) {
|
|
5790
6461
|
return {
|
|
5791
6462
|
data: null,
|
|
5792
|
-
error:
|
|
6463
|
+
error: err4 instanceof Error ? err4.message : String(err4)
|
|
5793
6464
|
};
|
|
5794
6465
|
}
|
|
5795
6466
|
}
|
|
@@ -5830,9 +6501,9 @@ function registerBalanceTopupApp(server2, ctx) {
|
|
|
5830
6501
|
title: "Balance Top-up Form",
|
|
5831
6502
|
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
6503
|
inputSchema: {
|
|
5833
|
-
iccid:
|
|
5834
|
-
delta:
|
|
5835
|
-
preview:
|
|
6504
|
+
iccid: z11.string().describe("The subscriber ICCID for the account lookup"),
|
|
6505
|
+
delta: z11.number().describe("Amount to add (positive) or deduct (negative) from the account balance"),
|
|
6506
|
+
preview: z11.boolean().default(true).describe("If true, return preview without writing. If false, execute the balance change.")
|
|
5836
6507
|
},
|
|
5837
6508
|
annotations: { destructiveHint: true },
|
|
5838
6509
|
_meta: {
|
|
@@ -5849,7 +6520,7 @@ function registerBalanceTopupApp(server2, ctx) {
|
|
|
5849
6520
|
ctx,
|
|
5850
6521
|
async ({ iccid, delta, preview }) => {
|
|
5851
6522
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
5852
|
-
const client = new
|
|
6523
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
5853
6524
|
if (preview === true) {
|
|
5854
6525
|
const result2 = await safeCallWithToken3(
|
|
5855
6526
|
client,
|
|
@@ -5879,8 +6550,10 @@ function registerBalanceTopupApp(server2, ctx) {
|
|
|
5879
6550
|
const execResult = await safeCallWithToken3(
|
|
5880
6551
|
client,
|
|
5881
6552
|
token2,
|
|
5882
|
-
|
|
5883
|
-
{
|
|
6553
|
+
// CAR-78: subscriber top-up uses modifySubscriberBalance with { subscriber, amount }
|
|
6554
|
+
// (delta). modifyAccountBalance is account-level and expects { accountId, amount, mode }.
|
|
6555
|
+
"modifySubscriberBalance",
|
|
6556
|
+
{ subscriber: iccid, amount: delta }
|
|
5884
6557
|
);
|
|
5885
6558
|
if (execResult.error) {
|
|
5886
6559
|
return {
|
|
@@ -5913,7 +6586,7 @@ function registerAllApps(server2, ctx) {
|
|
|
5913
6586
|
}
|
|
5914
6587
|
|
|
5915
6588
|
// src/prompts.ts
|
|
5916
|
-
import { z as
|
|
6589
|
+
import { z as z12 } from "zod";
|
|
5917
6590
|
function registerAllPrompts(server2) {
|
|
5918
6591
|
server2.registerPrompt(
|
|
5919
6592
|
"fleet_health_report",
|
|
@@ -5949,7 +6622,7 @@ Format as a structured report with sections, tables, and actionable recommendati
|
|
|
5949
6622
|
{
|
|
5950
6623
|
title: "Subscriber Deep Dive",
|
|
5951
6624
|
description: "Comprehensive analysis of a single subscriber: profile, packages, usage patterns, location history, recommendations.",
|
|
5952
|
-
argsSchema: { iccid:
|
|
6625
|
+
argsSchema: { iccid: z12.string().describe("The subscriber ICCID to analyse") }
|
|
5953
6626
|
},
|
|
5954
6627
|
async ({ iccid }) => ({
|
|
5955
6628
|
messages: [
|
|
@@ -6048,7 +6721,7 @@ Analyze and present:
|
|
|
6048
6721
|
{
|
|
6049
6722
|
title: "Bulk Operations Planner",
|
|
6050
6723
|
description: "Plan bulk operations safely: mass package assignments, account migrations, balance adjustments, or status changes.",
|
|
6051
|
-
argsSchema: { operation:
|
|
6724
|
+
argsSchema: { operation: z12.string().describe("Describe the bulk operation you want to perform") }
|
|
6052
6725
|
},
|
|
6053
6726
|
async ({ operation }) => ({
|
|
6054
6727
|
messages: [
|
|
@@ -6081,7 +6754,7 @@ NEVER execute destructive operations without confirmation.`
|
|
|
6081
6754
|
}
|
|
6082
6755
|
|
|
6083
6756
|
// src/tools-pricing.ts
|
|
6084
|
-
import { z as
|
|
6757
|
+
import { z as z13 } from "zod";
|
|
6085
6758
|
|
|
6086
6759
|
// src/credits.ts
|
|
6087
6760
|
var TIER_CREDIT_ALLOTMENTS = {
|
|
@@ -6832,7 +7505,7 @@ var CARRIER_SERVICE_CATALOG = [
|
|
|
6832
7505
|
id: "mcp",
|
|
6833
7506
|
name: "Carrier MCP",
|
|
6834
7507
|
category: "connectivity",
|
|
6835
|
-
description: "Model Context Protocol server \u2014
|
|
7508
|
+
description: "Model Context Protocol server \u2014 113 natural-language tools for MVNO/eSIM fleet management",
|
|
6836
7509
|
tier_required: "free",
|
|
6837
7510
|
scopes_required: ["read"],
|
|
6838
7511
|
endpoints: ["https://mcp.carrier.llc/mcp"],
|
|
@@ -6906,7 +7579,7 @@ var CARRIER_SERVICE_CATALOG = [
|
|
|
6906
7579
|
tier_required: "free",
|
|
6907
7580
|
scopes_required: ["read"],
|
|
6908
7581
|
endpoints: ["https://app.carrier.llc"],
|
|
6909
|
-
docs_url: "https://
|
|
7582
|
+
docs_url: "https://carrier.llc/docs"
|
|
6910
7583
|
}
|
|
6911
7584
|
];
|
|
6912
7585
|
var PROJECTS_TOOLS = [
|
|
@@ -7139,7 +7812,7 @@ var PROJECTS_TOOLS = [
|
|
|
7139
7812
|
auth_method: props2.auth_method ?? "oauth"
|
|
7140
7813
|
},
|
|
7141
7814
|
capabilities: {
|
|
7142
|
-
total_tools:
|
|
7815
|
+
total_tools: 113,
|
|
7143
7816
|
// full tool count
|
|
7144
7817
|
read_tools: 35,
|
|
7145
7818
|
write_tools: 20,
|
|
@@ -7263,7 +7936,7 @@ function generateCredentialRecommendations(hasToken, tokenAgeDays) {
|
|
|
7263
7936
|
const recommendations = [];
|
|
7264
7937
|
if (!hasToken) {
|
|
7265
7938
|
recommendations.push(
|
|
7266
|
-
"No eSIMVault token configured. Complete setup at https://app.carrier.llc/
|
|
7939
|
+
"No eSIMVault token configured. Complete setup at https://app.carrier.llc/onboarding"
|
|
7267
7940
|
);
|
|
7268
7941
|
}
|
|
7269
7942
|
if (tokenAgeDays !== null && tokenAgeDays > 90) {
|
|
@@ -7448,8 +8121,8 @@ function buildPricingHandler(tool, ctx) {
|
|
|
7448
8121
|
}
|
|
7449
8122
|
]
|
|
7450
8123
|
};
|
|
7451
|
-
} catch (
|
|
7452
|
-
const message =
|
|
8124
|
+
} catch (err4) {
|
|
8125
|
+
const message = err4 instanceof Error ? err4.message : String(err4);
|
|
7453
8126
|
ctx.audit({
|
|
7454
8127
|
tool_name: tool.name,
|
|
7455
8128
|
ocs_method: "billing",
|
|
@@ -7473,19 +8146,19 @@ function buildZodSchema(schema) {
|
|
|
7473
8146
|
for (const [key, prop] of Object.entries(properties)) {
|
|
7474
8147
|
let field;
|
|
7475
8148
|
if (prop.enum) {
|
|
7476
|
-
field =
|
|
8149
|
+
field = z13.enum(prop.enum);
|
|
7477
8150
|
} else if (prop.type === "number") {
|
|
7478
|
-
field =
|
|
8151
|
+
field = z13.number();
|
|
7479
8152
|
} else if (prop.type === "boolean") {
|
|
7480
|
-
field =
|
|
8153
|
+
field = z13.boolean();
|
|
7481
8154
|
} else if (prop.type === "array") {
|
|
7482
8155
|
if (prop.items?.type === "number") {
|
|
7483
|
-
field =
|
|
8156
|
+
field = z13.array(z13.number());
|
|
7484
8157
|
} else {
|
|
7485
|
-
field =
|
|
8158
|
+
field = z13.array(z13.string());
|
|
7486
8159
|
}
|
|
7487
8160
|
} else {
|
|
7488
|
-
field =
|
|
8161
|
+
field = z13.string();
|
|
7489
8162
|
}
|
|
7490
8163
|
const required = schema.required ?? [];
|
|
7491
8164
|
if (!required.includes(key)) {
|
|
@@ -7500,7 +8173,7 @@ function buildZodSchema(schema) {
|
|
|
7500
8173
|
}
|
|
7501
8174
|
|
|
7502
8175
|
// src/tools-ui-agent-schedule.ts
|
|
7503
|
-
import { z as
|
|
8176
|
+
import { z as z14 } from "zod";
|
|
7504
8177
|
|
|
7505
8178
|
// src/manus-common.ts
|
|
7506
8179
|
var MANUS_API_BASE = "https://api.manus.ai/v2";
|
|
@@ -7769,8 +8442,8 @@ function writeUsageThresholdAudit(env, severity, remainingCredits, month, taskCo
|
|
|
7769
8442
|
doubles: [remainingCredits],
|
|
7770
8443
|
indexes: ["manus_usage"]
|
|
7771
8444
|
});
|
|
7772
|
-
} catch (
|
|
7773
|
-
console.error(`[manus-usage] threshold audit write failed: ${
|
|
8445
|
+
} catch (err4) {
|
|
8446
|
+
console.error(`[manus-usage] threshold audit write failed: ${err4.message}`);
|
|
7774
8447
|
}
|
|
7775
8448
|
}
|
|
7776
8449
|
|
|
@@ -7807,12 +8480,12 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
7807
8480
|
title: "Create Manus Schedule (UI Agent)",
|
|
7808
8481
|
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
8482
|
inputSchema: {
|
|
7810
|
-
name:
|
|
7811
|
-
cron:
|
|
8483
|
+
name: z14.string().describe("Human-readable name for this schedule"),
|
|
8484
|
+
cron: z14.string().describe(
|
|
7812
8485
|
"Standard 5-field cron expression (minute hour day month weekday). Minimum interval: 5 minutes. Example: '0 */6 * * *' = every 6 hours."
|
|
7813
8486
|
),
|
|
7814
|
-
prompt_template:
|
|
7815
|
-
profile:
|
|
8487
|
+
prompt_template: z14.string().describe("Agent prompt/task template the Manus agent will execute on each run"),
|
|
8488
|
+
profile: z14.string().optional().describe("Manus agent profile to use. Defaults to 'manus-1.6-lite'.")
|
|
7816
8489
|
}
|
|
7817
8490
|
},
|
|
7818
8491
|
async (args) => {
|
|
@@ -7880,7 +8553,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
7880
8553
|
}
|
|
7881
8554
|
]
|
|
7882
8555
|
};
|
|
7883
|
-
} catch (
|
|
8556
|
+
} catch (err4) {
|
|
7884
8557
|
ctx.audit({
|
|
7885
8558
|
tool_name: "ui_agent_schedule_create",
|
|
7886
8559
|
ocs_method: "[manus:schedule.create]",
|
|
@@ -7889,7 +8562,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
7889
8562
|
duration_ms: Date.now() - start,
|
|
7890
8563
|
event_type: "ui_agent_dispatch"
|
|
7891
8564
|
});
|
|
7892
|
-
const msg =
|
|
8565
|
+
const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
|
|
7893
8566
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
7894
8567
|
}
|
|
7895
8568
|
}
|
|
@@ -7947,7 +8620,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
7947
8620
|
}
|
|
7948
8621
|
]
|
|
7949
8622
|
};
|
|
7950
|
-
} catch (
|
|
8623
|
+
} catch (err4) {
|
|
7951
8624
|
ctx.audit({
|
|
7952
8625
|
tool_name: "ui_agent_schedule_list",
|
|
7953
8626
|
ocs_method: "[manus:schedule.list]",
|
|
@@ -7955,7 +8628,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
7955
8628
|
dry_run: false,
|
|
7956
8629
|
duration_ms: Date.now() - start
|
|
7957
8630
|
});
|
|
7958
|
-
const msg =
|
|
8631
|
+
const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
|
|
7959
8632
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
7960
8633
|
}
|
|
7961
8634
|
}
|
|
@@ -7966,7 +8639,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
7966
8639
|
title: "Delete Manus Schedule",
|
|
7967
8640
|
description: "Permanently deletes a Manus recurring schedule. This cannot be undone. Use ui_agent_schedule_pause to temporarily suspend instead. Requires admin scope.",
|
|
7968
8641
|
inputSchema: {
|
|
7969
|
-
schedule_id:
|
|
8642
|
+
schedule_id: z14.string().describe("ID of the schedule to delete")
|
|
7970
8643
|
}
|
|
7971
8644
|
},
|
|
7972
8645
|
async (args) => {
|
|
@@ -8016,7 +8689,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8016
8689
|
}
|
|
8017
8690
|
]
|
|
8018
8691
|
};
|
|
8019
|
-
} catch (
|
|
8692
|
+
} catch (err4) {
|
|
8020
8693
|
ctx.audit({
|
|
8021
8694
|
tool_name: "ui_agent_schedule_delete",
|
|
8022
8695
|
ocs_method: "[manus:schedule.delete]",
|
|
@@ -8025,7 +8698,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8025
8698
|
duration_ms: Date.now() - start,
|
|
8026
8699
|
event_type: "ui_agent_dispatch"
|
|
8027
8700
|
});
|
|
8028
|
-
const msg =
|
|
8701
|
+
const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
|
|
8029
8702
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8030
8703
|
}
|
|
8031
8704
|
}
|
|
@@ -8036,7 +8709,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8036
8709
|
title: "Pause Manus Schedule",
|
|
8037
8710
|
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
8711
|
inputSchema: {
|
|
8039
|
-
schedule_id:
|
|
8712
|
+
schedule_id: z14.string().describe("ID of the schedule to pause")
|
|
8040
8713
|
}
|
|
8041
8714
|
},
|
|
8042
8715
|
async (args) => {
|
|
@@ -8086,7 +8759,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8086
8759
|
}
|
|
8087
8760
|
]
|
|
8088
8761
|
};
|
|
8089
|
-
} catch (
|
|
8762
|
+
} catch (err4) {
|
|
8090
8763
|
ctx.audit({
|
|
8091
8764
|
tool_name: "ui_agent_schedule_pause",
|
|
8092
8765
|
ocs_method: "[manus:schedule.pause]",
|
|
@@ -8095,7 +8768,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8095
8768
|
duration_ms: Date.now() - start,
|
|
8096
8769
|
event_type: "ui_agent_dispatch"
|
|
8097
8770
|
});
|
|
8098
|
-
const msg =
|
|
8771
|
+
const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
|
|
8099
8772
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8100
8773
|
}
|
|
8101
8774
|
}
|
|
@@ -8106,7 +8779,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8106
8779
|
title: "Resume Manus Schedule",
|
|
8107
8780
|
description: "Resumes a paused Manus recurring schedule. Requires admin scope.",
|
|
8108
8781
|
inputSchema: {
|
|
8109
|
-
schedule_id:
|
|
8782
|
+
schedule_id: z14.string().describe("ID of the schedule to resume")
|
|
8110
8783
|
}
|
|
8111
8784
|
},
|
|
8112
8785
|
async (args) => {
|
|
@@ -8156,7 +8829,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8156
8829
|
}
|
|
8157
8830
|
]
|
|
8158
8831
|
};
|
|
8159
|
-
} catch (
|
|
8832
|
+
} catch (err4) {
|
|
8160
8833
|
ctx.audit({
|
|
8161
8834
|
tool_name: "ui_agent_schedule_resume",
|
|
8162
8835
|
ocs_method: "[manus:schedule.resume]",
|
|
@@ -8165,7 +8838,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8165
8838
|
duration_ms: Date.now() - start,
|
|
8166
8839
|
event_type: "ui_agent_dispatch"
|
|
8167
8840
|
});
|
|
8168
|
-
const msg =
|
|
8841
|
+
const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
|
|
8169
8842
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8170
8843
|
}
|
|
8171
8844
|
}
|
|
@@ -8210,7 +8883,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8210
8883
|
}
|
|
8211
8884
|
]
|
|
8212
8885
|
};
|
|
8213
|
-
} catch (
|
|
8886
|
+
} catch (err4) {
|
|
8214
8887
|
ctx.audit({
|
|
8215
8888
|
tool_name: "ui_agent_usage",
|
|
8216
8889
|
ocs_method: "[manus:usage.get]",
|
|
@@ -8218,7 +8891,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8218
8891
|
dry_run: false,
|
|
8219
8892
|
duration_ms: Date.now() - start
|
|
8220
8893
|
});
|
|
8221
|
-
const msg =
|
|
8894
|
+
const msg = err4 instanceof Error ? err4.message : String(err4);
|
|
8222
8895
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8223
8896
|
}
|
|
8224
8897
|
}
|
|
@@ -8226,7 +8899,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8226
8899
|
}
|
|
8227
8900
|
|
|
8228
8901
|
// src/tools-ui-agent.ts
|
|
8229
|
-
import { z as
|
|
8902
|
+
import { z as z15 } from "zod";
|
|
8230
8903
|
|
|
8231
8904
|
// src/clerk.ts
|
|
8232
8905
|
import { createClerkClient } from "@clerk/backend";
|
|
@@ -8461,7 +9134,7 @@ Report success or failure with a clear summary.`;
|
|
|
8461
9134
|
}
|
|
8462
9135
|
]
|
|
8463
9136
|
};
|
|
8464
|
-
} catch (
|
|
9137
|
+
} catch (err4) {
|
|
8465
9138
|
ctx.audit({
|
|
8466
9139
|
tool_name: toolName,
|
|
8467
9140
|
ocs_method: `[ui-agent:${gapId}]`,
|
|
@@ -8475,7 +9148,7 @@ Report success or failure with a clear summary.`;
|
|
|
8475
9148
|
content: [
|
|
8476
9149
|
{
|
|
8477
9150
|
type: "text",
|
|
8478
|
-
text: `Error dispatching UI agent: ${
|
|
9151
|
+
text: `Error dispatching UI agent: ${err4 instanceof Error ? err4.message : String(err4)}`
|
|
8479
9152
|
}
|
|
8480
9153
|
]
|
|
8481
9154
|
};
|
|
@@ -8489,9 +9162,9 @@ function registerAllUiAgentTools(server2, ctx) {
|
|
|
8489
9162
|
title: "Create Steering List (UI Agent)",
|
|
8490
9163
|
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
9164
|
inputSchema: {
|
|
8492
|
-
name:
|
|
8493
|
-
description:
|
|
8494
|
-
dry_run:
|
|
9165
|
+
name: z15.string().describe("Name for the new steering list"),
|
|
9166
|
+
description: z15.string().optional().describe("Optional description for the steering list"),
|
|
9167
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8495
9168
|
}
|
|
8496
9169
|
},
|
|
8497
9170
|
wrapUiAgentHandler(
|
|
@@ -8514,11 +9187,11 @@ After creation, note the new steering list ID from the dashboard.
|
|
|
8514
9187
|
title: "Build Steering List (UI Agent)",
|
|
8515
9188
|
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
9189
|
inputSchema: {
|
|
8517
|
-
steering_list_id:
|
|
8518
|
-
add_operators:
|
|
8519
|
-
remove_operators:
|
|
8520
|
-
operator_type:
|
|
8521
|
-
dry_run:
|
|
9190
|
+
steering_list_id: z15.number().describe("ID of the steering list to modify"),
|
|
9191
|
+
add_operators: z15.array(z15.string()).optional().describe("MCC-MNC codes to add (e.g. ['20801', '26201'])"),
|
|
9192
|
+
remove_operators: z15.array(z15.string()).optional().describe("MCC-MNC codes to remove"),
|
|
9193
|
+
operator_type: z15.enum(["priority", "excluded"]).default("priority").describe("Whether operators are priority or excluded"),
|
|
9194
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8522
9195
|
}
|
|
8523
9196
|
},
|
|
8524
9197
|
wrapUiAgentHandler(
|
|
@@ -8541,9 +9214,9 @@ Operator type: ${args.operator_type ?? "priority"}
|
|
|
8541
9214
|
title: "Set Account Steering List (UI Agent)",
|
|
8542
9215
|
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
9216
|
inputSchema: {
|
|
8544
|
-
account_id:
|
|
8545
|
-
steering_list_id:
|
|
8546
|
-
dry_run:
|
|
9217
|
+
account_id: z15.number().describe("Account ID to assign the steering list to"),
|
|
9218
|
+
steering_list_id: z15.number().describe("Steering list ID to assign (0 to remove/unset)"),
|
|
9219
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8547
9220
|
}
|
|
8548
9221
|
},
|
|
8549
9222
|
wrapUiAgentHandler(
|
|
@@ -8565,10 +9238,10 @@ Open account ID ${args.account_id}.
|
|
|
8565
9238
|
title: "Create Account (UI Agent)",
|
|
8566
9239
|
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
9240
|
inputSchema: {
|
|
8568
|
-
name:
|
|
8569
|
-
description:
|
|
8570
|
-
initial_balance:
|
|
8571
|
-
dry_run:
|
|
9241
|
+
name: z15.string().describe("Name for the new account"),
|
|
9242
|
+
description: z15.string().optional().describe("Optional description"),
|
|
9243
|
+
initial_balance: z15.number().optional().describe("Initial balance in account currency (default 0)"),
|
|
9244
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8572
9245
|
}
|
|
8573
9246
|
},
|
|
8574
9247
|
wrapUiAgentHandler(
|
|
@@ -8592,10 +9265,10 @@ After creation, note the new account ID from the dashboard.
|
|
|
8592
9265
|
title: "Create Destination List (UI Agent)",
|
|
8593
9266
|
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
9267
|
inputSchema: {
|
|
8595
|
-
name:
|
|
8596
|
-
prefixes:
|
|
8597
|
-
description:
|
|
8598
|
-
dry_run:
|
|
9268
|
+
name: z15.string().describe("Name for the new destination list"),
|
|
9269
|
+
prefixes: z15.array(z15.string()).optional().describe("Phone number prefixes to include (e.g. ['+31', '+49'])"),
|
|
9270
|
+
description: z15.string().optional().describe("Optional description"),
|
|
9271
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8599
9272
|
}
|
|
8600
9273
|
},
|
|
8601
9274
|
wrapUiAgentHandler(
|
|
@@ -8618,11 +9291,11 @@ Create a new destination list with the following details:
|
|
|
8618
9291
|
title: "Edit Destination List (UI Agent)",
|
|
8619
9292
|
description: "Edits an existing destination list via the OCS web dashboard. Params: `destination_list_id`, `add_prefixes`, `remove_prefixes`, `new_name`.",
|
|
8620
9293
|
inputSchema: {
|
|
8621
|
-
destination_list_id:
|
|
8622
|
-
add_prefixes:
|
|
8623
|
-
remove_prefixes:
|
|
8624
|
-
new_name:
|
|
8625
|
-
dry_run:
|
|
9294
|
+
destination_list_id: z15.number().describe("ID of the destination list to edit"),
|
|
9295
|
+
add_prefixes: z15.array(z15.string()).optional().describe("Prefixes to add"),
|
|
9296
|
+
remove_prefixes: z15.array(z15.string()).optional().describe("Prefixes to remove"),
|
|
9297
|
+
new_name: z15.string().optional().describe("Rename the destination list"),
|
|
9298
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8626
9299
|
}
|
|
8627
9300
|
},
|
|
8628
9301
|
wrapUiAgentHandler(
|
|
@@ -8645,8 +9318,8 @@ Open destination list ID ${args.destination_list_id} for editing.
|
|
|
8645
9318
|
title: "Delete Destination List (UI Agent)",
|
|
8646
9319
|
description: "Deletes a destination list via the OCS web dashboard. Params: `destination_list_id`. WARNING: This is destructive and cannot be undone.",
|
|
8647
9320
|
inputSchema: {
|
|
8648
|
-
destination_list_id:
|
|
8649
|
-
dry_run:
|
|
9321
|
+
destination_list_id: z15.number().describe("ID of the destination list to delete"),
|
|
9322
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8650
9323
|
}
|
|
8651
9324
|
},
|
|
8652
9325
|
wrapUiAgentHandler(
|
|
@@ -8667,8 +9340,8 @@ Verify the list no longer appears in the dashboard.
|
|
|
8667
9340
|
title: "Delete Package Template (UI Agent)",
|
|
8668
9341
|
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
9342
|
inputSchema: {
|
|
8670
|
-
template_id:
|
|
8671
|
-
dry_run:
|
|
9343
|
+
template_id: z15.number().describe("ID of the package template to delete"),
|
|
9344
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8672
9345
|
}
|
|
8673
9346
|
},
|
|
8674
9347
|
wrapUiAgentHandler(
|
|
@@ -8689,11 +9362,11 @@ Verify the template no longer appears in the template list.
|
|
|
8689
9362
|
title: "Edit Location Zone (UI Agent)",
|
|
8690
9363
|
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
9364
|
inputSchema: {
|
|
8692
|
-
zone_id:
|
|
8693
|
-
new_name:
|
|
8694
|
-
add_countries:
|
|
8695
|
-
remove_countries:
|
|
8696
|
-
dry_run:
|
|
9365
|
+
zone_id: z15.number().describe("ID of the location zone to edit"),
|
|
9366
|
+
new_name: z15.string().optional().describe("Rename the location zone"),
|
|
9367
|
+
add_countries: z15.array(z15.string()).optional().describe("ISO country codes to add (e.g. ['NL', 'DE'])"),
|
|
9368
|
+
remove_countries: z15.array(z15.string()).optional().describe("ISO country codes to remove"),
|
|
9369
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8697
9370
|
}
|
|
8698
9371
|
},
|
|
8699
9372
|
wrapUiAgentHandler(
|
|
@@ -8716,8 +9389,8 @@ Open location zone ID ${args.zone_id} for editing.
|
|
|
8716
9389
|
title: "Delete Location Zone (UI Agent)",
|
|
8717
9390
|
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
9391
|
inputSchema: {
|
|
8719
|
-
zone_id:
|
|
8720
|
-
dry_run:
|
|
9392
|
+
zone_id: z15.number().describe("ID of the location zone to delete"),
|
|
9393
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8721
9394
|
}
|
|
8722
9395
|
},
|
|
8723
9396
|
wrapUiAgentHandler(
|
|
@@ -8736,7 +9409,7 @@ Verify the zone no longer appears in the zone list.
|
|
|
8736
9409
|
}
|
|
8737
9410
|
|
|
8738
9411
|
// src/tools-ui-agent-ask.ts
|
|
8739
|
-
import { z as
|
|
9412
|
+
import { z as z16 } from "zod";
|
|
8740
9413
|
|
|
8741
9414
|
// src/manus-webhook.ts
|
|
8742
9415
|
var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
@@ -8839,8 +9512,8 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
8839
9512
|
title: "Reply to Paused UI Agent Task",
|
|
8840
9513
|
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
9514
|
inputSchema: {
|
|
8842
|
-
task_id:
|
|
8843
|
-
reply:
|
|
9515
|
+
task_id: z16.string().describe("Manus task ID to resume (from ui_agent_list_pending)"),
|
|
9516
|
+
reply: z16.string().describe("Your answer to the agent's question (e.g. a 2FA code or confirmation)")
|
|
8844
9517
|
}
|
|
8845
9518
|
},
|
|
8846
9519
|
async (args) => {
|
|
@@ -8899,7 +9572,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
8899
9572
|
try {
|
|
8900
9573
|
const replyOutcome = await askReply(keys, task_id, reply);
|
|
8901
9574
|
result2 = replyOutcome.data;
|
|
8902
|
-
} catch (
|
|
9575
|
+
} catch (err4) {
|
|
8903
9576
|
ctx.audit({
|
|
8904
9577
|
tool_name: "ui_agent_reply",
|
|
8905
9578
|
ocs_method: "[ui-agent:ask-reply]",
|
|
@@ -8914,7 +9587,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
8914
9587
|
content: [
|
|
8915
9588
|
{
|
|
8916
9589
|
type: "text",
|
|
8917
|
-
text: `Error calling Manus task.reply: ${
|
|
9590
|
+
text: `Error calling Manus task.reply: ${err4 instanceof Error ? err4.message : String(err4)}`
|
|
8918
9591
|
}
|
|
8919
9592
|
]
|
|
8920
9593
|
};
|
|
@@ -8992,13 +9665,13 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
8992
9665
|
try {
|
|
8993
9666
|
const listing = await ctx.env.CARRIER_USERS.list({ prefix: PENDING_ASK_PREFIX });
|
|
8994
9667
|
keys = listing.keys;
|
|
8995
|
-
} catch (
|
|
9668
|
+
} catch (err4) {
|
|
8996
9669
|
return {
|
|
8997
9670
|
isError: true,
|
|
8998
9671
|
content: [
|
|
8999
9672
|
{
|
|
9000
9673
|
type: "text",
|
|
9001
|
-
text: `Error listing pending tasks: ${
|
|
9674
|
+
text: `Error listing pending tasks: ${err4 instanceof Error ? err4.message : String(err4)}`
|
|
9002
9675
|
}
|
|
9003
9676
|
]
|
|
9004
9677
|
};
|
|
@@ -9049,7 +9722,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
9049
9722
|
}
|
|
9050
9723
|
|
|
9051
9724
|
// src/stripe-connect-tools.ts
|
|
9052
|
-
import { z as
|
|
9725
|
+
import { z as z17 } from "zod";
|
|
9053
9726
|
import * as Sentry2 from "@sentry/cloudflare";
|
|
9054
9727
|
|
|
9055
9728
|
// src/audit.ts
|
|
@@ -9085,10 +9758,10 @@ async function issueConfirmToken(env, sub, toolName) {
|
|
|
9085
9758
|
await env.OAUTH_KV.put(key, token2, { expirationTtl: 300 });
|
|
9086
9759
|
return token2;
|
|
9087
9760
|
}
|
|
9088
|
-
function
|
|
9761
|
+
function ok2(text) {
|
|
9089
9762
|
return { content: [{ type: "text", text }] };
|
|
9090
9763
|
}
|
|
9091
|
-
function
|
|
9764
|
+
function err2(text) {
|
|
9092
9765
|
return { isError: true, content: [{ type: "text", text }] };
|
|
9093
9766
|
}
|
|
9094
9767
|
async function stripeGet(stripeKey, path, connectedAccountId) {
|
|
@@ -9123,7 +9796,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9123
9796
|
"stripe_connect_status",
|
|
9124
9797
|
"Read-only: returns the Stripe Connect account status, capabilities, and requirements for the authenticated operator.",
|
|
9125
9798
|
{
|
|
9126
|
-
operator_id:
|
|
9799
|
+
operator_id: z17.string().optional().describe("Override operator_id (admin use). Defaults to caller's org/user.")
|
|
9127
9800
|
},
|
|
9128
9801
|
async ({ operator_id }) => {
|
|
9129
9802
|
const opId = operator_id ?? operatorId;
|
|
@@ -9131,13 +9804,13 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9131
9804
|
const start = Date.now();
|
|
9132
9805
|
try {
|
|
9133
9806
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9134
|
-
if (!stripeKey) return
|
|
9807
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9135
9808
|
const accountId = await env.CARRIER_USERS.get(opKvKey);
|
|
9136
9809
|
if (!accountId) {
|
|
9137
|
-
return
|
|
9810
|
+
return ok2(JSON.stringify({ status: "not_connected", operator_id: opId }));
|
|
9138
9811
|
}
|
|
9139
9812
|
const result2 = await stripeGet(stripeKey, `/v1/accounts/${accountId}`);
|
|
9140
|
-
if (!result2.ok) return
|
|
9813
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9141
9814
|
const a = result2.data;
|
|
9142
9815
|
const status = a.charges_enabled && a.details_submitted ? "active" : "pending";
|
|
9143
9816
|
writeAudit(env, {
|
|
@@ -9149,7 +9822,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9149
9822
|
sub: props2.sub,
|
|
9150
9823
|
reseller_id: props2.reseller_id
|
|
9151
9824
|
});
|
|
9152
|
-
return
|
|
9825
|
+
return ok2(JSON.stringify({
|
|
9153
9826
|
status,
|
|
9154
9827
|
operator_id: opId,
|
|
9155
9828
|
account_id: accountId,
|
|
@@ -9163,7 +9836,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9163
9836
|
}));
|
|
9164
9837
|
} catch (e) {
|
|
9165
9838
|
Sentry2.captureException(e);
|
|
9166
|
-
return
|
|
9839
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9167
9840
|
}
|
|
9168
9841
|
}
|
|
9169
9842
|
);
|
|
@@ -9171,20 +9844,20 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9171
9844
|
"stripe_connect_payouts",
|
|
9172
9845
|
"Read-only: list recent payouts for the operator's connected Stripe account.",
|
|
9173
9846
|
{
|
|
9174
|
-
limit:
|
|
9175
|
-
status:
|
|
9847
|
+
limit: z17.number().int().min(1).max(50).default(10).describe("Number of payouts to return."),
|
|
9848
|
+
status: z17.enum(["pending", "paid", "failed", "canceled", "in_transit"]).optional().describe("Filter by payout status.")
|
|
9176
9849
|
},
|
|
9177
9850
|
async ({ limit, status }) => {
|
|
9178
9851
|
const start = Date.now();
|
|
9179
9852
|
try {
|
|
9180
9853
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9181
|
-
if (!stripeKey) return
|
|
9854
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9182
9855
|
const accountId = await getAccountId();
|
|
9183
|
-
if (!accountId) return
|
|
9856
|
+
if (!accountId) return err2("No connected Stripe account found.");
|
|
9184
9857
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
9185
9858
|
if (status) params.set("status", status);
|
|
9186
9859
|
const result2 = await stripeGet(stripeKey, `/v1/payouts?${params.toString()}`, accountId);
|
|
9187
|
-
if (!result2.ok) return
|
|
9860
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9188
9861
|
writeAudit(env, {
|
|
9189
9862
|
tool_name: "stripe_connect_payouts",
|
|
9190
9863
|
ocs_method: "stripe.payouts.list",
|
|
@@ -9194,10 +9867,10 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9194
9867
|
sub: props2.sub,
|
|
9195
9868
|
reseller_id: props2.reseller_id
|
|
9196
9869
|
});
|
|
9197
|
-
return
|
|
9870
|
+
return ok2(JSON.stringify({ payouts: result2.data.data, has_more: result2.data.has_more }));
|
|
9198
9871
|
} catch (e) {
|
|
9199
9872
|
Sentry2.captureException(e);
|
|
9200
|
-
return
|
|
9873
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9201
9874
|
}
|
|
9202
9875
|
}
|
|
9203
9876
|
);
|
|
@@ -9209,11 +9882,11 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9209
9882
|
const start = Date.now();
|
|
9210
9883
|
try {
|
|
9211
9884
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9212
|
-
if (!stripeKey) return
|
|
9885
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9213
9886
|
const accountId = await getAccountId();
|
|
9214
|
-
if (!accountId) return
|
|
9887
|
+
if (!accountId) return err2("No connected Stripe account found.");
|
|
9215
9888
|
const result2 = await stripeGet(stripeKey, `/v1/balance`, accountId);
|
|
9216
|
-
if (!result2.ok) return
|
|
9889
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9217
9890
|
writeAudit(env, {
|
|
9218
9891
|
tool_name: "stripe_connect_balance",
|
|
9219
9892
|
ocs_method: "stripe.balance.read",
|
|
@@ -9223,14 +9896,14 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9223
9896
|
sub: props2.sub,
|
|
9224
9897
|
reseller_id: props2.reseller_id
|
|
9225
9898
|
});
|
|
9226
|
-
return
|
|
9899
|
+
return ok2(JSON.stringify({
|
|
9227
9900
|
account_id: accountId,
|
|
9228
9901
|
available: result2.data.available ?? [],
|
|
9229
9902
|
pending: result2.data.pending ?? []
|
|
9230
9903
|
}));
|
|
9231
9904
|
} catch (e) {
|
|
9232
9905
|
Sentry2.captureException(e);
|
|
9233
|
-
return
|
|
9906
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9234
9907
|
}
|
|
9235
9908
|
}
|
|
9236
9909
|
);
|
|
@@ -9238,17 +9911,17 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9238
9911
|
"stripe_connect_refund",
|
|
9239
9912
|
"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
9913
|
{
|
|
9241
|
-
charge_id:
|
|
9242
|
-
amount_cents:
|
|
9243
|
-
reason:
|
|
9244
|
-
confirm_token:
|
|
9914
|
+
charge_id: z17.string().min(1).describe("Stripe charge ID (ch_...)."),
|
|
9915
|
+
amount_cents: z17.number().int().min(1).optional().describe("Partial refund amount in cents. Omit for full refund."),
|
|
9916
|
+
reason: z17.enum(["duplicate", "fraudulent", "requested_by_customer"]).optional(),
|
|
9917
|
+
confirm_token: z17.string().optional().describe("Confirmation token from previous call. Required to execute.")
|
|
9245
9918
|
},
|
|
9246
9919
|
async ({ charge_id, amount_cents, reason, confirm_token }) => {
|
|
9247
9920
|
const start = Date.now();
|
|
9248
9921
|
const toolName = "stripe_connect_refund";
|
|
9249
9922
|
if (!confirm_token) {
|
|
9250
9923
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
9251
|
-
return
|
|
9924
|
+
return ok2(
|
|
9252
9925
|
`HARD_BLOCK: Refund ${amount_cents ? `${amount_cents} cents on` : "(full) on"} charge ${charge_id} requires confirmation.
|
|
9253
9926
|
confirm_token: ${token2}
|
|
9254
9927
|
Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes.`
|
|
@@ -9256,18 +9929,18 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9256
9929
|
}
|
|
9257
9930
|
const valid = await verifyConfirmToken(env, props2.sub, toolName, confirm_token);
|
|
9258
9931
|
if (!valid) {
|
|
9259
|
-
return
|
|
9932
|
+
return err2("Invalid or expired confirm_token. Call without token to get a new one.");
|
|
9260
9933
|
}
|
|
9261
9934
|
try {
|
|
9262
9935
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9263
|
-
if (!stripeKey) return
|
|
9936
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9264
9937
|
const accountId = await getAccountId();
|
|
9265
|
-
if (!accountId) return
|
|
9938
|
+
if (!accountId) return err2("No connected Stripe account found.");
|
|
9266
9939
|
const params = { charge: charge_id };
|
|
9267
9940
|
if (amount_cents) params.amount = String(amount_cents);
|
|
9268
9941
|
if (reason) params.reason = reason;
|
|
9269
9942
|
const result2 = await stripePost(stripeKey, "/v1/refunds", params, accountId);
|
|
9270
|
-
if (!result2.ok) return
|
|
9943
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9271
9944
|
writeAudit(env, {
|
|
9272
9945
|
tool_name: toolName,
|
|
9273
9946
|
ocs_method: "stripe.refund.create",
|
|
@@ -9277,10 +9950,10 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9277
9950
|
sub: props2.sub,
|
|
9278
9951
|
reseller_id: props2.reseller_id
|
|
9279
9952
|
});
|
|
9280
|
-
return
|
|
9953
|
+
return ok2(`Refund issued: ${result2.data.id} \u2014 status: ${result2.data.status}`);
|
|
9281
9954
|
} catch (e) {
|
|
9282
9955
|
Sentry2.captureException(e);
|
|
9283
|
-
return
|
|
9956
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9284
9957
|
}
|
|
9285
9958
|
}
|
|
9286
9959
|
);
|
|
@@ -9288,20 +9961,20 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9288
9961
|
"stripe_connect_dispute_list",
|
|
9289
9962
|
"Read-only: list active disputes for the operator's connected Stripe account.",
|
|
9290
9963
|
{
|
|
9291
|
-
limit:
|
|
9292
|
-
status:
|
|
9964
|
+
limit: z17.number().int().min(1).max(50).default(10),
|
|
9965
|
+
status: z17.string().optional().describe("Filter by dispute status (e.g. needs_response, under_review).")
|
|
9293
9966
|
},
|
|
9294
9967
|
async ({ limit, status }) => {
|
|
9295
9968
|
const start = Date.now();
|
|
9296
9969
|
try {
|
|
9297
9970
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9298
|
-
if (!stripeKey) return
|
|
9971
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9299
9972
|
const accountId = await getAccountId();
|
|
9300
|
-
if (!accountId) return
|
|
9973
|
+
if (!accountId) return err2("No connected Stripe account found.");
|
|
9301
9974
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
9302
9975
|
if (status) params.set("status", status);
|
|
9303
9976
|
const result2 = await stripeGet(stripeKey, `/v1/disputes?${params.toString()}`, accountId);
|
|
9304
|
-
if (!result2.ok) return
|
|
9977
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9305
9978
|
writeAudit(env, {
|
|
9306
9979
|
tool_name: "stripe_connect_dispute_list",
|
|
9307
9980
|
ocs_method: "stripe.disputes.list",
|
|
@@ -9311,10 +9984,10 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9311
9984
|
sub: props2.sub,
|
|
9312
9985
|
reseller_id: props2.reseller_id
|
|
9313
9986
|
});
|
|
9314
|
-
return
|
|
9987
|
+
return ok2(JSON.stringify({ disputes: result2.data.data, has_more: result2.data.has_more }));
|
|
9315
9988
|
} catch (e) {
|
|
9316
9989
|
Sentry2.captureException(e);
|
|
9317
|
-
return
|
|
9990
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9318
9991
|
}
|
|
9319
9992
|
}
|
|
9320
9993
|
);
|
|
@@ -9322,18 +9995,18 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9322
9995
|
"radar_review_list",
|
|
9323
9996
|
"Read-only: list pending Radar reviews requiring manual platform decision.",
|
|
9324
9997
|
{
|
|
9325
|
-
open_only:
|
|
9326
|
-
limit:
|
|
9998
|
+
open_only: z17.boolean().default(true).describe("If true, only returns open (undecided) reviews."),
|
|
9999
|
+
limit: z17.number().int().min(1).max(50).default(10)
|
|
9327
10000
|
},
|
|
9328
10001
|
async ({ open_only, limit }) => {
|
|
9329
10002
|
const start = Date.now();
|
|
9330
10003
|
try {
|
|
9331
10004
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9332
|
-
if (!stripeKey) return
|
|
10005
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9333
10006
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
9334
10007
|
if (open_only) params.set("open", "true");
|
|
9335
10008
|
const result2 = await stripeGet(stripeKey, `/v1/radar/reviews?${params.toString()}`);
|
|
9336
|
-
if (!result2.ok) return
|
|
10009
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9337
10010
|
writeAudit(env, {
|
|
9338
10011
|
tool_name: "radar_review_list",
|
|
9339
10012
|
ocs_method: "stripe.radar.reviews.list",
|
|
@@ -9343,10 +10016,10 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9343
10016
|
sub: props2.sub,
|
|
9344
10017
|
reseller_id: props2.reseller_id
|
|
9345
10018
|
});
|
|
9346
|
-
return
|
|
10019
|
+
return ok2(JSON.stringify({ reviews: result2.data.data, has_more: result2.data.has_more }));
|
|
9347
10020
|
} catch (e) {
|
|
9348
10021
|
Sentry2.captureException(e);
|
|
9349
|
-
return
|
|
10022
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9350
10023
|
}
|
|
9351
10024
|
}
|
|
9352
10025
|
);
|
|
@@ -9354,31 +10027,31 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9354
10027
|
"radar_review_approve",
|
|
9355
10028
|
"Admin: approve a Radar review, allowing the charge to proceed. Requires confirm_token.",
|
|
9356
10029
|
{
|
|
9357
|
-
review_id:
|
|
9358
|
-
confirm_token:
|
|
10030
|
+
review_id: z17.string().min(1).describe("Stripe Radar review ID (prv_...)."),
|
|
10031
|
+
confirm_token: z17.string().optional()
|
|
9359
10032
|
},
|
|
9360
10033
|
async ({ review_id, confirm_token }) => {
|
|
9361
10034
|
const toolName = "radar_review_approve";
|
|
9362
10035
|
const start = Date.now();
|
|
9363
10036
|
if (!confirm_token) {
|
|
9364
10037
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
9365
|
-
return
|
|
10038
|
+
return ok2(
|
|
9366
10039
|
`HARD_BLOCK: Approving review ${review_id} allows the charge to proceed.
|
|
9367
10040
|
confirm_token: ${token2}
|
|
9368
10041
|
Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
9369
10042
|
);
|
|
9370
10043
|
}
|
|
9371
10044
|
const valid = await verifyConfirmToken(env, props2.sub, toolName, confirm_token);
|
|
9372
|
-
if (!valid) return
|
|
10045
|
+
if (!valid) return err2("Invalid or expired confirm_token.");
|
|
9373
10046
|
try {
|
|
9374
10047
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9375
|
-
if (!stripeKey) return
|
|
10048
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9376
10049
|
const result2 = await stripePost(
|
|
9377
10050
|
stripeKey,
|
|
9378
10051
|
`/v1/radar/reviews/${review_id}/approve`,
|
|
9379
10052
|
{}
|
|
9380
10053
|
);
|
|
9381
|
-
if (!result2.ok) return
|
|
10054
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9382
10055
|
writeAudit(env, {
|
|
9383
10056
|
tool_name: toolName,
|
|
9384
10057
|
ocs_method: "stripe.radar.review.approve",
|
|
@@ -9388,10 +10061,10 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
|
9388
10061
|
sub: props2.sub,
|
|
9389
10062
|
reseller_id: props2.reseller_id
|
|
9390
10063
|
});
|
|
9391
|
-
return
|
|
10064
|
+
return ok2(`Review ${review_id} approved. Charge will proceed.`);
|
|
9392
10065
|
} catch (e) {
|
|
9393
10066
|
Sentry2.captureException(e);
|
|
9394
|
-
return
|
|
10067
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9395
10068
|
}
|
|
9396
10069
|
}
|
|
9397
10070
|
);
|
|
@@ -9399,31 +10072,31 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
|
9399
10072
|
"radar_review_decline",
|
|
9400
10073
|
"Admin: decline a Radar review, blocking/closing the charge. Requires confirm_token.",
|
|
9401
10074
|
{
|
|
9402
|
-
review_id:
|
|
9403
|
-
confirm_token:
|
|
10075
|
+
review_id: z17.string().min(1),
|
|
10076
|
+
confirm_token: z17.string().optional()
|
|
9404
10077
|
},
|
|
9405
10078
|
async ({ review_id, confirm_token }) => {
|
|
9406
10079
|
const toolName = "radar_review_decline";
|
|
9407
10080
|
const start = Date.now();
|
|
9408
10081
|
if (!confirm_token) {
|
|
9409
10082
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
9410
|
-
return
|
|
10083
|
+
return ok2(
|
|
9411
10084
|
`HARD_BLOCK: Declining review ${review_id} will close/block the charge.
|
|
9412
10085
|
confirm_token: ${token2}
|
|
9413
10086
|
Expires in 5 minutes.`
|
|
9414
10087
|
);
|
|
9415
10088
|
}
|
|
9416
10089
|
const valid = await verifyConfirmToken(env, props2.sub, toolName, confirm_token);
|
|
9417
|
-
if (!valid) return
|
|
10090
|
+
if (!valid) return err2("Invalid or expired confirm_token.");
|
|
9418
10091
|
try {
|
|
9419
10092
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9420
|
-
if (!stripeKey) return
|
|
10093
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9421
10094
|
const result2 = await stripePost(
|
|
9422
10095
|
stripeKey,
|
|
9423
10096
|
`/v1/radar/reviews/${review_id}/approve`,
|
|
9424
10097
|
{ reason: "fraudulent" }
|
|
9425
10098
|
);
|
|
9426
|
-
if (!result2.ok) return
|
|
10099
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9427
10100
|
writeAudit(env, {
|
|
9428
10101
|
tool_name: toolName,
|
|
9429
10102
|
ocs_method: "stripe.radar.review.decline",
|
|
@@ -9433,10 +10106,10 @@ Expires in 5 minutes.`
|
|
|
9433
10106
|
sub: props2.sub,
|
|
9434
10107
|
reseller_id: props2.reseller_id
|
|
9435
10108
|
});
|
|
9436
|
-
return
|
|
10109
|
+
return ok2(`Review ${review_id} declined.`);
|
|
9437
10110
|
} catch (e) {
|
|
9438
10111
|
Sentry2.captureException(e);
|
|
9439
|
-
return
|
|
10112
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9440
10113
|
}
|
|
9441
10114
|
}
|
|
9442
10115
|
);
|
|
@@ -9444,32 +10117,32 @@ Expires in 5 minutes.`
|
|
|
9444
10117
|
"radar_value_list_add",
|
|
9445
10118
|
"Admin: add an item (email, IP, card fingerprint, country code) to a Stripe Radar block/allow list. Requires confirm_token.",
|
|
9446
10119
|
{
|
|
9447
|
-
value_list_id:
|
|
9448
|
-
value:
|
|
9449
|
-
confirm_token:
|
|
10120
|
+
value_list_id: z17.string().min(1).describe("Stripe Radar value list ID (rsl_...)."),
|
|
10121
|
+
value: z17.string().min(1).describe("The value to add (email, IP address, country code, etc.)."),
|
|
10122
|
+
confirm_token: z17.string().optional()
|
|
9450
10123
|
},
|
|
9451
10124
|
async ({ value_list_id, value, confirm_token }) => {
|
|
9452
10125
|
const toolName = "radar_value_list_add";
|
|
9453
10126
|
const start = Date.now();
|
|
9454
10127
|
if (!confirm_token) {
|
|
9455
10128
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
9456
|
-
return
|
|
10129
|
+
return ok2(
|
|
9457
10130
|
`HARD_BLOCK: Adding "${value}" to list ${value_list_id} will affect future charge decisions.
|
|
9458
10131
|
confirm_token: ${token2}
|
|
9459
10132
|
Expires in 5 minutes.`
|
|
9460
10133
|
);
|
|
9461
10134
|
}
|
|
9462
10135
|
const valid = await verifyConfirmToken(env, props2.sub, toolName, confirm_token);
|
|
9463
|
-
if (!valid) return
|
|
10136
|
+
if (!valid) return err2("Invalid or expired confirm_token.");
|
|
9464
10137
|
try {
|
|
9465
10138
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9466
|
-
if (!stripeKey) return
|
|
10139
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9467
10140
|
const result2 = await stripePost(
|
|
9468
10141
|
stripeKey,
|
|
9469
10142
|
"/v1/radar/value_list_items",
|
|
9470
10143
|
{ value_list: value_list_id, value }
|
|
9471
10144
|
);
|
|
9472
|
-
if (!result2.ok) return
|
|
10145
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9473
10146
|
writeAudit(env, {
|
|
9474
10147
|
tool_name: toolName,
|
|
9475
10148
|
ocs_method: "stripe.radar.value_list.add",
|
|
@@ -9479,10 +10152,10 @@ Expires in 5 minutes.`
|
|
|
9479
10152
|
sub: props2.sub,
|
|
9480
10153
|
reseller_id: props2.reseller_id
|
|
9481
10154
|
});
|
|
9482
|
-
return
|
|
10155
|
+
return ok2(`Added "${value}" to Radar list ${value_list_id}.`);
|
|
9483
10156
|
} catch (e) {
|
|
9484
10157
|
Sentry2.captureException(e);
|
|
9485
|
-
return
|
|
10158
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9486
10159
|
}
|
|
9487
10160
|
}
|
|
9488
10161
|
);
|
|
@@ -9490,11 +10163,11 @@ Expires in 5 minutes.`
|
|
|
9490
10163
|
"radar_rule_toggle",
|
|
9491
10164
|
"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
10165
|
{
|
|
9493
|
-
rule_id:
|
|
9494
|
-
enabled:
|
|
10166
|
+
rule_id: z17.string().min(1).describe("Stripe Radar rule ID."),
|
|
10167
|
+
enabled: z17.boolean().describe("true = enable, false = disable.")
|
|
9495
10168
|
},
|
|
9496
10169
|
async ({ rule_id, enabled }) => {
|
|
9497
|
-
return
|
|
10170
|
+
return ok2(
|
|
9498
10171
|
`Stripe Radar does not expose rule enable/disable via the public API.
|
|
9499
10172
|
To ${enabled ? "enable" : "disable"} rule ${rule_id}:
|
|
9500
10173
|
1. Open https://dashboard.stripe.com/radar/rules
|
|
@@ -9506,6 +10179,513 @@ Note: If you need this automated, use the Radar for Platforms beta \u2014 contac
|
|
|
9506
10179
|
);
|
|
9507
10180
|
}
|
|
9508
10181
|
|
|
10182
|
+
// src/tools-greenzone.ts
|
|
10183
|
+
import { z as z18 } from "zod";
|
|
10184
|
+
|
|
10185
|
+
// src/greenzone-whitelist.ts
|
|
10186
|
+
var STATE_KEY = "greenzone:state";
|
|
10187
|
+
var GREENZONE_PORTAL_PATH = "/greenzone";
|
|
10188
|
+
var MANAGE_HOSTS_PATH = "/greenzone/hosts";
|
|
10189
|
+
var MANAGE_IPS_PATH = "/greenzone/ips";
|
|
10190
|
+
async function readState(kv) {
|
|
10191
|
+
const raw = await kv.get(STATE_KEY, "json");
|
|
10192
|
+
if (!raw) {
|
|
10193
|
+
return {
|
|
10194
|
+
hosts: [],
|
|
10195
|
+
ips: [],
|
|
10196
|
+
last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10197
|
+
};
|
|
10198
|
+
}
|
|
10199
|
+
return raw;
|
|
10200
|
+
}
|
|
10201
|
+
async function writeState(kv, state) {
|
|
10202
|
+
await kv.put(STATE_KEY, JSON.stringify(state), {
|
|
10203
|
+
expirationTtl: 60 * 60 * 24 * 7
|
|
10204
|
+
// 7 days
|
|
10205
|
+
});
|
|
10206
|
+
}
|
|
10207
|
+
function stateAddHost(state, host) {
|
|
10208
|
+
const normalised = host.toLowerCase().trim();
|
|
10209
|
+
if (state.hosts.includes(normalised)) return state;
|
|
10210
|
+
return {
|
|
10211
|
+
...state,
|
|
10212
|
+
hosts: [...state.hosts, normalised].sort(),
|
|
10213
|
+
last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10214
|
+
};
|
|
10215
|
+
}
|
|
10216
|
+
function stateRemoveHost(state, host) {
|
|
10217
|
+
const normalised = host.toLowerCase().trim();
|
|
10218
|
+
return {
|
|
10219
|
+
...state,
|
|
10220
|
+
hosts: state.hosts.filter((h) => h !== normalised),
|
|
10221
|
+
last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10222
|
+
};
|
|
10223
|
+
}
|
|
10224
|
+
function stateAddIp(state, ip) {
|
|
10225
|
+
const normalised = ip.trim();
|
|
10226
|
+
if (state.ips.includes(normalised)) return state;
|
|
10227
|
+
return {
|
|
10228
|
+
...state,
|
|
10229
|
+
ips: [...state.ips, normalised].sort(),
|
|
10230
|
+
last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10231
|
+
};
|
|
10232
|
+
}
|
|
10233
|
+
function stateRemoveIp(state, ip) {
|
|
10234
|
+
const normalised = ip.trim();
|
|
10235
|
+
return {
|
|
10236
|
+
...state,
|
|
10237
|
+
ips: state.ips.filter((i) => i !== normalised),
|
|
10238
|
+
last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10239
|
+
};
|
|
10240
|
+
}
|
|
10241
|
+
function escapeForHasTextDoubleQuoted(fragment) {
|
|
10242
|
+
return fragment.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
10243
|
+
}
|
|
10244
|
+
function buildAddHostSteps(portalBaseUrl, host) {
|
|
10245
|
+
return [
|
|
10246
|
+
{
|
|
10247
|
+
tool: "new_tab",
|
|
10248
|
+
description: "Open a dedicated tab for Greenzone host whitelist operation"
|
|
10249
|
+
},
|
|
10250
|
+
{
|
|
10251
|
+
tool: "navigate",
|
|
10252
|
+
args: { url: `${portalBaseUrl}${MANAGE_HOSTS_PATH}` },
|
|
10253
|
+
description: `Navigate to Manage hosts page at ${portalBaseUrl}${MANAGE_HOSTS_PATH}`
|
|
10254
|
+
},
|
|
10255
|
+
{
|
|
10256
|
+
tool: "find",
|
|
10257
|
+
args: { query: "Add host button or input field" },
|
|
10258
|
+
description: "Find the 'Add host' button or input in Manage hosts menu"
|
|
10259
|
+
},
|
|
10260
|
+
{
|
|
10261
|
+
tool: "click",
|
|
10262
|
+
args: { selector: "[data-action='add-host'], button:has-text('Add'), a:has-text('Add host')" },
|
|
10263
|
+
description: "Click Add host to open the entry form"
|
|
10264
|
+
},
|
|
10265
|
+
{
|
|
10266
|
+
tool: "fill",
|
|
10267
|
+
args: { selector: "input[name='host'], input[placeholder*='host'], input[type='text']", value: host },
|
|
10268
|
+
description: `Fill in host value: ${host}`
|
|
10269
|
+
},
|
|
10270
|
+
{
|
|
10271
|
+
tool: "click",
|
|
10272
|
+
args: { selector: "button[type='submit'], button:has-text('Save'), button:has-text('Confirm')" },
|
|
10273
|
+
description: "Submit the form to save the host"
|
|
10274
|
+
},
|
|
10275
|
+
{
|
|
10276
|
+
tool: "screenshot",
|
|
10277
|
+
description: "Screenshot to verify host was added successfully"
|
|
10278
|
+
},
|
|
10279
|
+
{
|
|
10280
|
+
tool: "close",
|
|
10281
|
+
description: "Close the tab (always runs \u2014 success or error)"
|
|
10282
|
+
}
|
|
10283
|
+
];
|
|
10284
|
+
}
|
|
10285
|
+
function buildRemoveHostSteps(portalBaseUrl, host) {
|
|
10286
|
+
const hostKey = host.toLowerCase().trim();
|
|
10287
|
+
const hostText = escapeForHasTextDoubleQuoted(hostKey);
|
|
10288
|
+
return [
|
|
10289
|
+
{
|
|
10290
|
+
tool: "new_tab",
|
|
10291
|
+
description: "Open a dedicated tab for Greenzone host removal operation"
|
|
10292
|
+
},
|
|
10293
|
+
{
|
|
10294
|
+
tool: "navigate",
|
|
10295
|
+
args: { url: `${portalBaseUrl}${MANAGE_HOSTS_PATH}` },
|
|
10296
|
+
description: `Navigate to Manage hosts page at ${portalBaseUrl}${MANAGE_HOSTS_PATH}`
|
|
10297
|
+
},
|
|
10298
|
+
{
|
|
10299
|
+
tool: "find",
|
|
10300
|
+
args: { query: `row containing host ${hostKey}` },
|
|
10301
|
+
description: `Find the row for host: ${hostKey}`
|
|
10302
|
+
},
|
|
10303
|
+
{
|
|
10304
|
+
tool: "click",
|
|
10305
|
+
args: {
|
|
10306
|
+
selector: `tr:has-text("${hostText}") [data-action='delete'], tr:has-text("${hostText}") button:has-text('Remove'), tr:has-text("${hostText}") button:has-text('Delete')`
|
|
10307
|
+
},
|
|
10308
|
+
description: `Click Remove/Delete for host: ${hostKey}`
|
|
10309
|
+
},
|
|
10310
|
+
{
|
|
10311
|
+
tool: "click",
|
|
10312
|
+
args: { selector: "button:has-text('Confirm'), button:has-text('Yes'), [data-action='confirm']" },
|
|
10313
|
+
description: "Confirm the deletion in the confirmation dialog"
|
|
10314
|
+
},
|
|
10315
|
+
{
|
|
10316
|
+
tool: "screenshot",
|
|
10317
|
+
description: "Screenshot to verify host was removed"
|
|
10318
|
+
},
|
|
10319
|
+
{
|
|
10320
|
+
tool: "close",
|
|
10321
|
+
description: "Close the tab (always runs \u2014 success or error)"
|
|
10322
|
+
}
|
|
10323
|
+
];
|
|
10324
|
+
}
|
|
10325
|
+
function buildAddIpSteps(portalBaseUrl, ip) {
|
|
10326
|
+
return [
|
|
10327
|
+
{
|
|
10328
|
+
tool: "new_tab",
|
|
10329
|
+
description: "Open a dedicated tab for Greenzone IP whitelist operation"
|
|
10330
|
+
},
|
|
10331
|
+
{
|
|
10332
|
+
tool: "navigate",
|
|
10333
|
+
args: { url: `${portalBaseUrl}${MANAGE_IPS_PATH}` },
|
|
10334
|
+
description: `Navigate to Manage IPs page at ${portalBaseUrl}${MANAGE_IPS_PATH}`
|
|
10335
|
+
},
|
|
10336
|
+
{
|
|
10337
|
+
tool: "find",
|
|
10338
|
+
args: { query: "Add IP button or input field" },
|
|
10339
|
+
description: "Find the 'Add IP' button or input in Manage IPs menu"
|
|
10340
|
+
},
|
|
10341
|
+
{
|
|
10342
|
+
tool: "click",
|
|
10343
|
+
args: { selector: "[data-action='add-ip'], button:has-text('Add'), a:has-text('Add IP')" },
|
|
10344
|
+
description: "Click Add IP to open the entry form"
|
|
10345
|
+
},
|
|
10346
|
+
{
|
|
10347
|
+
tool: "fill",
|
|
10348
|
+
args: { selector: "input[name='ip'], input[placeholder*='IP'], input[placeholder*='ip'], input[type='text']", value: ip },
|
|
10349
|
+
description: `Fill in IP value: ${ip}`
|
|
10350
|
+
},
|
|
10351
|
+
{
|
|
10352
|
+
tool: "click",
|
|
10353
|
+
args: { selector: "button[type='submit'], button:has-text('Save'), button:has-text('Confirm')" },
|
|
10354
|
+
description: "Submit the form to save the IP"
|
|
10355
|
+
},
|
|
10356
|
+
{
|
|
10357
|
+
tool: "screenshot",
|
|
10358
|
+
description: "Screenshot to verify IP was added successfully"
|
|
10359
|
+
},
|
|
10360
|
+
{
|
|
10361
|
+
tool: "close",
|
|
10362
|
+
description: "Close the tab (always runs \u2014 success or error)"
|
|
10363
|
+
}
|
|
10364
|
+
];
|
|
10365
|
+
}
|
|
10366
|
+
function buildRemoveIpSteps(portalBaseUrl, ip) {
|
|
10367
|
+
const ipKey = ip.trim();
|
|
10368
|
+
const ipText = escapeForHasTextDoubleQuoted(ipKey);
|
|
10369
|
+
return [
|
|
10370
|
+
{
|
|
10371
|
+
tool: "new_tab",
|
|
10372
|
+
description: "Open a dedicated tab for Greenzone IP removal operation"
|
|
10373
|
+
},
|
|
10374
|
+
{
|
|
10375
|
+
tool: "navigate",
|
|
10376
|
+
args: { url: `${portalBaseUrl}${MANAGE_IPS_PATH}` },
|
|
10377
|
+
description: `Navigate to Manage IPs page at ${portalBaseUrl}${MANAGE_IPS_PATH}`
|
|
10378
|
+
},
|
|
10379
|
+
{
|
|
10380
|
+
tool: "find",
|
|
10381
|
+
args: { query: `row containing IP ${ipKey}` },
|
|
10382
|
+
description: `Find the row for IP: ${ipKey}`
|
|
10383
|
+
},
|
|
10384
|
+
{
|
|
10385
|
+
tool: "click",
|
|
10386
|
+
args: {
|
|
10387
|
+
selector: `tr:has-text("${ipText}") [data-action='delete'], tr:has-text("${ipText}") button:has-text('Remove'), tr:has-text("${ipText}") button:has-text('Delete')`
|
|
10388
|
+
},
|
|
10389
|
+
description: `Click Remove/Delete for IP: ${ipKey}`
|
|
10390
|
+
},
|
|
10391
|
+
{
|
|
10392
|
+
tool: "click",
|
|
10393
|
+
args: { selector: "button:has-text('Confirm'), button:has-text('Yes'), [data-action='confirm']" },
|
|
10394
|
+
description: "Confirm the deletion in the confirmation dialog"
|
|
10395
|
+
},
|
|
10396
|
+
{
|
|
10397
|
+
tool: "screenshot",
|
|
10398
|
+
description: "Screenshot to verify IP was removed"
|
|
10399
|
+
},
|
|
10400
|
+
{
|
|
10401
|
+
tool: "close",
|
|
10402
|
+
description: "Close the tab (always runs \u2014 success or error)"
|
|
10403
|
+
}
|
|
10404
|
+
];
|
|
10405
|
+
}
|
|
10406
|
+
function buildListSteps(portalBaseUrl) {
|
|
10407
|
+
return [
|
|
10408
|
+
{
|
|
10409
|
+
tool: "new_tab",
|
|
10410
|
+
description: "Open a dedicated tab for Greenzone whitelist read"
|
|
10411
|
+
},
|
|
10412
|
+
{
|
|
10413
|
+
tool: "navigate",
|
|
10414
|
+
args: { url: `${portalBaseUrl}${GREENZONE_PORTAL_PATH}` },
|
|
10415
|
+
description: `Navigate to Greenzone overview at ${portalBaseUrl}${GREENZONE_PORTAL_PATH}`
|
|
10416
|
+
},
|
|
10417
|
+
{
|
|
10418
|
+
tool: "find",
|
|
10419
|
+
args: { query: "Manage hosts and Manage IPs sections or links" },
|
|
10420
|
+
description: "Find Manage hosts and Manage IPs menu items"
|
|
10421
|
+
},
|
|
10422
|
+
{
|
|
10423
|
+
tool: "screenshot",
|
|
10424
|
+
description: "Screenshot of Greenzone overview for audit"
|
|
10425
|
+
},
|
|
10426
|
+
{
|
|
10427
|
+
tool: "navigate",
|
|
10428
|
+
args: { url: `${portalBaseUrl}${MANAGE_HOSTS_PATH}` },
|
|
10429
|
+
description: "Navigate to Manage hosts to read current host list"
|
|
10430
|
+
},
|
|
10431
|
+
{
|
|
10432
|
+
tool: "find",
|
|
10433
|
+
args: { query: "host entries table or list" },
|
|
10434
|
+
description: "Read all host entries from the page"
|
|
10435
|
+
},
|
|
10436
|
+
{
|
|
10437
|
+
tool: "navigate",
|
|
10438
|
+
args: { url: `${portalBaseUrl}${MANAGE_IPS_PATH}` },
|
|
10439
|
+
description: "Navigate to Manage IPs to read current IP list"
|
|
10440
|
+
},
|
|
10441
|
+
{
|
|
10442
|
+
tool: "find",
|
|
10443
|
+
args: { query: "IP entries table or list" },
|
|
10444
|
+
description: "Read all IP entries from the page"
|
|
10445
|
+
},
|
|
10446
|
+
{
|
|
10447
|
+
tool: "close",
|
|
10448
|
+
description: "Close the tab (always runs)"
|
|
10449
|
+
}
|
|
10450
|
+
];
|
|
10451
|
+
}
|
|
10452
|
+
function resolvePortalBaseUrl(env) {
|
|
10453
|
+
return env.GREENZONE_PORTAL_URL ?? env.CARRIER_OCS_PORTAL_URL ?? env.CARRIER_OCS_BASE_URL ?? "https://ocs.esimvault.cloud";
|
|
10454
|
+
}
|
|
10455
|
+
|
|
10456
|
+
// src/tools-greenzone.ts
|
|
10457
|
+
function getGreenzoneKv(env) {
|
|
10458
|
+
return env.GREENZONE_STATE_KV ?? null;
|
|
10459
|
+
}
|
|
10460
|
+
function ok3(payload) {
|
|
10461
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
10462
|
+
}
|
|
10463
|
+
function err3(message) {
|
|
10464
|
+
return { isError: true, content: [{ type: "text", text: message }] };
|
|
10465
|
+
}
|
|
10466
|
+
var KAPTURE_UNAVAILABLE_MSG = "Execute each Kapture step using mcp__kapture__* tools in sequence. Ensure the Kapture extension is active in Chrome before invoking.";
|
|
10467
|
+
function registerGreenzoneTools(server2, ctx) {
|
|
10468
|
+
server2.registerTool(
|
|
10469
|
+
"greenzone_whitelist_add",
|
|
10470
|
+
{
|
|
10471
|
+
title: "Greenzone Whitelist \u2014 Add",
|
|
10472
|
+
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.",
|
|
10473
|
+
inputSchema: {
|
|
10474
|
+
host: z18.string().optional().describe("Hostname to whitelist (e.g. 'example.com')"),
|
|
10475
|
+
ip: z18.string().optional().describe("IP address or CIDR to whitelist (e.g. '203.0.113.0/24')"),
|
|
10476
|
+
dry_run: z18.boolean().optional().describe("If true, return the Kapture steps without executing portal automation")
|
|
10477
|
+
},
|
|
10478
|
+
annotations: { destructiveHint: true }
|
|
10479
|
+
},
|
|
10480
|
+
async (args) => {
|
|
10481
|
+
if (!ctx.props.scope.includes("write")) {
|
|
10482
|
+
return err3(
|
|
10483
|
+
`Scope denied: greenzone_whitelist_add requires 'write' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
10484
|
+
);
|
|
10485
|
+
}
|
|
10486
|
+
const { host, ip, dry_run } = args;
|
|
10487
|
+
if (!host && !ip) {
|
|
10488
|
+
return err3("At least one of 'host' or 'ip' must be provided.");
|
|
10489
|
+
}
|
|
10490
|
+
const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
|
|
10491
|
+
const steps = [
|
|
10492
|
+
...host ? buildAddHostSteps(portalBaseUrl, host) : [],
|
|
10493
|
+
...ip ? buildAddIpSteps(portalBaseUrl, ip) : []
|
|
10494
|
+
];
|
|
10495
|
+
if (dry_run) {
|
|
10496
|
+
return ok3({
|
|
10497
|
+
dry_run: true,
|
|
10498
|
+
message: "Would execute the following Kapture steps against the OCS portal:",
|
|
10499
|
+
kapture_steps: steps,
|
|
10500
|
+
host,
|
|
10501
|
+
ip
|
|
10502
|
+
});
|
|
10503
|
+
}
|
|
10504
|
+
const kv = getGreenzoneKv(ctx.env);
|
|
10505
|
+
let state;
|
|
10506
|
+
let kvWarning;
|
|
10507
|
+
if (kv) {
|
|
10508
|
+
try {
|
|
10509
|
+
state = await readState(kv);
|
|
10510
|
+
if (host) state = stateAddHost(state, host);
|
|
10511
|
+
if (ip) state = stateAddIp(state, ip);
|
|
10512
|
+
await writeState(kv, state);
|
|
10513
|
+
} catch (e) {
|
|
10514
|
+
kvWarning = `KV write failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
10515
|
+
}
|
|
10516
|
+
} else {
|
|
10517
|
+
kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
|
|
10518
|
+
}
|
|
10519
|
+
return ok3({
|
|
10520
|
+
requires_kapture: true,
|
|
10521
|
+
message: "Execute the Kapture steps below to apply the change in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
|
|
10522
|
+
kapture_steps: steps,
|
|
10523
|
+
kv_state: state ?? null,
|
|
10524
|
+
kv_warning: kvWarning,
|
|
10525
|
+
portal_base_url: portalBaseUrl
|
|
10526
|
+
});
|
|
10527
|
+
}
|
|
10528
|
+
);
|
|
10529
|
+
server2.registerTool(
|
|
10530
|
+
"greenzone_whitelist_remove",
|
|
10531
|
+
{
|
|
10532
|
+
title: "Greenzone Whitelist \u2014 Remove",
|
|
10533
|
+
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.",
|
|
10534
|
+
inputSchema: {
|
|
10535
|
+
host: z18.string().optional().describe("Hostname to remove from whitelist"),
|
|
10536
|
+
ip: z18.string().optional().describe("IP address or CIDR to remove from whitelist"),
|
|
10537
|
+
dry_run: z18.boolean().optional().describe("If true, return the Kapture steps without executing portal automation")
|
|
10538
|
+
},
|
|
10539
|
+
annotations: { destructiveHint: true }
|
|
10540
|
+
},
|
|
10541
|
+
async (args) => {
|
|
10542
|
+
if (!ctx.props.scope.includes("write")) {
|
|
10543
|
+
return err3(
|
|
10544
|
+
`Scope denied: greenzone_whitelist_remove requires 'write' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
10545
|
+
);
|
|
10546
|
+
}
|
|
10547
|
+
const { host, ip, dry_run } = args;
|
|
10548
|
+
if (!host && !ip) {
|
|
10549
|
+
return err3("At least one of 'host' or 'ip' must be provided.");
|
|
10550
|
+
}
|
|
10551
|
+
const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
|
|
10552
|
+
const steps = [
|
|
10553
|
+
...host ? buildRemoveHostSteps(portalBaseUrl, host) : [],
|
|
10554
|
+
...ip ? buildRemoveIpSteps(portalBaseUrl, ip) : []
|
|
10555
|
+
];
|
|
10556
|
+
if (dry_run) {
|
|
10557
|
+
return ok3({
|
|
10558
|
+
dry_run: true,
|
|
10559
|
+
message: "Would execute the following Kapture steps to remove from OCS portal:",
|
|
10560
|
+
kapture_steps: steps,
|
|
10561
|
+
host,
|
|
10562
|
+
ip
|
|
10563
|
+
});
|
|
10564
|
+
}
|
|
10565
|
+
const kv = getGreenzoneKv(ctx.env);
|
|
10566
|
+
let state;
|
|
10567
|
+
let kvWarning;
|
|
10568
|
+
if (kv) {
|
|
10569
|
+
try {
|
|
10570
|
+
state = await readState(kv);
|
|
10571
|
+
if (host) state = stateRemoveHost(state, host);
|
|
10572
|
+
if (ip) state = stateRemoveIp(state, ip);
|
|
10573
|
+
await writeState(kv, state);
|
|
10574
|
+
} catch (e) {
|
|
10575
|
+
kvWarning = `KV write failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
10576
|
+
}
|
|
10577
|
+
} else {
|
|
10578
|
+
kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
|
|
10579
|
+
}
|
|
10580
|
+
return ok3({
|
|
10581
|
+
requires_kapture: true,
|
|
10582
|
+
message: "Execute the Kapture steps below to apply the removal in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
|
|
10583
|
+
kapture_steps: steps,
|
|
10584
|
+
kv_state: state ?? null,
|
|
10585
|
+
kv_warning: kvWarning,
|
|
10586
|
+
portal_base_url: portalBaseUrl
|
|
10587
|
+
});
|
|
10588
|
+
}
|
|
10589
|
+
);
|
|
10590
|
+
server2.registerTool(
|
|
10591
|
+
"greenzone_whitelist_list",
|
|
10592
|
+
{
|
|
10593
|
+
title: "Greenzone Whitelist \u2014 List",
|
|
10594
|
+
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.",
|
|
10595
|
+
inputSchema: {
|
|
10596
|
+
from_portal: z18.boolean().optional().describe(
|
|
10597
|
+
"When true, return Kapture steps to read live portal state instead of KV cache"
|
|
10598
|
+
)
|
|
10599
|
+
},
|
|
10600
|
+
annotations: { readOnlyHint: true }
|
|
10601
|
+
},
|
|
10602
|
+
async (args) => {
|
|
10603
|
+
if (!ctx.props.scope.includes("read")) {
|
|
10604
|
+
return err3(
|
|
10605
|
+
`Scope denied: greenzone_whitelist_list requires 'read' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
10606
|
+
);
|
|
10607
|
+
}
|
|
10608
|
+
const { from_portal } = args;
|
|
10609
|
+
if (from_portal) {
|
|
10610
|
+
const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
|
|
10611
|
+
return ok3({
|
|
10612
|
+
requires_kapture: true,
|
|
10613
|
+
message: "Execute the Kapture steps below to read live Greenzone whitelist state from the OCS portal.",
|
|
10614
|
+
kapture_steps: buildListSteps(portalBaseUrl),
|
|
10615
|
+
portal_base_url: portalBaseUrl
|
|
10616
|
+
});
|
|
10617
|
+
}
|
|
10618
|
+
const kv = getGreenzoneKv(ctx.env);
|
|
10619
|
+
if (!kv) {
|
|
10620
|
+
return ok3({
|
|
10621
|
+
source: "kv_cache",
|
|
10622
|
+
hosts: [],
|
|
10623
|
+
ips: [],
|
|
10624
|
+
last_synced_at: null,
|
|
10625
|
+
last_reconciled_at: null,
|
|
10626
|
+
reconcile_drift: false,
|
|
10627
|
+
warning: "GREENZONE_STATE_KV not bound in stdio mode. Use from_portal: true to read live portal state."
|
|
10628
|
+
});
|
|
10629
|
+
}
|
|
10630
|
+
try {
|
|
10631
|
+
const state = await readState(kv);
|
|
10632
|
+
return ok3({
|
|
10633
|
+
source: "kv_cache",
|
|
10634
|
+
hosts: state.hosts,
|
|
10635
|
+
ips: state.ips,
|
|
10636
|
+
last_synced_at: state.last_synced_at,
|
|
10637
|
+
last_reconciled_at: state.last_reconciled_at ?? null,
|
|
10638
|
+
reconcile_drift: state.reconcile_drift ?? false
|
|
10639
|
+
});
|
|
10640
|
+
} catch (e) {
|
|
10641
|
+
return err3(`Failed to read Greenzone state from KV: ${e instanceof Error ? e.message : String(e)}`);
|
|
10642
|
+
}
|
|
10643
|
+
}
|
|
10644
|
+
);
|
|
10645
|
+
}
|
|
10646
|
+
|
|
10647
|
+
// src/tools-ui-agent-generic.ts
|
|
10648
|
+
import { z as z19 } from "zod";
|
|
10649
|
+
var STDIO_ERROR = {
|
|
10650
|
+
isError: true,
|
|
10651
|
+
content: [
|
|
10652
|
+
{
|
|
10653
|
+
type: "text",
|
|
10654
|
+
text: JSON.stringify({
|
|
10655
|
+
error: "requires_worker_runtime",
|
|
10656
|
+
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.",
|
|
10657
|
+
remote_url: "https://mcp.carrier.llc/mcp"
|
|
10658
|
+
})
|
|
10659
|
+
}
|
|
10660
|
+
]
|
|
10661
|
+
};
|
|
10662
|
+
function registerUiAgentGenericTools(server2, _ctx) {
|
|
10663
|
+
server2.registerTool(
|
|
10664
|
+
"ui_agent_ask",
|
|
10665
|
+
{
|
|
10666
|
+
title: "Dispatch Generic Steel Browsing Agent",
|
|
10667
|
+
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.",
|
|
10668
|
+
inputSchema: {
|
|
10669
|
+
prompt: z19.string().min(1).max(4e3).describe("Natural-language task description for the Steel browsing agent."),
|
|
10670
|
+
max_steps: z19.number().int().min(1).max(30).optional().describe("Maximum agent steps (1\u201330, default 15)."),
|
|
10671
|
+
dry_run: z19.boolean().optional().describe("Preview cost estimate without dispatching.")
|
|
10672
|
+
}
|
|
10673
|
+
},
|
|
10674
|
+
async (_args) => STDIO_ERROR
|
|
10675
|
+
);
|
|
10676
|
+
server2.registerTool(
|
|
10677
|
+
"ui_agent_status",
|
|
10678
|
+
{
|
|
10679
|
+
title: "Get Steel Agent Task Status",
|
|
10680
|
+
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.",
|
|
10681
|
+
inputSchema: {
|
|
10682
|
+
task_id: z19.string().describe("Steel task ID returned by ui_agent_ask or any ui_* tool dispatch")
|
|
10683
|
+
}
|
|
10684
|
+
},
|
|
10685
|
+
async (_args) => STDIO_ERROR
|
|
10686
|
+
);
|
|
10687
|
+
}
|
|
10688
|
+
|
|
9509
10689
|
// src/index.ts
|
|
9510
10690
|
var baseUrl = process.env.CARRIER_OCS_BASE_URL ?? process.env.ESIMVAULT_BASE_URL ?? "https://ocs.esimvault.cloud";
|
|
9511
10691
|
var token = process.env.CARRIER_OCS_API_TOKEN ?? process.env.ESIMVAULT_API_TOKEN;
|
|
@@ -9543,7 +10723,7 @@ var audit = (_row) => {
|
|
|
9543
10723
|
var getUserToken = async (_sub) => token;
|
|
9544
10724
|
var toolCtx = { env: stdioEnv, props, audit, getUserToken };
|
|
9545
10725
|
var server = new McpServer(
|
|
9546
|
-
{ name: "carrier-mcp", version: "0.2.
|
|
10726
|
+
{ name: "carrier-mcp", version: "0.2.18" },
|
|
9547
10727
|
{
|
|
9548
10728
|
instructions: "Carrier MCP \u2014 single-user stdio mode. Full tool registry (mirrors the deployed Worker)."
|
|
9549
10729
|
}
|
|
@@ -9553,12 +10733,17 @@ registerIntelligenceTools(server, toolCtx);
|
|
|
9553
10733
|
registerAllBacklogTools(server, toolCtx);
|
|
9554
10734
|
registerAllCarrierAskTools(server, toolCtx);
|
|
9555
10735
|
registerListRecentOcsEventsTool(server, stdioEnv);
|
|
10736
|
+
registerRateLimitStatusTool(server, toolCtx);
|
|
10737
|
+
registerCountryHistoryTool(server, toolCtx);
|
|
10738
|
+
registerDepletionEventsTool(server, toolCtx);
|
|
9556
10739
|
registerAllApps(server, toolCtx);
|
|
9557
10740
|
registerAllPricingTools(server, toolCtx);
|
|
9558
10741
|
registerScheduleAndUsageTools(server, toolCtx);
|
|
9559
10742
|
registerAllUiAgentTools(server, toolCtx);
|
|
9560
10743
|
registerUiAgentAskTools(server, toolCtx);
|
|
9561
10744
|
registerStripeConnectTools(server, { env: stdioEnv, props });
|
|
10745
|
+
registerGreenzoneTools(server, toolCtx);
|
|
10746
|
+
registerUiAgentGenericTools(server, toolCtx);
|
|
9562
10747
|
registerAllPrompts(server);
|
|
9563
10748
|
var transport = new StdioServerTransport();
|
|
9564
10749
|
await server.connect(transport);
|