@carrierllc/mcp 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/{chunk-4XHSOF62.js → chunk-KHNJNJX3.js} +325 -4
- package/dist/chunk-KHNJNJX3.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +318 -196
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/plugin/carrier/README.md +1 -1
- package/dist/chunk-4XHSOF62.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
CARRIER_VERSION,
|
|
4
|
+
OCS_MAX_USAGE_WINDOW_DAYS,
|
|
4
5
|
ROUTER_RULES,
|
|
5
6
|
TARGET_IDS,
|
|
6
7
|
acquireEndpointSlot,
|
|
8
|
+
applyParamRenames,
|
|
7
9
|
applySubscriberFilters,
|
|
8
10
|
buildListSubscriberParams,
|
|
9
11
|
buildRouterCatalog,
|
|
10
12
|
buildSite,
|
|
11
13
|
clerkCliDeps,
|
|
12
14
|
configureClerkInstance,
|
|
15
|
+
daysUntil,
|
|
13
16
|
deploySite,
|
|
14
17
|
esimStatusPerAccountParams,
|
|
18
|
+
extractDailyUsage,
|
|
15
19
|
extractEsimStatusCounts,
|
|
20
|
+
filterActiveSubscribers,
|
|
16
21
|
fleetScreen,
|
|
22
|
+
formatBytes,
|
|
17
23
|
formatProbes,
|
|
18
24
|
generateStorefrontLogo,
|
|
19
25
|
getLimitForEndpoint,
|
|
20
26
|
getRateLimitWindowCounts,
|
|
21
27
|
imsiFromSubscriberRecord,
|
|
22
28
|
isTargetId,
|
|
29
|
+
lastNDaysPeriod,
|
|
23
30
|
loadStorefrontBrand,
|
|
24
31
|
locationParams,
|
|
25
32
|
mergeEnvLocal,
|
|
@@ -33,11 +40,14 @@ import {
|
|
|
33
40
|
recurringPackageParams,
|
|
34
41
|
renderHtml,
|
|
35
42
|
repairPlanFor,
|
|
43
|
+
runWithBudget,
|
|
36
44
|
storefrontClerkUrls,
|
|
37
45
|
subscriberIdParams,
|
|
46
|
+
subscriberRows,
|
|
38
47
|
usageOverPeriodParams,
|
|
39
|
-
verifyStorefront
|
|
40
|
-
|
|
48
|
+
verifyStorefront,
|
|
49
|
+
withOcsListSummary
|
|
50
|
+
} from "./chunk-KHNJNJX3.js";
|
|
41
51
|
import "./chunk-SHKKVIIA.js";
|
|
42
52
|
|
|
43
53
|
// src/index.ts
|
|
@@ -69,32 +79,35 @@ var OcsClient = class {
|
|
|
69
79
|
async call(method, params = {}) {
|
|
70
80
|
await acquireEndpointSlot(this.token, method, "interactive");
|
|
71
81
|
const url = `${this.baseUrl}/v1?token=${this.token}`;
|
|
72
|
-
const body2 = JSON.stringify({ [method]: params });
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (json.status?.code !== 0) {
|
|
83
|
-
throw new OcsApiError(json.status?.code ?? -1, json.status?.msg ?? "Unknown error", method);
|
|
84
|
-
}
|
|
85
|
-
if (method === "getCustomerTariff" && json["listTariffRule"] !== void 0) {
|
|
86
|
-
return json["listTariffRule"];
|
|
87
|
-
}
|
|
88
|
-
if (method === "getSubscriberLocationByCellId") {
|
|
89
|
-
const byMethod = json[method];
|
|
90
|
-
if (byMethod !== void 0) {
|
|
91
|
-
return byMethod;
|
|
82
|
+
const body2 = JSON.stringify({ [method]: applyParamRenames(method, params) });
|
|
83
|
+
return runWithBudget(method, async (signal) => {
|
|
84
|
+
const res = await fetch(url, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: body2,
|
|
88
|
+
signal
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
throw new OcsApiError(res.status, `HTTP ${res.status} ${res.statusText}`, method);
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const json = await res.json();
|
|
94
|
+
if (json.status?.code !== 0) {
|
|
95
|
+
throw new OcsApiError(json.status?.code ?? -1, json.status?.msg ?? "Unknown error", method);
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
if (method === "getCustomerTariff" && json["listTariffRule"] !== void 0) {
|
|
98
|
+
return json["listTariffRule"];
|
|
99
|
+
}
|
|
100
|
+
if (method === "getSubscriberLocationByCellId") {
|
|
101
|
+
const byMethod = json[method];
|
|
102
|
+
if (byMethod !== void 0) {
|
|
103
|
+
return byMethod;
|
|
104
|
+
}
|
|
105
|
+
if (json["subscriberLocation"] !== void 0) {
|
|
106
|
+
return json["subscriberLocation"];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return json[method] ?? json;
|
|
110
|
+
});
|
|
98
111
|
}
|
|
99
112
|
};
|
|
100
113
|
|
|
@@ -189,6 +202,9 @@ function estimateCostUsd(u, rates = HAIKU_RATES) {
|
|
|
189
202
|
var DEFAULT_BEDROCK_REGION = "us-east-1";
|
|
190
203
|
var DEFAULT_MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0";
|
|
191
204
|
var BEDROCK_TIMEOUT_MS = 3e4;
|
|
205
|
+
var MAX_ATTEMPTS = 4;
|
|
206
|
+
var INITIAL_RETRY_MS = 100;
|
|
207
|
+
var MIN_ATTEMPT_BUDGET_MS = 250;
|
|
192
208
|
var BedrockRateLimitError = class extends Error {
|
|
193
209
|
isRateLimit = true;
|
|
194
210
|
/**
|
|
@@ -218,7 +234,11 @@ function client(creds) {
|
|
|
218
234
|
secretAccessKey: creds.secretAccessKey,
|
|
219
235
|
...creds.sessionToken ? { sessionToken: creds.sessionToken } : {},
|
|
220
236
|
region,
|
|
221
|
-
service: "bedrock"
|
|
237
|
+
service: "bedrock",
|
|
238
|
+
// See MAX_ATTEMPTS. aws4fetch's retry loop cannot be cancelled, so this
|
|
239
|
+
// layer owns the policy instead. Changing this back to the default 10
|
|
240
|
+
// silently restores the ~50s stall that timeoutMs is supposed to prevent.
|
|
241
|
+
retries: 0
|
|
222
242
|
}),
|
|
223
243
|
region,
|
|
224
244
|
modelId: creds.modelId ?? DEFAULT_MODEL_ID
|
|
@@ -260,6 +280,47 @@ async function fail(resp) {
|
|
|
260
280
|
}
|
|
261
281
|
throw new BedrockError(text || `Bedrock returned HTTP ${resp.status}`, resp.status);
|
|
262
282
|
}
|
|
283
|
+
function timedOut() {
|
|
284
|
+
const err7 = new Error("Bedrock call timed out before a usable response");
|
|
285
|
+
err7.name = "TimeoutError";
|
|
286
|
+
return err7;
|
|
287
|
+
}
|
|
288
|
+
function sleep(ms, signal) {
|
|
289
|
+
if (signal.aborted) return Promise.resolve();
|
|
290
|
+
return new Promise((resolve) => {
|
|
291
|
+
const done = () => {
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
signal.removeEventListener("abort", done);
|
|
294
|
+
resolve();
|
|
295
|
+
};
|
|
296
|
+
const timer = setTimeout(done, ms);
|
|
297
|
+
signal.addEventListener("abort", done, { once: true });
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function retryable(status) {
|
|
301
|
+
return status === 429 || status >= 500;
|
|
302
|
+
}
|
|
303
|
+
async function sendWithRetry(aws, url, init, signal, deadlineAt) {
|
|
304
|
+
let throttled = null;
|
|
305
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
306
|
+
if (signal.aborted) break;
|
|
307
|
+
let resp;
|
|
308
|
+
try {
|
|
309
|
+
resp = await aws.fetch(url, init);
|
|
310
|
+
} catch (err7) {
|
|
311
|
+
if (signal.aborted && throttled) return throttled;
|
|
312
|
+
throw err7;
|
|
313
|
+
}
|
|
314
|
+
if (!retryable(resp.status)) return resp;
|
|
315
|
+
throttled = resp;
|
|
316
|
+
if (attempt === MAX_ATTEMPTS - 1) break;
|
|
317
|
+
const backoff = Math.random() * INITIAL_RETRY_MS * 2 ** attempt;
|
|
318
|
+
if (Date.now() + backoff + MIN_ATTEMPT_BUDGET_MS > deadlineAt) break;
|
|
319
|
+
await sleep(backoff, signal);
|
|
320
|
+
}
|
|
321
|
+
if (throttled) return throttled;
|
|
322
|
+
throw timedOut();
|
|
323
|
+
}
|
|
263
324
|
async function invokeTool(creds, req, signal) {
|
|
264
325
|
const json = await invokeModelRaw(
|
|
265
326
|
creds,
|
|
@@ -273,19 +334,24 @@ async function invokeTool(creds, req, signal) {
|
|
|
273
334
|
}
|
|
274
335
|
async function invokeModelRaw(creds, payload, opts = {}, signal) {
|
|
275
336
|
const { aws, region, modelId } = client(creds);
|
|
337
|
+
const timeoutMs = opts.timeoutMs ?? BEDROCK_TIMEOUT_MS;
|
|
338
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
276
339
|
const controller = new AbortController();
|
|
277
|
-
const timer = setTimeout(() => controller.abort(),
|
|
340
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
278
341
|
const onAbort = () => controller.abort();
|
|
279
342
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
280
343
|
try {
|
|
281
|
-
const resp = await
|
|
344
|
+
const resp = await sendWithRetry(
|
|
345
|
+
aws,
|
|
282
346
|
`https://bedrock-runtime.${region}.amazonaws.com/model/${encodeURIComponent(modelId)}/invoke`,
|
|
283
347
|
{
|
|
284
348
|
method: "POST",
|
|
285
349
|
headers: headersFor(opts.attribution, "application/json"),
|
|
286
350
|
body: JSON.stringify(payload),
|
|
287
351
|
signal: controller.signal
|
|
288
|
-
}
|
|
352
|
+
},
|
|
353
|
+
controller.signal,
|
|
354
|
+
deadlineAt
|
|
289
355
|
);
|
|
290
356
|
if (!resp.ok) await fail(resp);
|
|
291
357
|
return await resp.json();
|
|
@@ -769,11 +835,12 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
|
769
835
|
return result2;
|
|
770
836
|
};
|
|
771
837
|
}
|
|
772
|
-
async function ocsCall(env, token2, method, params = {}) {
|
|
838
|
+
async function ocsCall(env, token2, method, params = {}, listKind) {
|
|
773
839
|
const client2 = new OcsClient(env.CARRIER_OCS_BASE_URL, token2);
|
|
774
840
|
const result2 = await client2.call(method, params);
|
|
841
|
+
const payload = listKind ? withOcsListSummary(result2, listKind) : result2;
|
|
775
842
|
return {
|
|
776
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
843
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
777
844
|
};
|
|
778
845
|
}
|
|
779
846
|
async function resolveSubscriberByIccid(env, token2, iccid, cache) {
|
|
@@ -803,7 +870,7 @@ function registerAllTools(server2, ctx) {
|
|
|
803
870
|
"list_reseller_accounts",
|
|
804
871
|
{
|
|
805
872
|
title: "List Reseller Accounts",
|
|
806
|
-
description: "Use this to enumerate the accounts (sub-resellers or customer accounts) BELOW a reseller. Params: `resellerId` (integer, optional \u2014 omit to list accounts under the token owner's reseller). Returns: `{ reseller: [ { id, name, account: [ { id, name, balance, packageOnly } ] } ] }` \u2014 the accounts are NESTED under each reseller, not a flat top-level array. Do NOT use this to fetch a single subscriber's details \u2014 use `get_subscriber` instead. Do NOT use this to check eSIM activation counts \u2014 use `esim_status_per_account` for that. Do NOT use this to find a reseller's PARENT, balance or charging plans \u2014 those are above or on the reseller itself, not in this list; use `get_reseller_info`.",
|
|
873
|
+
description: "Use this to enumerate the accounts (sub-resellers or customer accounts) BELOW a reseller. Params: `resellerId` (integer, optional \u2014 omit to list accounts under the token owner's reseller). Returns: `{ summary, reseller: [ { id, name, account: [ { id, name, balance, packageOnly } ] } ] }` \u2014 the accounts are NESTED under each reseller, not a flat top-level array. `summary.total` therefore counts RESELLERS; the account count is `summary.nested.accounts`. For any question about accounts use that, never `total`. Do NOT use this to fetch a single subscriber's details \u2014 use `get_subscriber` instead. Do NOT use this to check eSIM activation counts \u2014 use `esim_status_per_account` for that. Do NOT use this to find a reseller's PARENT, balance or charging plans \u2014 those are above or on the reseller itself, not in this list; use `get_reseller_info`.",
|
|
807
874
|
inputSchema: {
|
|
808
875
|
resellerId: z.number().optional().describe("Filter to a specific reseller by ID (omit for token owner's reseller)")
|
|
809
876
|
},
|
|
@@ -817,7 +884,7 @@ function registerAllTools(server2, ctx) {
|
|
|
817
884
|
async ({ resellerId }, token2) => {
|
|
818
885
|
const params = {};
|
|
819
886
|
if (resellerId !== void 0) params.resellerId = resellerId;
|
|
820
|
-
return ocsCall(ctx.env, token2, "listResellerAccount", params);
|
|
887
|
+
return ocsCall(ctx.env, token2, "listResellerAccount", params, "resellers");
|
|
821
888
|
}
|
|
822
889
|
)
|
|
823
890
|
);
|
|
@@ -965,7 +1032,7 @@ function registerAllTools(server2, ctx) {
|
|
|
965
1032
|
"list_subscribers",
|
|
966
1033
|
{
|
|
967
1034
|
title: "List Subscribers",
|
|
968
|
-
description: "Use this to list subscribers with optional filters and pagination. Good for fleet enumeration, bulk status checks, and finding subscribers by account or status. OCS REQUIRES at least one search key \u2014 provide exactly one of: `imsi`, `iccid`, `activationCode`, `accountId`, or `msisdn`. Calling with no key will be rejected before reaching OCS. Params: `imsi` (string), `iccid` (string), `activationCode` (string), `accountId` (integer), `msisdn` (string), `status` (string, e.g. 'ACTIVE'/'SUSPENDED'), `offset` (integer, pagination \u2014 default 0), `limit` (integer, max results \u2014 always set to avoid unbounded fetches; recommended max 100 per call). Returns:
|
|
1035
|
+
description: "Use this to list subscribers with optional filters and pagination. Good for fleet enumeration, bulk status checks, and finding subscribers by account or status. OCS REQUIRES at least one search key \u2014 provide exactly one of: `imsi`, `iccid`, `activationCode`, `accountId`, or `msisdn`. Calling with no key will be rejected before reaching OCS. Params: `imsi` (string), `iccid` (string), `activationCode` (string), `accountId` (integer), `msisdn` (string), `status` (string, e.g. 'ACTIVE'/'SUSPENDED'), `offset` (integer, pagination \u2014 default 0), `limit` (integer, max results \u2014 always set to avoid unbounded fetches; recommended max 100 per call). Returns: `{ summary, subscriberList: [...] }` \u2014 subscriber summary records with ICCID, status and account, behind a server-counted `summary.total` (rows returned) and `summary.breakdown.status`. Answer 'how many' from `summary`, not by counting rows \u2014 but when `summary.moreAvailable` is set this is one page, so report `summary.upstreamTotal` for the fleet-wide figure, never `total`. Do NOT use this to fetch full details for a specific subscriber \u2014 use `get_subscriber` for that.",
|
|
969
1036
|
inputSchema: z.object({
|
|
970
1037
|
imsi: z.string().optional().describe("Filter by IMSI"),
|
|
971
1038
|
iccid: z.string().optional().describe("Filter by ICCID"),
|
|
@@ -994,7 +1061,12 @@ function registerAllTools(server2, ctx) {
|
|
|
994
1061
|
const raw = await client2.call("listSubscriber", params);
|
|
995
1062
|
const payload = applySubscriberFilters(raw, args);
|
|
996
1063
|
return {
|
|
997
|
-
content: [
|
|
1064
|
+
content: [
|
|
1065
|
+
{
|
|
1066
|
+
type: "text",
|
|
1067
|
+
text: JSON.stringify(withOcsListSummary(payload, "subscribers"), null, 2)
|
|
1068
|
+
}
|
|
1069
|
+
]
|
|
998
1070
|
};
|
|
999
1071
|
}
|
|
1000
1072
|
)
|
|
@@ -1321,7 +1393,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1321
1393
|
"list_subscriber_packages",
|
|
1322
1394
|
{
|
|
1323
1395
|
title: "List Subscriber Packages",
|
|
1324
|
-
description: "Use this to retrieve all prepaid packages currently assigned to a subscriber. Returns each package's allowance (data/voice/SMS), consumed usage, expiry date, status, and packageId. Always call this before any package modification tool (`modify_package_limits`, `modify_package_expiry`, `modify_package_status`, `delete_subscriber_package`) to confirm the correct packageId and current state. Params: `iccid` (subscriber identifier). Returns:
|
|
1396
|
+
description: "Use this to retrieve all prepaid packages currently assigned to a subscriber. Returns each package's allowance (data/voice/SMS), consumed usage, expiry date, status, and packageId. Always call this before any package modification tool (`modify_package_limits`, `modify_package_expiry`, `modify_package_status`, `delete_subscriber_package`) to confirm the correct packageId and current state. Params: `iccid` (subscriber identifier). Returns: `{ summary, prepaidPackage: [...] }` \u2014 package records with `packageId`, `name`, `status`, `dataLimit`, `dataUsed`, `expirationDate` and a `recurring` flag, behind a server-counted `summary.total` and a `summary.breakdown` over status and recurring. Answer 'how many' from `summary`, not by counting rows. Do NOT use this to browse the product catalog \u2014 use `list_package_templates` for that.",
|
|
1325
1397
|
inputSchema: { iccid: z.string().describe("The subscriber ICCID") },
|
|
1326
1398
|
annotations: { readOnlyHint: true }
|
|
1327
1399
|
},
|
|
@@ -1330,7 +1402,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1330
1402
|
"listSubscriberPrepaidPackages",
|
|
1331
1403
|
TOOL_SCOPES["list_subscriber_packages"],
|
|
1332
1404
|
ctx,
|
|
1333
|
-
async ({ iccid }, token2) => ocsCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid })
|
|
1405
|
+
async ({ iccid }, token2) => ocsCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }, "packages")
|
|
1334
1406
|
)
|
|
1335
1407
|
);
|
|
1336
1408
|
server2.registerTool(
|
|
@@ -1563,10 +1635,11 @@ function registerAllTools(server2, ctx) {
|
|
|
1563
1635
|
"list_package_templates",
|
|
1564
1636
|
{
|
|
1565
1637
|
title: "List Package Templates",
|
|
1566
|
-
description: "Use this to browse the product catalog of prepaid package templates available for assignment. Returns each template's name, data/voice/SMS limits, pricing, validity period, location zone, and recurring configuration. Call this before `assign_package` or `assign_recurring_package` to obtain valid `packageTemplateId` values. Params: `
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1638
|
+
description: "Use this to browse the product catalog of prepaid package templates available for assignment. Returns each template's name, data/voice/SMS limits, pricing, validity period, location zone, and recurring configuration. Call this before `assign_package` or `assign_recurring_package` to obtain valid `packageTemplateId` values. Params: none \u2014 returns the whole catalog for the token owner's reseller. Returns: `{ summary, template: [...] }`. `summary.total` is the number of templates, counted by the server over the complete catalog, and `summary.breakdown` counts the templates per `recurring` value and per location zone. Answer any 'how many' question from `summary` \u2014 it is authoritative, and counting the rows yourself gets it wrong. Each row carries `prepaidpackagetemplateid`, `prepaidpackagetemplatename`, `databyte` (bytes), `cost`, `perioddays`, `locationzoneid`, `rdbLocationZones.locationzonename` and `recurring`. Do NOT use this to list packages assigned to a specific subscriber \u2014 use `list_subscriber_packages`.",
|
|
1639
|
+
// No `accountId` — see the matching note in apps/mcp-server/src/tools.ts.
|
|
1640
|
+
// OCS rejected every call that carried it, and it is not a rename of any
|
|
1641
|
+
// of the five properties OCS does accept.
|
|
1642
|
+
inputSchema: {},
|
|
1570
1643
|
annotations: { readOnlyHint: true }
|
|
1571
1644
|
},
|
|
1572
1645
|
wrapHandler(
|
|
@@ -1574,11 +1647,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1574
1647
|
"listPrepaidPackageTemplate",
|
|
1575
1648
|
TOOL_SCOPES["list_package_templates"],
|
|
1576
1649
|
ctx,
|
|
1577
|
-
async (
|
|
1578
|
-
const params = {};
|
|
1579
|
-
if (accountId !== void 0) params.accountId = accountId;
|
|
1580
|
-
return ocsCall(ctx.env, token2, "listPrepaidPackageTemplate", params);
|
|
1581
|
-
}
|
|
1650
|
+
async (_args, token2) => ocsCall(ctx.env, token2, "listPrepaidPackageTemplate", {}, "templates")
|
|
1582
1651
|
)
|
|
1583
1652
|
);
|
|
1584
1653
|
server2.registerTool(
|
|
@@ -1831,7 +1900,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1831
1900
|
"get_tariff",
|
|
1832
1901
|
{
|
|
1833
1902
|
title: "Get Customer Tariff",
|
|
1834
|
-
description: `Use this to retrieve the tariff table for a reseller: per-country, per-operator wholesale data/voice/SMS rates. Useful for cost analysis, margin calculations, and identifying expensive roaming countries before steering decisions. Params: \`resellerId\` (integer, optional \u2014 omit to use the token owner's reseller); \`country\` (optional, ISO-3166 alpha-2 such as "nl" OR a full country name such as "Netherlands", case-insensitive); \`trafficType\` (optional, "data" | "voice" | "sms" \u2014 keeps only rules whose corresponding rate is greater than zero); \`verbose\` (optional boolean, default false \u2014 returns the full untouched OCS rule shape, capped at ${TARIFF_VERBOSE_MAX_RULES} rows, so combine it with the filters). Returns BY DEFAULT a projected result: \`totalRules\`, \`matchedRules\`, \`returnedRules\`, \`truncated\`, a hoisted \`currency\`, a \`fields\` legend and \`rules[]\` of short-keyed rows (\`iso\`, \`op\`, \`data\`, \`moCall\`, \`mtCall\`, \`moSms\`, \`mtSms\`, \`active\`). Zero rates and inactive flags are omitted per row. Nested operator detail (mccMncs, tadigs, continent, countryCode, utcOffset), the sponsor object, plan ids and the validity/discount flags are dropped unless \`verbose\` is true. Rows are capped at ${TARIFF_MAX_RULES}; when the cap bites, \`truncated\` is true and \`note\` says so explicitly and names the filter params \u2014 the result is never silently shortened. Filtering happens server-side on the complete OCS table, so a filtered call sees every matching rule. Response key in OCS is \`listTariffRule\`. Do NOT use this to assign a pricing plan to a subscriber \u2014 use \`modify_subscriber_mobile_plan\`. This shows the RESELLER's wholesale cost, not what end-users are charged.`,
|
|
1903
|
+
description: `Use this to retrieve the tariff table for a reseller: per-country, per-operator wholesale data/voice/SMS rates. Useful for cost analysis, margin calculations, and identifying expensive roaming countries before steering decisions. Params: \`resellerId\` (integer, optional \u2014 omit to use the token owner's reseller); \`country\` (optional, ISO-3166 alpha-2 such as "nl" OR a full country name such as "Netherlands", case-insensitive); \`trafficType\` (optional, "data" | "voice" | "sms" \u2014 keeps only rules whose corresponding rate is greater than zero); \`verbose\` (optional boolean, default false \u2014 returns the full untouched OCS rule shape, capped at ${TARIFF_VERBOSE_MAX_RULES} rows, so combine it with the filters). Returns BY DEFAULT a projected result: \`totalRules\`, \`matchedRules\`, \`returnedRules\`, \`truncated\`, a hoisted \`currency\`, a \`fields\` legend and \`rules[]\` of short-keyed rows (\`iso\`, \`op\`, \`data\`, \`moCall\`, \`mtCall\`, \`moSms\`, \`mtSms\`, \`active\`). Zero rates and inactive flags are omitted per row. Nested operator detail (mccMncs, tadigs, continent, countryCode, utcOffset), the sponsor object, plan ids and the validity/discount flags are dropped unless \`verbose\` is true. Rows are capped at ${TARIFF_MAX_RULES}; when the cap bites, \`truncated\` is true and \`note\` says so explicitly and names the filter params \u2014 the result is never silently shortened. Filtering happens server-side on the complete OCS table, so a filtered call sees every matching rule. Response key in OCS is \`listTariffRule\`. This is the slowest tool on the platform: the OCS table is ~3.4 MB and takes 3 s in a fast window and up to ~2 minutes in a slow one, so the call carries a 180 s budget. Expect to wait, and do not call it in a loop. Do NOT use this to assign a pricing plan to a subscriber \u2014 use \`modify_subscriber_mobile_plan\`. This shows the RESELLER's wholesale cost, not what end-users are charged.`,
|
|
1835
1904
|
inputSchema: {
|
|
1836
1905
|
resellerId: z.number().optional().describe("Reseller ID (omit to use token owner's reseller)"),
|
|
1837
1906
|
country: z.string().optional().describe(
|
|
@@ -1957,8 +2026,9 @@ var ocs_methods_default = {
|
|
|
1957
2026
|
scope: "read",
|
|
1958
2027
|
description: "Retrieve reseller details",
|
|
1959
2028
|
params: {
|
|
1960
|
-
resellerId: { type: "number", required: false }
|
|
2029
|
+
resellerId: { type: "number", required: false, ocs_field: "id" }
|
|
1961
2030
|
},
|
|
2031
|
+
params_note: "`resellerId` is the MCP-facing name. OCS getResellerInfo accepts exactly one property, `id`; @carrier/ocs-client translates it at the serialisation chokepoint (OCS_PARAM_RENAMES).",
|
|
1962
2032
|
response: {},
|
|
1963
2033
|
annotations: "readOnlyHint",
|
|
1964
2034
|
verified_against_server: true,
|
|
@@ -2363,9 +2433,8 @@ var ocs_methods_default = {
|
|
|
2363
2433
|
category: "templates",
|
|
2364
2434
|
scope: "read",
|
|
2365
2435
|
description: "List all prepaid package templates",
|
|
2366
|
-
params: {
|
|
2367
|
-
|
|
2368
|
-
},
|
|
2436
|
+
params: {},
|
|
2437
|
+
params_note: "No account filter. OCS listPrepaidPackageTemplate accepts only locationZoneId, templateId, destinationListId, resellerId, sponsorId. The former `accountId` param was rejected by OCS on every call and is not a rename of any of those five: accounts are a different id space from resellers and sponsors.",
|
|
2369
2438
|
response: {},
|
|
2370
2439
|
annotations: "readOnlyHint",
|
|
2371
2440
|
verified_against_server: true,
|
|
@@ -3499,26 +3568,9 @@ async function fetchActiveSubscribers(env, token2, accountId, resellerId) {
|
|
|
3499
3568
|
{ accountId: acctId }
|
|
3500
3569
|
);
|
|
3501
3570
|
if (subResult.error) return { data: null, error: subResult.error };
|
|
3502
|
-
|
|
3503
|
-
const list = Array.isArray(raw) ? raw : raw?.subscriberList ?? [];
|
|
3504
|
-
aggregated.push(...list);
|
|
3571
|
+
aggregated.push(...subscriberRows(subResult.data));
|
|
3505
3572
|
}
|
|
3506
|
-
|
|
3507
|
-
return { data: active, error: null };
|
|
3508
|
-
}
|
|
3509
|
-
function formatBytes(bytes) {
|
|
3510
|
-
if (bytes === 0) return "0 B";
|
|
3511
|
-
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
3512
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
3513
|
-
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
|
|
3514
|
-
}
|
|
3515
|
-
function daysUntil(dateStr) {
|
|
3516
|
-
const now = /* @__PURE__ */ new Date();
|
|
3517
|
-
const target = new Date(dateStr);
|
|
3518
|
-
return Math.ceil((target.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24));
|
|
3519
|
-
}
|
|
3520
|
-
function toISODate(d) {
|
|
3521
|
-
return d.toISOString().split("T")[0];
|
|
3573
|
+
return { data: filterActiveSubscribers(aggregated), error: null };
|
|
3522
3574
|
}
|
|
3523
3575
|
function result(text, isError = false) {
|
|
3524
3576
|
return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
|
|
@@ -3598,10 +3650,10 @@ function registerIntelligenceTools(server2, ctx) {
|
|
|
3598
3650
|
}
|
|
3599
3651
|
}
|
|
3600
3652
|
const now = /* @__PURE__ */ new Date();
|
|
3601
|
-
const
|
|
3653
|
+
const eventWindow = lastNDaysPeriod(3, now);
|
|
3602
3654
|
const events = await safeCall(ctx.env, token2, "subscriberNetworkEventsOverPeriod", {
|
|
3603
3655
|
subscriber: { iccid },
|
|
3604
|
-
period:
|
|
3656
|
+
period: eventWindow
|
|
3605
3657
|
});
|
|
3606
3658
|
if (events.data && Array.isArray(events.data)) {
|
|
3607
3659
|
if (events.data.length === 0) {
|
|
@@ -3778,11 +3830,11 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3778
3830
|
}, async ({ iccid }) => {
|
|
3779
3831
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
3780
3832
|
const now = /* @__PURE__ */ new Date();
|
|
3781
|
-
const
|
|
3833
|
+
const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
|
|
3782
3834
|
const [usageResult, pkgResult] = await Promise.all([
|
|
3783
3835
|
safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
|
|
3784
3836
|
subscriber: { iccid },
|
|
3785
|
-
period:
|
|
3837
|
+
period: usagePeriod
|
|
3786
3838
|
}),
|
|
3787
3839
|
safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid })
|
|
3788
3840
|
]);
|
|
@@ -3790,13 +3842,8 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3790
3842
|
const sections = [`# Usage Anomaly Report: ${iccid}
|
|
3791
3843
|
`];
|
|
3792
3844
|
const anomalies = [];
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
for (const entry of usageResult.data) {
|
|
3796
|
-
const bytes = Number(entry.dataBytes ?? entry.dataVolume ?? entry.totalData ?? 0);
|
|
3797
|
-
const date = String(entry.date ?? entry.day ?? "?");
|
|
3798
|
-
dailyData.push({ date, bytes });
|
|
3799
|
-
}
|
|
3845
|
+
const dailyData = extractDailyUsage(usageResult.data);
|
|
3846
|
+
if (dailyData.length > 0) {
|
|
3800
3847
|
if (dailyData.length >= 2) {
|
|
3801
3848
|
const volumes = dailyData.map((d) => d.bytes);
|
|
3802
3849
|
const mean = volumes.reduce((a, b) => a + b, 0) / volumes.length;
|
|
@@ -3864,24 +3911,21 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3864
3911
|
}, async ({ iccid }) => {
|
|
3865
3912
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
3866
3913
|
const now = /* @__PURE__ */ new Date();
|
|
3867
|
-
const
|
|
3914
|
+
const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
|
|
3868
3915
|
const [usageResult, pkgResult, templatesResult] = await Promise.all([
|
|
3869
3916
|
safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
|
|
3870
3917
|
subscriber: { iccid },
|
|
3871
|
-
period:
|
|
3918
|
+
period: usagePeriod
|
|
3872
3919
|
}),
|
|
3873
3920
|
safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }),
|
|
3874
3921
|
safeCall(ctx.env, token2, "listPrepaidPackageTemplate", {})
|
|
3875
3922
|
]);
|
|
3876
3923
|
const sections = [`# Package Optimization: ${iccid}
|
|
3877
3924
|
`];
|
|
3925
|
+
const optimizeRows = extractDailyUsage(usageResult.data);
|
|
3878
3926
|
let avgDailyData = 0;
|
|
3879
|
-
if (
|
|
3880
|
-
|
|
3881
|
-
(sum, e) => sum + Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0),
|
|
3882
|
-
0
|
|
3883
|
-
);
|
|
3884
|
-
avgDailyData = totalData / usageResult.data.length;
|
|
3927
|
+
if (optimizeRows.length > 0) {
|
|
3928
|
+
avgDailyData = optimizeRows.reduce((sum, r) => sum + r.bytes, 0) / optimizeRows.length;
|
|
3885
3929
|
sections.push(`## Current Usage Pattern`);
|
|
3886
3930
|
sections.push(`- Average daily data: ${formatBytes(avgDailyData)}`);
|
|
3887
3931
|
sections.push(`- Projected monthly: ${formatBytes(avgDailyData * 30)}`);
|
|
@@ -3952,22 +3996,21 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3952
3996
|
}, async ({ iccid }) => {
|
|
3953
3997
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
3954
3998
|
const now = /* @__PURE__ */ new Date();
|
|
3955
|
-
const
|
|
3999
|
+
const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
|
|
3956
4000
|
const [subResult, usageResult, pkgResult, activeResult] = await Promise.all([
|
|
3957
4001
|
safeCall(ctx.env, token2, "getSingleSubscriber", { iccid }),
|
|
3958
4002
|
safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
|
|
3959
4003
|
subscriber: { iccid },
|
|
3960
|
-
period:
|
|
4004
|
+
period: usagePeriod
|
|
3961
4005
|
}),
|
|
3962
4006
|
safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }),
|
|
3963
4007
|
safeCall(ctx.env, token2, "getSubscriberActivePeriod", { iccid })
|
|
3964
4008
|
]);
|
|
3965
4009
|
let riskScore = 0;
|
|
3966
4010
|
const factors = [];
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
);
|
|
4011
|
+
const churnRows = extractDailyUsage(usageResult.data);
|
|
4012
|
+
if (churnRows.length >= 3) {
|
|
4013
|
+
const volumes = churnRows.map((r) => r.bytes);
|
|
3971
4014
|
const firstHalf = volumes.slice(0, Math.floor(volumes.length / 2));
|
|
3972
4015
|
const secondHalf = volumes.slice(Math.floor(volumes.length / 2));
|
|
3973
4016
|
const avgFirst = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length;
|
|
@@ -3984,7 +4027,7 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3984
4027
|
factors.push({ factor: "Moderately declining usage", impact, detail: `Usage dropped ${Math.abs(trend * 100).toFixed(0)}%` });
|
|
3985
4028
|
}
|
|
3986
4029
|
}
|
|
3987
|
-
} else if (
|
|
4030
|
+
} else if (churnRows.length === 0) {
|
|
3988
4031
|
riskScore += 25;
|
|
3989
4032
|
factors.push({ factor: "No recent usage", impact: 25, detail: "Zero data activity in last 7 days" });
|
|
3990
4033
|
}
|
|
@@ -4279,7 +4322,7 @@ ${networksSorted.length} different networks in ${country} \u2014 possible steeri
|
|
|
4279
4322
|
const sections = [`# High Cost Subscriber Report
|
|
4280
4323
|
`];
|
|
4281
4324
|
const now = /* @__PURE__ */ new Date();
|
|
4282
|
-
const
|
|
4325
|
+
const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
|
|
4283
4326
|
const highCostSubs = [];
|
|
4284
4327
|
const batchSize = 5;
|
|
4285
4328
|
const subs = subsResult.data.slice(0, sampleSize);
|
|
@@ -4291,19 +4334,13 @@ ${networksSorted.length} different networks in ${country} \u2014 possible steeri
|
|
|
4291
4334
|
const [usage, pkgs, loc] = await Promise.all([
|
|
4292
4335
|
safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
|
|
4293
4336
|
subscriber: { iccid },
|
|
4294
|
-
period:
|
|
4337
|
+
period: usagePeriod
|
|
4295
4338
|
}),
|
|
4296
4339
|
safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }),
|
|
4297
4340
|
safeCall(ctx.env, token2, "getSubscriberLocation", { iccid })
|
|
4298
4341
|
]);
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
const totalBytes = usage.data.reduce(
|
|
4302
|
-
(sum, e) => sum + Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0),
|
|
4303
|
-
0
|
|
4304
|
-
);
|
|
4305
|
-
dailyAvgBytes = totalBytes / usage.data.length;
|
|
4306
|
-
}
|
|
4342
|
+
const dailyRows = extractDailyUsage(usage.data);
|
|
4343
|
+
const dailyAvgBytes = dailyRows.length > 0 ? dailyRows.reduce((sum, r) => sum + r.bytes, 0) / dailyRows.length : 0;
|
|
4307
4344
|
if (pkgs.data && Array.isArray(pkgs.data)) {
|
|
4308
4345
|
const activePkg = pkgs.data.find(
|
|
4309
4346
|
(p) => String(p.status ?? "").toUpperCase() === "ACTIVE"
|
|
@@ -4378,7 +4415,7 @@ ${networksSorted.length} different networks in ${country} \u2014 possible steeri
|
|
|
4378
4415
|
});
|
|
4379
4416
|
server2.registerTool("detect_country_entry", {
|
|
4380
4417
|
title: "Detect Country Entry",
|
|
4381
|
-
description: "Detects when a subscriber has entered a new country by reading networkInfo.lastMcc from getSingleSubscriber (one cheap OCS call \u2014 avoids the per-call cost of getSubscriberLocationByCellId). Resolves MCC \u2192 ISO 3166-1 alpha-2 and optionally diffs against a caller-supplied expectedCountry to return countryChanged. Designed for downstream country-entry upsell workflows (e.g. mango.talk SMS/push offers). COST NOTE: This tool makes exactly one OCS call per invocation. Consumers running polling crons MUST enforce their own rate floor \u2014 this layer provides no throttle.",
|
|
4418
|
+
description: "Detects when a subscriber has entered a new country by reading networkInfo.lastMcc from getSingleSubscriber (one cheap OCS call \u2014 avoids the per-call cost of getSubscriberLocationByCellId). Resolves MCC \u2192 ISO 3166-1 alpha-2 and optionally diffs against a caller-supplied expectedCountry to return countryChanged. Designed for downstream country-entry upsell workflows (e.g. mango.talk SMS/push offers). NOT AN AI TOOL: this is a deterministic MCC lookup and returns no model-written analysis, unlike the eight intelligence composites. COST NOTE: This tool makes exactly one OCS call per invocation. Consumers running polling crons MUST enforce their own rate floor \u2014 this layer provides no throttle.",
|
|
4382
4419
|
inputSchema: {
|
|
4383
4420
|
subscriber: z2.union([
|
|
4384
4421
|
z2.object({ subscriberId: z2.number() }).describe("Internal subscriber ID"),
|
|
@@ -6925,8 +6962,11 @@ var WALLET_TOOL_SCOPES = {
|
|
|
6925
6962
|
wallet_topup_checkout: "write",
|
|
6926
6963
|
wallet_auto_topup: "write"
|
|
6927
6964
|
};
|
|
6928
|
-
function
|
|
6929
|
-
return {
|
|
6965
|
+
function okStructured(payload) {
|
|
6966
|
+
return {
|
|
6967
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
6968
|
+
structuredContent: payload
|
|
6969
|
+
};
|
|
6930
6970
|
}
|
|
6931
6971
|
function err(text) {
|
|
6932
6972
|
return { isError: true, content: [{ type: "text", text }] };
|
|
@@ -6940,6 +6980,17 @@ function registerWalletTools(server2, ctx) {
|
|
|
6940
6980
|
title: "Wallet Balance",
|
|
6941
6981
|
description: "Read the caller organisation's Carrier prepaid wallet: current balance and auto-top-up settings. The wallet is what Carrier bills your own eSIM spend against \u2014 it is not an OCS balance and not your MCP plan credits. Params: none (the organisation is taken from the session). Returns: { org_id, balance_eur_cents, auto_topup_enabled, auto_topup_threshold_cents, auto_topup_pack_cents } \u2014 all amounts in EUR cents, not euros. Do NOT use this for a reseller account balance in OCS \u2014 use `get_reseller_info` or `list_reseller_accounts`. Do NOT use this for a subscriber's OCS balance \u2014 use `get_subscriber`. Do NOT use this for MCP plan credits \u2014 use `credit_balance`. Do NOT use this for money held at Stripe \u2014 use `stripe_connect_balance`.",
|
|
6942
6982
|
inputSchema: {},
|
|
6983
|
+
// Derived from the object literal this handler builds below, not from the
|
|
6984
|
+
// description. Every field is unconditional on the success path and comes
|
|
6985
|
+
// straight off the Wallet record (wallet-client.ts:31-34), so all five are
|
|
6986
|
+
// required rather than optional.
|
|
6987
|
+
outputSchema: {
|
|
6988
|
+
org_id: z7.string().describe("Carrier organisation the wallet belongs to"),
|
|
6989
|
+
balance_eur_cents: z7.number().int().describe("Current balance in EUR cents"),
|
|
6990
|
+
auto_topup_enabled: z7.boolean(),
|
|
6991
|
+
auto_topup_threshold_cents: z7.number().int().describe("Balance at or below which an auto top-up fires, EUR cents"),
|
|
6992
|
+
auto_topup_pack_cents: z7.number().int().describe("Amount an auto top-up charges, EUR cents")
|
|
6993
|
+
},
|
|
6943
6994
|
annotations: annotationsFor("wallet_balance", "read")
|
|
6944
6995
|
},
|
|
6945
6996
|
async () => {
|
|
@@ -6958,15 +7009,13 @@ function registerWalletTools(server2, ctx) {
|
|
|
6958
7009
|
sub: props2.sub,
|
|
6959
7010
|
reseller_id: props2.reseller_id
|
|
6960
7011
|
});
|
|
6961
|
-
return
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
})
|
|
6969
|
-
);
|
|
7012
|
+
return okStructured({
|
|
7013
|
+
org_id: orgId,
|
|
7014
|
+
balance_eur_cents: wallet.balance_eur_cents,
|
|
7015
|
+
auto_topup_enabled: wallet.auto_topup_enabled,
|
|
7016
|
+
auto_topup_threshold_cents: wallet.auto_topup_threshold_cents,
|
|
7017
|
+
auto_topup_pack_cents: wallet.auto_topup_pack_cents
|
|
7018
|
+
});
|
|
6970
7019
|
} catch (e) {
|
|
6971
7020
|
if (e instanceof WalletClientError) return err(`Wallet error: ${e.message}`);
|
|
6972
7021
|
Sentry2.captureException(e);
|
|
@@ -6984,6 +7033,31 @@ function registerWalletTools(server2, ctx) {
|
|
|
6984
7033
|
"Pack id (pack_500/pack_1000/pack_2500/pack_5000) or exact EUR-cents amount. Omit to list packs."
|
|
6985
7034
|
)
|
|
6986
7035
|
},
|
|
7036
|
+
/**
|
|
7037
|
+
* This tool really does return two different shapes, so the schema says
|
|
7038
|
+
* so rather than pretending otherwise. `pack` omitted returns the
|
|
7039
|
+
* catalogue arm (`packs`); `pack` supplied returns the session arm. An
|
|
7040
|
+
* `outputSchema` is an object shape, not a union type, so the two arms
|
|
7041
|
+
* are expressed as mutually exclusive optional groups and the pairing is
|
|
7042
|
+
* documented here. Marking either arm required would break the other.
|
|
7043
|
+
*/
|
|
7044
|
+
outputSchema: {
|
|
7045
|
+
packs: z7.array(
|
|
7046
|
+
z7.object({
|
|
7047
|
+
id: z7.string(),
|
|
7048
|
+
eur_cents: z7.number().int(),
|
|
7049
|
+
bonus_pct: z7.number(),
|
|
7050
|
+
bonus_eur_cents: z7.number().int(),
|
|
7051
|
+
credited_eur_cents: z7.number().int()
|
|
7052
|
+
})
|
|
7053
|
+
).optional().describe("Catalogue arm \u2014 present only when `pack` was omitted."),
|
|
7054
|
+
org_id: z7.string().optional().describe("Session arm."),
|
|
7055
|
+
pack_id: z7.string().optional().describe("Session arm."),
|
|
7056
|
+
pack_eur_cents: z7.number().int().optional().describe("Session arm."),
|
|
7057
|
+
bonus_eur_cents: z7.number().int().optional().describe("Session arm."),
|
|
7058
|
+
checkout_session_id: z7.string().optional().describe("Session arm."),
|
|
7059
|
+
checkout_url: z7.string().nullable().optional().describe("Session arm. Stripe may return null for a session URL.")
|
|
7060
|
+
},
|
|
6987
7061
|
annotations: annotationsFor("wallet_topup_checkout", "write")
|
|
6988
7062
|
},
|
|
6989
7063
|
async ({ pack }) => {
|
|
@@ -6992,17 +7066,15 @@ function registerWalletTools(server2, ctx) {
|
|
|
6992
7066
|
const orgId = defaultOrgId;
|
|
6993
7067
|
const start = Date.now();
|
|
6994
7068
|
if (!pack) {
|
|
6995
|
-
return
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
})
|
|
7005
|
-
);
|
|
7069
|
+
return okStructured({
|
|
7070
|
+
packs: PACKS.map((p) => ({
|
|
7071
|
+
id: p.id,
|
|
7072
|
+
eur_cents: p.eurCents,
|
|
7073
|
+
bonus_pct: p.bonusPct,
|
|
7074
|
+
bonus_eur_cents: bonusCentsFor(p),
|
|
7075
|
+
credited_eur_cents: p.eurCents + bonusCentsFor(p)
|
|
7076
|
+
}))
|
|
7077
|
+
});
|
|
7006
7078
|
}
|
|
7007
7079
|
const selected = resolvePack(pack);
|
|
7008
7080
|
if (!selected) {
|
|
@@ -7021,16 +7093,14 @@ function registerWalletTools(server2, ctx) {
|
|
|
7021
7093
|
sub: props2.sub,
|
|
7022
7094
|
reseller_id: props2.reseller_id
|
|
7023
7095
|
});
|
|
7024
|
-
return
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
})
|
|
7033
|
-
);
|
|
7096
|
+
return okStructured({
|
|
7097
|
+
org_id: orgId,
|
|
7098
|
+
pack_id: selected.id,
|
|
7099
|
+
pack_eur_cents: selected.eurCents,
|
|
7100
|
+
bonus_eur_cents: bonusCentsFor(selected),
|
|
7101
|
+
checkout_session_id: session.id,
|
|
7102
|
+
checkout_url: session.url
|
|
7103
|
+
});
|
|
7034
7104
|
} catch (e) {
|
|
7035
7105
|
Sentry2.captureException(e);
|
|
7036
7106
|
return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7045,6 +7115,26 @@ function registerWalletTools(server2, ctx) {
|
|
|
7045
7115
|
inputSchema: {
|
|
7046
7116
|
pack_cents: z7.number().int().positive().optional().describe("Override pack amount in EUR cents (must match a catalog pack).")
|
|
7047
7117
|
},
|
|
7118
|
+
/**
|
|
7119
|
+
* Only the charged arm is described here — `charged: false` returns
|
|
7120
|
+
* through `err()`, and the SDK skips output validation on an `isError`
|
|
7121
|
+
* result.
|
|
7122
|
+
*
|
|
7123
|
+
* `balance_after_cents` is optional and the description above is wrong to
|
|
7124
|
+
* promise it. runAutoTopup has a second success arm (wallet-tools.ts:337)
|
|
7125
|
+
* where the card was charged but crediting Atlas failed: it carries
|
|
7126
|
+
* `paymentIntentId` and a `reason`, and no balance. Requiring the field
|
|
7127
|
+
* would turn that arm — a real charge the caller must be told about —
|
|
7128
|
+
* into a validation error.
|
|
7129
|
+
*/
|
|
7130
|
+
outputSchema: {
|
|
7131
|
+
org_id: z7.string(),
|
|
7132
|
+
charged: z7.literal(true),
|
|
7133
|
+
credited_eur_cents: z7.number().int(),
|
|
7134
|
+
balance_after_cents: z7.number().int().optional().describe("Absent when the charge succeeded but crediting is pending."),
|
|
7135
|
+
payment_intent_id: z7.string().optional(),
|
|
7136
|
+
note: z7.string().optional().describe("Present when the charge landed but the credit is still pending.")
|
|
7137
|
+
},
|
|
7048
7138
|
annotations: annotationsFor("wallet_auto_topup", "write")
|
|
7049
7139
|
},
|
|
7050
7140
|
async ({ pack_cents }) => {
|
|
@@ -7068,16 +7158,18 @@ function registerWalletTools(server2, ctx) {
|
|
|
7068
7158
|
JSON.stringify({ org_id: orgId, charged: false, reason: result2.reason })
|
|
7069
7159
|
);
|
|
7070
7160
|
}
|
|
7071
|
-
return
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
}
|
|
7080
|
-
|
|
7161
|
+
return okStructured({
|
|
7162
|
+
org_id: orgId,
|
|
7163
|
+
charged: true,
|
|
7164
|
+
credited_eur_cents: result2.creditedCents,
|
|
7165
|
+
// Spread rather than assign, so the key is absent instead of set to
|
|
7166
|
+
// `undefined`. JSON.stringify drops an undefined value anyway, so
|
|
7167
|
+
// assigning it would make `content` and `structuredContent` disagree
|
|
7168
|
+
// about whether the field is there.
|
|
7169
|
+
...result2.balanceAfterCents !== void 0 ? { balance_after_cents: result2.balanceAfterCents } : {},
|
|
7170
|
+
...result2.paymentIntentId !== void 0 ? { payment_intent_id: result2.paymentIntentId } : {},
|
|
7171
|
+
...result2.reason ? { note: result2.reason } : {}
|
|
7172
|
+
});
|
|
7081
7173
|
} catch (e) {
|
|
7082
7174
|
Sentry2.captureException(e);
|
|
7083
7175
|
return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7103,25 +7195,30 @@ async function issueConfirmToken(env, sub, toolName) {
|
|
|
7103
7195
|
await env.OAUTH_KV.put(key, token2, { expirationTtl: 300 });
|
|
7104
7196
|
return token2;
|
|
7105
7197
|
}
|
|
7106
|
-
function
|
|
7198
|
+
function ok(text) {
|
|
7107
7199
|
return { content: [{ type: "text", text }] };
|
|
7108
7200
|
}
|
|
7109
7201
|
function err2(text) {
|
|
7110
7202
|
return { isError: true, content: [{ type: "text", text }] };
|
|
7111
7203
|
}
|
|
7112
7204
|
async function stripeGet(stripeKey, path, connectedAccountId) {
|
|
7113
|
-
const headers = {
|
|
7205
|
+
const headers = {
|
|
7206
|
+
Authorization: `Bearer ${stripeKey}`,
|
|
7207
|
+
"Stripe-Version": "2025-08-27.basil"
|
|
7208
|
+
};
|
|
7114
7209
|
if (connectedAccountId) headers["Stripe-Account"] = connectedAccountId;
|
|
7115
7210
|
const res = await fetch(`https://api.stripe.com${path}`, { headers });
|
|
7116
7211
|
const data = await res.json();
|
|
7117
7212
|
return { ok: res.ok, status: res.status, data };
|
|
7118
7213
|
}
|
|
7119
|
-
async function stripePost(stripeKey, path, params, connectedAccountId) {
|
|
7214
|
+
async function stripePost(stripeKey, path, params, connectedAccountId, idempotencyKey) {
|
|
7120
7215
|
const headers = {
|
|
7121
7216
|
Authorization: `Bearer ${stripeKey}`,
|
|
7217
|
+
"Stripe-Version": "2025-08-27.basil",
|
|
7122
7218
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
7123
7219
|
};
|
|
7124
7220
|
if (connectedAccountId) headers["Stripe-Account"] = connectedAccountId;
|
|
7221
|
+
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
|
|
7125
7222
|
const res = await fetch(`https://api.stripe.com${path}`, {
|
|
7126
7223
|
method: "POST",
|
|
7127
7224
|
headers,
|
|
@@ -7130,6 +7227,9 @@ async function stripePost(stripeKey, path, params, connectedAccountId) {
|
|
|
7130
7227
|
const data = await res.json();
|
|
7131
7228
|
return { ok: res.ok, status: res.status, data };
|
|
7132
7229
|
}
|
|
7230
|
+
function refundIdempotencyKey(sub, charge, amount, reason) {
|
|
7231
|
+
return `carrier-refund:${sub}:${charge}:${amount ?? "full"}:${reason ?? "none"}`;
|
|
7232
|
+
}
|
|
7133
7233
|
var STRIPE_CONNECT_TOOL_SCOPES = {
|
|
7134
7234
|
stripe_connect_status: "read",
|
|
7135
7235
|
stripe_connect_payouts: "read",
|
|
@@ -7177,7 +7277,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7177
7277
|
if (!stripeKey) return err2("Stripe not configured");
|
|
7178
7278
|
const accountId = await env.CARRIER_USERS.get(opKvKey);
|
|
7179
7279
|
if (!accountId) {
|
|
7180
|
-
return
|
|
7280
|
+
return ok(JSON.stringify({ status: "not_connected", operator_id: opId }));
|
|
7181
7281
|
}
|
|
7182
7282
|
const result2 = await stripeGet(stripeKey, `/v1/accounts/${accountId}`);
|
|
7183
7283
|
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
@@ -7192,7 +7292,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7192
7292
|
sub: props2.sub,
|
|
7193
7293
|
reseller_id: props2.reseller_id
|
|
7194
7294
|
});
|
|
7195
|
-
return
|
|
7295
|
+
return ok(JSON.stringify({
|
|
7196
7296
|
status,
|
|
7197
7297
|
operator_id: opId,
|
|
7198
7298
|
account_id: accountId,
|
|
@@ -7243,7 +7343,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7243
7343
|
sub: props2.sub,
|
|
7244
7344
|
reseller_id: props2.reseller_id
|
|
7245
7345
|
});
|
|
7246
|
-
return
|
|
7346
|
+
return ok(JSON.stringify({ payouts: result2.data.data, has_more: result2.data.has_more }));
|
|
7247
7347
|
} catch (e) {
|
|
7248
7348
|
Sentry3.captureException(e);
|
|
7249
7349
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7278,7 +7378,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7278
7378
|
sub: props2.sub,
|
|
7279
7379
|
reseller_id: props2.reseller_id
|
|
7280
7380
|
});
|
|
7281
|
-
return
|
|
7381
|
+
return ok(JSON.stringify({
|
|
7282
7382
|
account_id: accountId,
|
|
7283
7383
|
available: result2.data.available ?? [],
|
|
7284
7384
|
pending: result2.data.pending ?? []
|
|
@@ -7309,7 +7409,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7309
7409
|
const toolName = "stripe_connect_refund";
|
|
7310
7410
|
if (!confirm_token) {
|
|
7311
7411
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
7312
|
-
return
|
|
7412
|
+
return ok(
|
|
7313
7413
|
`HARD_BLOCK: Refund ${amount_cents ? `${amount_cents} cents on` : "(full) on"} charge ${charge_id} requires confirmation.
|
|
7314
7414
|
confirm_token: ${token2}
|
|
7315
7415
|
Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes.`
|
|
@@ -7327,7 +7427,13 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7327
7427
|
const params = { charge: charge_id };
|
|
7328
7428
|
if (amount_cents) params.amount = String(amount_cents);
|
|
7329
7429
|
if (reason) params.reason = reason;
|
|
7330
|
-
const result2 = await stripePost(
|
|
7430
|
+
const result2 = await stripePost(
|
|
7431
|
+
stripeKey,
|
|
7432
|
+
"/v1/refunds",
|
|
7433
|
+
params,
|
|
7434
|
+
accountId,
|
|
7435
|
+
refundIdempotencyKey(props2.sub, charge_id, amount_cents, reason)
|
|
7436
|
+
);
|
|
7331
7437
|
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
7332
7438
|
writeAudit(env, {
|
|
7333
7439
|
tool_name: toolName,
|
|
@@ -7338,7 +7444,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7338
7444
|
sub: props2.sub,
|
|
7339
7445
|
reseller_id: props2.reseller_id
|
|
7340
7446
|
});
|
|
7341
|
-
return
|
|
7447
|
+
return ok(`Refund issued: ${result2.data.id} \u2014 status: ${result2.data.status}`);
|
|
7342
7448
|
} catch (e) {
|
|
7343
7449
|
Sentry3.captureException(e);
|
|
7344
7450
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7378,7 +7484,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7378
7484
|
sub: props2.sub,
|
|
7379
7485
|
reseller_id: props2.reseller_id
|
|
7380
7486
|
});
|
|
7381
|
-
return
|
|
7487
|
+
return ok(JSON.stringify({ disputes: result2.data.data, has_more: result2.data.has_more }));
|
|
7382
7488
|
} catch (e) {
|
|
7383
7489
|
Sentry3.captureException(e);
|
|
7384
7490
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7421,7 +7527,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7421
7527
|
sub: props2.sub,
|
|
7422
7528
|
reseller_id: props2.reseller_id
|
|
7423
7529
|
});
|
|
7424
|
-
return
|
|
7530
|
+
return ok(JSON.stringify({ reviews: result2.data.data, has_more: result2.data.has_more }));
|
|
7425
7531
|
} catch (e) {
|
|
7426
7532
|
Sentry3.captureException(e);
|
|
7427
7533
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7451,7 +7557,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7451
7557
|
const start = Date.now();
|
|
7452
7558
|
if (!confirm_token) {
|
|
7453
7559
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
7454
|
-
return
|
|
7560
|
+
return ok(
|
|
7455
7561
|
`HARD_BLOCK: Approving review ${review_id} allows the charge to proceed.
|
|
7456
7562
|
confirm_token: ${token2}
|
|
7457
7563
|
Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
@@ -7477,7 +7583,7 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
|
7477
7583
|
sub: props2.sub,
|
|
7478
7584
|
reseller_id: props2.reseller_id
|
|
7479
7585
|
});
|
|
7480
|
-
return
|
|
7586
|
+
return ok(`Review ${review_id} approved. Charge will proceed.`);
|
|
7481
7587
|
} catch (e) {
|
|
7482
7588
|
Sentry3.captureException(e);
|
|
7483
7589
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7507,7 +7613,7 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
|
7507
7613
|
const start = Date.now();
|
|
7508
7614
|
if (!confirm_token) {
|
|
7509
7615
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
7510
|
-
return
|
|
7616
|
+
return ok(
|
|
7511
7617
|
`HARD_BLOCK: Declining review ${review_id} will close/block the charge.
|
|
7512
7618
|
confirm_token: ${token2}
|
|
7513
7619
|
Expires in 5 minutes.`
|
|
@@ -7533,7 +7639,7 @@ Expires in 5 minutes.`
|
|
|
7533
7639
|
sub: props2.sub,
|
|
7534
7640
|
reseller_id: props2.reseller_id
|
|
7535
7641
|
});
|
|
7536
|
-
return
|
|
7642
|
+
return ok(`Review ${review_id} declined.`);
|
|
7537
7643
|
} catch (e) {
|
|
7538
7644
|
Sentry3.captureException(e);
|
|
7539
7645
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7564,7 +7670,7 @@ Expires in 5 minutes.`
|
|
|
7564
7670
|
const start = Date.now();
|
|
7565
7671
|
if (!confirm_token) {
|
|
7566
7672
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
7567
|
-
return
|
|
7673
|
+
return ok(
|
|
7568
7674
|
`HARD_BLOCK: Adding "${value}" to list ${value_list_id} will affect future charge decisions.
|
|
7569
7675
|
confirm_token: ${token2}
|
|
7570
7676
|
Expires in 5 minutes.`
|
|
@@ -7590,7 +7696,7 @@ Expires in 5 minutes.`
|
|
|
7590
7696
|
sub: props2.sub,
|
|
7591
7697
|
reseller_id: props2.reseller_id
|
|
7592
7698
|
});
|
|
7593
|
-
return
|
|
7699
|
+
return ok(`Added "${value}" to Radar list ${value_list_id}.`);
|
|
7594
7700
|
} catch (e) {
|
|
7595
7701
|
Sentry3.captureException(e);
|
|
7596
7702
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7616,7 +7722,7 @@ Expires in 5 minutes.`
|
|
|
7616
7722
|
"This tool acts on Carrier's platform Stripe account and is restricted to Carrier's own organization."
|
|
7617
7723
|
);
|
|
7618
7724
|
}
|
|
7619
|
-
return
|
|
7725
|
+
return ok(
|
|
7620
7726
|
`Stripe Radar does not expose rule enable/disable via the public API.
|
|
7621
7727
|
To ${enabled ? "enable" : "disable"} rule ${rule_id}:
|
|
7622
7728
|
1. Open https://dashboard.stripe.com/radar/rules
|
|
@@ -7633,9 +7739,9 @@ import { z as z9 } from "zod";
|
|
|
7633
7739
|
var STOREFRONT_LOGO_TOOL_SCOPES = {
|
|
7634
7740
|
generate_storefront_logo: "read"
|
|
7635
7741
|
};
|
|
7636
|
-
var
|
|
7742
|
+
var ok2 = (text) => ({ content: [{ type: "text", text }] });
|
|
7637
7743
|
var err3 = (text) => ({ isError: true, content: [{ type: "text", text }] });
|
|
7638
|
-
function registerStorefrontLogoTools(server2, env = {}) {
|
|
7744
|
+
function registerStorefrontLogoTools(server2, env = {}, props2) {
|
|
7639
7745
|
server2.registerTool(
|
|
7640
7746
|
"generate_storefront_logo",
|
|
7641
7747
|
{
|
|
@@ -7649,6 +7755,8 @@ function registerStorefrontLogoTools(server2, env = {}) {
|
|
|
7649
7755
|
annotations: annotationsFor("generate_storefront_logo", "read")
|
|
7650
7756
|
},
|
|
7651
7757
|
async ({ name, accent, tagline }) => {
|
|
7758
|
+
const denied = denyUnlessScoped(props2, "generate_storefront_logo", STOREFRONT_LOGO_TOOL_SCOPES);
|
|
7759
|
+
if (denied) return denied;
|
|
7652
7760
|
try {
|
|
7653
7761
|
const hex = accent ? accent.startsWith("#") ? accent : `#${accent}` : void 0;
|
|
7654
7762
|
const logo = await generateStorefrontLogo({
|
|
@@ -7657,7 +7765,7 @@ function registerStorefrontLogoTools(server2, env = {}) {
|
|
|
7657
7765
|
tagline,
|
|
7658
7766
|
env: { ...env, ...process.env }
|
|
7659
7767
|
});
|
|
7660
|
-
return
|
|
7768
|
+
return ok2(
|
|
7661
7769
|
JSON.stringify({
|
|
7662
7770
|
brand: name,
|
|
7663
7771
|
source: logo.source,
|
|
@@ -7682,11 +7790,11 @@ var STOREFRONT_DEPLOY_TOOL_SCOPES = {
|
|
|
7682
7790
|
deploy_storefront: "write",
|
|
7683
7791
|
provision_storefront_clerk: "write"
|
|
7684
7792
|
};
|
|
7685
|
-
var
|
|
7793
|
+
var ok3 = (value) => ({
|
|
7686
7794
|
content: [{ type: "text", text: JSON.stringify(value) }]
|
|
7687
7795
|
});
|
|
7688
7796
|
var err4 = (text) => ({ isError: true, content: [{ type: "text", text }] });
|
|
7689
|
-
function registerStorefrontDeployTools(server2) {
|
|
7797
|
+
function registerStorefrontDeployTools(server2, props2) {
|
|
7690
7798
|
server2.registerTool(
|
|
7691
7799
|
"list_deploy_targets",
|
|
7692
7800
|
{
|
|
@@ -7698,10 +7806,12 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7698
7806
|
annotations: annotationsFor("list_deploy_targets", "read")
|
|
7699
7807
|
},
|
|
7700
7808
|
async ({ dir }) => {
|
|
7809
|
+
const denied = denyUnlessScoped(props2, "list_deploy_targets", STOREFRONT_DEPLOY_TOOL_SCOPES);
|
|
7810
|
+
if (denied) return denied;
|
|
7701
7811
|
try {
|
|
7702
7812
|
const statuses = await probeAll(dir);
|
|
7703
7813
|
const ranked = rankTargets(statuses);
|
|
7704
|
-
return
|
|
7814
|
+
return ok3({
|
|
7705
7815
|
targets: statuses,
|
|
7706
7816
|
default: ranked[0]?.id ?? null,
|
|
7707
7817
|
reason: ranked.length ? void 0 : "No host is installed and logged in."
|
|
@@ -7728,6 +7838,8 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7728
7838
|
annotations: annotationsFor("deploy_storefront", "write")
|
|
7729
7839
|
},
|
|
7730
7840
|
async ({ dir, target, name, skip_build, push_secrets, verify, domain }) => {
|
|
7841
|
+
const denied = denyUnlessScoped(props2, "deploy_storefront", STOREFRONT_DEPLOY_TOOL_SCOPES);
|
|
7842
|
+
if (denied) return denied;
|
|
7731
7843
|
try {
|
|
7732
7844
|
if (target && !isTargetId(target)) return err4(`Unknown target "${target}".`);
|
|
7733
7845
|
const brand = await loadStorefrontBrand(dir, name ? { name } : void 0);
|
|
@@ -7747,7 +7859,7 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7747
7859
|
return err4(result2.reason ?? "Deploy failed.");
|
|
7748
7860
|
}
|
|
7749
7861
|
const verification = verify !== false && result2.url ? await verifyStorefront(result2.url, dir, process.env) : void 0;
|
|
7750
|
-
return
|
|
7862
|
+
return ok3({
|
|
7751
7863
|
deployed: true,
|
|
7752
7864
|
target: result2.target,
|
|
7753
7865
|
project: result2.projectName,
|
|
@@ -7780,6 +7892,12 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7780
7892
|
annotations: annotationsFor("provision_storefront_clerk", "write")
|
|
7781
7893
|
},
|
|
7782
7894
|
async ({ dir, name, production, no_create, url }) => {
|
|
7895
|
+
const denied = denyUnlessScoped(
|
|
7896
|
+
props2,
|
|
7897
|
+
"provision_storefront_clerk",
|
|
7898
|
+
STOREFRONT_DEPLOY_TOOL_SCOPES
|
|
7899
|
+
);
|
|
7900
|
+
if (denied) return denied;
|
|
7783
7901
|
try {
|
|
7784
7902
|
const brand = await loadStorefrontBrand(dir, name ? { name } : void 0);
|
|
7785
7903
|
const result2 = await provisionClerk({
|
|
@@ -7792,7 +7910,7 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7792
7910
|
cli: clerkCliDeps()
|
|
7793
7911
|
});
|
|
7794
7912
|
if (!result2.ok || !result2.credentials) {
|
|
7795
|
-
return
|
|
7913
|
+
return ok3({
|
|
7796
7914
|
provisioned: false,
|
|
7797
7915
|
tier: result2.tier,
|
|
7798
7916
|
reason: result2.reason,
|
|
@@ -7808,7 +7926,7 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7808
7926
|
allowedOrigins,
|
|
7809
7927
|
redirectUrls
|
|
7810
7928
|
});
|
|
7811
|
-
return
|
|
7929
|
+
return ok3({
|
|
7812
7930
|
provisioned: true,
|
|
7813
7931
|
tier: result2.tier,
|
|
7814
7932
|
application_id: result2.credentials.applicationId ?? null,
|
|
@@ -10415,7 +10533,7 @@ function registerRateLimitStatusTool(server2, ctx) {
|
|
|
10415
10533
|
|
|
10416
10534
|
// src/tools-depletion-events.ts
|
|
10417
10535
|
import { z as z14 } from "zod";
|
|
10418
|
-
function
|
|
10536
|
+
function ok4(payload) {
|
|
10419
10537
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
10420
10538
|
}
|
|
10421
10539
|
function err5(message) {
|
|
@@ -10475,7 +10593,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10475
10593
|
if (subscriberId) {
|
|
10476
10594
|
const raw = await kv.get(`bundle-depleted:${subscriberId}`, "text");
|
|
10477
10595
|
if (!raw) {
|
|
10478
|
-
return
|
|
10596
|
+
return ok4({
|
|
10479
10597
|
subscriber_id: subscriberId,
|
|
10480
10598
|
depleted: false,
|
|
10481
10599
|
event: null,
|
|
@@ -10486,7 +10604,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10486
10604
|
try {
|
|
10487
10605
|
event = JSON.parse(raw);
|
|
10488
10606
|
} catch {
|
|
10489
|
-
return
|
|
10607
|
+
return ok4({
|
|
10490
10608
|
subscriber_id: subscriberId,
|
|
10491
10609
|
depleted: false,
|
|
10492
10610
|
event: null,
|
|
@@ -10494,7 +10612,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10494
10612
|
});
|
|
10495
10613
|
}
|
|
10496
10614
|
if (!await ownsEvent(ctx, scopeToken, event.iccid)) {
|
|
10497
|
-
return
|
|
10615
|
+
return ok4({
|
|
10498
10616
|
subscriber_id: subscriberId,
|
|
10499
10617
|
depleted: false,
|
|
10500
10618
|
event: null,
|
|
@@ -10502,14 +10620,14 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10502
10620
|
});
|
|
10503
10621
|
}
|
|
10504
10622
|
if (since && event.depletedAt < since) {
|
|
10505
|
-
return
|
|
10623
|
+
return ok4({
|
|
10506
10624
|
subscriber_id: subscriberId,
|
|
10507
10625
|
depleted: false,
|
|
10508
10626
|
event: null,
|
|
10509
10627
|
note: `No bundle depletion after ${since}.`
|
|
10510
10628
|
});
|
|
10511
10629
|
}
|
|
10512
|
-
return
|
|
10630
|
+
return ok4({
|
|
10513
10631
|
subscriber_id: subscriberId,
|
|
10514
10632
|
depleted: true,
|
|
10515
10633
|
event: {
|
|
@@ -10523,7 +10641,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10523
10641
|
}
|
|
10524
10642
|
const listResult = await kv.list({ prefix: "bundle-depleted:", limit: 20 });
|
|
10525
10643
|
if (listResult.keys.length === 0) {
|
|
10526
|
-
return
|
|
10644
|
+
return ok4({
|
|
10527
10645
|
depletions: [],
|
|
10528
10646
|
total: 0,
|
|
10529
10647
|
note: "No bundle depletion events in the last 7 days."
|
|
@@ -10550,7 +10668,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10550
10668
|
})
|
|
10551
10669
|
);
|
|
10552
10670
|
events.sort((a, b) => b.depleted_at.localeCompare(a.depleted_at));
|
|
10553
|
-
return
|
|
10671
|
+
return ok4({
|
|
10554
10672
|
depletions: events,
|
|
10555
10673
|
total: events.length,
|
|
10556
10674
|
list_truncated: !listResult.list_complete
|
|
@@ -11926,7 +12044,7 @@ function resolvePortalBaseUrl(env) {
|
|
|
11926
12044
|
function getGreenzoneKv(env) {
|
|
11927
12045
|
return env.GREENZONE_STATE_KV ?? null;
|
|
11928
12046
|
}
|
|
11929
|
-
function
|
|
12047
|
+
function ok5(payload) {
|
|
11930
12048
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
11931
12049
|
}
|
|
11932
12050
|
function err6(message) {
|
|
@@ -11962,7 +12080,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
11962
12080
|
...ip ? buildAddIpSteps(portalBaseUrl, ip) : []
|
|
11963
12081
|
];
|
|
11964
12082
|
if (dry_run) {
|
|
11965
|
-
return
|
|
12083
|
+
return ok5({
|
|
11966
12084
|
dry_run: true,
|
|
11967
12085
|
message: "Would execute the following Kapture steps against the OCS portal:",
|
|
11968
12086
|
kapture_steps: steps,
|
|
@@ -11985,7 +12103,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
11985
12103
|
} else {
|
|
11986
12104
|
kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
|
|
11987
12105
|
}
|
|
11988
|
-
return
|
|
12106
|
+
return ok5({
|
|
11989
12107
|
requires_kapture: true,
|
|
11990
12108
|
message: "Execute the Kapture steps below to apply the change in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
|
|
11991
12109
|
kapture_steps: steps,
|
|
@@ -12023,7 +12141,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
12023
12141
|
...ip ? buildRemoveIpSteps(portalBaseUrl, ip) : []
|
|
12024
12142
|
];
|
|
12025
12143
|
if (dry_run) {
|
|
12026
|
-
return
|
|
12144
|
+
return ok5({
|
|
12027
12145
|
dry_run: true,
|
|
12028
12146
|
message: "Would execute the following Kapture steps to remove from OCS portal:",
|
|
12029
12147
|
kapture_steps: steps,
|
|
@@ -12046,7 +12164,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
12046
12164
|
} else {
|
|
12047
12165
|
kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
|
|
12048
12166
|
}
|
|
12049
|
-
return
|
|
12167
|
+
return ok5({
|
|
12050
12168
|
requires_kapture: true,
|
|
12051
12169
|
message: "Execute the Kapture steps below to apply the removal in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
|
|
12052
12170
|
kapture_steps: steps,
|
|
@@ -12077,7 +12195,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
12077
12195
|
const { from_portal } = args;
|
|
12078
12196
|
if (from_portal) {
|
|
12079
12197
|
const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
|
|
12080
|
-
return
|
|
12198
|
+
return ok5({
|
|
12081
12199
|
requires_kapture: true,
|
|
12082
12200
|
message: "Execute the Kapture steps below to read live Greenzone whitelist state from the OCS portal.",
|
|
12083
12201
|
kapture_steps: buildListSteps(portalBaseUrl),
|
|
@@ -12086,7 +12204,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
12086
12204
|
}
|
|
12087
12205
|
const kv = getGreenzoneKv(ctx.env);
|
|
12088
12206
|
if (!kv) {
|
|
12089
|
-
return
|
|
12207
|
+
return ok5({
|
|
12090
12208
|
source: "kv_cache",
|
|
12091
12209
|
hosts: [],
|
|
12092
12210
|
ips: [],
|
|
@@ -12098,7 +12216,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
12098
12216
|
}
|
|
12099
12217
|
try {
|
|
12100
12218
|
const state = await readState(kv);
|
|
12101
|
-
return
|
|
12219
|
+
return ok5({
|
|
12102
12220
|
source: "kv_cache",
|
|
12103
12221
|
hosts: state.hosts,
|
|
12104
12222
|
ips: state.ips,
|
|
@@ -12229,8 +12347,12 @@ registerStripeConnectTools(server, { env: stdioEnv, props });
|
|
|
12229
12347
|
registerWalletTools(server, { env: stdioEnv, props });
|
|
12230
12348
|
registerGreenzoneTools(server, toolCtx);
|
|
12231
12349
|
registerUiAgentGenericTools(server, toolCtx);
|
|
12232
|
-
registerStorefrontLogoTools(
|
|
12233
|
-
|
|
12350
|
+
registerStorefrontLogoTools(
|
|
12351
|
+
server,
|
|
12352
|
+
stdioEnv,
|
|
12353
|
+
props
|
|
12354
|
+
);
|
|
12355
|
+
registerStorefrontDeployTools(server, props);
|
|
12234
12356
|
registerAllPrompts(server);
|
|
12235
12357
|
var transport = new StdioServerTransport();
|
|
12236
12358
|
await server.connect(transport);
|