@carrierllc/mcp 0.2.15 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +644 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +1264 -243
- package/dist/index.js.map +1 -1
- package/package.json +21 -14
- 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 +48 -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 +16 -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 +20 -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/skills/carrier-operations/SKILL.md +36 -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]/page.tsx +28 -0
- package/templates/storefront/src/app/apple-icon.tsx +29 -0
- package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +52 -0
- package/templates/storefront/src/app/checkout/[templateId]/page.tsx +24 -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]]/page.tsx +13 -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/conversion-events.ts +36 -0
- package/templates/storefront/src/lib/sanitize-auth-redirect.ts +16 -0
- package/templates/storefront/src/middleware.ts +25 -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-67QO5NVD.js
|
|
12
|
+
var ENDPOINT_LIMITS_PER_MIN = {
|
|
13
|
+
// Global sentinel (keyed as "__global__")
|
|
14
|
+
__global__: 600,
|
|
15
|
+
// 30/min — tightest endpoint
|
|
16
|
+
subscriberNetworkEventsOverPeriod: 30,
|
|
17
|
+
// 100/min
|
|
18
|
+
subscriberUsageOverPeriod: 100,
|
|
19
|
+
// 150/min
|
|
20
|
+
listLocationZoneElement: 150,
|
|
21
|
+
sendMtSms: 150,
|
|
22
|
+
// 300/min (explicit; also the fallback default)
|
|
23
|
+
affectPlanToSubscriber: 300,
|
|
24
|
+
affectRecurringPackageToSubscriber: 300,
|
|
25
|
+
affectSubscriberFakePhoneNumber: 300,
|
|
26
|
+
affectSubscriberRealPhoneNumber: 300,
|
|
27
|
+
esimStatusPerAccount: 300,
|
|
28
|
+
getSubscriberActivePeriod: 300,
|
|
29
|
+
hlrGetBitrate: 300,
|
|
30
|
+
hlrSetBitrate: 300,
|
|
31
|
+
listSubscriber: 300,
|
|
32
|
+
listSubscriberPrepaidPackages: 300,
|
|
33
|
+
moveSubscriberRangeToAccount: 300,
|
|
34
|
+
pushSteeringToSubs: 300,
|
|
35
|
+
setSubscriberTrafficRestrictions: 300,
|
|
36
|
+
getSingleSubscriber: 300,
|
|
37
|
+
getSubscriberLocationByCellId: 300
|
|
38
|
+
};
|
|
39
|
+
var DEFAULT_LIMIT_PER_MIN = 300;
|
|
40
|
+
var WINDOW_MS2 = 6e4;
|
|
41
|
+
var METRIC_EMIT_MIN_INTERVAL_MS = 1e4;
|
|
42
|
+
var BATCH_FRACTION = 0.2;
|
|
43
|
+
function resellerKeyHash(resellerKey) {
|
|
44
|
+
if (!resellerKey) return "00000000";
|
|
45
|
+
let h = 5381;
|
|
46
|
+
for (let i = 0; i < resellerKey.length; i++) {
|
|
47
|
+
h = ((h << 5) + h ^ resellerKey.charCodeAt(i)) >>> 0;
|
|
48
|
+
}
|
|
49
|
+
return h.toString(16).padStart(8, "0");
|
|
50
|
+
}
|
|
51
|
+
function emitMetric(endpoint, resellerKey, callsInWindow, limitPerMin) {
|
|
52
|
+
const pct = Math.round(callsInWindow / limitPerMin * 100);
|
|
53
|
+
const reseller_hash = resellerKeyHash(resellerKey);
|
|
54
|
+
console.log(
|
|
55
|
+
JSON.stringify({
|
|
56
|
+
metric: "carrier_ocs_calls_per_min",
|
|
57
|
+
endpoint,
|
|
58
|
+
reseller_hash,
|
|
59
|
+
calls_in_window: callsInWindow,
|
|
60
|
+
limit_per_min: limitPerMin,
|
|
61
|
+
utilisation_pct: pct,
|
|
62
|
+
alert: pct >= 80,
|
|
63
|
+
ts: Date.now()
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
if (pct >= 80) {
|
|
67
|
+
console.warn(
|
|
68
|
+
`[rate-governor] ALERT: ${endpoint} at ${pct}% capacity (${callsInWindow}/${limitPerMin} per min) reseller=${reseller_hash}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var _endpointWindows = /* @__PURE__ */ new Map();
|
|
73
|
+
function _getEndpointState(key) {
|
|
74
|
+
let s = _endpointWindows.get(key);
|
|
75
|
+
if (!s) {
|
|
76
|
+
s = {
|
|
77
|
+
log: [],
|
|
78
|
+
batchLog: [],
|
|
79
|
+
gate: Promise.resolve(),
|
|
80
|
+
lastMetricEmitAt: 0,
|
|
81
|
+
metricBelowHighUtil: true
|
|
82
|
+
};
|
|
83
|
+
_endpointWindows.set(key, s);
|
|
84
|
+
}
|
|
85
|
+
return s;
|
|
86
|
+
}
|
|
87
|
+
function _prune(log, windowStart) {
|
|
88
|
+
let i = 0;
|
|
89
|
+
while (i < log.length && log[i] <= windowStart) i++;
|
|
90
|
+
return i > 0 ? log.slice(i) : log;
|
|
91
|
+
}
|
|
92
|
+
async function acquireEndpointSlot(resellerId, endpoint, priority = "interactive") {
|
|
93
|
+
const proc = globalThis["process"];
|
|
94
|
+
if (proc?.env?.["RATE_FLOOR_DISABLED"] === "true" || globalThis["RATE_FLOOR_DISABLED"] === "true") {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await _acquireOneSlot(resellerId, "__global__", priority);
|
|
98
|
+
await _acquireOneSlot(resellerId, endpoint, priority);
|
|
99
|
+
}
|
|
100
|
+
async function _acquireOneSlot(resellerId, endpoint, priority) {
|
|
101
|
+
const bucketKey = `${resellerId}:${endpoint}`;
|
|
102
|
+
const limitPerMin = ENDPOINT_LIMITS_PER_MIN[endpoint] ?? DEFAULT_LIMIT_PER_MIN;
|
|
103
|
+
const batchCap = Math.floor(limitPerMin * BATCH_FRACTION);
|
|
104
|
+
const state = _getEndpointState(bucketKey);
|
|
105
|
+
const ticket = state.gate.then(async () => {
|
|
106
|
+
while (true) {
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
const windowStart = now - WINDOW_MS2;
|
|
109
|
+
state.log = _prune(state.log, windowStart);
|
|
110
|
+
state.batchLog = _prune(state.batchLog, windowStart);
|
|
111
|
+
const totalInWindow = state.log.length;
|
|
112
|
+
const batchInWindow = state.batchLog.length;
|
|
113
|
+
if (totalInWindow > 0 && endpoint !== "__global__") {
|
|
114
|
+
const utilPct = Math.round(totalInWindow / limitPerMin * 100);
|
|
115
|
+
const intervalOk = now - state.lastMetricEmitAt >= METRIC_EMIT_MIN_INTERVAL_MS;
|
|
116
|
+
const enteredHighUtil = utilPct >= 80 && state.metricBelowHighUtil;
|
|
117
|
+
if (intervalOk || enteredHighUtil) {
|
|
118
|
+
emitMetric(endpoint, resellerId, totalInWindow, limitPerMin);
|
|
119
|
+
state.lastMetricEmitAt = now;
|
|
120
|
+
if (utilPct >= 80) {
|
|
121
|
+
state.metricBelowHighUtil = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (utilPct < 80) {
|
|
125
|
+
state.metricBelowHighUtil = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const globalFull = totalInWindow >= limitPerMin;
|
|
129
|
+
const batchFull = priority === "batch" && batchInWindow >= batchCap;
|
|
130
|
+
if (!globalFull && !batchFull) {
|
|
131
|
+
state.log.push(now);
|
|
132
|
+
if (priority === "batch") {
|
|
133
|
+
state.batchLog.push(now);
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
let waitMs;
|
|
138
|
+
if (globalFull) {
|
|
139
|
+
const oldest = state.log[0];
|
|
140
|
+
waitMs = WINDOW_MS2 - (now - oldest) + 1;
|
|
141
|
+
} else {
|
|
142
|
+
const oldestBatch = state.batchLog[0];
|
|
143
|
+
waitMs = WINDOW_MS2 - (now - oldestBatch) + 1;
|
|
144
|
+
}
|
|
145
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
state.gate = ticket;
|
|
149
|
+
return ticket;
|
|
150
|
+
}
|
|
151
|
+
function getLimitForEndpoint(endpoint) {
|
|
152
|
+
return ENDPOINT_LIMITS_PER_MIN[endpoint] ?? DEFAULT_LIMIT_PER_MIN;
|
|
153
|
+
}
|
|
154
|
+
function getRateLimitWindowCounts(resellerKey, endpoint) {
|
|
155
|
+
const bucketKey = `${resellerKey}:${endpoint}`;
|
|
156
|
+
const state = _endpointWindows.get(bucketKey);
|
|
157
|
+
if (!state) {
|
|
158
|
+
return { calls_in_window: 0, batch_calls_in_window: 0 };
|
|
159
|
+
}
|
|
160
|
+
const windowStart = Date.now() - WINDOW_MS2;
|
|
161
|
+
let calls_in_window = 0;
|
|
162
|
+
for (const ts of state.log) {
|
|
163
|
+
if (ts > windowStart) calls_in_window++;
|
|
164
|
+
}
|
|
165
|
+
let batch_calls_in_window = 0;
|
|
166
|
+
for (const ts of state.batchLog) {
|
|
167
|
+
if (ts > windowStart) batch_calls_in_window++;
|
|
168
|
+
}
|
|
169
|
+
return { calls_in_window, batch_calls_in_window };
|
|
170
|
+
}
|
|
171
|
+
|
|
11
172
|
// src/client.ts
|
|
12
|
-
var
|
|
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"];
|
|
@@ -295,9 +457,9 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
|
295
457
|
try {
|
|
296
458
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
297
459
|
result2 = await handler(args, token2);
|
|
298
|
-
} catch (
|
|
460
|
+
} catch (err4) {
|
|
299
461
|
try {
|
|
300
|
-
Sentry.captureException(
|
|
462
|
+
Sentry.captureException(err4, {
|
|
301
463
|
tags: {
|
|
302
464
|
tool: toolName,
|
|
303
465
|
feature: "mcp",
|
|
@@ -306,8 +468,8 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
|
306
468
|
});
|
|
307
469
|
} catch {
|
|
308
470
|
}
|
|
309
|
-
const message =
|
|
310
|
-
const ocsCode =
|
|
471
|
+
const message = err4 instanceof Error ? err4.message : String(err4);
|
|
472
|
+
const ocsCode = err4 instanceof OcsApiError2 ? err4.code : void 0;
|
|
311
473
|
ctx.audit({
|
|
312
474
|
tool_name: toolName,
|
|
313
475
|
ocs_method: ocsMethod,
|
|
@@ -335,7 +497,7 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
|
335
497
|
};
|
|
336
498
|
}
|
|
337
499
|
async function ocsCall(env, token2, method, params = {}) {
|
|
338
|
-
const client = new
|
|
500
|
+
const client = new OcsClient2(env.CARRIER_OCS_BASE_URL, token2);
|
|
339
501
|
const result2 = await client.call(method, params);
|
|
340
502
|
return {
|
|
341
503
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
@@ -344,13 +506,13 @@ async function ocsCall(env, token2, method, params = {}) {
|
|
|
344
506
|
async function resolveSubscriberByIccid(env, token2, iccid, cache) {
|
|
345
507
|
const hit = cache.get(iccid);
|
|
346
508
|
if (hit) return hit;
|
|
347
|
-
const client = new
|
|
509
|
+
const client = new OcsClient2(env.CARRIER_OCS_BASE_URL, token2);
|
|
348
510
|
const record = await client.call("getSingleSubscriber", { iccid });
|
|
349
511
|
cache.set(iccid, record);
|
|
350
512
|
return record;
|
|
351
513
|
}
|
|
352
514
|
async function getDefaultResellerId(env, token2) {
|
|
353
|
-
const client = new
|
|
515
|
+
const client = new OcsClient2(env.CARRIER_OCS_BASE_URL, token2);
|
|
354
516
|
const info = await client.call("getResellerInfo", {});
|
|
355
517
|
const id = info?.id;
|
|
356
518
|
if (typeof id !== "number") {
|
|
@@ -567,7 +729,7 @@ function registerAllTools(server2, ctx) {
|
|
|
567
729
|
if (args.msisdn) params.msisdn = args.msisdn;
|
|
568
730
|
if (args.status) params.status = args.status;
|
|
569
731
|
if (args.offset !== void 0) params.offset = args.offset;
|
|
570
|
-
const client = new
|
|
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;
|
|
@@ -938,17 +1100,8 @@ function registerAllTools(server2, ctx) {
|
|
|
938
1100
|
accountForSubs: account_for_subs
|
|
939
1101
|
});
|
|
940
1102
|
}
|
|
941
|
-
const cache = /* @__PURE__ */ new Map();
|
|
942
|
-
const sub = await resolveSubscriberByIccid(ctx.env, token2, iccid, cache);
|
|
943
|
-
const subscriberId = sub.id ?? sub.subscriberId;
|
|
944
|
-
if (subscriberId === void 0) {
|
|
945
|
-
return {
|
|
946
|
-
isError: true,
|
|
947
|
-
content: [{ type: "text", text: `Error: Could not resolve subscriberId for ICCID ${iccid}` }]
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
1103
|
return ocsCall(ctx.env, token2, "affectPackageToSubscriber", {
|
|
951
|
-
subscriber:
|
|
1104
|
+
subscriber: { iccid },
|
|
952
1105
|
packageTemplateId
|
|
953
1106
|
});
|
|
954
1107
|
}
|
|
@@ -2977,11 +3130,11 @@ var ocsAppMethods = ocsSpec.v1_app_methods;
|
|
|
2977
3130
|
// src/intelligence.ts
|
|
2978
3131
|
async function safeCall(env, token2, method, params = {}) {
|
|
2979
3132
|
try {
|
|
2980
|
-
const client = new
|
|
3133
|
+
const client = new OcsClient2(env.CARRIER_OCS_BASE_URL, token2);
|
|
2981
3134
|
const data = await client.call(method, params);
|
|
2982
3135
|
return { data, error: null };
|
|
2983
|
-
} catch (
|
|
2984
|
-
return { data: null, error:
|
|
3136
|
+
} catch (err4) {
|
|
3137
|
+
return { data: null, error: err4 instanceof Error ? err4.message : String(err4) };
|
|
2985
3138
|
}
|
|
2986
3139
|
}
|
|
2987
3140
|
async function fetchActiveSubscribers(env, token2, accountId, resellerId) {
|
|
@@ -3981,6 +4134,11 @@ var DRY_RUN_FIELD2 = {
|
|
|
3981
4134
|
"If true, do not call OCS \u2014 return the would-be request for confirmation"
|
|
3982
4135
|
)
|
|
3983
4136
|
};
|
|
4137
|
+
function normalizeRadioType(value) {
|
|
4138
|
+
if (/^nb-?iot$/i.test(value)) return "NB-IoT";
|
|
4139
|
+
return value;
|
|
4140
|
+
}
|
|
4141
|
+
var VALID_RADIO_TYPES = /* @__PURE__ */ new Set(["2G", "3G", "4G", "5G", "NB-IoT"]);
|
|
3984
4142
|
var BACKLOG_TOOL_SCOPES = {
|
|
3985
4143
|
affect_subscriber_phone_number: "write",
|
|
3986
4144
|
carrier_webhook_config: "read",
|
|
@@ -4015,7 +4173,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4015
4173
|
ctx,
|
|
4016
4174
|
async ({ iccid, phone_number, phone_type }, token2) => {
|
|
4017
4175
|
const ocsMethod = phone_type === "fake" ? "affectSubscriberFakePhoneNumber" : "affectSubscriberRealPhoneNumber";
|
|
4018
|
-
const client = new
|
|
4176
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4019
4177
|
const result2 = await client.call(ocsMethod, { subscriber: iccid, phoneNumber: phone_number });
|
|
4020
4178
|
return {
|
|
4021
4179
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
@@ -4027,9 +4185,11 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4027
4185
|
"get_subscriber_location_by_cell_id",
|
|
4028
4186
|
{
|
|
4029
4187
|
title: "Get Location by Cell ID (GeoSense)",
|
|
4030
|
-
description: "Powered by Bridge4IP GeoSense \u2014 cell-level location resolution with sub-cell accuracy where available. Use this to resolve a cell tower tuple (radio type + MCC + MNC + LAC + optional cellId) to a latitude/longitude estimate. No subscriber identifier required \u2014 caller supplies cell parameters directly. Useful for fraud detection, roaming cost attribution, and network troubleshooting when you have raw cell info from an external source (e.g. a Relay LU webhook event). Params: `radio_type` ('2G'|'3G'|'4G'|'5G'|'NB-IoT'), `mcc` (int), `mnc` (int), `lac` (int), `cell_id` (int, optional but strongly recommended for accuracy), `signal_strength` (number dBm, optional). Returns: { latitude, longitude, accuracy } \u2014 accuracy is median error in meters at 50% confidence. Without cell_id accuracy degrades significantly (>10 km). Do NOT use this for bulk fleet location sweeps \u2014 one OCS call per cell tower; use `audit_network_coverage` for fleet-level analysis instead.",
|
|
4188
|
+
description: "Powered by Bridge4IP GeoSense \u2014 cell-level location resolution with sub-cell accuracy where available. Use this to resolve a cell tower tuple (radio type + MCC + MNC + LAC + optional cellId) to a latitude/longitude estimate. No subscriber identifier required \u2014 caller supplies cell parameters directly. Useful for fraud detection, roaming cost attribution, and network troubleshooting when you have raw cell info from an external source (e.g. a Relay LU webhook event). Params: `radio_type` ('2G'|'3G'|'4G'|'5G'|'NB-IoT'; also accepts 'NBIOT'|'nb-iot'|'nbiot' \u2014 normalised to 'NB-IoT'), `mcc` (int), `mnc` (int), `lac` (int), `cell_id` (int, optional but strongly recommended for accuracy), `signal_strength` (number dBm, optional). Returns: { latitude, longitude, accuracy } \u2014 accuracy is median error in meters at 50% confidence. Without cell_id accuracy degrades significantly (>10 km). Do NOT use this for bulk fleet location sweeps \u2014 one OCS call per cell tower; use `audit_network_coverage` for fleet-level analysis instead.",
|
|
4031
4189
|
inputSchema: {
|
|
4032
|
-
radio_type: z3.
|
|
4190
|
+
radio_type: z3.string().transform(normalizeRadioType).refine((v) => VALID_RADIO_TYPES.has(v), {
|
|
4191
|
+
message: "radio_type must be one of: 2G, 3G, 4G, 5G, NB-IoT (also accepts NBIOT, nb-iot, nbiot)"
|
|
4192
|
+
}).describe("Radio access technology type: '2G'|'3G'|'4G'|'5G'|'NB-IoT'. Also accepts 'NBIOT'|'nb-iot'|'nbiot' \u2014 normalised to 'NB-IoT'."),
|
|
4033
4193
|
mcc: z3.number().int().describe("Mobile Country Code (e.g. 250 for Russia, 234 for UK)"),
|
|
4034
4194
|
mnc: z3.number().int().describe("Mobile Network Code"),
|
|
4035
4195
|
lac: z3.number().int().describe("Location Area Code"),
|
|
@@ -4051,7 +4211,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4051
4211
|
cell_id,
|
|
4052
4212
|
signal_strength
|
|
4053
4213
|
}, token2) => {
|
|
4054
|
-
const client = new
|
|
4214
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4055
4215
|
const params = { radioType: radio_type, mcc, mnc, lac };
|
|
4056
4216
|
if (cell_id !== void 0) params.cellId = cell_id;
|
|
4057
4217
|
if (signal_strength !== void 0) params.signalStrength = signal_strength;
|
|
@@ -4079,7 +4239,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4079
4239
|
ctx,
|
|
4080
4240
|
async ({ resellerId }, token2) => {
|
|
4081
4241
|
const id = resellerId ?? await (async () => {
|
|
4082
|
-
const client2 = new
|
|
4242
|
+
const client2 = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4083
4243
|
const info = await client2.call("getResellerInfo", {});
|
|
4084
4244
|
const resolvedId = info?.id;
|
|
4085
4245
|
if (typeof resolvedId !== "number") {
|
|
@@ -4087,7 +4247,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4087
4247
|
}
|
|
4088
4248
|
return resolvedId;
|
|
4089
4249
|
})();
|
|
4090
|
-
const client = new
|
|
4250
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4091
4251
|
const result2 = await client.call("listDetailedDestinationList", id);
|
|
4092
4252
|
return {
|
|
4093
4253
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
@@ -4113,7 +4273,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4113
4273
|
BACKLOG_TOOL_SCOPES["modify_subscriber_mobile_plan"],
|
|
4114
4274
|
ctx,
|
|
4115
4275
|
async ({ iccid, mobile_plan_id }, token2) => {
|
|
4116
|
-
const client = new
|
|
4276
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4117
4277
|
const result2 = await client.call(
|
|
4118
4278
|
"modifySubscriberMobilePlan",
|
|
4119
4279
|
{ subscriber: iccid, mobilePlanId: mobile_plan_id }
|
|
@@ -4147,7 +4307,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4147
4307
|
const params = { subscriber: iccid, packageId: package_id };
|
|
4148
4308
|
if (start_date !== void 0) params.startDate = start_date;
|
|
4149
4309
|
if (end_date !== void 0) params.endDate = end_date;
|
|
4150
|
-
const client = new
|
|
4310
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4151
4311
|
const result2 = await client.call(
|
|
4152
4312
|
"modifySubscriberPrepaidPackageActivePeriod",
|
|
4153
4313
|
params
|
|
@@ -4176,7 +4336,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4176
4336
|
BACKLOG_TOOL_SCOPES["modify_subscriber_voip_plan"],
|
|
4177
4337
|
ctx,
|
|
4178
4338
|
async ({ iccid, voip_plan_id }, token2) => {
|
|
4179
|
-
const client = new
|
|
4339
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4180
4340
|
const result2 = await client.call(
|
|
4181
4341
|
"modifySubscriberVoipPlan",
|
|
4182
4342
|
{ subscriber: iccid, voipPlanId: voip_plan_id }
|
|
@@ -4207,7 +4367,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4207
4367
|
const cache = /* @__PURE__ */ new Map();
|
|
4208
4368
|
const sub = await resolveSubscriberByIccid(ctx.env, token2, iccid, cache);
|
|
4209
4369
|
const subscriberId = sub.id ?? sub.subscriberId ?? iccid;
|
|
4210
|
-
const client = new
|
|
4370
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4211
4371
|
const result2 = await client.call(
|
|
4212
4372
|
"pushSteeringToSubs",
|
|
4213
4373
|
{ subscriber: subscriberId }
|
|
@@ -4235,7 +4395,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4235
4395
|
BACKLOG_TOOL_SCOPES["reset_subscriber_gz_counter"],
|
|
4236
4396
|
ctx,
|
|
4237
4397
|
async ({ iccid }, token2) => {
|
|
4238
|
-
const client = new
|
|
4398
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4239
4399
|
const result2 = await client.call("resetSubsGzCounter", { subscriber: iccid });
|
|
4240
4400
|
return {
|
|
4241
4401
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
@@ -4259,7 +4419,7 @@ function registerAllBacklogTools(server2, ctx) {
|
|
|
4259
4419
|
BACKLOG_TOOL_SCOPES["carrier_webhook_config"],
|
|
4260
4420
|
ctx,
|
|
4261
4421
|
async ({ reseller_id }, token2) => {
|
|
4262
|
-
const client = new
|
|
4422
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
4263
4423
|
let resellerId = reseller_id;
|
|
4264
4424
|
if (resellerId === void 0) {
|
|
4265
4425
|
resellerId = await getDefaultResellerId(ctx.env, token2);
|
|
@@ -4308,10 +4468,20 @@ var TOOL_REGISTRY = /* @__PURE__ */ new Set([
|
|
|
4308
4468
|
"high_cost_subscribers",
|
|
4309
4469
|
// event ring buffer (1)
|
|
4310
4470
|
"list_recent_ocs_events",
|
|
4471
|
+
// CAR-39: rate-limit governor (1)
|
|
4472
|
+
"rate_limit_status",
|
|
4473
|
+
// CAR-38: Relay LU country history (1)
|
|
4474
|
+
"subscriber_country_history",
|
|
4475
|
+
// bundle depletion events (1) — CAR-42 wave-2
|
|
4476
|
+
"subscriber_depletion_events",
|
|
4311
4477
|
// MCP App tools (3)
|
|
4312
4478
|
"fleet_health_app",
|
|
4313
4479
|
"provision_esim_wizard",
|
|
4314
4480
|
"balance_topup_form",
|
|
4481
|
+
// greenzone whitelist tools (3) — portal-only, executed via Kapture browser automation
|
|
4482
|
+
"greenzone_whitelist_add",
|
|
4483
|
+
"greenzone_whitelist_remove",
|
|
4484
|
+
"greenzone_whitelist_list",
|
|
4315
4485
|
// router tools (self-reference)
|
|
4316
4486
|
"carrier_ask",
|
|
4317
4487
|
"carrier_ask_describe"
|
|
@@ -4526,8 +4696,22 @@ var ROUTER_TOOLS = [
|
|
|
4526
4696
|
},
|
|
4527
4697
|
{
|
|
4528
4698
|
name: "get_subscriber_location_by_cell_id",
|
|
4529
|
-
description: "Get granular
|
|
4530
|
-
input_schema: {
|
|
4699
|
+
description: "Get granular cell-tower location via GeoSense using radio_type, mcc, mnc, lac, and optional cell_id. Use radio_type '4G' when the user means LTE \u2014 the literal 'LTE' is not valid for the underlying API. NB-IoT aliases NBIOT, nbiot, nb-iot normalise to NB-IoT. Intent: 'cell-level location', 'cell tower for subscriber', 'exact location', 'find subscriber by cell id', 'locate SIM by cell tower', 'NB-IoT subscriber location', 'which cell is this SIM on'.",
|
|
4700
|
+
input_schema: {
|
|
4701
|
+
type: "object",
|
|
4702
|
+
required: ["radio_type", "mcc", "mnc", "lac"],
|
|
4703
|
+
properties: {
|
|
4704
|
+
iccid: { type: "string" },
|
|
4705
|
+
radio_type: {
|
|
4706
|
+
type: "string",
|
|
4707
|
+
description: "One of 2G, 3G, 4G, 5G, NB-IoT. Use 4G for LTE networks. NB-IoT also accepts NBIOT, nbiot, nb-iot."
|
|
4708
|
+
},
|
|
4709
|
+
mcc: { type: "number", description: "Mobile Country Code" },
|
|
4710
|
+
mnc: { type: "number", description: "Mobile Network Code" },
|
|
4711
|
+
lac: { type: "number", description: "Location Area Code" },
|
|
4712
|
+
cell_id: { type: "number", description: "Cell tower ID (optional but recommended)" }
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4531
4715
|
},
|
|
4532
4716
|
{
|
|
4533
4717
|
name: "list_steering_lists",
|
|
@@ -4604,6 +4788,26 @@ var ROUTER_TOOLS = [
|
|
|
4604
4788
|
description: "List recent OCS events for a subscriber (last 50, 24h window). Intent: 'recent events', 'event history', 'OCS log'.",
|
|
4605
4789
|
input_schema: { type: "object", properties: { iccid: { type: "string" } } }
|
|
4606
4790
|
},
|
|
4791
|
+
{
|
|
4792
|
+
name: "rate_limit_status",
|
|
4793
|
+
description: "Inspect Bridge4IP OCS rate-limit governor state \u2014 bucket fill levels and reseller quota per endpoint. Intent: 'are we rate limited', 'ocs quota', 'rate limit status', 'how full is the api bucket', 'check rate limit', 'api capacity'.",
|
|
4794
|
+
input_schema: {
|
|
4795
|
+
type: "object",
|
|
4796
|
+
properties: { endpoint: { type: "string" } }
|
|
4797
|
+
}
|
|
4798
|
+
},
|
|
4799
|
+
{
|
|
4800
|
+
name: "subscriber_country_history",
|
|
4801
|
+
description: "Return cross-border movement history for a subscriber from Relay LU events. Intent: 'where has subscriber X traveled', 'country history for ICCID', 'border crossings', 'which countries did this SIM visit', 'travel history'.",
|
|
4802
|
+
input_schema: {
|
|
4803
|
+
type: "object",
|
|
4804
|
+
properties: {
|
|
4805
|
+
subscriberId: { type: "string" },
|
|
4806
|
+
limit: { type: "number" }
|
|
4807
|
+
},
|
|
4808
|
+
required: ["subscriberId"]
|
|
4809
|
+
}
|
|
4810
|
+
},
|
|
4607
4811
|
{
|
|
4608
4812
|
name: "affect_subscriber_phone_number",
|
|
4609
4813
|
description: "Assign or unassign a phone number to a subscriber. Intent: 'assign phone number', 'give SIM a number', 'remove number'.",
|
|
@@ -4688,6 +4892,44 @@ var ROUTER_TOOLS = [
|
|
|
4688
4892
|
name: "carrier_ask_describe",
|
|
4689
4893
|
description: "Get documentation for a specific carrier tool. Intent: 'describe tool X', 'how does assign_package work', 'tool documentation'.",
|
|
4690
4894
|
input_schema: { type: "object", properties: { tool_name: { type: "string" } } }
|
|
4895
|
+
},
|
|
4896
|
+
{
|
|
4897
|
+
name: "greenzone_whitelist_add",
|
|
4898
|
+
description: "Add a host and/or IP to the GreenZone whitelist. Operates via portal-only Kapture browser automation \u2014 no direct REST API. Intent: 'add to greenzone whitelist', 'whitelist this host in greenzone', 'allow host in greenzone', 'greenzone add host', 'add IP to greenzone'.",
|
|
4899
|
+
input_schema: {
|
|
4900
|
+
type: "object",
|
|
4901
|
+
properties: {
|
|
4902
|
+
host: { type: "string", description: "Hostname to whitelist (e.g. api.example.com)" },
|
|
4903
|
+
ip: { type: "string", description: "IP address to whitelist (optional)" }
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
},
|
|
4907
|
+
{
|
|
4908
|
+
name: "greenzone_whitelist_remove",
|
|
4909
|
+
description: "Remove a host and/or IP from the GreenZone whitelist. Operates via portal-only Kapture browser automation \u2014 no direct REST API. Intent: 'remove from greenzone whitelist', 'de-whitelist host in greenzone', 'block host in greenzone', 'greenzone remove host'.",
|
|
4910
|
+
input_schema: {
|
|
4911
|
+
type: "object",
|
|
4912
|
+
properties: {
|
|
4913
|
+
host: { type: "string", description: "Hostname to remove" },
|
|
4914
|
+
ip: { type: "string", description: "IP address to remove (optional)" }
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
},
|
|
4918
|
+
{
|
|
4919
|
+
name: "greenzone_whitelist_list",
|
|
4920
|
+
description: "List all hosts and IPs in the GreenZone whitelist (reads from KV cache populated by last portal sync). Intent: 'show greenzone whitelist', 'list greenzone hosts', 'what hosts are in greenzone', 'greenzone whitelist', 'show greenzone entries', 'which IPs are whitelisted in greenzone'.",
|
|
4921
|
+
input_schema: { type: "object", properties: {} }
|
|
4922
|
+
},
|
|
4923
|
+
{
|
|
4924
|
+
name: "subscriber_depletion_events",
|
|
4925
|
+
description: "Look up bundle depletion events from prepaid-usage webhooks (last_slice notifications, 7-day retention). Intent: 'did subscriber X run out of data', 'depletion history for ICCID', 'bundle depleted events', 'which subscribers ran out of data', 'recent depletions', 'show data exhaustion events', 'has this SIM hit its data cap', 'list depleted subscribers'.",
|
|
4926
|
+
input_schema: {
|
|
4927
|
+
type: "object",
|
|
4928
|
+
properties: {
|
|
4929
|
+
subscriberId: { type: "string", description: "OCS subscriber ID \u2014 omit to list fleet-wide depletions" },
|
|
4930
|
+
since: { type: "string", description: "ISO-8601 timestamp \u2014 filter to events at or after this time" }
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4691
4933
|
}
|
|
4692
4934
|
];
|
|
4693
4935
|
var ROUTER_SYSTEM_PROMPT = `You are a carrier fleet operations router. Your job is to map a user's natural-language intent to exactly ONE tool from the Carrier MCP tool registry.
|
|
@@ -4720,10 +4962,10 @@ async function callBedrock(env, payload) {
|
|
|
4720
4962
|
const body = await resp.text();
|
|
4721
4963
|
if (resp.status === 429) {
|
|
4722
4964
|
const retryAfter = resp.headers.get("retry-after");
|
|
4723
|
-
const
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
throw
|
|
4965
|
+
const err4 = new Error(`Bedrock rate limit: ${body}`);
|
|
4966
|
+
err4.isRateLimit = true;
|
|
4967
|
+
err4.retryAfter = retryAfter;
|
|
4968
|
+
throw err4;
|
|
4727
4969
|
}
|
|
4728
4970
|
throw new Error(`Bedrock invoke failed: ${resp.status} ${body}`);
|
|
4729
4971
|
}
|
|
@@ -4761,9 +5003,9 @@ Pre-resolved context: ${JSON.stringify(definedContext)}` : "";
|
|
|
4761
5003
|
AWS_REGION: env.AWS_REGION,
|
|
4762
5004
|
BEDROCK_MODEL_ID: env.BEDROCK_MODEL_ID
|
|
4763
5005
|
}, payload);
|
|
4764
|
-
} catch (
|
|
4765
|
-
if (
|
|
4766
|
-
const retryAfter =
|
|
5006
|
+
} catch (err4) {
|
|
5007
|
+
if (err4 instanceof Error && err4.isRateLimit) {
|
|
5008
|
+
const retryAfter = err4.retryAfter;
|
|
4767
5009
|
const parsedSeconds = retryAfter ? parseInt(retryAfter, 10) : 60;
|
|
4768
5010
|
return {
|
|
4769
5011
|
match: "rate_limited",
|
|
@@ -4771,7 +5013,7 @@ Pre-resolved context: ${JSON.stringify(definedContext)}` : "";
|
|
|
4771
5013
|
suggestion: "Bedrock rate limit reached. Please retry after the indicated delay."
|
|
4772
5014
|
};
|
|
4773
5015
|
}
|
|
4774
|
-
throw
|
|
5016
|
+
throw err4;
|
|
4775
5017
|
}
|
|
4776
5018
|
const toolUseBlock = response.content.find(
|
|
4777
5019
|
(block) => block.type === "tool_use"
|
|
@@ -4945,7 +5187,7 @@ function registerAllCarrierAskTools(server2, ctx) {
|
|
|
4945
5187
|
let route;
|
|
4946
5188
|
try {
|
|
4947
5189
|
route = await _routeIntent(intent, context, ctx.env);
|
|
4948
|
-
} catch (
|
|
5190
|
+
} catch (err4) {
|
|
4949
5191
|
writeCarrierAskAudit(ctx.env, {
|
|
4950
5192
|
intent_hash: intentHash,
|
|
4951
5193
|
match: "error",
|
|
@@ -4962,7 +5204,7 @@ function registerAllCarrierAskTools(server2, ctx) {
|
|
|
4962
5204
|
type: "text",
|
|
4963
5205
|
text: JSON.stringify({
|
|
4964
5206
|
error: "routing_error",
|
|
4965
|
-
message:
|
|
5207
|
+
message: err4 instanceof Error ? err4.message : "An unexpected error occurred during routing."
|
|
4966
5208
|
})
|
|
4967
5209
|
}
|
|
4968
5210
|
],
|
|
@@ -5104,24 +5346,10 @@ function buildExamples(toolName) {
|
|
|
5104
5346
|
|
|
5105
5347
|
// src/list-recent-ocs-events.ts
|
|
5106
5348
|
import { z as z5 } from "zod";
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
"esim.disabled",
|
|
5110
|
-
"package.expiry_warning",
|
|
5111
|
-
"location.changed",
|
|
5112
|
-
"balance.low"
|
|
5113
|
-
];
|
|
5114
|
-
var listRecentOcsEventsSchema = {
|
|
5115
|
-
iccid: z5.string().regex(/^\d{19,20}$/).describe("ICCID, 19 or 20 digits, ITU-T E.118 format"),
|
|
5116
|
-
limit: z5.number().int().min(1).max(50).default(20).describe("Max events to return, newest first"),
|
|
5117
|
-
event_types: z5.array(z5.enum(ALLOWED_EVENT_TYPES)).optional().describe("Filter to specific event types"),
|
|
5118
|
-
since: z5.string().datetime().optional().describe("ISO-8601 timestamp; only events after this point")
|
|
5119
|
-
};
|
|
5349
|
+
|
|
5350
|
+
// ../../packages/ocs-spec/src/ocs-event-buffer-read.ts
|
|
5120
5351
|
async function listRecentOcsEvents(iccid, limit, eventTypes, since, kv) {
|
|
5121
|
-
const routingRaw = await kv.get(
|
|
5122
|
-
`iccid:${iccid}`,
|
|
5123
|
-
"json"
|
|
5124
|
-
);
|
|
5352
|
+
const routingRaw = await kv.get(`iccid:${iccid}`, "json");
|
|
5125
5353
|
const resellerId = routingRaw?.reseller_id ?? 0;
|
|
5126
5354
|
const ringKey = `events:${resellerId}:${iccid}`;
|
|
5127
5355
|
const itemPrefix = `${ringKey}:evt:`;
|
|
@@ -5157,8 +5385,7 @@ async function listRecentOcsEvents(iccid, limit, eventTypes, since, kv) {
|
|
|
5157
5385
|
const sinceMs = since ? new Date(since).getTime() : null;
|
|
5158
5386
|
const filtered = allSorted.filter((e) => {
|
|
5159
5387
|
if (sinceMs !== null && e.occurred_at * 1e3 <= sinceMs) return false;
|
|
5160
|
-
if (eventTypes && eventTypes.length > 0 && !eventTypes.includes(e.event_type))
|
|
5161
|
-
return false;
|
|
5388
|
+
if (eventTypes && eventTypes.length > 0 && !eventTypes.includes(e.event_type)) return false;
|
|
5162
5389
|
return true;
|
|
5163
5390
|
});
|
|
5164
5391
|
const filteredCount = filtered.length;
|
|
@@ -5184,6 +5411,25 @@ async function listRecentOcsEvents(iccid, limit, eventTypes, since, kv) {
|
|
|
5184
5411
|
buffer_newest_event_timestamp: bufferNewest
|
|
5185
5412
|
};
|
|
5186
5413
|
}
|
|
5414
|
+
|
|
5415
|
+
// src/list-recent-ocs-events.ts
|
|
5416
|
+
var ALLOWED_EVENT_TYPES = [
|
|
5417
|
+
"esim.activated",
|
|
5418
|
+
"esim.disabled",
|
|
5419
|
+
"package.expiry_warning",
|
|
5420
|
+
"location.changed",
|
|
5421
|
+
"country.entered",
|
|
5422
|
+
"balance.low"
|
|
5423
|
+
];
|
|
5424
|
+
var listRecentOcsEventsSchema = {
|
|
5425
|
+
iccid: z5.string().regex(/^\d{19,20}$/).describe("ICCID, 19 or 20 digits, ITU-T E.118 format"),
|
|
5426
|
+
limit: z5.number().int().min(1).max(50).default(20).describe("Max events to return, newest first"),
|
|
5427
|
+
event_types: z5.array(z5.enum(ALLOWED_EVENT_TYPES)).optional().describe("Filter to specific event types"),
|
|
5428
|
+
since: z5.string().datetime().optional().describe("ISO-8601 timestamp; only events after this point")
|
|
5429
|
+
};
|
|
5430
|
+
async function listRecentOcsEvents2(iccid, limit, eventTypes, since, kv) {
|
|
5431
|
+
return listRecentOcsEvents(iccid, limit, eventTypes, since, kv);
|
|
5432
|
+
}
|
|
5187
5433
|
function registerListRecentOcsEventsTool(server2, env) {
|
|
5188
5434
|
server2.registerTool(
|
|
5189
5435
|
"list_recent_ocs_events",
|
|
@@ -5200,7 +5446,7 @@ function registerListRecentOcsEventsTool(server2, env) {
|
|
|
5200
5446
|
async (args) => {
|
|
5201
5447
|
const { iccid, limit, event_types, since } = args;
|
|
5202
5448
|
try {
|
|
5203
|
-
const result2 = await
|
|
5449
|
+
const result2 = await listRecentOcsEvents2(
|
|
5204
5450
|
iccid,
|
|
5205
5451
|
limit,
|
|
5206
5452
|
event_types,
|
|
@@ -5210,8 +5456,8 @@ function registerListRecentOcsEventsTool(server2, env) {
|
|
|
5210
5456
|
return {
|
|
5211
5457
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
5212
5458
|
};
|
|
5213
|
-
} catch (
|
|
5214
|
-
const message =
|
|
5459
|
+
} catch (err4) {
|
|
5460
|
+
const message = err4 instanceof Error ? err4.message : String(err4);
|
|
5215
5461
|
return {
|
|
5216
5462
|
isError: true,
|
|
5217
5463
|
content: [{ type: "text", text: `Error reading OCS event buffer: ${message}` }]
|
|
@@ -5221,8 +5467,271 @@ function registerListRecentOcsEventsTool(server2, env) {
|
|
|
5221
5467
|
);
|
|
5222
5468
|
}
|
|
5223
5469
|
|
|
5224
|
-
// src/
|
|
5470
|
+
// src/tools-rate-governor.ts
|
|
5225
5471
|
import { z as z6 } from "zod";
|
|
5472
|
+
var MONITORED_ENDPOINTS = [
|
|
5473
|
+
"subscriberNetworkEventsOverPeriod",
|
|
5474
|
+
// 30/min
|
|
5475
|
+
"subscriberUsageOverPeriod",
|
|
5476
|
+
// 100/min
|
|
5477
|
+
"listLocationZoneElement",
|
|
5478
|
+
// 150/min
|
|
5479
|
+
"sendMtSms",
|
|
5480
|
+
// 150/min
|
|
5481
|
+
"affectPlanToSubscriber",
|
|
5482
|
+
// 300/min
|
|
5483
|
+
"affectRecurringPackageToSubscriber",
|
|
5484
|
+
// 300/min
|
|
5485
|
+
"listSubscriber",
|
|
5486
|
+
// 300/min
|
|
5487
|
+
"listSubscriberPrepaidPackages",
|
|
5488
|
+
// 300/min
|
|
5489
|
+
"getSingleSubscriber",
|
|
5490
|
+
// 300/min
|
|
5491
|
+
"getSubscriberLocationByCellId",
|
|
5492
|
+
// 300/min
|
|
5493
|
+
"__global__"
|
|
5494
|
+
// 600/min
|
|
5495
|
+
];
|
|
5496
|
+
function registerRateLimitStatusTool(server2, ctx) {
|
|
5497
|
+
server2.registerTool(
|
|
5498
|
+
"rate_limit_status",
|
|
5499
|
+
{
|
|
5500
|
+
title: "API Rate-Limit Governor Status",
|
|
5501
|
+
description: "Inspect Bridge4IP OCS rate-limit governor state \u2014 bucket fill levels, per-endpoint limits, and 80% alert thresholds. Use this to answer: 'are we rate limited', 'check ocs quota', 'how full is the rate limit bucket', 'rate limit status'. Params: `endpoint` (optional \u2014 OCS camelCase method name to inspect a single bucket, e.g. 'subscriberUsageOverPeriod'; omit for all monitored endpoints). Returns: per-endpoint { limit_per_min, batch_cap_per_min, calls_in_window, batch_calls_in_window, utilisation_pct, alert (true when \u226580% of limit) }. Note: counts come from the in-process sliding-window log (same key as OCS calls: API token + endpoint) \u2014 they reset when the Worker isolate recycles. Use as a relative signal, not an exact counter. Do NOT use this to adjust limits \u2014 limits are set by Bridge4IP contract and cannot be changed via MCP.",
|
|
5502
|
+
inputSchema: {
|
|
5503
|
+
endpoint: z6.string().optional().describe(
|
|
5504
|
+
"OCS method name in camelCase (e.g. 'subscriberUsageOverPeriod'). Omit to return status for all monitored endpoints."
|
|
5505
|
+
)
|
|
5506
|
+
},
|
|
5507
|
+
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
5508
|
+
},
|
|
5509
|
+
wrapHandler(
|
|
5510
|
+
"rate_limit_status",
|
|
5511
|
+
"[rate-governor:in-process]",
|
|
5512
|
+
"read",
|
|
5513
|
+
ctx,
|
|
5514
|
+
async ({ endpoint }, token2) => {
|
|
5515
|
+
const targets = endpoint ? [endpoint] : MONITORED_ENDPOINTS;
|
|
5516
|
+
const rows = targets.map((ep) => {
|
|
5517
|
+
const limit = getLimitForEndpoint(ep);
|
|
5518
|
+
const batch_cap_per_min = Math.floor(limit * 0.2);
|
|
5519
|
+
const { calls_in_window, batch_calls_in_window } = getRateLimitWindowCounts(token2, ep);
|
|
5520
|
+
const utilisation_pct = limit > 0 ? Math.round(calls_in_window / limit * 100) : 0;
|
|
5521
|
+
const alert = utilisation_pct >= 80;
|
|
5522
|
+
return {
|
|
5523
|
+
endpoint: ep,
|
|
5524
|
+
limit_per_min: limit,
|
|
5525
|
+
batch_cap_per_min,
|
|
5526
|
+
calls_in_window,
|
|
5527
|
+
batch_calls_in_window,
|
|
5528
|
+
utilisation_pct,
|
|
5529
|
+
alert
|
|
5530
|
+
};
|
|
5531
|
+
});
|
|
5532
|
+
const resellerId = String(ctx.props.reseller_id);
|
|
5533
|
+
return {
|
|
5534
|
+
content: [
|
|
5535
|
+
{
|
|
5536
|
+
type: "text",
|
|
5537
|
+
text: JSON.stringify(
|
|
5538
|
+
{
|
|
5539
|
+
reseller_id: resellerId,
|
|
5540
|
+
window_seconds: 60,
|
|
5541
|
+
batch_fraction: 0.2,
|
|
5542
|
+
endpoints: rows,
|
|
5543
|
+
alert_threshold_pct: 80,
|
|
5544
|
+
metric_name: "carrier_ocs_calls_per_min",
|
|
5545
|
+
metric_location: "CF Workers Tail / BetterStack \u2014 structured JSON log lines with metric='carrier_ocs_calls_per_min'"
|
|
5546
|
+
},
|
|
5547
|
+
null,
|
|
5548
|
+
2
|
|
5549
|
+
)
|
|
5550
|
+
}
|
|
5551
|
+
]
|
|
5552
|
+
};
|
|
5553
|
+
}
|
|
5554
|
+
)
|
|
5555
|
+
);
|
|
5556
|
+
}
|
|
5557
|
+
|
|
5558
|
+
// src/tools-depletion-events.ts
|
|
5559
|
+
import { z as z7 } from "zod";
|
|
5560
|
+
function ok(payload) {
|
|
5561
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
5562
|
+
}
|
|
5563
|
+
function err(message) {
|
|
5564
|
+
return { isError: true, content: [{ type: "text", text: message }] };
|
|
5565
|
+
}
|
|
5566
|
+
function registerDepletionEventsTool(server2, ctx) {
|
|
5567
|
+
server2.registerTool(
|
|
5568
|
+
"subscriber_depletion_events",
|
|
5569
|
+
{
|
|
5570
|
+
title: "Subscriber Bundle Depletion Events",
|
|
5571
|
+
description: "Look up bundle depletion events for a subscriber (or list recent fleet-wide depletions). Returns the most recent bundle.depleted event stored when the prepaid usage webhook received a last_slice notification. Events are retained for 7 days. Use subscriberId to check a specific subscriber, or omit to list the 20 most recent fleet-wide depletions. Requires 'read' scope.",
|
|
5572
|
+
inputSchema: {
|
|
5573
|
+
subscriberId: z7.string().optional().describe(
|
|
5574
|
+
"OCS subscriber ID to look up. Omit to list the 20 most recent fleet-wide depletions."
|
|
5575
|
+
),
|
|
5576
|
+
since: z7.string().optional().describe(
|
|
5577
|
+
"ISO-8601 timestamp \u2014 only return events at or after this time (for single-subscriber lookup)."
|
|
5578
|
+
)
|
|
5579
|
+
},
|
|
5580
|
+
annotations: { readOnlyHint: true }
|
|
5581
|
+
},
|
|
5582
|
+
async (args) => {
|
|
5583
|
+
if (!ctx.props.scope.includes("read")) {
|
|
5584
|
+
return err(
|
|
5585
|
+
`Scope denied: subscriber_depletion_events requires 'read' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
5586
|
+
);
|
|
5587
|
+
}
|
|
5588
|
+
const kv = ctx.env.RATE_LIMIT_KV;
|
|
5589
|
+
if (!kv) {
|
|
5590
|
+
return err(
|
|
5591
|
+
"RATE_LIMIT_KV binding is not available in this environment. Add the binding to wrangler.jsonc and redeploy."
|
|
5592
|
+
);
|
|
5593
|
+
}
|
|
5594
|
+
const { subscriberId, since } = args;
|
|
5595
|
+
if (subscriberId) {
|
|
5596
|
+
const raw = await kv.get(`bundle-depleted:${subscriberId}`, "text");
|
|
5597
|
+
if (!raw) {
|
|
5598
|
+
return ok({
|
|
5599
|
+
subscriber_id: subscriberId,
|
|
5600
|
+
depleted: false,
|
|
5601
|
+
event: null,
|
|
5602
|
+
note: "No bundle depletion event found in the last 7 days."
|
|
5603
|
+
});
|
|
5604
|
+
}
|
|
5605
|
+
let event;
|
|
5606
|
+
try {
|
|
5607
|
+
event = JSON.parse(raw);
|
|
5608
|
+
} catch {
|
|
5609
|
+
return ok({
|
|
5610
|
+
subscriber_id: subscriberId,
|
|
5611
|
+
depleted: false,
|
|
5612
|
+
event: null,
|
|
5613
|
+
note: "Stored depletion entry was invalid JSON and was ignored."
|
|
5614
|
+
});
|
|
5615
|
+
}
|
|
5616
|
+
if (since && event.depletedAt < since) {
|
|
5617
|
+
return ok({
|
|
5618
|
+
subscriber_id: subscriberId,
|
|
5619
|
+
depleted: false,
|
|
5620
|
+
event: null,
|
|
5621
|
+
note: `No bundle depletion after ${since}.`
|
|
5622
|
+
});
|
|
5623
|
+
}
|
|
5624
|
+
return ok({
|
|
5625
|
+
subscriber_id: subscriberId,
|
|
5626
|
+
depleted: true,
|
|
5627
|
+
event: {
|
|
5628
|
+
subscriber_id: event.subscriberId,
|
|
5629
|
+
iccid: event.iccid,
|
|
5630
|
+
msisdn: event.msisdn,
|
|
5631
|
+
plan_id: event.planId,
|
|
5632
|
+
depleted_at: event.depletedAt
|
|
5633
|
+
}
|
|
5634
|
+
});
|
|
5635
|
+
}
|
|
5636
|
+
const listResult = await kv.list({ prefix: "bundle-depleted:", limit: 20 });
|
|
5637
|
+
if (listResult.keys.length === 0) {
|
|
5638
|
+
return ok({
|
|
5639
|
+
depletions: [],
|
|
5640
|
+
total: 0,
|
|
5641
|
+
note: "No bundle depletion events in the last 7 days."
|
|
5642
|
+
});
|
|
5643
|
+
}
|
|
5644
|
+
const events = [];
|
|
5645
|
+
await Promise.all(
|
|
5646
|
+
listResult.keys.map(async (k) => {
|
|
5647
|
+
const raw = await kv.get(k.name, "text");
|
|
5648
|
+
if (!raw) return;
|
|
5649
|
+
try {
|
|
5650
|
+
const ev = JSON.parse(raw);
|
|
5651
|
+
if (since && ev.depletedAt < since) return;
|
|
5652
|
+
events.push({
|
|
5653
|
+
subscriber_id: ev.subscriberId,
|
|
5654
|
+
iccid: ev.iccid,
|
|
5655
|
+
msisdn: ev.msisdn,
|
|
5656
|
+
plan_id: ev.planId,
|
|
5657
|
+
depleted_at: ev.depletedAt
|
|
5658
|
+
});
|
|
5659
|
+
} catch {
|
|
5660
|
+
}
|
|
5661
|
+
})
|
|
5662
|
+
);
|
|
5663
|
+
events.sort((a, b) => b.depleted_at.localeCompare(a.depleted_at));
|
|
5664
|
+
return ok({
|
|
5665
|
+
depletions: events,
|
|
5666
|
+
total: events.length,
|
|
5667
|
+
list_truncated: !listResult.list_complete
|
|
5668
|
+
});
|
|
5669
|
+
}
|
|
5670
|
+
);
|
|
5671
|
+
}
|
|
5672
|
+
|
|
5673
|
+
// src/tools-country-history.ts
|
|
5674
|
+
import { z as z8 } from "zod";
|
|
5675
|
+
function registerCountryHistoryTool(server2, ctx) {
|
|
5676
|
+
server2.registerTool(
|
|
5677
|
+
"subscriber_country_history",
|
|
5678
|
+
{
|
|
5679
|
+
title: "Subscriber Country History",
|
|
5680
|
+
description: "Return cross-border movement history for a subscriber from Relay LU events. Reads country.entered events from the OCS ring-buffer (24h window, up to `limit` entries per request) written by the Bridge4IP Relay LU webhook. Each entry includes the new country, previous country, MCC/MNC, and timestamp. Use this to answer: 'where has subscriber X traveled', 'show country history for ICCID', 'border crossings for this SIM'. Params: `subscriberId` (ICCID, 19\u201320 digits), `limit` (1\u201350, default 20, newest first). Returns: ordered list of country.entered events with { new_country, previous_country, mcc, mnc, timestamp, source }. Empty list when no cross-border events recorded. Do NOT use this for raw location events \u2014 use `list_recent_ocs_events` with event_types=['location.changed'] for finer-grained location data.",
|
|
5681
|
+
inputSchema: {
|
|
5682
|
+
subscriberId: z8.string().regex(/^\d{19,20}$/, "subscriberId must be a 19\u201320 digit ICCID").describe("Subscriber ICCID (19\u201320 digits, ITU-T E.118)"),
|
|
5683
|
+
limit: z8.number().int().min(1).max(50).default(20).optional().describe("Maximum number of events to return, newest first (default 20, max 50)")
|
|
5684
|
+
},
|
|
5685
|
+
annotations: { readOnlyHint: true, idempotentHint: true }
|
|
5686
|
+
},
|
|
5687
|
+
wrapHandler(
|
|
5688
|
+
"subscriber_country_history",
|
|
5689
|
+
"[country-history:kv]",
|
|
5690
|
+
"read",
|
|
5691
|
+
ctx,
|
|
5692
|
+
async ({ subscriberId, limit }) => {
|
|
5693
|
+
const effectiveLimit = limit ?? 20;
|
|
5694
|
+
const result2 = await listRecentOcsEvents2(
|
|
5695
|
+
subscriberId,
|
|
5696
|
+
effectiveLimit,
|
|
5697
|
+
["country.entered"],
|
|
5698
|
+
void 0,
|
|
5699
|
+
ctx.env.OCS_EVENT_ROUTING
|
|
5700
|
+
);
|
|
5701
|
+
const countryEvents = result2.events;
|
|
5702
|
+
return {
|
|
5703
|
+
content: [
|
|
5704
|
+
{
|
|
5705
|
+
type: "text",
|
|
5706
|
+
text: JSON.stringify(
|
|
5707
|
+
{
|
|
5708
|
+
subscriber_id: subscriberId,
|
|
5709
|
+
country_events: countryEvents.map((e) => ({
|
|
5710
|
+
event_id: e.event_id,
|
|
5711
|
+
new_country: e.data["new_country"] ?? null,
|
|
5712
|
+
previous_country: e.data["previous_country"] ?? null,
|
|
5713
|
+
mcc: e.data["mcc"] ?? null,
|
|
5714
|
+
mnc: e.data["mnc"] ?? null,
|
|
5715
|
+
timestamp: e.timestamp,
|
|
5716
|
+
source: e.data["source"] ?? "relay-lu"
|
|
5717
|
+
})),
|
|
5718
|
+
total_country_events: countryEvents.length,
|
|
5719
|
+
total_in_buffer: result2.total_in_buffer,
|
|
5720
|
+
note: countryEvents.length === 0 ? "No cross-border events recorded in the 24h ring-buffer for this subscriber." : void 0
|
|
5721
|
+
},
|
|
5722
|
+
null,
|
|
5723
|
+
2
|
|
5724
|
+
)
|
|
5725
|
+
}
|
|
5726
|
+
]
|
|
5727
|
+
};
|
|
5728
|
+
}
|
|
5729
|
+
)
|
|
5730
|
+
);
|
|
5731
|
+
}
|
|
5732
|
+
|
|
5733
|
+
// src/apps/fleet-health-app.ts
|
|
5734
|
+
import { z as z9 } from "zod";
|
|
5226
5735
|
import {
|
|
5227
5736
|
registerAppTool,
|
|
5228
5737
|
registerAppResource,
|
|
@@ -5231,10 +5740,10 @@ import {
|
|
|
5231
5740
|
async function safeCallWithToken(client, _token, method, params = {}) {
|
|
5232
5741
|
try {
|
|
5233
5742
|
return { data: await client.call(method, params), error: null };
|
|
5234
|
-
} catch (
|
|
5743
|
+
} catch (err4) {
|
|
5235
5744
|
return {
|
|
5236
5745
|
data: null,
|
|
5237
|
-
error:
|
|
5746
|
+
error: err4 instanceof Error ? err4.message : String(err4)
|
|
5238
5747
|
};
|
|
5239
5748
|
}
|
|
5240
5749
|
}
|
|
@@ -5281,7 +5790,7 @@ function registerFleetHealthApp(server2, ctx) {
|
|
|
5281
5790
|
title: "Fleet Health Dashboard",
|
|
5282
5791
|
description: "Renders an interactive Fleet Health Dashboard with eSIM status charts, top-10 account breakdown, and low-balance alerts. Returns structuredContent for the chart panel.",
|
|
5283
5792
|
inputSchema: {
|
|
5284
|
-
accountId:
|
|
5793
|
+
accountId: z9.number().optional().describe("Filter to a specific account (omit for all)")
|
|
5285
5794
|
},
|
|
5286
5795
|
annotations: { readOnlyHint: true },
|
|
5287
5796
|
_meta: {
|
|
@@ -5293,7 +5802,7 @@ function registerFleetHealthApp(server2, ctx) {
|
|
|
5293
5802
|
},
|
|
5294
5803
|
async ({ accountId }) => {
|
|
5295
5804
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
5296
|
-
const client = new
|
|
5805
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
5297
5806
|
const [statusResult, accountsResult] = await Promise.all([
|
|
5298
5807
|
safeCallWithToken(
|
|
5299
5808
|
client,
|
|
@@ -5379,7 +5888,7 @@ function registerFleetHealthApp(server2, ctx) {
|
|
|
5379
5888
|
}
|
|
5380
5889
|
|
|
5381
5890
|
// src/apps/provisioning-wizard.ts
|
|
5382
|
-
import { z as
|
|
5891
|
+
import { z as z10 } from "zod";
|
|
5383
5892
|
import {
|
|
5384
5893
|
registerAppTool as registerAppTool2,
|
|
5385
5894
|
registerAppResource as registerAppResource2,
|
|
@@ -5432,10 +5941,10 @@ async function deleteWizardSession(env, sub, wizardId) {
|
|
|
5432
5941
|
async function safeCallWithToken2(client, _token, method, params = {}) {
|
|
5433
5942
|
try {
|
|
5434
5943
|
return { data: await client.call(method, params), error: null };
|
|
5435
|
-
} catch (
|
|
5944
|
+
} catch (err4) {
|
|
5436
5945
|
return {
|
|
5437
5946
|
data: null,
|
|
5438
|
-
error:
|
|
5947
|
+
error: err4 instanceof Error ? err4.message : String(err4)
|
|
5439
5948
|
};
|
|
5440
5949
|
}
|
|
5441
5950
|
}
|
|
@@ -5484,10 +5993,10 @@ function registerProvisioningWizard(server2, ctx) {
|
|
|
5484
5993
|
title: "eSIM Provisioning Wizard",
|
|
5485
5994
|
description: "Interactive 3-step wizard to provision an eSIM: select subscriber, choose package template, preview (dry_run) and confirm execution.",
|
|
5486
5995
|
inputSchema: {
|
|
5487
|
-
step:
|
|
5488
|
-
wizardId:
|
|
5489
|
-
subscriber_iccid:
|
|
5490
|
-
package_template_id:
|
|
5996
|
+
step: z10.enum(["init", "select-package", "preview", "confirm"]).describe("Current wizard step"),
|
|
5997
|
+
wizardId: z10.string().optional().describe("Wizard session ID (absent on init)"),
|
|
5998
|
+
subscriber_iccid: z10.string().optional().describe("Subscriber ICCID (required for select-package)"),
|
|
5999
|
+
package_template_id: z10.number().optional().describe("Package template ID (required for preview)")
|
|
5491
6000
|
},
|
|
5492
6001
|
_meta: {
|
|
5493
6002
|
ui: {
|
|
@@ -5498,7 +6007,7 @@ function registerProvisioningWizard(server2, ctx) {
|
|
|
5498
6007
|
},
|
|
5499
6008
|
async ({ step, wizardId, subscriber_iccid, package_template_id }) => {
|
|
5500
6009
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
5501
|
-
const client = new
|
|
6010
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
5502
6011
|
if (step === "init") {
|
|
5503
6012
|
const newWizardId = generateWizardId();
|
|
5504
6013
|
const subscribersResult = await safeCallWithToken2(
|
|
@@ -5786,7 +6295,7 @@ function registerProvisioningWizard(server2, ctx) {
|
|
|
5786
6295
|
}
|
|
5787
6296
|
|
|
5788
6297
|
// src/apps/balance-topup.ts
|
|
5789
|
-
import { z as
|
|
6298
|
+
import { z as z11 } from "zod";
|
|
5790
6299
|
import {
|
|
5791
6300
|
registerAppTool as registerAppTool3,
|
|
5792
6301
|
registerAppResource as registerAppResource3,
|
|
@@ -5795,10 +6304,10 @@ import {
|
|
|
5795
6304
|
async function safeCallWithToken3(client, _token, method, params = {}) {
|
|
5796
6305
|
try {
|
|
5797
6306
|
return { data: await client.call(method, params), error: null };
|
|
5798
|
-
} catch (
|
|
6307
|
+
} catch (err4) {
|
|
5799
6308
|
return {
|
|
5800
6309
|
data: null,
|
|
5801
|
-
error:
|
|
6310
|
+
error: err4 instanceof Error ? err4.message : String(err4)
|
|
5802
6311
|
};
|
|
5803
6312
|
}
|
|
5804
6313
|
}
|
|
@@ -5839,9 +6348,9 @@ function registerBalanceTopupApp(server2, ctx) {
|
|
|
5839
6348
|
title: "Balance Top-up Form",
|
|
5840
6349
|
description: "Enterprise admin tool: preview or commit an account balance adjustment. Set preview=true to fetch projection (no OCS write); preview=false (default) to execute. Requires admin scope + recent MFA.",
|
|
5841
6350
|
inputSchema: {
|
|
5842
|
-
iccid:
|
|
5843
|
-
delta:
|
|
5844
|
-
preview:
|
|
6351
|
+
iccid: z11.string().describe("The subscriber ICCID for the account lookup"),
|
|
6352
|
+
delta: z11.number().describe("Amount to add (positive) or deduct (negative) from the account balance"),
|
|
6353
|
+
preview: z11.boolean().default(true).describe("If true, return preview without writing. If false, execute the balance change.")
|
|
5845
6354
|
},
|
|
5846
6355
|
annotations: { destructiveHint: true },
|
|
5847
6356
|
_meta: {
|
|
@@ -5858,7 +6367,7 @@ function registerBalanceTopupApp(server2, ctx) {
|
|
|
5858
6367
|
ctx,
|
|
5859
6368
|
async ({ iccid, delta, preview }) => {
|
|
5860
6369
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
5861
|
-
const client = new
|
|
6370
|
+
const client = new OcsClient2(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
5862
6371
|
if (preview === true) {
|
|
5863
6372
|
const result2 = await safeCallWithToken3(
|
|
5864
6373
|
client,
|
|
@@ -5922,7 +6431,7 @@ function registerAllApps(server2, ctx) {
|
|
|
5922
6431
|
}
|
|
5923
6432
|
|
|
5924
6433
|
// src/prompts.ts
|
|
5925
|
-
import { z as
|
|
6434
|
+
import { z as z12 } from "zod";
|
|
5926
6435
|
function registerAllPrompts(server2) {
|
|
5927
6436
|
server2.registerPrompt(
|
|
5928
6437
|
"fleet_health_report",
|
|
@@ -5958,7 +6467,7 @@ Format as a structured report with sections, tables, and actionable recommendati
|
|
|
5958
6467
|
{
|
|
5959
6468
|
title: "Subscriber Deep Dive",
|
|
5960
6469
|
description: "Comprehensive analysis of a single subscriber: profile, packages, usage patterns, location history, recommendations.",
|
|
5961
|
-
argsSchema: { iccid:
|
|
6470
|
+
argsSchema: { iccid: z12.string().describe("The subscriber ICCID to analyse") }
|
|
5962
6471
|
},
|
|
5963
6472
|
async ({ iccid }) => ({
|
|
5964
6473
|
messages: [
|
|
@@ -6057,7 +6566,7 @@ Analyze and present:
|
|
|
6057
6566
|
{
|
|
6058
6567
|
title: "Bulk Operations Planner",
|
|
6059
6568
|
description: "Plan bulk operations safely: mass package assignments, account migrations, balance adjustments, or status changes.",
|
|
6060
|
-
argsSchema: { operation:
|
|
6569
|
+
argsSchema: { operation: z12.string().describe("Describe the bulk operation you want to perform") }
|
|
6061
6570
|
},
|
|
6062
6571
|
async ({ operation }) => ({
|
|
6063
6572
|
messages: [
|
|
@@ -6090,7 +6599,7 @@ NEVER execute destructive operations without confirmation.`
|
|
|
6090
6599
|
}
|
|
6091
6600
|
|
|
6092
6601
|
// src/tools-pricing.ts
|
|
6093
|
-
import { z as
|
|
6602
|
+
import { z as z13 } from "zod";
|
|
6094
6603
|
|
|
6095
6604
|
// src/credits.ts
|
|
6096
6605
|
var TIER_CREDIT_ALLOTMENTS = {
|
|
@@ -7457,8 +7966,8 @@ function buildPricingHandler(tool, ctx) {
|
|
|
7457
7966
|
}
|
|
7458
7967
|
]
|
|
7459
7968
|
};
|
|
7460
|
-
} catch (
|
|
7461
|
-
const message =
|
|
7969
|
+
} catch (err4) {
|
|
7970
|
+
const message = err4 instanceof Error ? err4.message : String(err4);
|
|
7462
7971
|
ctx.audit({
|
|
7463
7972
|
tool_name: tool.name,
|
|
7464
7973
|
ocs_method: "billing",
|
|
@@ -7482,19 +7991,19 @@ function buildZodSchema(schema) {
|
|
|
7482
7991
|
for (const [key, prop] of Object.entries(properties)) {
|
|
7483
7992
|
let field;
|
|
7484
7993
|
if (prop.enum) {
|
|
7485
|
-
field =
|
|
7994
|
+
field = z13.enum(prop.enum);
|
|
7486
7995
|
} else if (prop.type === "number") {
|
|
7487
|
-
field =
|
|
7996
|
+
field = z13.number();
|
|
7488
7997
|
} else if (prop.type === "boolean") {
|
|
7489
|
-
field =
|
|
7998
|
+
field = z13.boolean();
|
|
7490
7999
|
} else if (prop.type === "array") {
|
|
7491
8000
|
if (prop.items?.type === "number") {
|
|
7492
|
-
field =
|
|
8001
|
+
field = z13.array(z13.number());
|
|
7493
8002
|
} else {
|
|
7494
|
-
field =
|
|
8003
|
+
field = z13.array(z13.string());
|
|
7495
8004
|
}
|
|
7496
8005
|
} else {
|
|
7497
|
-
field =
|
|
8006
|
+
field = z13.string();
|
|
7498
8007
|
}
|
|
7499
8008
|
const required = schema.required ?? [];
|
|
7500
8009
|
if (!required.includes(key)) {
|
|
@@ -7509,7 +8018,7 @@ function buildZodSchema(schema) {
|
|
|
7509
8018
|
}
|
|
7510
8019
|
|
|
7511
8020
|
// src/tools-ui-agent-schedule.ts
|
|
7512
|
-
import { z as
|
|
8021
|
+
import { z as z14 } from "zod";
|
|
7513
8022
|
|
|
7514
8023
|
// src/manus-common.ts
|
|
7515
8024
|
var MANUS_API_BASE = "https://api.manus.ai/v2";
|
|
@@ -7778,8 +8287,8 @@ function writeUsageThresholdAudit(env, severity, remainingCredits, month, taskCo
|
|
|
7778
8287
|
doubles: [remainingCredits],
|
|
7779
8288
|
indexes: ["manus_usage"]
|
|
7780
8289
|
});
|
|
7781
|
-
} catch (
|
|
7782
|
-
console.error(`[manus-usage] threshold audit write failed: ${
|
|
8290
|
+
} catch (err4) {
|
|
8291
|
+
console.error(`[manus-usage] threshold audit write failed: ${err4.message}`);
|
|
7783
8292
|
}
|
|
7784
8293
|
}
|
|
7785
8294
|
|
|
@@ -7816,12 +8325,12 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
7816
8325
|
title: "Create Manus Schedule (UI Agent)",
|
|
7817
8326
|
description: "Creates a recurring Manus agent run on a cron schedule. Use this to automate periodic OCS audits, fleet health checks, or any recurring browser-automation task. Minimum interval: 5 minutes (*/1, */2, */3, */4 and '* * * * *' are rejected). Requires admin scope. Returns schedule_id.",
|
|
7818
8327
|
inputSchema: {
|
|
7819
|
-
name:
|
|
7820
|
-
cron:
|
|
8328
|
+
name: z14.string().describe("Human-readable name for this schedule"),
|
|
8329
|
+
cron: z14.string().describe(
|
|
7821
8330
|
"Standard 5-field cron expression (minute hour day month weekday). Minimum interval: 5 minutes. Example: '0 */6 * * *' = every 6 hours."
|
|
7822
8331
|
),
|
|
7823
|
-
prompt_template:
|
|
7824
|
-
profile:
|
|
8332
|
+
prompt_template: z14.string().describe("Agent prompt/task template the Manus agent will execute on each run"),
|
|
8333
|
+
profile: z14.string().optional().describe("Manus agent profile to use. Defaults to 'manus-1.6-lite'.")
|
|
7825
8334
|
}
|
|
7826
8335
|
},
|
|
7827
8336
|
async (args) => {
|
|
@@ -7889,7 +8398,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
7889
8398
|
}
|
|
7890
8399
|
]
|
|
7891
8400
|
};
|
|
7892
|
-
} catch (
|
|
8401
|
+
} catch (err4) {
|
|
7893
8402
|
ctx.audit({
|
|
7894
8403
|
tool_name: "ui_agent_schedule_create",
|
|
7895
8404
|
ocs_method: "[manus:schedule.create]",
|
|
@@ -7898,7 +8407,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
7898
8407
|
duration_ms: Date.now() - start,
|
|
7899
8408
|
event_type: "ui_agent_dispatch"
|
|
7900
8409
|
});
|
|
7901
|
-
const msg =
|
|
8410
|
+
const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
|
|
7902
8411
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
7903
8412
|
}
|
|
7904
8413
|
}
|
|
@@ -7956,7 +8465,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
7956
8465
|
}
|
|
7957
8466
|
]
|
|
7958
8467
|
};
|
|
7959
|
-
} catch (
|
|
8468
|
+
} catch (err4) {
|
|
7960
8469
|
ctx.audit({
|
|
7961
8470
|
tool_name: "ui_agent_schedule_list",
|
|
7962
8471
|
ocs_method: "[manus:schedule.list]",
|
|
@@ -7964,7 +8473,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
7964
8473
|
dry_run: false,
|
|
7965
8474
|
duration_ms: Date.now() - start
|
|
7966
8475
|
});
|
|
7967
|
-
const msg =
|
|
8476
|
+
const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
|
|
7968
8477
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
7969
8478
|
}
|
|
7970
8479
|
}
|
|
@@ -7975,7 +8484,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
7975
8484
|
title: "Delete Manus Schedule",
|
|
7976
8485
|
description: "Permanently deletes a Manus recurring schedule. This cannot be undone. Use ui_agent_schedule_pause to temporarily suspend instead. Requires admin scope.",
|
|
7977
8486
|
inputSchema: {
|
|
7978
|
-
schedule_id:
|
|
8487
|
+
schedule_id: z14.string().describe("ID of the schedule to delete")
|
|
7979
8488
|
}
|
|
7980
8489
|
},
|
|
7981
8490
|
async (args) => {
|
|
@@ -8025,7 +8534,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8025
8534
|
}
|
|
8026
8535
|
]
|
|
8027
8536
|
};
|
|
8028
|
-
} catch (
|
|
8537
|
+
} catch (err4) {
|
|
8029
8538
|
ctx.audit({
|
|
8030
8539
|
tool_name: "ui_agent_schedule_delete",
|
|
8031
8540
|
ocs_method: "[manus:schedule.delete]",
|
|
@@ -8034,7 +8543,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8034
8543
|
duration_ms: Date.now() - start,
|
|
8035
8544
|
event_type: "ui_agent_dispatch"
|
|
8036
8545
|
});
|
|
8037
|
-
const msg =
|
|
8546
|
+
const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
|
|
8038
8547
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8039
8548
|
}
|
|
8040
8549
|
}
|
|
@@ -8045,7 +8554,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8045
8554
|
title: "Pause Manus Schedule",
|
|
8046
8555
|
description: "Pauses an active Manus recurring schedule. The schedule is preserved and can be resumed later with ui_agent_schedule_resume. Requires admin scope.",
|
|
8047
8556
|
inputSchema: {
|
|
8048
|
-
schedule_id:
|
|
8557
|
+
schedule_id: z14.string().describe("ID of the schedule to pause")
|
|
8049
8558
|
}
|
|
8050
8559
|
},
|
|
8051
8560
|
async (args) => {
|
|
@@ -8095,7 +8604,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8095
8604
|
}
|
|
8096
8605
|
]
|
|
8097
8606
|
};
|
|
8098
|
-
} catch (
|
|
8607
|
+
} catch (err4) {
|
|
8099
8608
|
ctx.audit({
|
|
8100
8609
|
tool_name: "ui_agent_schedule_pause",
|
|
8101
8610
|
ocs_method: "[manus:schedule.pause]",
|
|
@@ -8104,7 +8613,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8104
8613
|
duration_ms: Date.now() - start,
|
|
8105
8614
|
event_type: "ui_agent_dispatch"
|
|
8106
8615
|
});
|
|
8107
|
-
const msg =
|
|
8616
|
+
const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
|
|
8108
8617
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8109
8618
|
}
|
|
8110
8619
|
}
|
|
@@ -8115,7 +8624,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8115
8624
|
title: "Resume Manus Schedule",
|
|
8116
8625
|
description: "Resumes a paused Manus recurring schedule. Requires admin scope.",
|
|
8117
8626
|
inputSchema: {
|
|
8118
|
-
schedule_id:
|
|
8627
|
+
schedule_id: z14.string().describe("ID of the schedule to resume")
|
|
8119
8628
|
}
|
|
8120
8629
|
},
|
|
8121
8630
|
async (args) => {
|
|
@@ -8165,7 +8674,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8165
8674
|
}
|
|
8166
8675
|
]
|
|
8167
8676
|
};
|
|
8168
|
-
} catch (
|
|
8677
|
+
} catch (err4) {
|
|
8169
8678
|
ctx.audit({
|
|
8170
8679
|
tool_name: "ui_agent_schedule_resume",
|
|
8171
8680
|
ocs_method: "[manus:schedule.resume]",
|
|
@@ -8174,7 +8683,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8174
8683
|
duration_ms: Date.now() - start,
|
|
8175
8684
|
event_type: "ui_agent_dispatch"
|
|
8176
8685
|
});
|
|
8177
|
-
const msg =
|
|
8686
|
+
const msg = err4 instanceof ManusScheduleError ? `Manus API error (HTTP ${err4.statusCode}): ${err4.message}` : err4 instanceof Error ? err4.message : String(err4);
|
|
8178
8687
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8179
8688
|
}
|
|
8180
8689
|
}
|
|
@@ -8219,7 +8728,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8219
8728
|
}
|
|
8220
8729
|
]
|
|
8221
8730
|
};
|
|
8222
|
-
} catch (
|
|
8731
|
+
} catch (err4) {
|
|
8223
8732
|
ctx.audit({
|
|
8224
8733
|
tool_name: "ui_agent_usage",
|
|
8225
8734
|
ocs_method: "[manus:usage.get]",
|
|
@@ -8227,7 +8736,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8227
8736
|
dry_run: false,
|
|
8228
8737
|
duration_ms: Date.now() - start
|
|
8229
8738
|
});
|
|
8230
|
-
const msg =
|
|
8739
|
+
const msg = err4 instanceof Error ? err4.message : String(err4);
|
|
8231
8740
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8232
8741
|
}
|
|
8233
8742
|
}
|
|
@@ -8235,7 +8744,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8235
8744
|
}
|
|
8236
8745
|
|
|
8237
8746
|
// src/tools-ui-agent.ts
|
|
8238
|
-
import { z as
|
|
8747
|
+
import { z as z15 } from "zod";
|
|
8239
8748
|
|
|
8240
8749
|
// src/clerk.ts
|
|
8241
8750
|
import { createClerkClient } from "@clerk/backend";
|
|
@@ -8470,7 +8979,7 @@ Report success or failure with a clear summary.`;
|
|
|
8470
8979
|
}
|
|
8471
8980
|
]
|
|
8472
8981
|
};
|
|
8473
|
-
} catch (
|
|
8982
|
+
} catch (err4) {
|
|
8474
8983
|
ctx.audit({
|
|
8475
8984
|
tool_name: toolName,
|
|
8476
8985
|
ocs_method: `[ui-agent:${gapId}]`,
|
|
@@ -8484,7 +8993,7 @@ Report success or failure with a clear summary.`;
|
|
|
8484
8993
|
content: [
|
|
8485
8994
|
{
|
|
8486
8995
|
type: "text",
|
|
8487
|
-
text: `Error dispatching UI agent: ${
|
|
8996
|
+
text: `Error dispatching UI agent: ${err4 instanceof Error ? err4.message : String(err4)}`
|
|
8488
8997
|
}
|
|
8489
8998
|
]
|
|
8490
8999
|
};
|
|
@@ -8498,9 +9007,9 @@ function registerAllUiAgentTools(server2, ctx) {
|
|
|
8498
9007
|
title: "Create Steering List (UI Agent)",
|
|
8499
9008
|
description: "Creates a new network steering list (OPLMN preference configuration) via the OCS web dashboard. This operation is not available via the OCS REST API. A Manus browser agent will be dispatched to perform the operation. Params: `name` (steering list name), `description` (optional). Returns: dispatch confirmation with Manus task ID for tracking.",
|
|
8500
9009
|
inputSchema: {
|
|
8501
|
-
name:
|
|
8502
|
-
description:
|
|
8503
|
-
dry_run:
|
|
9010
|
+
name: z15.string().describe("Name for the new steering list"),
|
|
9011
|
+
description: z15.string().optional().describe("Optional description for the steering list"),
|
|
9012
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8504
9013
|
}
|
|
8505
9014
|
},
|
|
8506
9015
|
wrapUiAgentHandler(
|
|
@@ -8523,11 +9032,11 @@ After creation, note the new steering list ID from the dashboard.
|
|
|
8523
9032
|
title: "Build Steering List (UI Agent)",
|
|
8524
9033
|
description: "Adds or removes operators (MCC-MNC) from an existing steering list via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `steering_list_id`, `add_operators` (array of MCC-MNC to add), `remove_operators` (array to remove), `operator_type` ('priority' or 'excluded').",
|
|
8525
9034
|
inputSchema: {
|
|
8526
|
-
steering_list_id:
|
|
8527
|
-
add_operators:
|
|
8528
|
-
remove_operators:
|
|
8529
|
-
operator_type:
|
|
8530
|
-
dry_run:
|
|
9035
|
+
steering_list_id: z15.number().describe("ID of the steering list to modify"),
|
|
9036
|
+
add_operators: z15.array(z15.string()).optional().describe("MCC-MNC codes to add (e.g. ['20801', '26201'])"),
|
|
9037
|
+
remove_operators: z15.array(z15.string()).optional().describe("MCC-MNC codes to remove"),
|
|
9038
|
+
operator_type: z15.enum(["priority", "excluded"]).default("priority").describe("Whether operators are priority or excluded"),
|
|
9039
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8531
9040
|
}
|
|
8532
9041
|
},
|
|
8533
9042
|
wrapUiAgentHandler(
|
|
@@ -8550,9 +9059,9 @@ Operator type: ${args.operator_type ?? "priority"}
|
|
|
8550
9059
|
title: "Set Account Steering List (UI Agent)",
|
|
8551
9060
|
description: "Assigns or removes a steering list at the account level via the OCS web dashboard. The subscriber-level counterpart `modify_subscriber_steering_list` is available via API; this account-level operation is UI-only. Params: `account_id`, `steering_list_id` (0 to remove).",
|
|
8552
9061
|
inputSchema: {
|
|
8553
|
-
account_id:
|
|
8554
|
-
steering_list_id:
|
|
8555
|
-
dry_run:
|
|
9062
|
+
account_id: z15.number().describe("Account ID to assign the steering list to"),
|
|
9063
|
+
steering_list_id: z15.number().describe("Steering list ID to assign (0 to remove/unset)"),
|
|
9064
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8556
9065
|
}
|
|
8557
9066
|
},
|
|
8558
9067
|
wrapUiAgentHandler(
|
|
@@ -8574,10 +9083,10 @@ Open account ID ${args.account_id}.
|
|
|
8574
9083
|
title: "Create Account (UI Agent)",
|
|
8575
9084
|
description: "Creates a new sub-account under the reseller via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `name` (account name), `description` (optional), `initial_balance` (optional, default 0).",
|
|
8576
9085
|
inputSchema: {
|
|
8577
|
-
name:
|
|
8578
|
-
description:
|
|
8579
|
-
initial_balance:
|
|
8580
|
-
dry_run:
|
|
9086
|
+
name: z15.string().describe("Name for the new account"),
|
|
9087
|
+
description: z15.string().optional().describe("Optional description"),
|
|
9088
|
+
initial_balance: z15.number().optional().describe("Initial balance in account currency (default 0)"),
|
|
9089
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8581
9090
|
}
|
|
8582
9091
|
},
|
|
8583
9092
|
wrapUiAgentHandler(
|
|
@@ -8601,10 +9110,10 @@ After creation, note the new account ID from the dashboard.
|
|
|
8601
9110
|
title: "Create Destination List (UI Agent)",
|
|
8602
9111
|
description: "Creates a new destination list (named set of phone number prefixes for MOC call permissions) via the OCS web dashboard. Params: `name`, `prefixes` (array of prefix strings), `description`.",
|
|
8603
9112
|
inputSchema: {
|
|
8604
|
-
name:
|
|
8605
|
-
prefixes:
|
|
8606
|
-
description:
|
|
8607
|
-
dry_run:
|
|
9113
|
+
name: z15.string().describe("Name for the new destination list"),
|
|
9114
|
+
prefixes: z15.array(z15.string()).optional().describe("Phone number prefixes to include (e.g. ['+31', '+49'])"),
|
|
9115
|
+
description: z15.string().optional().describe("Optional description"),
|
|
9116
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8608
9117
|
}
|
|
8609
9118
|
},
|
|
8610
9119
|
wrapUiAgentHandler(
|
|
@@ -8627,11 +9136,11 @@ Create a new destination list with the following details:
|
|
|
8627
9136
|
title: "Edit Destination List (UI Agent)",
|
|
8628
9137
|
description: "Edits an existing destination list via the OCS web dashboard. Params: `destination_list_id`, `add_prefixes`, `remove_prefixes`, `new_name`.",
|
|
8629
9138
|
inputSchema: {
|
|
8630
|
-
destination_list_id:
|
|
8631
|
-
add_prefixes:
|
|
8632
|
-
remove_prefixes:
|
|
8633
|
-
new_name:
|
|
8634
|
-
dry_run:
|
|
9139
|
+
destination_list_id: z15.number().describe("ID of the destination list to edit"),
|
|
9140
|
+
add_prefixes: z15.array(z15.string()).optional().describe("Prefixes to add"),
|
|
9141
|
+
remove_prefixes: z15.array(z15.string()).optional().describe("Prefixes to remove"),
|
|
9142
|
+
new_name: z15.string().optional().describe("Rename the destination list"),
|
|
9143
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8635
9144
|
}
|
|
8636
9145
|
},
|
|
8637
9146
|
wrapUiAgentHandler(
|
|
@@ -8654,8 +9163,8 @@ Open destination list ID ${args.destination_list_id} for editing.
|
|
|
8654
9163
|
title: "Delete Destination List (UI Agent)",
|
|
8655
9164
|
description: "Deletes a destination list via the OCS web dashboard. Params: `destination_list_id`. WARNING: This is destructive and cannot be undone.",
|
|
8656
9165
|
inputSchema: {
|
|
8657
|
-
destination_list_id:
|
|
8658
|
-
dry_run:
|
|
9166
|
+
destination_list_id: z15.number().describe("ID of the destination list to delete"),
|
|
9167
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8659
9168
|
}
|
|
8660
9169
|
},
|
|
8661
9170
|
wrapUiAgentHandler(
|
|
@@ -8676,8 +9185,8 @@ Verify the list no longer appears in the dashboard.
|
|
|
8676
9185
|
title: "Delete Package Template (UI Agent)",
|
|
8677
9186
|
description: "Deletes a package template from the product catalog via the OCS web dashboard. This operation is not available via the OCS REST API. Params: `template_id`. WARNING: This is destructive.",
|
|
8678
9187
|
inputSchema: {
|
|
8679
|
-
template_id:
|
|
8680
|
-
dry_run:
|
|
9188
|
+
template_id: z15.number().describe("ID of the package template to delete"),
|
|
9189
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8681
9190
|
}
|
|
8682
9191
|
},
|
|
8683
9192
|
wrapUiAgentHandler(
|
|
@@ -8698,11 +9207,11 @@ Verify the template no longer appears in the template list.
|
|
|
8698
9207
|
title: "Edit Location Zone (UI Agent)",
|
|
8699
9208
|
description: "Edits an existing location zone via the OCS web dashboard. `create_location_zone` is available via API; edit is UI-only. Params: `zone_id`, `new_name`, `add_countries`, `remove_countries`.",
|
|
8700
9209
|
inputSchema: {
|
|
8701
|
-
zone_id:
|
|
8702
|
-
new_name:
|
|
8703
|
-
add_countries:
|
|
8704
|
-
remove_countries:
|
|
8705
|
-
dry_run:
|
|
9210
|
+
zone_id: z15.number().describe("ID of the location zone to edit"),
|
|
9211
|
+
new_name: z15.string().optional().describe("Rename the location zone"),
|
|
9212
|
+
add_countries: z15.array(z15.string()).optional().describe("ISO country codes to add (e.g. ['NL', 'DE'])"),
|
|
9213
|
+
remove_countries: z15.array(z15.string()).optional().describe("ISO country codes to remove"),
|
|
9214
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8706
9215
|
}
|
|
8707
9216
|
},
|
|
8708
9217
|
wrapUiAgentHandler(
|
|
@@ -8725,8 +9234,8 @@ Open location zone ID ${args.zone_id} for editing.
|
|
|
8725
9234
|
title: "Delete Location Zone (UI Agent)",
|
|
8726
9235
|
description: "Deletes a location zone via the OCS web dashboard. `create_location_zone` is available via API; delete is UI-only. Params: `zone_id`. WARNING: Zones in use by active templates may not be deletable.",
|
|
8727
9236
|
inputSchema: {
|
|
8728
|
-
zone_id:
|
|
8729
|
-
dry_run:
|
|
9237
|
+
zone_id: z15.number().describe("ID of the location zone to delete"),
|
|
9238
|
+
dry_run: z15.boolean().optional().describe("Preview the agent prompt without dispatching")
|
|
8730
9239
|
}
|
|
8731
9240
|
},
|
|
8732
9241
|
wrapUiAgentHandler(
|
|
@@ -8745,7 +9254,7 @@ Verify the zone no longer appears in the zone list.
|
|
|
8745
9254
|
}
|
|
8746
9255
|
|
|
8747
9256
|
// src/tools-ui-agent-ask.ts
|
|
8748
|
-
import { z as
|
|
9257
|
+
import { z as z16 } from "zod";
|
|
8749
9258
|
|
|
8750
9259
|
// src/manus-webhook.ts
|
|
8751
9260
|
var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
@@ -8848,8 +9357,8 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
8848
9357
|
title: "Reply to Paused UI Agent Task",
|
|
8849
9358
|
description: "Resumes a Manus browser automation task that paused with stop_reason 'ask'. Use ui_agent_list_pending to find tasks waiting for input. Provide the task_id and your reply (e.g. a 2FA code, a field value, or a yes/no answer). The reply content is never recorded in audit logs \u2014 only its length is logged.",
|
|
8850
9359
|
inputSchema: {
|
|
8851
|
-
task_id:
|
|
8852
|
-
reply:
|
|
9360
|
+
task_id: z16.string().describe("Manus task ID to resume (from ui_agent_list_pending)"),
|
|
9361
|
+
reply: z16.string().describe("Your answer to the agent's question (e.g. a 2FA code or confirmation)")
|
|
8853
9362
|
}
|
|
8854
9363
|
},
|
|
8855
9364
|
async (args) => {
|
|
@@ -8908,7 +9417,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
8908
9417
|
try {
|
|
8909
9418
|
const replyOutcome = await askReply(keys, task_id, reply);
|
|
8910
9419
|
result2 = replyOutcome.data;
|
|
8911
|
-
} catch (
|
|
9420
|
+
} catch (err4) {
|
|
8912
9421
|
ctx.audit({
|
|
8913
9422
|
tool_name: "ui_agent_reply",
|
|
8914
9423
|
ocs_method: "[ui-agent:ask-reply]",
|
|
@@ -8923,7 +9432,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
8923
9432
|
content: [
|
|
8924
9433
|
{
|
|
8925
9434
|
type: "text",
|
|
8926
|
-
text: `Error calling Manus task.reply: ${
|
|
9435
|
+
text: `Error calling Manus task.reply: ${err4 instanceof Error ? err4.message : String(err4)}`
|
|
8927
9436
|
}
|
|
8928
9437
|
]
|
|
8929
9438
|
};
|
|
@@ -9001,13 +9510,13 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
9001
9510
|
try {
|
|
9002
9511
|
const listing = await ctx.env.CARRIER_USERS.list({ prefix: PENDING_ASK_PREFIX });
|
|
9003
9512
|
keys = listing.keys;
|
|
9004
|
-
} catch (
|
|
9513
|
+
} catch (err4) {
|
|
9005
9514
|
return {
|
|
9006
9515
|
isError: true,
|
|
9007
9516
|
content: [
|
|
9008
9517
|
{
|
|
9009
9518
|
type: "text",
|
|
9010
|
-
text: `Error listing pending tasks: ${
|
|
9519
|
+
text: `Error listing pending tasks: ${err4 instanceof Error ? err4.message : String(err4)}`
|
|
9011
9520
|
}
|
|
9012
9521
|
]
|
|
9013
9522
|
};
|
|
@@ -9058,7 +9567,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
9058
9567
|
}
|
|
9059
9568
|
|
|
9060
9569
|
// src/stripe-connect-tools.ts
|
|
9061
|
-
import { z as
|
|
9570
|
+
import { z as z17 } from "zod";
|
|
9062
9571
|
import * as Sentry2 from "@sentry/cloudflare";
|
|
9063
9572
|
|
|
9064
9573
|
// src/audit.ts
|
|
@@ -9094,10 +9603,10 @@ async function issueConfirmToken(env, sub, toolName) {
|
|
|
9094
9603
|
await env.OAUTH_KV.put(key, token2, { expirationTtl: 300 });
|
|
9095
9604
|
return token2;
|
|
9096
9605
|
}
|
|
9097
|
-
function
|
|
9606
|
+
function ok2(text) {
|
|
9098
9607
|
return { content: [{ type: "text", text }] };
|
|
9099
9608
|
}
|
|
9100
|
-
function
|
|
9609
|
+
function err2(text) {
|
|
9101
9610
|
return { isError: true, content: [{ type: "text", text }] };
|
|
9102
9611
|
}
|
|
9103
9612
|
async function stripeGet(stripeKey, path, connectedAccountId) {
|
|
@@ -9132,7 +9641,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9132
9641
|
"stripe_connect_status",
|
|
9133
9642
|
"Read-only: returns the Stripe Connect account status, capabilities, and requirements for the authenticated operator.",
|
|
9134
9643
|
{
|
|
9135
|
-
operator_id:
|
|
9644
|
+
operator_id: z17.string().optional().describe("Override operator_id (admin use). Defaults to caller's org/user.")
|
|
9136
9645
|
},
|
|
9137
9646
|
async ({ operator_id }) => {
|
|
9138
9647
|
const opId = operator_id ?? operatorId;
|
|
@@ -9140,13 +9649,13 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9140
9649
|
const start = Date.now();
|
|
9141
9650
|
try {
|
|
9142
9651
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9143
|
-
if (!stripeKey) return
|
|
9652
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9144
9653
|
const accountId = await env.CARRIER_USERS.get(opKvKey);
|
|
9145
9654
|
if (!accountId) {
|
|
9146
|
-
return
|
|
9655
|
+
return ok2(JSON.stringify({ status: "not_connected", operator_id: opId }));
|
|
9147
9656
|
}
|
|
9148
9657
|
const result2 = await stripeGet(stripeKey, `/v1/accounts/${accountId}`);
|
|
9149
|
-
if (!result2.ok) return
|
|
9658
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9150
9659
|
const a = result2.data;
|
|
9151
9660
|
const status = a.charges_enabled && a.details_submitted ? "active" : "pending";
|
|
9152
9661
|
writeAudit(env, {
|
|
@@ -9158,7 +9667,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9158
9667
|
sub: props2.sub,
|
|
9159
9668
|
reseller_id: props2.reseller_id
|
|
9160
9669
|
});
|
|
9161
|
-
return
|
|
9670
|
+
return ok2(JSON.stringify({
|
|
9162
9671
|
status,
|
|
9163
9672
|
operator_id: opId,
|
|
9164
9673
|
account_id: accountId,
|
|
@@ -9172,7 +9681,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9172
9681
|
}));
|
|
9173
9682
|
} catch (e) {
|
|
9174
9683
|
Sentry2.captureException(e);
|
|
9175
|
-
return
|
|
9684
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9176
9685
|
}
|
|
9177
9686
|
}
|
|
9178
9687
|
);
|
|
@@ -9180,20 +9689,20 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9180
9689
|
"stripe_connect_payouts",
|
|
9181
9690
|
"Read-only: list recent payouts for the operator's connected Stripe account.",
|
|
9182
9691
|
{
|
|
9183
|
-
limit:
|
|
9184
|
-
status:
|
|
9692
|
+
limit: z17.number().int().min(1).max(50).default(10).describe("Number of payouts to return."),
|
|
9693
|
+
status: z17.enum(["pending", "paid", "failed", "canceled", "in_transit"]).optional().describe("Filter by payout status.")
|
|
9185
9694
|
},
|
|
9186
9695
|
async ({ limit, status }) => {
|
|
9187
9696
|
const start = Date.now();
|
|
9188
9697
|
try {
|
|
9189
9698
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9190
|
-
if (!stripeKey) return
|
|
9699
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9191
9700
|
const accountId = await getAccountId();
|
|
9192
|
-
if (!accountId) return
|
|
9701
|
+
if (!accountId) return err2("No connected Stripe account found.");
|
|
9193
9702
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
9194
9703
|
if (status) params.set("status", status);
|
|
9195
9704
|
const result2 = await stripeGet(stripeKey, `/v1/payouts?${params.toString()}`, accountId);
|
|
9196
|
-
if (!result2.ok) return
|
|
9705
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9197
9706
|
writeAudit(env, {
|
|
9198
9707
|
tool_name: "stripe_connect_payouts",
|
|
9199
9708
|
ocs_method: "stripe.payouts.list",
|
|
@@ -9203,10 +9712,10 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9203
9712
|
sub: props2.sub,
|
|
9204
9713
|
reseller_id: props2.reseller_id
|
|
9205
9714
|
});
|
|
9206
|
-
return
|
|
9715
|
+
return ok2(JSON.stringify({ payouts: result2.data.data, has_more: result2.data.has_more }));
|
|
9207
9716
|
} catch (e) {
|
|
9208
9717
|
Sentry2.captureException(e);
|
|
9209
|
-
return
|
|
9718
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9210
9719
|
}
|
|
9211
9720
|
}
|
|
9212
9721
|
);
|
|
@@ -9218,11 +9727,11 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9218
9727
|
const start = Date.now();
|
|
9219
9728
|
try {
|
|
9220
9729
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9221
|
-
if (!stripeKey) return
|
|
9730
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9222
9731
|
const accountId = await getAccountId();
|
|
9223
|
-
if (!accountId) return
|
|
9732
|
+
if (!accountId) return err2("No connected Stripe account found.");
|
|
9224
9733
|
const result2 = await stripeGet(stripeKey, `/v1/balance`, accountId);
|
|
9225
|
-
if (!result2.ok) return
|
|
9734
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9226
9735
|
writeAudit(env, {
|
|
9227
9736
|
tool_name: "stripe_connect_balance",
|
|
9228
9737
|
ocs_method: "stripe.balance.read",
|
|
@@ -9232,14 +9741,14 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9232
9741
|
sub: props2.sub,
|
|
9233
9742
|
reseller_id: props2.reseller_id
|
|
9234
9743
|
});
|
|
9235
|
-
return
|
|
9744
|
+
return ok2(JSON.stringify({
|
|
9236
9745
|
account_id: accountId,
|
|
9237
9746
|
available: result2.data.available ?? [],
|
|
9238
9747
|
pending: result2.data.pending ?? []
|
|
9239
9748
|
}));
|
|
9240
9749
|
} catch (e) {
|
|
9241
9750
|
Sentry2.captureException(e);
|
|
9242
|
-
return
|
|
9751
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9243
9752
|
}
|
|
9244
9753
|
}
|
|
9245
9754
|
);
|
|
@@ -9247,17 +9756,17 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9247
9756
|
"stripe_connect_refund",
|
|
9248
9757
|
"Admin: issue a refund on a charge via the operator's connected Stripe account. Requires confirm_token (call without token first to get one).",
|
|
9249
9758
|
{
|
|
9250
|
-
charge_id:
|
|
9251
|
-
amount_cents:
|
|
9252
|
-
reason:
|
|
9253
|
-
confirm_token:
|
|
9759
|
+
charge_id: z17.string().min(1).describe("Stripe charge ID (ch_...)."),
|
|
9760
|
+
amount_cents: z17.number().int().min(1).optional().describe("Partial refund amount in cents. Omit for full refund."),
|
|
9761
|
+
reason: z17.enum(["duplicate", "fraudulent", "requested_by_customer"]).optional(),
|
|
9762
|
+
confirm_token: z17.string().optional().describe("Confirmation token from previous call. Required to execute.")
|
|
9254
9763
|
},
|
|
9255
9764
|
async ({ charge_id, amount_cents, reason, confirm_token }) => {
|
|
9256
9765
|
const start = Date.now();
|
|
9257
9766
|
const toolName = "stripe_connect_refund";
|
|
9258
9767
|
if (!confirm_token) {
|
|
9259
9768
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
9260
|
-
return
|
|
9769
|
+
return ok2(
|
|
9261
9770
|
`HARD_BLOCK: Refund ${amount_cents ? `${amount_cents} cents on` : "(full) on"} charge ${charge_id} requires confirmation.
|
|
9262
9771
|
confirm_token: ${token2}
|
|
9263
9772
|
Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes.`
|
|
@@ -9265,18 +9774,18 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9265
9774
|
}
|
|
9266
9775
|
const valid = await verifyConfirmToken(env, props2.sub, toolName, confirm_token);
|
|
9267
9776
|
if (!valid) {
|
|
9268
|
-
return
|
|
9777
|
+
return err2("Invalid or expired confirm_token. Call without token to get a new one.");
|
|
9269
9778
|
}
|
|
9270
9779
|
try {
|
|
9271
9780
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9272
|
-
if (!stripeKey) return
|
|
9781
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9273
9782
|
const accountId = await getAccountId();
|
|
9274
|
-
if (!accountId) return
|
|
9783
|
+
if (!accountId) return err2("No connected Stripe account found.");
|
|
9275
9784
|
const params = { charge: charge_id };
|
|
9276
9785
|
if (amount_cents) params.amount = String(amount_cents);
|
|
9277
9786
|
if (reason) params.reason = reason;
|
|
9278
9787
|
const result2 = await stripePost(stripeKey, "/v1/refunds", params, accountId);
|
|
9279
|
-
if (!result2.ok) return
|
|
9788
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9280
9789
|
writeAudit(env, {
|
|
9281
9790
|
tool_name: toolName,
|
|
9282
9791
|
ocs_method: "stripe.refund.create",
|
|
@@ -9286,10 +9795,10 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9286
9795
|
sub: props2.sub,
|
|
9287
9796
|
reseller_id: props2.reseller_id
|
|
9288
9797
|
});
|
|
9289
|
-
return
|
|
9798
|
+
return ok2(`Refund issued: ${result2.data.id} \u2014 status: ${result2.data.status}`);
|
|
9290
9799
|
} catch (e) {
|
|
9291
9800
|
Sentry2.captureException(e);
|
|
9292
|
-
return
|
|
9801
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9293
9802
|
}
|
|
9294
9803
|
}
|
|
9295
9804
|
);
|
|
@@ -9297,20 +9806,20 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9297
9806
|
"stripe_connect_dispute_list",
|
|
9298
9807
|
"Read-only: list active disputes for the operator's connected Stripe account.",
|
|
9299
9808
|
{
|
|
9300
|
-
limit:
|
|
9301
|
-
status:
|
|
9809
|
+
limit: z17.number().int().min(1).max(50).default(10),
|
|
9810
|
+
status: z17.string().optional().describe("Filter by dispute status (e.g. needs_response, under_review).")
|
|
9302
9811
|
},
|
|
9303
9812
|
async ({ limit, status }) => {
|
|
9304
9813
|
const start = Date.now();
|
|
9305
9814
|
try {
|
|
9306
9815
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9307
|
-
if (!stripeKey) return
|
|
9816
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9308
9817
|
const accountId = await getAccountId();
|
|
9309
|
-
if (!accountId) return
|
|
9818
|
+
if (!accountId) return err2("No connected Stripe account found.");
|
|
9310
9819
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
9311
9820
|
if (status) params.set("status", status);
|
|
9312
9821
|
const result2 = await stripeGet(stripeKey, `/v1/disputes?${params.toString()}`, accountId);
|
|
9313
|
-
if (!result2.ok) return
|
|
9822
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9314
9823
|
writeAudit(env, {
|
|
9315
9824
|
tool_name: "stripe_connect_dispute_list",
|
|
9316
9825
|
ocs_method: "stripe.disputes.list",
|
|
@@ -9320,10 +9829,10 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9320
9829
|
sub: props2.sub,
|
|
9321
9830
|
reseller_id: props2.reseller_id
|
|
9322
9831
|
});
|
|
9323
|
-
return
|
|
9832
|
+
return ok2(JSON.stringify({ disputes: result2.data.data, has_more: result2.data.has_more }));
|
|
9324
9833
|
} catch (e) {
|
|
9325
9834
|
Sentry2.captureException(e);
|
|
9326
|
-
return
|
|
9835
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9327
9836
|
}
|
|
9328
9837
|
}
|
|
9329
9838
|
);
|
|
@@ -9331,18 +9840,18 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9331
9840
|
"radar_review_list",
|
|
9332
9841
|
"Read-only: list pending Radar reviews requiring manual platform decision.",
|
|
9333
9842
|
{
|
|
9334
|
-
open_only:
|
|
9335
|
-
limit:
|
|
9843
|
+
open_only: z17.boolean().default(true).describe("If true, only returns open (undecided) reviews."),
|
|
9844
|
+
limit: z17.number().int().min(1).max(50).default(10)
|
|
9336
9845
|
},
|
|
9337
9846
|
async ({ open_only, limit }) => {
|
|
9338
9847
|
const start = Date.now();
|
|
9339
9848
|
try {
|
|
9340
9849
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9341
|
-
if (!stripeKey) return
|
|
9850
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9342
9851
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
9343
9852
|
if (open_only) params.set("open", "true");
|
|
9344
9853
|
const result2 = await stripeGet(stripeKey, `/v1/radar/reviews?${params.toString()}`);
|
|
9345
|
-
if (!result2.ok) return
|
|
9854
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9346
9855
|
writeAudit(env, {
|
|
9347
9856
|
tool_name: "radar_review_list",
|
|
9348
9857
|
ocs_method: "stripe.radar.reviews.list",
|
|
@@ -9352,10 +9861,10 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9352
9861
|
sub: props2.sub,
|
|
9353
9862
|
reseller_id: props2.reseller_id
|
|
9354
9863
|
});
|
|
9355
|
-
return
|
|
9864
|
+
return ok2(JSON.stringify({ reviews: result2.data.data, has_more: result2.data.has_more }));
|
|
9356
9865
|
} catch (e) {
|
|
9357
9866
|
Sentry2.captureException(e);
|
|
9358
|
-
return
|
|
9867
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9359
9868
|
}
|
|
9360
9869
|
}
|
|
9361
9870
|
);
|
|
@@ -9363,31 +9872,31 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9363
9872
|
"radar_review_approve",
|
|
9364
9873
|
"Admin: approve a Radar review, allowing the charge to proceed. Requires confirm_token.",
|
|
9365
9874
|
{
|
|
9366
|
-
review_id:
|
|
9367
|
-
confirm_token:
|
|
9875
|
+
review_id: z17.string().min(1).describe("Stripe Radar review ID (prv_...)."),
|
|
9876
|
+
confirm_token: z17.string().optional()
|
|
9368
9877
|
},
|
|
9369
9878
|
async ({ review_id, confirm_token }) => {
|
|
9370
9879
|
const toolName = "radar_review_approve";
|
|
9371
9880
|
const start = Date.now();
|
|
9372
9881
|
if (!confirm_token) {
|
|
9373
9882
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
9374
|
-
return
|
|
9883
|
+
return ok2(
|
|
9375
9884
|
`HARD_BLOCK: Approving review ${review_id} allows the charge to proceed.
|
|
9376
9885
|
confirm_token: ${token2}
|
|
9377
9886
|
Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
9378
9887
|
);
|
|
9379
9888
|
}
|
|
9380
9889
|
const valid = await verifyConfirmToken(env, props2.sub, toolName, confirm_token);
|
|
9381
|
-
if (!valid) return
|
|
9890
|
+
if (!valid) return err2("Invalid or expired confirm_token.");
|
|
9382
9891
|
try {
|
|
9383
9892
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9384
|
-
if (!stripeKey) return
|
|
9893
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9385
9894
|
const result2 = await stripePost(
|
|
9386
9895
|
stripeKey,
|
|
9387
9896
|
`/v1/radar/reviews/${review_id}/approve`,
|
|
9388
9897
|
{}
|
|
9389
9898
|
);
|
|
9390
|
-
if (!result2.ok) return
|
|
9899
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9391
9900
|
writeAudit(env, {
|
|
9392
9901
|
tool_name: toolName,
|
|
9393
9902
|
ocs_method: "stripe.radar.review.approve",
|
|
@@ -9397,10 +9906,10 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
|
9397
9906
|
sub: props2.sub,
|
|
9398
9907
|
reseller_id: props2.reseller_id
|
|
9399
9908
|
});
|
|
9400
|
-
return
|
|
9909
|
+
return ok2(`Review ${review_id} approved. Charge will proceed.`);
|
|
9401
9910
|
} catch (e) {
|
|
9402
9911
|
Sentry2.captureException(e);
|
|
9403
|
-
return
|
|
9912
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9404
9913
|
}
|
|
9405
9914
|
}
|
|
9406
9915
|
);
|
|
@@ -9408,31 +9917,31 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
|
9408
9917
|
"radar_review_decline",
|
|
9409
9918
|
"Admin: decline a Radar review, blocking/closing the charge. Requires confirm_token.",
|
|
9410
9919
|
{
|
|
9411
|
-
review_id:
|
|
9412
|
-
confirm_token:
|
|
9920
|
+
review_id: z17.string().min(1),
|
|
9921
|
+
confirm_token: z17.string().optional()
|
|
9413
9922
|
},
|
|
9414
9923
|
async ({ review_id, confirm_token }) => {
|
|
9415
9924
|
const toolName = "radar_review_decline";
|
|
9416
9925
|
const start = Date.now();
|
|
9417
9926
|
if (!confirm_token) {
|
|
9418
9927
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
9419
|
-
return
|
|
9928
|
+
return ok2(
|
|
9420
9929
|
`HARD_BLOCK: Declining review ${review_id} will close/block the charge.
|
|
9421
9930
|
confirm_token: ${token2}
|
|
9422
9931
|
Expires in 5 minutes.`
|
|
9423
9932
|
);
|
|
9424
9933
|
}
|
|
9425
9934
|
const valid = await verifyConfirmToken(env, props2.sub, toolName, confirm_token);
|
|
9426
|
-
if (!valid) return
|
|
9935
|
+
if (!valid) return err2("Invalid or expired confirm_token.");
|
|
9427
9936
|
try {
|
|
9428
9937
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9429
|
-
if (!stripeKey) return
|
|
9938
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9430
9939
|
const result2 = await stripePost(
|
|
9431
9940
|
stripeKey,
|
|
9432
9941
|
`/v1/radar/reviews/${review_id}/approve`,
|
|
9433
9942
|
{ reason: "fraudulent" }
|
|
9434
9943
|
);
|
|
9435
|
-
if (!result2.ok) return
|
|
9944
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9436
9945
|
writeAudit(env, {
|
|
9437
9946
|
tool_name: toolName,
|
|
9438
9947
|
ocs_method: "stripe.radar.review.decline",
|
|
@@ -9442,10 +9951,10 @@ Expires in 5 minutes.`
|
|
|
9442
9951
|
sub: props2.sub,
|
|
9443
9952
|
reseller_id: props2.reseller_id
|
|
9444
9953
|
});
|
|
9445
|
-
return
|
|
9954
|
+
return ok2(`Review ${review_id} declined.`);
|
|
9446
9955
|
} catch (e) {
|
|
9447
9956
|
Sentry2.captureException(e);
|
|
9448
|
-
return
|
|
9957
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9449
9958
|
}
|
|
9450
9959
|
}
|
|
9451
9960
|
);
|
|
@@ -9453,32 +9962,32 @@ Expires in 5 minutes.`
|
|
|
9453
9962
|
"radar_value_list_add",
|
|
9454
9963
|
"Admin: add an item (email, IP, card fingerprint, country code) to a Stripe Radar block/allow list. Requires confirm_token.",
|
|
9455
9964
|
{
|
|
9456
|
-
value_list_id:
|
|
9457
|
-
value:
|
|
9458
|
-
confirm_token:
|
|
9965
|
+
value_list_id: z17.string().min(1).describe("Stripe Radar value list ID (rsl_...)."),
|
|
9966
|
+
value: z17.string().min(1).describe("The value to add (email, IP address, country code, etc.)."),
|
|
9967
|
+
confirm_token: z17.string().optional()
|
|
9459
9968
|
},
|
|
9460
9969
|
async ({ value_list_id, value, confirm_token }) => {
|
|
9461
9970
|
const toolName = "radar_value_list_add";
|
|
9462
9971
|
const start = Date.now();
|
|
9463
9972
|
if (!confirm_token) {
|
|
9464
9973
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
9465
|
-
return
|
|
9974
|
+
return ok2(
|
|
9466
9975
|
`HARD_BLOCK: Adding "${value}" to list ${value_list_id} will affect future charge decisions.
|
|
9467
9976
|
confirm_token: ${token2}
|
|
9468
9977
|
Expires in 5 minutes.`
|
|
9469
9978
|
);
|
|
9470
9979
|
}
|
|
9471
9980
|
const valid = await verifyConfirmToken(env, props2.sub, toolName, confirm_token);
|
|
9472
|
-
if (!valid) return
|
|
9981
|
+
if (!valid) return err2("Invalid or expired confirm_token.");
|
|
9473
9982
|
try {
|
|
9474
9983
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
9475
|
-
if (!stripeKey) return
|
|
9984
|
+
if (!stripeKey) return err2("Stripe not configured");
|
|
9476
9985
|
const result2 = await stripePost(
|
|
9477
9986
|
stripeKey,
|
|
9478
9987
|
"/v1/radar/value_list_items",
|
|
9479
9988
|
{ value_list: value_list_id, value }
|
|
9480
9989
|
);
|
|
9481
|
-
if (!result2.ok) return
|
|
9990
|
+
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
9482
9991
|
writeAudit(env, {
|
|
9483
9992
|
tool_name: toolName,
|
|
9484
9993
|
ocs_method: "stripe.radar.value_list.add",
|
|
@@ -9488,10 +9997,10 @@ Expires in 5 minutes.`
|
|
|
9488
9997
|
sub: props2.sub,
|
|
9489
9998
|
reseller_id: props2.reseller_id
|
|
9490
9999
|
});
|
|
9491
|
-
return
|
|
10000
|
+
return ok2(`Added "${value}" to Radar list ${value_list_id}.`);
|
|
9492
10001
|
} catch (e) {
|
|
9493
10002
|
Sentry2.captureException(e);
|
|
9494
|
-
return
|
|
10003
|
+
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
9495
10004
|
}
|
|
9496
10005
|
}
|
|
9497
10006
|
);
|
|
@@ -9499,11 +10008,11 @@ Expires in 5 minutes.`
|
|
|
9499
10008
|
"radar_rule_toggle",
|
|
9500
10009
|
"Admin: enable or disable a Stripe Radar rule. NOTE: Stripe does not expose rule CRUD via the public API \u2014 this tool returns Dashboard instructions.",
|
|
9501
10010
|
{
|
|
9502
|
-
rule_id:
|
|
9503
|
-
enabled:
|
|
10011
|
+
rule_id: z17.string().min(1).describe("Stripe Radar rule ID."),
|
|
10012
|
+
enabled: z17.boolean().describe("true = enable, false = disable.")
|
|
9504
10013
|
},
|
|
9505
10014
|
async ({ rule_id, enabled }) => {
|
|
9506
|
-
return
|
|
10015
|
+
return ok2(
|
|
9507
10016
|
`Stripe Radar does not expose rule enable/disable via the public API.
|
|
9508
10017
|
To ${enabled ? "enable" : "disable"} rule ${rule_id}:
|
|
9509
10018
|
1. Open https://dashboard.stripe.com/radar/rules
|
|
@@ -9515,6 +10024,513 @@ Note: If you need this automated, use the Radar for Platforms beta \u2014 contac
|
|
|
9515
10024
|
);
|
|
9516
10025
|
}
|
|
9517
10026
|
|
|
10027
|
+
// src/tools-greenzone.ts
|
|
10028
|
+
import { z as z18 } from "zod";
|
|
10029
|
+
|
|
10030
|
+
// src/greenzone-whitelist.ts
|
|
10031
|
+
var STATE_KEY = "greenzone:state";
|
|
10032
|
+
var GREENZONE_PORTAL_PATH = "/greenzone";
|
|
10033
|
+
var MANAGE_HOSTS_PATH = "/greenzone/hosts";
|
|
10034
|
+
var MANAGE_IPS_PATH = "/greenzone/ips";
|
|
10035
|
+
async function readState(kv) {
|
|
10036
|
+
const raw = await kv.get(STATE_KEY, "json");
|
|
10037
|
+
if (!raw) {
|
|
10038
|
+
return {
|
|
10039
|
+
hosts: [],
|
|
10040
|
+
ips: [],
|
|
10041
|
+
last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10042
|
+
};
|
|
10043
|
+
}
|
|
10044
|
+
return raw;
|
|
10045
|
+
}
|
|
10046
|
+
async function writeState(kv, state) {
|
|
10047
|
+
await kv.put(STATE_KEY, JSON.stringify(state), {
|
|
10048
|
+
expirationTtl: 60 * 60 * 24 * 7
|
|
10049
|
+
// 7 days
|
|
10050
|
+
});
|
|
10051
|
+
}
|
|
10052
|
+
function stateAddHost(state, host) {
|
|
10053
|
+
const normalised = host.toLowerCase().trim();
|
|
10054
|
+
if (state.hosts.includes(normalised)) return state;
|
|
10055
|
+
return {
|
|
10056
|
+
...state,
|
|
10057
|
+
hosts: [...state.hosts, normalised].sort(),
|
|
10058
|
+
last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10059
|
+
};
|
|
10060
|
+
}
|
|
10061
|
+
function stateRemoveHost(state, host) {
|
|
10062
|
+
const normalised = host.toLowerCase().trim();
|
|
10063
|
+
return {
|
|
10064
|
+
...state,
|
|
10065
|
+
hosts: state.hosts.filter((h) => h !== normalised),
|
|
10066
|
+
last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10067
|
+
};
|
|
10068
|
+
}
|
|
10069
|
+
function stateAddIp(state, ip) {
|
|
10070
|
+
const normalised = ip.trim();
|
|
10071
|
+
if (state.ips.includes(normalised)) return state;
|
|
10072
|
+
return {
|
|
10073
|
+
...state,
|
|
10074
|
+
ips: [...state.ips, normalised].sort(),
|
|
10075
|
+
last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10076
|
+
};
|
|
10077
|
+
}
|
|
10078
|
+
function stateRemoveIp(state, ip) {
|
|
10079
|
+
const normalised = ip.trim();
|
|
10080
|
+
return {
|
|
10081
|
+
...state,
|
|
10082
|
+
ips: state.ips.filter((i) => i !== normalised),
|
|
10083
|
+
last_synced_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10084
|
+
};
|
|
10085
|
+
}
|
|
10086
|
+
function escapeForHasTextDoubleQuoted(fragment) {
|
|
10087
|
+
return fragment.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
10088
|
+
}
|
|
10089
|
+
function buildAddHostSteps(portalBaseUrl, host) {
|
|
10090
|
+
return [
|
|
10091
|
+
{
|
|
10092
|
+
tool: "new_tab",
|
|
10093
|
+
description: "Open a dedicated tab for Greenzone host whitelist operation"
|
|
10094
|
+
},
|
|
10095
|
+
{
|
|
10096
|
+
tool: "navigate",
|
|
10097
|
+
args: { url: `${portalBaseUrl}${MANAGE_HOSTS_PATH}` },
|
|
10098
|
+
description: `Navigate to Manage hosts page at ${portalBaseUrl}${MANAGE_HOSTS_PATH}`
|
|
10099
|
+
},
|
|
10100
|
+
{
|
|
10101
|
+
tool: "find",
|
|
10102
|
+
args: { query: "Add host button or input field" },
|
|
10103
|
+
description: "Find the 'Add host' button or input in Manage hosts menu"
|
|
10104
|
+
},
|
|
10105
|
+
{
|
|
10106
|
+
tool: "click",
|
|
10107
|
+
args: { selector: "[data-action='add-host'], button:has-text('Add'), a:has-text('Add host')" },
|
|
10108
|
+
description: "Click Add host to open the entry form"
|
|
10109
|
+
},
|
|
10110
|
+
{
|
|
10111
|
+
tool: "fill",
|
|
10112
|
+
args: { selector: "input[name='host'], input[placeholder*='host'], input[type='text']", value: host },
|
|
10113
|
+
description: `Fill in host value: ${host}`
|
|
10114
|
+
},
|
|
10115
|
+
{
|
|
10116
|
+
tool: "click",
|
|
10117
|
+
args: { selector: "button[type='submit'], button:has-text('Save'), button:has-text('Confirm')" },
|
|
10118
|
+
description: "Submit the form to save the host"
|
|
10119
|
+
},
|
|
10120
|
+
{
|
|
10121
|
+
tool: "screenshot",
|
|
10122
|
+
description: "Screenshot to verify host was added successfully"
|
|
10123
|
+
},
|
|
10124
|
+
{
|
|
10125
|
+
tool: "close",
|
|
10126
|
+
description: "Close the tab (always runs \u2014 success or error)"
|
|
10127
|
+
}
|
|
10128
|
+
];
|
|
10129
|
+
}
|
|
10130
|
+
function buildRemoveHostSteps(portalBaseUrl, host) {
|
|
10131
|
+
const hostKey = host.toLowerCase().trim();
|
|
10132
|
+
const hostText = escapeForHasTextDoubleQuoted(hostKey);
|
|
10133
|
+
return [
|
|
10134
|
+
{
|
|
10135
|
+
tool: "new_tab",
|
|
10136
|
+
description: "Open a dedicated tab for Greenzone host removal operation"
|
|
10137
|
+
},
|
|
10138
|
+
{
|
|
10139
|
+
tool: "navigate",
|
|
10140
|
+
args: { url: `${portalBaseUrl}${MANAGE_HOSTS_PATH}` },
|
|
10141
|
+
description: `Navigate to Manage hosts page at ${portalBaseUrl}${MANAGE_HOSTS_PATH}`
|
|
10142
|
+
},
|
|
10143
|
+
{
|
|
10144
|
+
tool: "find",
|
|
10145
|
+
args: { query: `row containing host ${hostKey}` },
|
|
10146
|
+
description: `Find the row for host: ${hostKey}`
|
|
10147
|
+
},
|
|
10148
|
+
{
|
|
10149
|
+
tool: "click",
|
|
10150
|
+
args: {
|
|
10151
|
+
selector: `tr:has-text("${hostText}") [data-action='delete'], tr:has-text("${hostText}") button:has-text('Remove'), tr:has-text("${hostText}") button:has-text('Delete')`
|
|
10152
|
+
},
|
|
10153
|
+
description: `Click Remove/Delete for host: ${hostKey}`
|
|
10154
|
+
},
|
|
10155
|
+
{
|
|
10156
|
+
tool: "click",
|
|
10157
|
+
args: { selector: "button:has-text('Confirm'), button:has-text('Yes'), [data-action='confirm']" },
|
|
10158
|
+
description: "Confirm the deletion in the confirmation dialog"
|
|
10159
|
+
},
|
|
10160
|
+
{
|
|
10161
|
+
tool: "screenshot",
|
|
10162
|
+
description: "Screenshot to verify host was removed"
|
|
10163
|
+
},
|
|
10164
|
+
{
|
|
10165
|
+
tool: "close",
|
|
10166
|
+
description: "Close the tab (always runs \u2014 success or error)"
|
|
10167
|
+
}
|
|
10168
|
+
];
|
|
10169
|
+
}
|
|
10170
|
+
function buildAddIpSteps(portalBaseUrl, ip) {
|
|
10171
|
+
return [
|
|
10172
|
+
{
|
|
10173
|
+
tool: "new_tab",
|
|
10174
|
+
description: "Open a dedicated tab for Greenzone IP whitelist operation"
|
|
10175
|
+
},
|
|
10176
|
+
{
|
|
10177
|
+
tool: "navigate",
|
|
10178
|
+
args: { url: `${portalBaseUrl}${MANAGE_IPS_PATH}` },
|
|
10179
|
+
description: `Navigate to Manage IPs page at ${portalBaseUrl}${MANAGE_IPS_PATH}`
|
|
10180
|
+
},
|
|
10181
|
+
{
|
|
10182
|
+
tool: "find",
|
|
10183
|
+
args: { query: "Add IP button or input field" },
|
|
10184
|
+
description: "Find the 'Add IP' button or input in Manage IPs menu"
|
|
10185
|
+
},
|
|
10186
|
+
{
|
|
10187
|
+
tool: "click",
|
|
10188
|
+
args: { selector: "[data-action='add-ip'], button:has-text('Add'), a:has-text('Add IP')" },
|
|
10189
|
+
description: "Click Add IP to open the entry form"
|
|
10190
|
+
},
|
|
10191
|
+
{
|
|
10192
|
+
tool: "fill",
|
|
10193
|
+
args: { selector: "input[name='ip'], input[placeholder*='IP'], input[placeholder*='ip'], input[type='text']", value: ip },
|
|
10194
|
+
description: `Fill in IP value: ${ip}`
|
|
10195
|
+
},
|
|
10196
|
+
{
|
|
10197
|
+
tool: "click",
|
|
10198
|
+
args: { selector: "button[type='submit'], button:has-text('Save'), button:has-text('Confirm')" },
|
|
10199
|
+
description: "Submit the form to save the IP"
|
|
10200
|
+
},
|
|
10201
|
+
{
|
|
10202
|
+
tool: "screenshot",
|
|
10203
|
+
description: "Screenshot to verify IP was added successfully"
|
|
10204
|
+
},
|
|
10205
|
+
{
|
|
10206
|
+
tool: "close",
|
|
10207
|
+
description: "Close the tab (always runs \u2014 success or error)"
|
|
10208
|
+
}
|
|
10209
|
+
];
|
|
10210
|
+
}
|
|
10211
|
+
function buildRemoveIpSteps(portalBaseUrl, ip) {
|
|
10212
|
+
const ipKey = ip.trim();
|
|
10213
|
+
const ipText = escapeForHasTextDoubleQuoted(ipKey);
|
|
10214
|
+
return [
|
|
10215
|
+
{
|
|
10216
|
+
tool: "new_tab",
|
|
10217
|
+
description: "Open a dedicated tab for Greenzone IP removal operation"
|
|
10218
|
+
},
|
|
10219
|
+
{
|
|
10220
|
+
tool: "navigate",
|
|
10221
|
+
args: { url: `${portalBaseUrl}${MANAGE_IPS_PATH}` },
|
|
10222
|
+
description: `Navigate to Manage IPs page at ${portalBaseUrl}${MANAGE_IPS_PATH}`
|
|
10223
|
+
},
|
|
10224
|
+
{
|
|
10225
|
+
tool: "find",
|
|
10226
|
+
args: { query: `row containing IP ${ipKey}` },
|
|
10227
|
+
description: `Find the row for IP: ${ipKey}`
|
|
10228
|
+
},
|
|
10229
|
+
{
|
|
10230
|
+
tool: "click",
|
|
10231
|
+
args: {
|
|
10232
|
+
selector: `tr:has-text("${ipText}") [data-action='delete'], tr:has-text("${ipText}") button:has-text('Remove'), tr:has-text("${ipText}") button:has-text('Delete')`
|
|
10233
|
+
},
|
|
10234
|
+
description: `Click Remove/Delete for IP: ${ipKey}`
|
|
10235
|
+
},
|
|
10236
|
+
{
|
|
10237
|
+
tool: "click",
|
|
10238
|
+
args: { selector: "button:has-text('Confirm'), button:has-text('Yes'), [data-action='confirm']" },
|
|
10239
|
+
description: "Confirm the deletion in the confirmation dialog"
|
|
10240
|
+
},
|
|
10241
|
+
{
|
|
10242
|
+
tool: "screenshot",
|
|
10243
|
+
description: "Screenshot to verify IP was removed"
|
|
10244
|
+
},
|
|
10245
|
+
{
|
|
10246
|
+
tool: "close",
|
|
10247
|
+
description: "Close the tab (always runs \u2014 success or error)"
|
|
10248
|
+
}
|
|
10249
|
+
];
|
|
10250
|
+
}
|
|
10251
|
+
function buildListSteps(portalBaseUrl) {
|
|
10252
|
+
return [
|
|
10253
|
+
{
|
|
10254
|
+
tool: "new_tab",
|
|
10255
|
+
description: "Open a dedicated tab for Greenzone whitelist read"
|
|
10256
|
+
},
|
|
10257
|
+
{
|
|
10258
|
+
tool: "navigate",
|
|
10259
|
+
args: { url: `${portalBaseUrl}${GREENZONE_PORTAL_PATH}` },
|
|
10260
|
+
description: `Navigate to Greenzone overview at ${portalBaseUrl}${GREENZONE_PORTAL_PATH}`
|
|
10261
|
+
},
|
|
10262
|
+
{
|
|
10263
|
+
tool: "find",
|
|
10264
|
+
args: { query: "Manage hosts and Manage IPs sections or links" },
|
|
10265
|
+
description: "Find Manage hosts and Manage IPs menu items"
|
|
10266
|
+
},
|
|
10267
|
+
{
|
|
10268
|
+
tool: "screenshot",
|
|
10269
|
+
description: "Screenshot of Greenzone overview for audit"
|
|
10270
|
+
},
|
|
10271
|
+
{
|
|
10272
|
+
tool: "navigate",
|
|
10273
|
+
args: { url: `${portalBaseUrl}${MANAGE_HOSTS_PATH}` },
|
|
10274
|
+
description: "Navigate to Manage hosts to read current host list"
|
|
10275
|
+
},
|
|
10276
|
+
{
|
|
10277
|
+
tool: "find",
|
|
10278
|
+
args: { query: "host entries table or list" },
|
|
10279
|
+
description: "Read all host entries from the page"
|
|
10280
|
+
},
|
|
10281
|
+
{
|
|
10282
|
+
tool: "navigate",
|
|
10283
|
+
args: { url: `${portalBaseUrl}${MANAGE_IPS_PATH}` },
|
|
10284
|
+
description: "Navigate to Manage IPs to read current IP list"
|
|
10285
|
+
},
|
|
10286
|
+
{
|
|
10287
|
+
tool: "find",
|
|
10288
|
+
args: { query: "IP entries table or list" },
|
|
10289
|
+
description: "Read all IP entries from the page"
|
|
10290
|
+
},
|
|
10291
|
+
{
|
|
10292
|
+
tool: "close",
|
|
10293
|
+
description: "Close the tab (always runs)"
|
|
10294
|
+
}
|
|
10295
|
+
];
|
|
10296
|
+
}
|
|
10297
|
+
function resolvePortalBaseUrl(env) {
|
|
10298
|
+
return env.GREENZONE_PORTAL_URL ?? env.CARRIER_OCS_PORTAL_URL ?? env.CARRIER_OCS_BASE_URL ?? "https://ocs.esimvault.cloud";
|
|
10299
|
+
}
|
|
10300
|
+
|
|
10301
|
+
// src/tools-greenzone.ts
|
|
10302
|
+
function getGreenzoneKv(env) {
|
|
10303
|
+
return env.GREENZONE_STATE_KV ?? null;
|
|
10304
|
+
}
|
|
10305
|
+
function ok3(payload) {
|
|
10306
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
10307
|
+
}
|
|
10308
|
+
function err3(message) {
|
|
10309
|
+
return { isError: true, content: [{ type: "text", text: message }] };
|
|
10310
|
+
}
|
|
10311
|
+
var KAPTURE_UNAVAILABLE_MSG = "Execute each Kapture step using mcp__kapture__* tools in sequence. Ensure the Kapture extension is active in Chrome before invoking.";
|
|
10312
|
+
function registerGreenzoneTools(server2, ctx) {
|
|
10313
|
+
server2.registerTool(
|
|
10314
|
+
"greenzone_whitelist_add",
|
|
10315
|
+
{
|
|
10316
|
+
title: "Greenzone Whitelist \u2014 Add",
|
|
10317
|
+
description: "Add a hostname and/or IP address to the Bridge4IP Greenzone whitelist. The Greenzone whitelist controls which hosts/IPs are exempt from data charging after a subscriber's bundle is depleted. This operates via browser automation (Kapture) against the OCS portal \u2014 no REST API exists for this surface. Provide `host` (e.g. 'example.com') and/or `ip` (e.g. '203.0.113.0/24'). At least one of host or ip must be provided. Returns Kapture step sequence for Claude Code to execute. Requires 'write' scope and Kapture running in Sam's Chrome.",
|
|
10318
|
+
inputSchema: {
|
|
10319
|
+
host: z18.string().optional().describe("Hostname to whitelist (e.g. 'example.com')"),
|
|
10320
|
+
ip: z18.string().optional().describe("IP address or CIDR to whitelist (e.g. '203.0.113.0/24')"),
|
|
10321
|
+
dry_run: z18.boolean().optional().describe("If true, return the Kapture steps without executing portal automation")
|
|
10322
|
+
},
|
|
10323
|
+
annotations: { destructiveHint: true }
|
|
10324
|
+
},
|
|
10325
|
+
async (args) => {
|
|
10326
|
+
if (!ctx.props.scope.includes("write")) {
|
|
10327
|
+
return err3(
|
|
10328
|
+
`Scope denied: greenzone_whitelist_add requires 'write' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
10329
|
+
);
|
|
10330
|
+
}
|
|
10331
|
+
const { host, ip, dry_run } = args;
|
|
10332
|
+
if (!host && !ip) {
|
|
10333
|
+
return err3("At least one of 'host' or 'ip' must be provided.");
|
|
10334
|
+
}
|
|
10335
|
+
const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
|
|
10336
|
+
const steps = [
|
|
10337
|
+
...host ? buildAddHostSteps(portalBaseUrl, host) : [],
|
|
10338
|
+
...ip ? buildAddIpSteps(portalBaseUrl, ip) : []
|
|
10339
|
+
];
|
|
10340
|
+
if (dry_run) {
|
|
10341
|
+
return ok3({
|
|
10342
|
+
dry_run: true,
|
|
10343
|
+
message: "Would execute the following Kapture steps against the OCS portal:",
|
|
10344
|
+
kapture_steps: steps,
|
|
10345
|
+
host,
|
|
10346
|
+
ip
|
|
10347
|
+
});
|
|
10348
|
+
}
|
|
10349
|
+
const kv = getGreenzoneKv(ctx.env);
|
|
10350
|
+
let state;
|
|
10351
|
+
let kvWarning;
|
|
10352
|
+
if (kv) {
|
|
10353
|
+
try {
|
|
10354
|
+
state = await readState(kv);
|
|
10355
|
+
if (host) state = stateAddHost(state, host);
|
|
10356
|
+
if (ip) state = stateAddIp(state, ip);
|
|
10357
|
+
await writeState(kv, state);
|
|
10358
|
+
} catch (e) {
|
|
10359
|
+
kvWarning = `KV write failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
10360
|
+
}
|
|
10361
|
+
} else {
|
|
10362
|
+
kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
|
|
10363
|
+
}
|
|
10364
|
+
return ok3({
|
|
10365
|
+
requires_kapture: true,
|
|
10366
|
+
message: "Execute the Kapture steps below to apply the change in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
|
|
10367
|
+
kapture_steps: steps,
|
|
10368
|
+
kv_state: state ?? null,
|
|
10369
|
+
kv_warning: kvWarning,
|
|
10370
|
+
portal_base_url: portalBaseUrl
|
|
10371
|
+
});
|
|
10372
|
+
}
|
|
10373
|
+
);
|
|
10374
|
+
server2.registerTool(
|
|
10375
|
+
"greenzone_whitelist_remove",
|
|
10376
|
+
{
|
|
10377
|
+
title: "Greenzone Whitelist \u2014 Remove",
|
|
10378
|
+
description: "Remove a hostname and/or IP address from the Bridge4IP Greenzone whitelist. This operates via browser automation (Kapture) against the OCS portal. Provide `host` and/or `ip` to remove \u2014 at least one required. Returns Kapture step sequence for Claude Code to execute. Requires 'write' scope and Kapture running in Sam's Chrome.",
|
|
10379
|
+
inputSchema: {
|
|
10380
|
+
host: z18.string().optional().describe("Hostname to remove from whitelist"),
|
|
10381
|
+
ip: z18.string().optional().describe("IP address or CIDR to remove from whitelist"),
|
|
10382
|
+
dry_run: z18.boolean().optional().describe("If true, return the Kapture steps without executing portal automation")
|
|
10383
|
+
},
|
|
10384
|
+
annotations: { destructiveHint: true }
|
|
10385
|
+
},
|
|
10386
|
+
async (args) => {
|
|
10387
|
+
if (!ctx.props.scope.includes("write")) {
|
|
10388
|
+
return err3(
|
|
10389
|
+
`Scope denied: greenzone_whitelist_remove requires 'write' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
10390
|
+
);
|
|
10391
|
+
}
|
|
10392
|
+
const { host, ip, dry_run } = args;
|
|
10393
|
+
if (!host && !ip) {
|
|
10394
|
+
return err3("At least one of 'host' or 'ip' must be provided.");
|
|
10395
|
+
}
|
|
10396
|
+
const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
|
|
10397
|
+
const steps = [
|
|
10398
|
+
...host ? buildRemoveHostSteps(portalBaseUrl, host) : [],
|
|
10399
|
+
...ip ? buildRemoveIpSteps(portalBaseUrl, ip) : []
|
|
10400
|
+
];
|
|
10401
|
+
if (dry_run) {
|
|
10402
|
+
return ok3({
|
|
10403
|
+
dry_run: true,
|
|
10404
|
+
message: "Would execute the following Kapture steps to remove from OCS portal:",
|
|
10405
|
+
kapture_steps: steps,
|
|
10406
|
+
host,
|
|
10407
|
+
ip
|
|
10408
|
+
});
|
|
10409
|
+
}
|
|
10410
|
+
const kv = getGreenzoneKv(ctx.env);
|
|
10411
|
+
let state;
|
|
10412
|
+
let kvWarning;
|
|
10413
|
+
if (kv) {
|
|
10414
|
+
try {
|
|
10415
|
+
state = await readState(kv);
|
|
10416
|
+
if (host) state = stateRemoveHost(state, host);
|
|
10417
|
+
if (ip) state = stateRemoveIp(state, ip);
|
|
10418
|
+
await writeState(kv, state);
|
|
10419
|
+
} catch (e) {
|
|
10420
|
+
kvWarning = `KV write failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
10421
|
+
}
|
|
10422
|
+
} else {
|
|
10423
|
+
kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
|
|
10424
|
+
}
|
|
10425
|
+
return ok3({
|
|
10426
|
+
requires_kapture: true,
|
|
10427
|
+
message: "Execute the Kapture steps below to apply the removal in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
|
|
10428
|
+
kapture_steps: steps,
|
|
10429
|
+
kv_state: state ?? null,
|
|
10430
|
+
kv_warning: kvWarning,
|
|
10431
|
+
portal_base_url: portalBaseUrl
|
|
10432
|
+
});
|
|
10433
|
+
}
|
|
10434
|
+
);
|
|
10435
|
+
server2.registerTool(
|
|
10436
|
+
"greenzone_whitelist_list",
|
|
10437
|
+
{
|
|
10438
|
+
title: "Greenzone Whitelist \u2014 List",
|
|
10439
|
+
description: "List current Greenzone whitelist entries from the KV cache. Returns all whitelisted hosts and IPs, when the state was last synced, and whether the last hourly reconciliation detected drift from the portal. This is a fast read from KV \u2014 no browser automation required. Use `from_portal: true` to trigger a live portal read via Kapture instead of KV. Requires 'read' scope.",
|
|
10440
|
+
inputSchema: {
|
|
10441
|
+
from_portal: z18.boolean().optional().describe(
|
|
10442
|
+
"When true, return Kapture steps to read live portal state instead of KV cache"
|
|
10443
|
+
)
|
|
10444
|
+
},
|
|
10445
|
+
annotations: { readOnlyHint: true }
|
|
10446
|
+
},
|
|
10447
|
+
async (args) => {
|
|
10448
|
+
if (!ctx.props.scope.includes("read")) {
|
|
10449
|
+
return err3(
|
|
10450
|
+
`Scope denied: greenzone_whitelist_list requires 'read' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
10451
|
+
);
|
|
10452
|
+
}
|
|
10453
|
+
const { from_portal } = args;
|
|
10454
|
+
if (from_portal) {
|
|
10455
|
+
const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
|
|
10456
|
+
return ok3({
|
|
10457
|
+
requires_kapture: true,
|
|
10458
|
+
message: "Execute the Kapture steps below to read live Greenzone whitelist state from the OCS portal.",
|
|
10459
|
+
kapture_steps: buildListSteps(portalBaseUrl),
|
|
10460
|
+
portal_base_url: portalBaseUrl
|
|
10461
|
+
});
|
|
10462
|
+
}
|
|
10463
|
+
const kv = getGreenzoneKv(ctx.env);
|
|
10464
|
+
if (!kv) {
|
|
10465
|
+
return ok3({
|
|
10466
|
+
source: "kv_cache",
|
|
10467
|
+
hosts: [],
|
|
10468
|
+
ips: [],
|
|
10469
|
+
last_synced_at: null,
|
|
10470
|
+
last_reconciled_at: null,
|
|
10471
|
+
reconcile_drift: false,
|
|
10472
|
+
warning: "GREENZONE_STATE_KV not bound in stdio mode. Use from_portal: true to read live portal state."
|
|
10473
|
+
});
|
|
10474
|
+
}
|
|
10475
|
+
try {
|
|
10476
|
+
const state = await readState(kv);
|
|
10477
|
+
return ok3({
|
|
10478
|
+
source: "kv_cache",
|
|
10479
|
+
hosts: state.hosts,
|
|
10480
|
+
ips: state.ips,
|
|
10481
|
+
last_synced_at: state.last_synced_at,
|
|
10482
|
+
last_reconciled_at: state.last_reconciled_at ?? null,
|
|
10483
|
+
reconcile_drift: state.reconcile_drift ?? false
|
|
10484
|
+
});
|
|
10485
|
+
} catch (e) {
|
|
10486
|
+
return err3(`Failed to read Greenzone state from KV: ${e instanceof Error ? e.message : String(e)}`);
|
|
10487
|
+
}
|
|
10488
|
+
}
|
|
10489
|
+
);
|
|
10490
|
+
}
|
|
10491
|
+
|
|
10492
|
+
// src/tools-ui-agent-generic.ts
|
|
10493
|
+
import { z as z19 } from "zod";
|
|
10494
|
+
var STDIO_ERROR = {
|
|
10495
|
+
isError: true,
|
|
10496
|
+
content: [
|
|
10497
|
+
{
|
|
10498
|
+
type: "text",
|
|
10499
|
+
text: JSON.stringify({
|
|
10500
|
+
error: "requires_worker_runtime",
|
|
10501
|
+
message: "ui_agent_ask and ui_agent_status require the remote Carrier MCP Worker deployment (mcp.carrier.llc/mcp) \u2014 they cannot run in stdio/local mode because Steel browser sessions and KV task state require Cloudflare Workers runtime bindings. Connect to the remote MCP endpoint to use these tools.",
|
|
10502
|
+
remote_url: "https://mcp.carrier.llc/mcp"
|
|
10503
|
+
})
|
|
10504
|
+
}
|
|
10505
|
+
]
|
|
10506
|
+
};
|
|
10507
|
+
function registerUiAgentGenericTools(server2, _ctx) {
|
|
10508
|
+
server2.registerTool(
|
|
10509
|
+
"ui_agent_ask",
|
|
10510
|
+
{
|
|
10511
|
+
title: "Dispatch Generic Steel Browsing Agent",
|
|
10512
|
+
description: "Dispatch a Steel browsing agent task with a natural-language prompt. The agent will navigate the web, use Tavily web search if needed, and return the result. Use for ad-hoc research, page extraction, form filling outside the OCS portal. Pro/Enterprise scope required. Cost ~$0.20-1.00 per task depending on complexity. Returns task_id immediately \u2014 poll with ui_agent_status for completion. Set dry_run=true to preview cost estimate without executing. NOTE: requires remote Worker deployment (mcp.carrier.llc/mcp) \u2014 not available in stdio mode.",
|
|
10513
|
+
inputSchema: {
|
|
10514
|
+
prompt: z19.string().min(1).max(4e3).describe("Natural-language task description for the Steel browsing agent."),
|
|
10515
|
+
max_steps: z19.number().int().min(1).max(30).optional().describe("Maximum agent steps (1\u201330, default 15)."),
|
|
10516
|
+
dry_run: z19.boolean().optional().describe("Preview cost estimate without dispatching.")
|
|
10517
|
+
}
|
|
10518
|
+
},
|
|
10519
|
+
async (_args) => STDIO_ERROR
|
|
10520
|
+
);
|
|
10521
|
+
server2.registerTool(
|
|
10522
|
+
"ui_agent_status",
|
|
10523
|
+
{
|
|
10524
|
+
title: "Get Steel Agent Task Status",
|
|
10525
|
+
description: "Poll the status of a Steel browser agent task dispatched by ui_agent_ask or any ui_* tool. Returns current status (pending | running | completed | failed) and result when available. NOTE: requires remote Worker deployment (mcp.carrier.llc/mcp) \u2014 not available in stdio mode.",
|
|
10526
|
+
inputSchema: {
|
|
10527
|
+
task_id: z19.string().describe("Steel task ID returned by ui_agent_ask or any ui_* tool dispatch")
|
|
10528
|
+
}
|
|
10529
|
+
},
|
|
10530
|
+
async (_args) => STDIO_ERROR
|
|
10531
|
+
);
|
|
10532
|
+
}
|
|
10533
|
+
|
|
9518
10534
|
// src/index.ts
|
|
9519
10535
|
var baseUrl = process.env.CARRIER_OCS_BASE_URL ?? process.env.ESIMVAULT_BASE_URL ?? "https://ocs.esimvault.cloud";
|
|
9520
10536
|
var token = process.env.CARRIER_OCS_API_TOKEN ?? process.env.ESIMVAULT_API_TOKEN;
|
|
@@ -9562,12 +10578,17 @@ registerIntelligenceTools(server, toolCtx);
|
|
|
9562
10578
|
registerAllBacklogTools(server, toolCtx);
|
|
9563
10579
|
registerAllCarrierAskTools(server, toolCtx);
|
|
9564
10580
|
registerListRecentOcsEventsTool(server, stdioEnv);
|
|
10581
|
+
registerRateLimitStatusTool(server, toolCtx);
|
|
10582
|
+
registerCountryHistoryTool(server, toolCtx);
|
|
10583
|
+
registerDepletionEventsTool(server, toolCtx);
|
|
9565
10584
|
registerAllApps(server, toolCtx);
|
|
9566
10585
|
registerAllPricingTools(server, toolCtx);
|
|
9567
10586
|
registerScheduleAndUsageTools(server, toolCtx);
|
|
9568
10587
|
registerAllUiAgentTools(server, toolCtx);
|
|
9569
10588
|
registerUiAgentAskTools(server, toolCtx);
|
|
9570
10589
|
registerStripeConnectTools(server, { env: stdioEnv, props });
|
|
10590
|
+
registerGreenzoneTools(server, toolCtx);
|
|
10591
|
+
registerUiAgentGenericTools(server, toolCtx);
|
|
9571
10592
|
registerAllPrompts(server);
|
|
9572
10593
|
var transport = new StdioServerTransport();
|
|
9573
10594
|
await server.connect(transport);
|