@carrierllc/mcp 0.2.20 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-NHQDO2E7.js → chunk-TABEJ7SM.js} +2 -2
- package/dist/chunk-TABEJ7SM.js.map +1 -0
- package/dist/cli.js +594 -70
- package/dist/cli.js.map +1 -1
- package/dist/index.js +494 -86
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-NHQDO2E7.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
networkEventsOverPeriodParams,
|
|
11
11
|
subscriberIdParams,
|
|
12
12
|
usageOverPeriodParams
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-TABEJ7SM.js";
|
|
14
14
|
|
|
15
15
|
// src/index.ts
|
|
16
16
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -310,9 +310,9 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
|
310
310
|
try {
|
|
311
311
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
312
312
|
result2 = await handler(args, token2);
|
|
313
|
-
} catch (
|
|
313
|
+
} catch (err6) {
|
|
314
314
|
try {
|
|
315
|
-
Sentry.captureException(
|
|
315
|
+
Sentry.captureException(err6, {
|
|
316
316
|
tags: {
|
|
317
317
|
tool: toolName,
|
|
318
318
|
feature: "mcp",
|
|
@@ -321,8 +321,8 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
|
321
321
|
});
|
|
322
322
|
} catch {
|
|
323
323
|
}
|
|
324
|
-
const message =
|
|
325
|
-
const ocsCode =
|
|
324
|
+
const message = err6 instanceof Error ? err6.message : String(err6);
|
|
325
|
+
const ocsCode = err6 instanceof OcsApiError ? err6.code : void 0;
|
|
326
326
|
ctx.audit({
|
|
327
327
|
tool_name: toolName,
|
|
328
328
|
ocs_method: ocsMethod,
|
|
@@ -3042,8 +3042,8 @@ async function safeCall(env, token2, method, params = {}) {
|
|
|
3042
3042
|
const client = new OcsClient(env.CARRIER_OCS_BASE_URL, token2);
|
|
3043
3043
|
const data = await client.call(method, params);
|
|
3044
3044
|
return { data, error: null };
|
|
3045
|
-
} catch (
|
|
3046
|
-
return { data: null, error:
|
|
3045
|
+
} catch (err6) {
|
|
3046
|
+
return { data: null, error: err6 instanceof Error ? err6.message : String(err6) };
|
|
3047
3047
|
}
|
|
3048
3048
|
}
|
|
3049
3049
|
async function fetchActiveSubscribers(env, token2, accountId, resellerId) {
|
|
@@ -4925,10 +4925,10 @@ async function callBedrock(env, payload) {
|
|
|
4925
4925
|
const body = await resp.text();
|
|
4926
4926
|
if (resp.status === 429) {
|
|
4927
4927
|
const retryAfter = resp.headers.get("retry-after");
|
|
4928
|
-
const
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
throw
|
|
4928
|
+
const err6 = new Error(`Bedrock rate limit: ${body}`);
|
|
4929
|
+
err6.isRateLimit = true;
|
|
4930
|
+
err6.retryAfter = retryAfter;
|
|
4931
|
+
throw err6;
|
|
4932
4932
|
}
|
|
4933
4933
|
throw new Error(`Bedrock invoke failed: ${resp.status} ${body}`);
|
|
4934
4934
|
}
|
|
@@ -4966,9 +4966,9 @@ Pre-resolved context: ${JSON.stringify(definedContext)}` : "";
|
|
|
4966
4966
|
AWS_REGION: env.AWS_REGION,
|
|
4967
4967
|
BEDROCK_MODEL_ID: env.BEDROCK_MODEL_ID
|
|
4968
4968
|
}, payload);
|
|
4969
|
-
} catch (
|
|
4970
|
-
if (
|
|
4971
|
-
const retryAfter =
|
|
4969
|
+
} catch (err6) {
|
|
4970
|
+
if (err6 instanceof Error && err6.isRateLimit) {
|
|
4971
|
+
const retryAfter = err6.retryAfter;
|
|
4972
4972
|
const parsedSeconds = retryAfter ? parseInt(retryAfter, 10) : 60;
|
|
4973
4973
|
return {
|
|
4974
4974
|
match: "rate_limited",
|
|
@@ -4976,7 +4976,7 @@ Pre-resolved context: ${JSON.stringify(definedContext)}` : "";
|
|
|
4976
4976
|
suggestion: "Bedrock rate limit reached. Please retry after the indicated delay."
|
|
4977
4977
|
};
|
|
4978
4978
|
}
|
|
4979
|
-
throw
|
|
4979
|
+
throw err6;
|
|
4980
4980
|
}
|
|
4981
4981
|
const toolUseBlock = response.content.find(
|
|
4982
4982
|
(block) => block.type === "tool_use"
|
|
@@ -5150,7 +5150,7 @@ function registerAllCarrierAskTools(server2, ctx) {
|
|
|
5150
5150
|
let route;
|
|
5151
5151
|
try {
|
|
5152
5152
|
route = await _routeIntent(intent, context, ctx.env);
|
|
5153
|
-
} catch (
|
|
5153
|
+
} catch (err6) {
|
|
5154
5154
|
writeCarrierAskAudit(ctx.env, {
|
|
5155
5155
|
intent_hash: intentHash,
|
|
5156
5156
|
match: "error",
|
|
@@ -5167,7 +5167,7 @@ function registerAllCarrierAskTools(server2, ctx) {
|
|
|
5167
5167
|
type: "text",
|
|
5168
5168
|
text: JSON.stringify({
|
|
5169
5169
|
error: "routing_error",
|
|
5170
|
-
message:
|
|
5170
|
+
message: err6 instanceof Error ? err6.message : "An unexpected error occurred during routing."
|
|
5171
5171
|
})
|
|
5172
5172
|
}
|
|
5173
5173
|
],
|
|
@@ -5419,8 +5419,8 @@ function registerListRecentOcsEventsTool(server2, env) {
|
|
|
5419
5419
|
return {
|
|
5420
5420
|
content: [{ type: "text", text: JSON.stringify(result2, null, 2) }]
|
|
5421
5421
|
};
|
|
5422
|
-
} catch (
|
|
5423
|
-
const message =
|
|
5422
|
+
} catch (err6) {
|
|
5423
|
+
const message = err6 instanceof Error ? err6.message : String(err6);
|
|
5424
5424
|
return {
|
|
5425
5425
|
isError: true,
|
|
5426
5426
|
content: [{ type: "text", text: `Error reading OCS event buffer: ${message}` }]
|
|
@@ -5703,10 +5703,10 @@ import {
|
|
|
5703
5703
|
async function safeCallWithToken(client, _token, method, params = {}) {
|
|
5704
5704
|
try {
|
|
5705
5705
|
return { data: await client.call(method, params), error: null };
|
|
5706
|
-
} catch (
|
|
5706
|
+
} catch (err6) {
|
|
5707
5707
|
return {
|
|
5708
5708
|
data: null,
|
|
5709
|
-
error:
|
|
5709
|
+
error: err6 instanceof Error ? err6.message : String(err6)
|
|
5710
5710
|
};
|
|
5711
5711
|
}
|
|
5712
5712
|
}
|
|
@@ -5904,10 +5904,10 @@ async function deleteWizardSession(env, sub, wizardId) {
|
|
|
5904
5904
|
async function safeCallWithToken2(client, _token, method, params = {}) {
|
|
5905
5905
|
try {
|
|
5906
5906
|
return { data: await client.call(method, params), error: null };
|
|
5907
|
-
} catch (
|
|
5907
|
+
} catch (err6) {
|
|
5908
5908
|
return {
|
|
5909
5909
|
data: null,
|
|
5910
|
-
error:
|
|
5910
|
+
error: err6 instanceof Error ? err6.message : String(err6)
|
|
5911
5911
|
};
|
|
5912
5912
|
}
|
|
5913
5913
|
}
|
|
@@ -6267,10 +6267,10 @@ import {
|
|
|
6267
6267
|
async function safeCallWithToken3(client, _token, method, params = {}) {
|
|
6268
6268
|
try {
|
|
6269
6269
|
return { data: await client.call(method, params), error: null };
|
|
6270
|
-
} catch (
|
|
6270
|
+
} catch (err6) {
|
|
6271
6271
|
return {
|
|
6272
6272
|
data: null,
|
|
6273
|
-
error:
|
|
6273
|
+
error: err6 instanceof Error ? err6.message : String(err6)
|
|
6274
6274
|
};
|
|
6275
6275
|
}
|
|
6276
6276
|
}
|
|
@@ -7931,8 +7931,8 @@ function buildPricingHandler(tool, ctx) {
|
|
|
7931
7931
|
}
|
|
7932
7932
|
]
|
|
7933
7933
|
};
|
|
7934
|
-
} catch (
|
|
7935
|
-
const message =
|
|
7934
|
+
} catch (err6) {
|
|
7935
|
+
const message = err6 instanceof Error ? err6.message : String(err6);
|
|
7936
7936
|
ctx.audit({
|
|
7937
7937
|
tool_name: tool.name,
|
|
7938
7938
|
ocs_method: "billing",
|
|
@@ -8252,8 +8252,8 @@ function writeUsageThresholdAudit(env, severity, remainingCredits, month, taskCo
|
|
|
8252
8252
|
doubles: [remainingCredits],
|
|
8253
8253
|
indexes: ["manus_usage"]
|
|
8254
8254
|
});
|
|
8255
|
-
} catch (
|
|
8256
|
-
console.error(`[manus-usage] threshold audit write failed: ${
|
|
8255
|
+
} catch (err6) {
|
|
8256
|
+
console.error(`[manus-usage] threshold audit write failed: ${err6.message}`);
|
|
8257
8257
|
}
|
|
8258
8258
|
}
|
|
8259
8259
|
|
|
@@ -8363,7 +8363,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8363
8363
|
}
|
|
8364
8364
|
]
|
|
8365
8365
|
};
|
|
8366
|
-
} catch (
|
|
8366
|
+
} catch (err6) {
|
|
8367
8367
|
ctx.audit({
|
|
8368
8368
|
tool_name: "ui_agent_schedule_create",
|
|
8369
8369
|
ocs_method: "[manus:schedule.create]",
|
|
@@ -8372,7 +8372,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8372
8372
|
duration_ms: Date.now() - start,
|
|
8373
8373
|
event_type: "ui_agent_dispatch"
|
|
8374
8374
|
});
|
|
8375
|
-
const msg =
|
|
8375
|
+
const msg = err6 instanceof ManusScheduleError ? `Manus API error (HTTP ${err6.statusCode}): ${err6.message}` : err6 instanceof Error ? err6.message : String(err6);
|
|
8376
8376
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8377
8377
|
}
|
|
8378
8378
|
}
|
|
@@ -8430,7 +8430,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8430
8430
|
}
|
|
8431
8431
|
]
|
|
8432
8432
|
};
|
|
8433
|
-
} catch (
|
|
8433
|
+
} catch (err6) {
|
|
8434
8434
|
ctx.audit({
|
|
8435
8435
|
tool_name: "ui_agent_schedule_list",
|
|
8436
8436
|
ocs_method: "[manus:schedule.list]",
|
|
@@ -8438,7 +8438,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8438
8438
|
dry_run: false,
|
|
8439
8439
|
duration_ms: Date.now() - start
|
|
8440
8440
|
});
|
|
8441
|
-
const msg =
|
|
8441
|
+
const msg = err6 instanceof ManusScheduleError ? `Manus API error (HTTP ${err6.statusCode}): ${err6.message}` : err6 instanceof Error ? err6.message : String(err6);
|
|
8442
8442
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8443
8443
|
}
|
|
8444
8444
|
}
|
|
@@ -8499,7 +8499,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8499
8499
|
}
|
|
8500
8500
|
]
|
|
8501
8501
|
};
|
|
8502
|
-
} catch (
|
|
8502
|
+
} catch (err6) {
|
|
8503
8503
|
ctx.audit({
|
|
8504
8504
|
tool_name: "ui_agent_schedule_delete",
|
|
8505
8505
|
ocs_method: "[manus:schedule.delete]",
|
|
@@ -8508,7 +8508,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8508
8508
|
duration_ms: Date.now() - start,
|
|
8509
8509
|
event_type: "ui_agent_dispatch"
|
|
8510
8510
|
});
|
|
8511
|
-
const msg =
|
|
8511
|
+
const msg = err6 instanceof ManusScheduleError ? `Manus API error (HTTP ${err6.statusCode}): ${err6.message}` : err6 instanceof Error ? err6.message : String(err6);
|
|
8512
8512
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8513
8513
|
}
|
|
8514
8514
|
}
|
|
@@ -8569,7 +8569,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8569
8569
|
}
|
|
8570
8570
|
]
|
|
8571
8571
|
};
|
|
8572
|
-
} catch (
|
|
8572
|
+
} catch (err6) {
|
|
8573
8573
|
ctx.audit({
|
|
8574
8574
|
tool_name: "ui_agent_schedule_pause",
|
|
8575
8575
|
ocs_method: "[manus:schedule.pause]",
|
|
@@ -8578,7 +8578,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8578
8578
|
duration_ms: Date.now() - start,
|
|
8579
8579
|
event_type: "ui_agent_dispatch"
|
|
8580
8580
|
});
|
|
8581
|
-
const msg =
|
|
8581
|
+
const msg = err6 instanceof ManusScheduleError ? `Manus API error (HTTP ${err6.statusCode}): ${err6.message}` : err6 instanceof Error ? err6.message : String(err6);
|
|
8582
8582
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8583
8583
|
}
|
|
8584
8584
|
}
|
|
@@ -8639,7 +8639,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8639
8639
|
}
|
|
8640
8640
|
]
|
|
8641
8641
|
};
|
|
8642
|
-
} catch (
|
|
8642
|
+
} catch (err6) {
|
|
8643
8643
|
ctx.audit({
|
|
8644
8644
|
tool_name: "ui_agent_schedule_resume",
|
|
8645
8645
|
ocs_method: "[manus:schedule.resume]",
|
|
@@ -8648,7 +8648,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8648
8648
|
duration_ms: Date.now() - start,
|
|
8649
8649
|
event_type: "ui_agent_dispatch"
|
|
8650
8650
|
});
|
|
8651
|
-
const msg =
|
|
8651
|
+
const msg = err6 instanceof ManusScheduleError ? `Manus API error (HTTP ${err6.statusCode}): ${err6.message}` : err6 instanceof Error ? err6.message : String(err6);
|
|
8652
8652
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8653
8653
|
}
|
|
8654
8654
|
}
|
|
@@ -8693,7 +8693,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8693
8693
|
}
|
|
8694
8694
|
]
|
|
8695
8695
|
};
|
|
8696
|
-
} catch (
|
|
8696
|
+
} catch (err6) {
|
|
8697
8697
|
ctx.audit({
|
|
8698
8698
|
tool_name: "ui_agent_usage",
|
|
8699
8699
|
ocs_method: "[manus:usage.get]",
|
|
@@ -8701,7 +8701,7 @@ function registerScheduleAndUsageTools(server2, ctx) {
|
|
|
8701
8701
|
dry_run: false,
|
|
8702
8702
|
duration_ms: Date.now() - start
|
|
8703
8703
|
});
|
|
8704
|
-
const msg =
|
|
8704
|
+
const msg = err6 instanceof Error ? err6.message : String(err6);
|
|
8705
8705
|
return { isError: true, content: [{ type: "text", text: msg }] };
|
|
8706
8706
|
}
|
|
8707
8707
|
}
|
|
@@ -8944,7 +8944,7 @@ Report success or failure with a clear summary.`;
|
|
|
8944
8944
|
}
|
|
8945
8945
|
]
|
|
8946
8946
|
};
|
|
8947
|
-
} catch (
|
|
8947
|
+
} catch (err6) {
|
|
8948
8948
|
ctx.audit({
|
|
8949
8949
|
tool_name: toolName,
|
|
8950
8950
|
ocs_method: `[ui-agent:${gapId}]`,
|
|
@@ -8958,7 +8958,7 @@ Report success or failure with a clear summary.`;
|
|
|
8958
8958
|
content: [
|
|
8959
8959
|
{
|
|
8960
8960
|
type: "text",
|
|
8961
|
-
text: `Error dispatching UI agent: ${
|
|
8961
|
+
text: `Error dispatching UI agent: ${err6 instanceof Error ? err6.message : String(err6)}`
|
|
8962
8962
|
}
|
|
8963
8963
|
]
|
|
8964
8964
|
};
|
|
@@ -9382,7 +9382,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
9382
9382
|
try {
|
|
9383
9383
|
const replyOutcome = await askReply(keys, task_id, reply);
|
|
9384
9384
|
result2 = replyOutcome.data;
|
|
9385
|
-
} catch (
|
|
9385
|
+
} catch (err6) {
|
|
9386
9386
|
ctx.audit({
|
|
9387
9387
|
tool_name: "ui_agent_reply",
|
|
9388
9388
|
ocs_method: "[ui-agent:ask-reply]",
|
|
@@ -9397,7 +9397,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
9397
9397
|
content: [
|
|
9398
9398
|
{
|
|
9399
9399
|
type: "text",
|
|
9400
|
-
text: `Error calling Manus task.reply: ${
|
|
9400
|
+
text: `Error calling Manus task.reply: ${err6 instanceof Error ? err6.message : String(err6)}`
|
|
9401
9401
|
}
|
|
9402
9402
|
]
|
|
9403
9403
|
};
|
|
@@ -9475,13 +9475,13 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
9475
9475
|
try {
|
|
9476
9476
|
const listing = await ctx.env.CARRIER_USERS.list({ prefix: PENDING_ASK_PREFIX });
|
|
9477
9477
|
keys = listing.keys;
|
|
9478
|
-
} catch (
|
|
9478
|
+
} catch (err6) {
|
|
9479
9479
|
return {
|
|
9480
9480
|
isError: true,
|
|
9481
9481
|
content: [
|
|
9482
9482
|
{
|
|
9483
9483
|
type: "text",
|
|
9484
|
-
text: `Error listing pending tasks: ${
|
|
9484
|
+
text: `Error listing pending tasks: ${err6 instanceof Error ? err6.message : String(err6)}`
|
|
9485
9485
|
}
|
|
9486
9486
|
]
|
|
9487
9487
|
};
|
|
@@ -9652,7 +9652,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
9652
9652
|
);
|
|
9653
9653
|
server2.tool(
|
|
9654
9654
|
"stripe_connect_payouts",
|
|
9655
|
-
"List money Stripe has already sent from the operator's connected account to their bank, newest first. Read-only. Params: `limit` (1-50, default 10). Returns: payouts
|
|
9655
|
+
"List money Stripe has already sent from the operator's connected account to their bank, newest first. Read-only. Params: `limit` (1-50, default 10), `status` (optional Stripe payout status, e.g. 'paid', 'pending', 'failed'). Returns: { payouts: [{ id, amount, currency, status, arrival_date, automatic, created }], has_more } \u2014 amount in the currency's smallest unit. Do NOT use this for money still sitting at Stripe and not yet paid out \u2014 use `stripe_connect_balance`.",
|
|
9656
9656
|
{
|
|
9657
9657
|
limit: z17.number().int().min(1).max(50).default(10).describe("Number of payouts to return."),
|
|
9658
9658
|
status: z17.enum(["pending", "paid", "failed", "canceled", "in_transit"]).optional().describe("Filter by payout status.")
|
|
@@ -9769,7 +9769,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
9769
9769
|
);
|
|
9770
9770
|
server2.tool(
|
|
9771
9771
|
"stripe_connect_dispute_list",
|
|
9772
|
-
"List chargebacks customers have raised with their bank against the operator's connected Stripe account. Read-only \u2014 responding to one is Dashboard work. Params: `limit` (1-50, default 10), `status` (optional Stripe dispute status, e.g. 'needs_response', 'under_review'). Returns: disputes
|
|
9772
|
+
"List chargebacks customers have raised with their bank against the operator's connected Stripe account. Read-only \u2014 responding to one is Dashboard work. Params: `limit` (1-50, default 10), `status` (optional Stripe dispute status, e.g. 'needs_response', 'under_review'). Returns: { disputes: [{ id, charge, amount, currency, status, reason, evidence_details.due_by }], has_more } \u2014 amount in the currency's smallest unit, `due_by` a Unix timestamp or null. Do NOT use this for charges Stripe is holding for fraud review before they settle \u2014 those are `radar_review_list`. Check this before refunding: refunding a disputed charge can cost the operator the amount twice.",
|
|
9773
9773
|
{
|
|
9774
9774
|
limit: z17.number().int().min(1).max(50).default(10),
|
|
9775
9775
|
status: z17.string().optional().describe("Filter by dispute status (e.g. needs_response, under_review).")
|
|
@@ -9989,8 +9989,407 @@ Note: If you need this automated, use the Radar for Platforms beta \u2014 contac
|
|
|
9989
9989
|
);
|
|
9990
9990
|
}
|
|
9991
9991
|
|
|
9992
|
-
// src/tools
|
|
9992
|
+
// src/wallet-tools.ts
|
|
9993
9993
|
import { z as z18 } from "zod";
|
|
9994
|
+
import * as Sentry3 from "@sentry/cloudflare";
|
|
9995
|
+
|
|
9996
|
+
// src/wallet-client.ts
|
|
9997
|
+
var DEFAULT_ATLAS_BASE_URL = "https://atlas.carrier.llc";
|
|
9998
|
+
var WalletClientError = class extends Error {
|
|
9999
|
+
status;
|
|
10000
|
+
constructor(message, status) {
|
|
10001
|
+
super(message);
|
|
10002
|
+
this.name = "WalletClientError";
|
|
10003
|
+
this.status = status;
|
|
10004
|
+
}
|
|
10005
|
+
};
|
|
10006
|
+
function resolveConfig(env) {
|
|
10007
|
+
const key = env.CARRIER_INTERNAL_API_KEY;
|
|
10008
|
+
if (!key) {
|
|
10009
|
+
throw new WalletClientError(
|
|
10010
|
+
"CARRIER_INTERNAL_API_KEY not configured \u2014 cannot reach ATLAS wallet",
|
|
10011
|
+
0
|
|
10012
|
+
);
|
|
10013
|
+
}
|
|
10014
|
+
const baseUrl2 = (env.ATLAS_BASE_URL ?? DEFAULT_ATLAS_BASE_URL).replace(
|
|
10015
|
+
/\/+$/,
|
|
10016
|
+
""
|
|
10017
|
+
);
|
|
10018
|
+
return { baseUrl: baseUrl2, key };
|
|
10019
|
+
}
|
|
10020
|
+
async function atlasFetch(env, path, init) {
|
|
10021
|
+
const { baseUrl: baseUrl2, key } = resolveConfig(env);
|
|
10022
|
+
const headers = { "X-Carrier-Internal-Key": key };
|
|
10023
|
+
let body;
|
|
10024
|
+
if (init.body !== void 0) {
|
|
10025
|
+
headers["Content-Type"] = "application/json";
|
|
10026
|
+
body = JSON.stringify(init.body);
|
|
10027
|
+
}
|
|
10028
|
+
let res;
|
|
10029
|
+
try {
|
|
10030
|
+
res = await fetch(`${baseUrl2}${path}`, { method: init.method, headers, body });
|
|
10031
|
+
} catch (e) {
|
|
10032
|
+
throw new WalletClientError(
|
|
10033
|
+
`ATLAS wallet request failed: ${e instanceof Error ? e.message : "network error"}`,
|
|
10034
|
+
0
|
|
10035
|
+
);
|
|
10036
|
+
}
|
|
10037
|
+
if (!res.ok) {
|
|
10038
|
+
const detail = await res.text().catch(() => "");
|
|
10039
|
+
throw new WalletClientError(
|
|
10040
|
+
`ATLAS wallet ${init.method} ${path} -> HTTP ${res.status}${detail ? `: ${detail}` : ""}`,
|
|
10041
|
+
res.status
|
|
10042
|
+
);
|
|
10043
|
+
}
|
|
10044
|
+
return await res.json();
|
|
10045
|
+
}
|
|
10046
|
+
async function walletTopup(env, orgId, args) {
|
|
10047
|
+
return atlasFetch(
|
|
10048
|
+
env,
|
|
10049
|
+
`/internal/wallet/${encodeURIComponent(orgId)}/topup`,
|
|
10050
|
+
{
|
|
10051
|
+
method: "POST",
|
|
10052
|
+
body: {
|
|
10053
|
+
eur_cents: args.eurCents,
|
|
10054
|
+
ref: args.ref,
|
|
10055
|
+
...args.type ? { type: args.type } : {}
|
|
10056
|
+
}
|
|
10057
|
+
}
|
|
10058
|
+
);
|
|
10059
|
+
}
|
|
10060
|
+
async function getWallet(env, orgId) {
|
|
10061
|
+
return atlasFetch(env, `/internal/wallet/${encodeURIComponent(orgId)}`, {
|
|
10062
|
+
method: "GET"
|
|
10063
|
+
});
|
|
10064
|
+
}
|
|
10065
|
+
|
|
10066
|
+
// src/billing-sub.ts
|
|
10067
|
+
async function resolveBillingSub(env, orgId) {
|
|
10068
|
+
if (!env.CARRIER_USERS) return orgId;
|
|
10069
|
+
const pointer = await env.CARRIER_USERS.get(
|
|
10070
|
+
`org:${orgId}:billing_owner_sub`
|
|
10071
|
+
).catch(() => null);
|
|
10072
|
+
return pointer ?? orgId;
|
|
10073
|
+
}
|
|
10074
|
+
|
|
10075
|
+
// src/wallet-tools.ts
|
|
10076
|
+
var PACKS = [
|
|
10077
|
+
{ id: "pack_500", eurCents: 5e4, bonusPct: 0 },
|
|
10078
|
+
{ id: "pack_1000", eurCents: 1e5, bonusPct: 3 },
|
|
10079
|
+
{ id: "pack_2500", eurCents: 25e4, bonusPct: 6 },
|
|
10080
|
+
{ id: "pack_5000", eurCents: 5e5, bonusPct: 10 }
|
|
10081
|
+
];
|
|
10082
|
+
function bonusCentsFor(pack) {
|
|
10083
|
+
return Math.floor(pack.eurCents * pack.bonusPct / 100);
|
|
10084
|
+
}
|
|
10085
|
+
function resolvePack(idOrCents) {
|
|
10086
|
+
if (typeof idOrCents === "number") {
|
|
10087
|
+
return PACKS.find((p) => p.eurCents === idOrCents);
|
|
10088
|
+
}
|
|
10089
|
+
return PACKS.find((p) => p.id === idOrCents) ?? PACKS.find((p) => String(p.eurCents) === idOrCents);
|
|
10090
|
+
}
|
|
10091
|
+
var STRIPE_API = "https://api.stripe.com";
|
|
10092
|
+
async function stripePostForm(stripeKey, path, params, opts) {
|
|
10093
|
+
const headers = {
|
|
10094
|
+
Authorization: `Bearer ${stripeKey}`,
|
|
10095
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
10096
|
+
};
|
|
10097
|
+
if (opts?.idempotencyKey) {
|
|
10098
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
10099
|
+
}
|
|
10100
|
+
const res = await fetch(`${STRIPE_API}${path}`, {
|
|
10101
|
+
method: "POST",
|
|
10102
|
+
headers,
|
|
10103
|
+
body: params.toString()
|
|
10104
|
+
});
|
|
10105
|
+
const data = await res.json();
|
|
10106
|
+
return { ok: res.ok, status: res.status, data };
|
|
10107
|
+
}
|
|
10108
|
+
var DEFAULT_SUCCESS_URL = "https://app.carrier.llc/billing?topup=success";
|
|
10109
|
+
var DEFAULT_CANCEL_URL = "https://app.carrier.llc/billing?topup=cancelled";
|
|
10110
|
+
async function createTopupCheckout(env, args) {
|
|
10111
|
+
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
10112
|
+
if (!stripeKey) throw new Error("Stripe not configured (STRIPE_SECRET_KEY absent)");
|
|
10113
|
+
const bonusCents = bonusCentsFor(args.pack);
|
|
10114
|
+
const successUrl = args.successUrl ?? env.WALLET_CHECKOUT_SUCCESS_URL ?? DEFAULT_SUCCESS_URL;
|
|
10115
|
+
const cancelUrl = args.cancelUrl ?? env.WALLET_CHECKOUT_CANCEL_URL ?? DEFAULT_CANCEL_URL;
|
|
10116
|
+
const bonusLabel = bonusCents > 0 ? ` (+\u20AC${(bonusCents / 100).toFixed(2)} bonus)` : "";
|
|
10117
|
+
const billingSub = await resolveBillingSub(env, args.orgId);
|
|
10118
|
+
const existingCustomer = await env.CARRIER_USERS.get(`stripe_customer_id:${billingSub}`).catch(() => null);
|
|
10119
|
+
const params = new URLSearchParams({
|
|
10120
|
+
mode: "payment",
|
|
10121
|
+
success_url: successUrl,
|
|
10122
|
+
cancel_url: cancelUrl,
|
|
10123
|
+
"line_items[0][quantity]": "1",
|
|
10124
|
+
"line_items[0][price_data][currency]": "eur",
|
|
10125
|
+
"line_items[0][price_data][unit_amount]": String(args.pack.eurCents),
|
|
10126
|
+
"line_items[0][price_data][product_data][name]": `Carrier wallet top-up \u2014 \u20AC${(args.pack.eurCents / 100).toFixed(2)}${bonusLabel}`,
|
|
10127
|
+
client_reference_id: args.orgId,
|
|
10128
|
+
"metadata[kind]": "wallet_topup",
|
|
10129
|
+
"metadata[org_id]": args.orgId,
|
|
10130
|
+
"metadata[pack_id]": args.pack.id,
|
|
10131
|
+
"metadata[pack_eur_cents]": String(args.pack.eurCents),
|
|
10132
|
+
"metadata[bonus_eur_cents]": String(bonusCents),
|
|
10133
|
+
// Save the payment method for reuse so subsequent off-session auto-top-up
|
|
10134
|
+
// charges (runAutoTopup) can charge it without the customer present.
|
|
10135
|
+
"payment_intent_data[setup_future_usage]": "off_session",
|
|
10136
|
+
// Mirror metadata onto the resulting PaymentIntent so the
|
|
10137
|
+
// payment_intent.succeeded webhook can also credit idempotently.
|
|
10138
|
+
"payment_intent_data[metadata][kind]": "wallet_topup",
|
|
10139
|
+
"payment_intent_data[metadata][org_id]": args.orgId,
|
|
10140
|
+
"payment_intent_data[metadata][pack_eur_cents]": String(args.pack.eurCents),
|
|
10141
|
+
"payment_intent_data[metadata][bonus_eur_cents]": String(bonusCents)
|
|
10142
|
+
});
|
|
10143
|
+
if (existingCustomer) {
|
|
10144
|
+
params.set("customer", existingCustomer);
|
|
10145
|
+
} else {
|
|
10146
|
+
params.set("customer_creation", "always");
|
|
10147
|
+
}
|
|
10148
|
+
const result2 = await stripePostForm(
|
|
10149
|
+
stripeKey,
|
|
10150
|
+
"/v1/checkout/sessions",
|
|
10151
|
+
params
|
|
10152
|
+
);
|
|
10153
|
+
if (!result2.ok) {
|
|
10154
|
+
throw new Error(`Stripe checkout session create failed: ${JSON.stringify(result2.data)}`);
|
|
10155
|
+
}
|
|
10156
|
+
return { id: result2.data.id, url: result2.data.url };
|
|
10157
|
+
}
|
|
10158
|
+
async function runAutoTopup(env, args) {
|
|
10159
|
+
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
10160
|
+
if (!stripeKey) {
|
|
10161
|
+
return { charged: false, creditedCents: 0, reason: "stripe_not_configured" };
|
|
10162
|
+
}
|
|
10163
|
+
let packCents = args.packCents;
|
|
10164
|
+
try {
|
|
10165
|
+
const wallet = await getWallet(env, args.orgId);
|
|
10166
|
+
if (packCents === void 0) packCents = wallet.auto_topup_pack_cents;
|
|
10167
|
+
} catch (e) {
|
|
10168
|
+
if (e instanceof WalletClientError) {
|
|
10169
|
+
return { charged: false, creditedCents: 0, reason: `wallet_unreachable: ${e.message}` };
|
|
10170
|
+
}
|
|
10171
|
+
throw e;
|
|
10172
|
+
}
|
|
10173
|
+
if (!packCents || packCents <= 0) {
|
|
10174
|
+
return { charged: false, creditedCents: 0, reason: "no_pack_configured" };
|
|
10175
|
+
}
|
|
10176
|
+
const pack = resolvePack(packCents);
|
|
10177
|
+
if (!pack) {
|
|
10178
|
+
return { charged: false, creditedCents: 0, reason: `unknown_pack: ${packCents}` };
|
|
10179
|
+
}
|
|
10180
|
+
if (!env.CARRIER_USERS) {
|
|
10181
|
+
return { charged: false, creditedCents: 0, reason: "no_payment_method" };
|
|
10182
|
+
}
|
|
10183
|
+
const customerId = await env.CARRIER_USERS.get(`stripe_customer_id:${args.sub}`).catch(
|
|
10184
|
+
() => null
|
|
10185
|
+
);
|
|
10186
|
+
const pmId = await env.CARRIER_USERS.get(`stripe_pm_id:${args.sub}`).catch(() => null);
|
|
10187
|
+
if (!customerId || !pmId) {
|
|
10188
|
+
return { charged: false, creditedCents: 0, reason: "no_payment_method" };
|
|
10189
|
+
}
|
|
10190
|
+
const bonusCents = bonusCentsFor(pack);
|
|
10191
|
+
const body = new URLSearchParams({
|
|
10192
|
+
amount: String(pack.eurCents),
|
|
10193
|
+
currency: "eur",
|
|
10194
|
+
customer: customerId,
|
|
10195
|
+
payment_method: pmId,
|
|
10196
|
+
confirm: "true",
|
|
10197
|
+
off_session: "true",
|
|
10198
|
+
description: `Carrier wallet auto-top-up \u2014 \u20AC${(pack.eurCents / 100).toFixed(2)}`,
|
|
10199
|
+
"metadata[kind]": "wallet_topup",
|
|
10200
|
+
"metadata[org_id]": args.orgId,
|
|
10201
|
+
"metadata[pack_eur_cents]": String(pack.eurCents),
|
|
10202
|
+
"metadata[bonus_eur_cents]": String(bonusCents),
|
|
10203
|
+
"metadata[auto_topup]": "true"
|
|
10204
|
+
});
|
|
10205
|
+
const resp = await stripePostForm(
|
|
10206
|
+
stripeKey,
|
|
10207
|
+
"/v1/payment_intents",
|
|
10208
|
+
body
|
|
10209
|
+
);
|
|
10210
|
+
if (!resp.ok) {
|
|
10211
|
+
return {
|
|
10212
|
+
charged: false,
|
|
10213
|
+
creditedCents: 0,
|
|
10214
|
+
reason: `stripe_error: ${JSON.stringify(resp.data)}`
|
|
10215
|
+
};
|
|
10216
|
+
}
|
|
10217
|
+
if (resp.data.status !== "succeeded") {
|
|
10218
|
+
return {
|
|
10219
|
+
charged: false,
|
|
10220
|
+
creditedCents: 0,
|
|
10221
|
+
reason: `payment_status: ${resp.data.status}`,
|
|
10222
|
+
paymentIntentId: resp.data.id
|
|
10223
|
+
};
|
|
10224
|
+
}
|
|
10225
|
+
const creditedCents = pack.eurCents + bonusCents;
|
|
10226
|
+
try {
|
|
10227
|
+
const topup = await walletTopup(env, args.orgId, {
|
|
10228
|
+
eurCents: creditedCents,
|
|
10229
|
+
ref: resp.data.id,
|
|
10230
|
+
type: "auto_topup"
|
|
10231
|
+
});
|
|
10232
|
+
return {
|
|
10233
|
+
charged: true,
|
|
10234
|
+
creditedCents,
|
|
10235
|
+
balanceAfterCents: topup.balance_after_cents,
|
|
10236
|
+
paymentIntentId: resp.data.id
|
|
10237
|
+
};
|
|
10238
|
+
} catch (e) {
|
|
10239
|
+
Sentry3.captureException(e);
|
|
10240
|
+
return {
|
|
10241
|
+
charged: true,
|
|
10242
|
+
creditedCents,
|
|
10243
|
+
paymentIntentId: resp.data.id,
|
|
10244
|
+
reason: `credited_via_webhook_pending: ${e instanceof Error ? e.message : "atlas error"}`
|
|
10245
|
+
};
|
|
10246
|
+
}
|
|
10247
|
+
}
|
|
10248
|
+
function ok3(text) {
|
|
10249
|
+
return { content: [{ type: "text", text }] };
|
|
10250
|
+
}
|
|
10251
|
+
function err3(text) {
|
|
10252
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
10253
|
+
}
|
|
10254
|
+
function registerWalletTools(server2, ctx) {
|
|
10255
|
+
const { env, props: props2 } = ctx;
|
|
10256
|
+
const defaultOrgId = props2.org_id ?? props2.sub;
|
|
10257
|
+
server2.tool(
|
|
10258
|
+
"wallet_balance",
|
|
10259
|
+
"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`.",
|
|
10260
|
+
{},
|
|
10261
|
+
async () => {
|
|
10262
|
+
const orgId = defaultOrgId;
|
|
10263
|
+
const start = Date.now();
|
|
10264
|
+
try {
|
|
10265
|
+
const wallet = await getWallet(env, orgId);
|
|
10266
|
+
writeAudit(env, {
|
|
10267
|
+
tool_name: "wallet_balance",
|
|
10268
|
+
ocs_method: "atlas.wallet.read",
|
|
10269
|
+
status: "ok",
|
|
10270
|
+
dry_run: false,
|
|
10271
|
+
duration_ms: Date.now() - start,
|
|
10272
|
+
sub: props2.sub,
|
|
10273
|
+
reseller_id: props2.reseller_id
|
|
10274
|
+
});
|
|
10275
|
+
return ok3(
|
|
10276
|
+
JSON.stringify({
|
|
10277
|
+
org_id: orgId,
|
|
10278
|
+
balance_eur_cents: wallet.balance_eur_cents,
|
|
10279
|
+
auto_topup_enabled: wallet.auto_topup_enabled,
|
|
10280
|
+
auto_topup_threshold_cents: wallet.auto_topup_threshold_cents,
|
|
10281
|
+
auto_topup_pack_cents: wallet.auto_topup_pack_cents
|
|
10282
|
+
})
|
|
10283
|
+
);
|
|
10284
|
+
} catch (e) {
|
|
10285
|
+
if (e instanceof WalletClientError) return err3(`Wallet error: ${e.message}`);
|
|
10286
|
+
Sentry3.captureException(e);
|
|
10287
|
+
return err3(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
10288
|
+
}
|
|
10289
|
+
}
|
|
10290
|
+
);
|
|
10291
|
+
server2.tool(
|
|
10292
|
+
"wallet_topup_checkout",
|
|
10293
|
+
"Create a Stripe Checkout link so a human can top up the Carrier prepaid wallet by card. This tool charges nothing itself: it returns a hosted URL, and the wallet is credited (pack amount + bonus) only after someone completes the payment in a browser. Params: `pack` (optional \u2014 pack id `pack_500`/`pack_1000`/`pack_2500`/`pack_5000`, or an exact EUR-cents amount matching a catalog pack; omit to list the packs and their bonuses without creating a session). Returns: { pack_id, pack_eur_cents, bonus_eur_cents, checkout_session_id, checkout_url } \u2014 amounts in EUR cents. Do NOT use this when the wallet must be funded now with no human in the loop \u2014 use `wallet_auto_topup`, which charges the saved card immediately. Do NOT use this to move money in OCS \u2014 `modify_account_balance` and `modify_subscriber_balance` are unrelated ledgers.",
|
|
10294
|
+
{
|
|
10295
|
+
pack: z18.string().optional().describe(
|
|
10296
|
+
"Pack id (pack_500/pack_1000/pack_2500/pack_5000) or exact EUR-cents amount. Omit to list packs."
|
|
10297
|
+
)
|
|
10298
|
+
},
|
|
10299
|
+
async ({ pack }) => {
|
|
10300
|
+
const orgId = defaultOrgId;
|
|
10301
|
+
const start = Date.now();
|
|
10302
|
+
if (!pack) {
|
|
10303
|
+
return ok3(
|
|
10304
|
+
JSON.stringify({
|
|
10305
|
+
packs: PACKS.map((p) => ({
|
|
10306
|
+
id: p.id,
|
|
10307
|
+
eur_cents: p.eurCents,
|
|
10308
|
+
bonus_pct: p.bonusPct,
|
|
10309
|
+
bonus_eur_cents: bonusCentsFor(p),
|
|
10310
|
+
credited_eur_cents: p.eurCents + bonusCentsFor(p)
|
|
10311
|
+
}))
|
|
10312
|
+
})
|
|
10313
|
+
);
|
|
10314
|
+
}
|
|
10315
|
+
const selected = resolvePack(pack);
|
|
10316
|
+
if (!selected) {
|
|
10317
|
+
return err3(
|
|
10318
|
+
`Unknown pack "${pack}". Valid: ${PACKS.map((p) => p.id).join(", ")}.`
|
|
10319
|
+
);
|
|
10320
|
+
}
|
|
10321
|
+
try {
|
|
10322
|
+
const session = await createTopupCheckout(env, { orgId, pack: selected });
|
|
10323
|
+
writeAudit(env, {
|
|
10324
|
+
tool_name: "wallet_topup_checkout",
|
|
10325
|
+
ocs_method: "stripe.checkout.session.create",
|
|
10326
|
+
status: "ok",
|
|
10327
|
+
dry_run: false,
|
|
10328
|
+
duration_ms: Date.now() - start,
|
|
10329
|
+
sub: props2.sub,
|
|
10330
|
+
reseller_id: props2.reseller_id
|
|
10331
|
+
});
|
|
10332
|
+
return ok3(
|
|
10333
|
+
JSON.stringify({
|
|
10334
|
+
org_id: orgId,
|
|
10335
|
+
pack_id: selected.id,
|
|
10336
|
+
pack_eur_cents: selected.eurCents,
|
|
10337
|
+
bonus_eur_cents: bonusCentsFor(selected),
|
|
10338
|
+
checkout_session_id: session.id,
|
|
10339
|
+
checkout_url: session.url
|
|
10340
|
+
})
|
|
10341
|
+
);
|
|
10342
|
+
} catch (e) {
|
|
10343
|
+
Sentry3.captureException(e);
|
|
10344
|
+
return err3(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
10345
|
+
}
|
|
10346
|
+
}
|
|
10347
|
+
);
|
|
10348
|
+
server2.tool(
|
|
10349
|
+
"wallet_auto_topup",
|
|
10350
|
+
"Charge the organisation's saved card off-session, right now, and credit the Carrier prepaid wallet with the pack. This spends real money without any browser step or human confirmation, so only call it when the user has asked to be charged. Params: `pack_cents` (optional integer, EUR cents \u2014 must match a catalog pack; omit to use the organisation's configured auto-top-up pack). Returns on success: { org_id, charged: true, credited_eur_cents, balance_after_cents, payment_intent_id } \u2014 amounts in EUR cents. Returns { charged: false, reason } as an error when the card declines or no payment method is saved. Do NOT use this to let a human pay in a browser \u2014 use `wallet_topup_checkout`, which only returns a link. Do NOT use this to change the threshold that triggers automatic top-ups; this tool performs one charge and does not edit the configuration. Do NOT use this to credit an OCS reseller account or subscriber \u2014 see `modify_account_balance` and `modify_subscriber_balance`.",
|
|
10351
|
+
{
|
|
10352
|
+
pack_cents: z18.number().int().positive().optional().describe("Override pack amount in EUR cents (must match a catalog pack).")
|
|
10353
|
+
},
|
|
10354
|
+
async ({ pack_cents }) => {
|
|
10355
|
+
const orgId = defaultOrgId;
|
|
10356
|
+
const start = Date.now();
|
|
10357
|
+
try {
|
|
10358
|
+
const result2 = await runAutoTopup(env, { orgId, sub: props2.sub, packCents: pack_cents });
|
|
10359
|
+
writeAudit(env, {
|
|
10360
|
+
tool_name: "wallet_auto_topup",
|
|
10361
|
+
ocs_method: "stripe.payment_intent.off_session",
|
|
10362
|
+
status: result2.charged ? "ok" : "error",
|
|
10363
|
+
dry_run: false,
|
|
10364
|
+
duration_ms: Date.now() - start,
|
|
10365
|
+
sub: props2.sub,
|
|
10366
|
+
reseller_id: props2.reseller_id
|
|
10367
|
+
});
|
|
10368
|
+
if (!result2.charged) {
|
|
10369
|
+
return err3(
|
|
10370
|
+
JSON.stringify({ org_id: orgId, charged: false, reason: result2.reason })
|
|
10371
|
+
);
|
|
10372
|
+
}
|
|
10373
|
+
return ok3(
|
|
10374
|
+
JSON.stringify({
|
|
10375
|
+
org_id: orgId,
|
|
10376
|
+
charged: true,
|
|
10377
|
+
credited_eur_cents: result2.creditedCents,
|
|
10378
|
+
balance_after_cents: result2.balanceAfterCents,
|
|
10379
|
+
payment_intent_id: result2.paymentIntentId,
|
|
10380
|
+
...result2.reason ? { note: result2.reason } : {}
|
|
10381
|
+
})
|
|
10382
|
+
);
|
|
10383
|
+
} catch (e) {
|
|
10384
|
+
Sentry3.captureException(e);
|
|
10385
|
+
return err3(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
10386
|
+
}
|
|
10387
|
+
}
|
|
10388
|
+
);
|
|
10389
|
+
}
|
|
10390
|
+
|
|
10391
|
+
// src/tools-greenzone.ts
|
|
10392
|
+
import { z as z19 } from "zod";
|
|
9994
10393
|
|
|
9995
10394
|
// src/greenzone-whitelist.ts
|
|
9996
10395
|
var STATE_KEY = "greenzone:state";
|
|
@@ -10267,10 +10666,10 @@ function resolvePortalBaseUrl(env) {
|
|
|
10267
10666
|
function getGreenzoneKv(env) {
|
|
10268
10667
|
return env.GREENZONE_STATE_KV ?? null;
|
|
10269
10668
|
}
|
|
10270
|
-
function
|
|
10669
|
+
function ok4(payload) {
|
|
10271
10670
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
10272
10671
|
}
|
|
10273
|
-
function
|
|
10672
|
+
function err4(message) {
|
|
10274
10673
|
return { isError: true, content: [{ type: "text", text: message }] };
|
|
10275
10674
|
}
|
|
10276
10675
|
var KAPTURE_UNAVAILABLE_MSG = "Execute each Kapture step using mcp__kapture__* tools in sequence. Ensure the Kapture extension is active in Chrome before invoking.";
|
|
@@ -10281,21 +10680,21 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
10281
10680
|
title: "Greenzone Whitelist \u2014 Add",
|
|
10282
10681
|
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.",
|
|
10283
10682
|
inputSchema: {
|
|
10284
|
-
host:
|
|
10285
|
-
ip:
|
|
10286
|
-
dry_run:
|
|
10683
|
+
host: z19.string().optional().describe("Hostname to whitelist (e.g. 'example.com')"),
|
|
10684
|
+
ip: z19.string().optional().describe("IP address or CIDR to whitelist (e.g. '203.0.113.0/24')"),
|
|
10685
|
+
dry_run: z19.boolean().optional().describe("If true, return the Kapture steps without executing portal automation")
|
|
10287
10686
|
},
|
|
10288
10687
|
annotations: { destructiveHint: true }
|
|
10289
10688
|
},
|
|
10290
10689
|
async (args) => {
|
|
10291
10690
|
if (!ctx.props.scope.includes("write")) {
|
|
10292
|
-
return
|
|
10691
|
+
return err4(
|
|
10293
10692
|
`Scope denied: greenzone_whitelist_add requires 'write' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
10294
10693
|
);
|
|
10295
10694
|
}
|
|
10296
10695
|
const { host, ip, dry_run } = args;
|
|
10297
10696
|
if (!host && !ip) {
|
|
10298
|
-
return
|
|
10697
|
+
return err4("At least one of 'host' or 'ip' must be provided.");
|
|
10299
10698
|
}
|
|
10300
10699
|
const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
|
|
10301
10700
|
const steps = [
|
|
@@ -10303,7 +10702,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
10303
10702
|
...ip ? buildAddIpSteps(portalBaseUrl, ip) : []
|
|
10304
10703
|
];
|
|
10305
10704
|
if (dry_run) {
|
|
10306
|
-
return
|
|
10705
|
+
return ok4({
|
|
10307
10706
|
dry_run: true,
|
|
10308
10707
|
message: "Would execute the following Kapture steps against the OCS portal:",
|
|
10309
10708
|
kapture_steps: steps,
|
|
@@ -10326,7 +10725,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
10326
10725
|
} else {
|
|
10327
10726
|
kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
|
|
10328
10727
|
}
|
|
10329
|
-
return
|
|
10728
|
+
return ok4({
|
|
10330
10729
|
requires_kapture: true,
|
|
10331
10730
|
message: "Execute the Kapture steps below to apply the change in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
|
|
10332
10731
|
kapture_steps: steps,
|
|
@@ -10342,21 +10741,21 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
10342
10741
|
title: "Greenzone Whitelist \u2014 Remove",
|
|
10343
10742
|
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.",
|
|
10344
10743
|
inputSchema: {
|
|
10345
|
-
host:
|
|
10346
|
-
ip:
|
|
10347
|
-
dry_run:
|
|
10744
|
+
host: z19.string().optional().describe("Hostname to remove from whitelist"),
|
|
10745
|
+
ip: z19.string().optional().describe("IP address or CIDR to remove from whitelist"),
|
|
10746
|
+
dry_run: z19.boolean().optional().describe("If true, return the Kapture steps without executing portal automation")
|
|
10348
10747
|
},
|
|
10349
10748
|
annotations: { destructiveHint: true }
|
|
10350
10749
|
},
|
|
10351
10750
|
async (args) => {
|
|
10352
10751
|
if (!ctx.props.scope.includes("write")) {
|
|
10353
|
-
return
|
|
10752
|
+
return err4(
|
|
10354
10753
|
`Scope denied: greenzone_whitelist_remove requires 'write' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
10355
10754
|
);
|
|
10356
10755
|
}
|
|
10357
10756
|
const { host, ip, dry_run } = args;
|
|
10358
10757
|
if (!host && !ip) {
|
|
10359
|
-
return
|
|
10758
|
+
return err4("At least one of 'host' or 'ip' must be provided.");
|
|
10360
10759
|
}
|
|
10361
10760
|
const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
|
|
10362
10761
|
const steps = [
|
|
@@ -10364,7 +10763,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
10364
10763
|
...ip ? buildRemoveIpSteps(portalBaseUrl, ip) : []
|
|
10365
10764
|
];
|
|
10366
10765
|
if (dry_run) {
|
|
10367
|
-
return
|
|
10766
|
+
return ok4({
|
|
10368
10767
|
dry_run: true,
|
|
10369
10768
|
message: "Would execute the following Kapture steps to remove from OCS portal:",
|
|
10370
10769
|
kapture_steps: steps,
|
|
@@ -10387,7 +10786,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
10387
10786
|
} else {
|
|
10388
10787
|
kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
|
|
10389
10788
|
}
|
|
10390
|
-
return
|
|
10789
|
+
return ok4({
|
|
10391
10790
|
requires_kapture: true,
|
|
10392
10791
|
message: "Execute the Kapture steps below to apply the removal in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
|
|
10393
10792
|
kapture_steps: steps,
|
|
@@ -10403,7 +10802,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
10403
10802
|
title: "Greenzone Whitelist \u2014 List",
|
|
10404
10803
|
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.",
|
|
10405
10804
|
inputSchema: {
|
|
10406
|
-
from_portal:
|
|
10805
|
+
from_portal: z19.boolean().optional().describe(
|
|
10407
10806
|
"When true, return Kapture steps to read live portal state instead of KV cache"
|
|
10408
10807
|
)
|
|
10409
10808
|
},
|
|
@@ -10411,14 +10810,14 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
10411
10810
|
},
|
|
10412
10811
|
async (args) => {
|
|
10413
10812
|
if (!ctx.props.scope.includes("read")) {
|
|
10414
|
-
return
|
|
10813
|
+
return err4(
|
|
10415
10814
|
`Scope denied: greenzone_whitelist_list requires 'read' scope. Your token has: [${ctx.props.scope.join(", ")}].`
|
|
10416
10815
|
);
|
|
10417
10816
|
}
|
|
10418
10817
|
const { from_portal } = args;
|
|
10419
10818
|
if (from_portal) {
|
|
10420
10819
|
const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
|
|
10421
|
-
return
|
|
10820
|
+
return ok4({
|
|
10422
10821
|
requires_kapture: true,
|
|
10423
10822
|
message: "Execute the Kapture steps below to read live Greenzone whitelist state from the OCS portal.",
|
|
10424
10823
|
kapture_steps: buildListSteps(portalBaseUrl),
|
|
@@ -10427,7 +10826,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
10427
10826
|
}
|
|
10428
10827
|
const kv = getGreenzoneKv(ctx.env);
|
|
10429
10828
|
if (!kv) {
|
|
10430
|
-
return
|
|
10829
|
+
return ok4({
|
|
10431
10830
|
source: "kv_cache",
|
|
10432
10831
|
hosts: [],
|
|
10433
10832
|
ips: [],
|
|
@@ -10439,7 +10838,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
10439
10838
|
}
|
|
10440
10839
|
try {
|
|
10441
10840
|
const state = await readState(kv);
|
|
10442
|
-
return
|
|
10841
|
+
return ok4({
|
|
10443
10842
|
source: "kv_cache",
|
|
10444
10843
|
hosts: state.hosts,
|
|
10445
10844
|
ips: state.ips,
|
|
@@ -10448,14 +10847,14 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
10448
10847
|
reconcile_drift: state.reconcile_drift ?? false
|
|
10449
10848
|
});
|
|
10450
10849
|
} catch (e) {
|
|
10451
|
-
return
|
|
10850
|
+
return err4(`Failed to read Greenzone state from KV: ${e instanceof Error ? e.message : String(e)}`);
|
|
10452
10851
|
}
|
|
10453
10852
|
}
|
|
10454
10853
|
);
|
|
10455
10854
|
}
|
|
10456
10855
|
|
|
10457
10856
|
// src/tools-ui-agent-generic.ts
|
|
10458
|
-
import { z as
|
|
10857
|
+
import { z as z20 } from "zod";
|
|
10459
10858
|
var STDIO_ERROR = {
|
|
10460
10859
|
isError: true,
|
|
10461
10860
|
content: [
|
|
@@ -10476,9 +10875,9 @@ function registerUiAgentGenericTools(server2, _ctx) {
|
|
|
10476
10875
|
title: "Dispatch Generic Steel Browsing Agent",
|
|
10477
10876
|
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.",
|
|
10478
10877
|
inputSchema: {
|
|
10479
|
-
prompt:
|
|
10480
|
-
max_steps:
|
|
10481
|
-
dry_run:
|
|
10878
|
+
prompt: z20.string().min(1).max(4e3).describe("Natural-language task description for the Steel browsing agent."),
|
|
10879
|
+
max_steps: z20.number().int().min(1).max(30).optional().describe("Maximum agent steps (1\u201330, default 15)."),
|
|
10880
|
+
dry_run: z20.boolean().optional().describe("Preview cost estimate without dispatching.")
|
|
10482
10881
|
}
|
|
10483
10882
|
},
|
|
10484
10883
|
async (_args) => STDIO_ERROR
|
|
@@ -10489,7 +10888,7 @@ function registerUiAgentGenericTools(server2, _ctx) {
|
|
|
10489
10888
|
title: "Get Steel Agent Task Status",
|
|
10490
10889
|
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.",
|
|
10491
10890
|
inputSchema: {
|
|
10492
|
-
task_id:
|
|
10891
|
+
task_id: z20.string().describe("Steel task ID returned by ui_agent_ask or any ui_* tool dispatch")
|
|
10493
10892
|
}
|
|
10494
10893
|
},
|
|
10495
10894
|
async (_args) => STDIO_ERROR
|
|
@@ -10497,17 +10896,17 @@ function registerUiAgentGenericTools(server2, _ctx) {
|
|
|
10497
10896
|
}
|
|
10498
10897
|
|
|
10499
10898
|
// src/tools-storefront-logo.ts
|
|
10500
|
-
import { z as
|
|
10501
|
-
var
|
|
10502
|
-
var
|
|
10899
|
+
import { z as z21 } from "zod";
|
|
10900
|
+
var ok5 = (text) => ({ content: [{ type: "text", text }] });
|
|
10901
|
+
var err5 = (text) => ({ isError: true, content: [{ type: "text", text }] });
|
|
10503
10902
|
function registerStorefrontLogoTools(server2, env = {}) {
|
|
10504
10903
|
server2.tool(
|
|
10505
10904
|
"generate_storefront_logo",
|
|
10506
10905
|
"Generate a storefront logo (SVG always; PNG when an image key is configured on the Carrier worker). Works for any authenticated Carrier MCP user \u2014 no local image-API key required. Write the SVG to public/brand/logo.svg in the storefront.",
|
|
10507
10906
|
{
|
|
10508
|
-
name:
|
|
10509
|
-
accent:
|
|
10510
|
-
tagline:
|
|
10907
|
+
name: z21.string().min(1).describe("Brand / shop name, e.g. Bananas"),
|
|
10908
|
+
accent: z21.string().regex(/^#?[0-9A-Fa-f]{6}$/).optional().describe("Accent hex, e.g. #FF8A00"),
|
|
10909
|
+
tagline: z21.string().optional().describe("Optional tagline for AI-enhanced marks")
|
|
10511
10910
|
},
|
|
10512
10911
|
async ({ name, accent, tagline }) => {
|
|
10513
10912
|
try {
|
|
@@ -10518,7 +10917,7 @@ function registerStorefrontLogoTools(server2, env = {}) {
|
|
|
10518
10917
|
tagline,
|
|
10519
10918
|
env: { ...env, ...process.env }
|
|
10520
10919
|
});
|
|
10521
|
-
return
|
|
10920
|
+
return ok5(
|
|
10522
10921
|
JSON.stringify({
|
|
10523
10922
|
brand: name,
|
|
10524
10923
|
source: logo.source,
|
|
@@ -10530,7 +10929,7 @@ function registerStorefrontLogoTools(server2, env = {}) {
|
|
|
10530
10929
|
})
|
|
10531
10930
|
);
|
|
10532
10931
|
} catch (e) {
|
|
10533
|
-
return
|
|
10932
|
+
return err5(e instanceof Error ? e.message : "logo generation failed");
|
|
10534
10933
|
}
|
|
10535
10934
|
}
|
|
10536
10935
|
);
|
|
@@ -10559,7 +10958,15 @@ var stdioEnv = {
|
|
|
10559
10958
|
CARRIER_USERS: null,
|
|
10560
10959
|
DOWNLOADS: null,
|
|
10561
10960
|
MANUS_API_KEY: "",
|
|
10562
|
-
STRIPE_SECRET_KEY: ""
|
|
10961
|
+
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY ?? "",
|
|
10962
|
+
// Prepaid-wallet configuration. Read from the environment so a local operator
|
|
10963
|
+
// who holds these values gets working wallet tools; when absent the tools stay
|
|
10964
|
+
// registered (catalog parity with the Worker) and return an explicit
|
|
10965
|
+
// configuration error instead of silently disappearing from tools/list.
|
|
10966
|
+
ATLAS_BASE_URL: process.env.ATLAS_BASE_URL,
|
|
10967
|
+
CARRIER_INTERNAL_API_KEY: process.env.CARRIER_INTERNAL_API_KEY,
|
|
10968
|
+
WALLET_CHECKOUT_SUCCESS_URL: process.env.WALLET_CHECKOUT_SUCCESS_URL,
|
|
10969
|
+
WALLET_CHECKOUT_CANCEL_URL: process.env.WALLET_CHECKOUT_CANCEL_URL
|
|
10563
10970
|
};
|
|
10564
10971
|
var props = {
|
|
10565
10972
|
sub: "stdio@local",
|
|
@@ -10592,6 +10999,7 @@ registerScheduleAndUsageTools(server, toolCtx);
|
|
|
10592
10999
|
registerAllUiAgentTools(server, toolCtx);
|
|
10593
11000
|
registerUiAgentAskTools(server, toolCtx);
|
|
10594
11001
|
registerStripeConnectTools(server, { env: stdioEnv, props });
|
|
11002
|
+
registerWalletTools(server, { env: stdioEnv, props });
|
|
10595
11003
|
registerGreenzoneTools(server, toolCtx);
|
|
10596
11004
|
registerUiAgentGenericTools(server, toolCtx);
|
|
10597
11005
|
registerStorefrontLogoTools(server, stdioEnv);
|