@carrierllc/mcp 0.13.39 → 0.13.41

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/index.js CHANGED
@@ -80,7 +80,7 @@ import {
80
80
  verifyStorefront,
81
81
  withOcsListSummary,
82
82
  withTemplateResellerId
83
- } from "./chunk-I3Z5WYDG.js";
83
+ } from "./chunk-6OAZJBLA.js";
84
84
  import "./chunk-SHKKVIIA.js";
85
85
 
86
86
  // src/index.ts
@@ -89,8 +89,95 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
89
89
 
90
90
  // src/tools.ts
91
91
  import { z } from "zod";
92
+
93
+ // src/durable-issuance.ts
94
+ async function issueInventory(ctx, args, extra) {
95
+ const key = args.request_key;
96
+ const error = (code, message, detail = {}) => ({ isError: true, content: [{ type: "text", text: JSON.stringify({ ...detail, code, message, request_key: key }) }] });
97
+ const token2 = await ctx.getApiToken?.(extra);
98
+ if (!token2) return error("CARRIER_API_AUTH_REQUIRED", "An authenticated Carrier API credential is required. No dispatch made.");
99
+ const body2 = {
100
+ packageTemplateId: args.packageTemplateId,
101
+ ...args.account_for_subs !== void 0 ? { accountId: args.account_for_subs } : {},
102
+ ...Object.fromEntries(["startDate", "endDate", "validityPeriod"].filter((k) => args[k] !== void 0).map((k) => [k, args[k]])),
103
+ ...args.business_reference !== void 0 ? { businessReference: args.business_reference } : {}
104
+ };
105
+ try {
106
+ const response = await fetch(`${ctx.env.CARRIER_API_BASE_URL ?? "https://api.carrier.llc"}/v1/subscribers/issue-from-inventory`, {
107
+ method: "POST",
108
+ redirect: "error",
109
+ headers: { "Authorization": `Bearer ${token2}`, "Content-Type": "application/json", "X-Idempotency-Key": String(key) },
110
+ body: JSON.stringify(body2)
111
+ });
112
+ const payload = await response.json();
113
+ if (!payload || typeof payload !== "object" || Array.isArray(payload) || response.ok && !payload.data) throw new Error("Malformed API response");
114
+ if (response.status >= 500 && payload.code !== "OUTCOME_UNKNOWN" && payload.upstream_dispatched !== false) {
115
+ return error("OUTCOME_UNKNOWN", "Issuance outcome is unknown. Preserve this request_key for reconciliation; do not issue with a new key.", { api_error: payload });
116
+ }
117
+ return { ...!response.ok ? { isError: true } : {}, content: [{ type: "text", text: JSON.stringify({ ...payload, request_key: key }) }] };
118
+ } catch {
119
+ return error("OUTCOME_UNKNOWN", "Issuance outcome is unknown. Preserve this request_key for reconciliation; do not issue with a new key.");
120
+ }
121
+ }
122
+
123
+ // src/tools.ts
92
124
  import * as Sentry from "@sentry/cloudflare";
93
125
 
126
+ // src/ocs-scoping.ts
127
+ function positiveAccountId(raw) {
128
+ if (typeof raw !== "number" && (typeof raw !== "string" || !/^[1-9]\d*$/.test(raw))) return void 0;
129
+ const id = Number(raw);
130
+ return Number.isSafeInteger(id) && id > 0 ? id : void 0;
131
+ }
132
+ function tenantAccountIds(auth) {
133
+ if (auth.managed) {
134
+ const id = positiveAccountId(auth.accountId);
135
+ return id === void 0 ? [] : [id];
136
+ }
137
+ if (auth.restricted) return (auth.allowedAccountIds ?? []).filter((id) => positiveAccountId(id) !== void 0);
138
+ if (auth.preferredAccountId !== void 0) {
139
+ const id = positiveAccountId(auth.preferredAccountId);
140
+ return id === void 0 ? [] : [id];
141
+ }
142
+ return void 0;
143
+ }
144
+ function managedOwnAccountId(auth) {
145
+ const ids = tenantAccountIds(auth);
146
+ return ids === void 0 ? void 0 : ids[0] ?? -1;
147
+ }
148
+ function subscriberAccountId(data) {
149
+ if (!data || typeof data !== "object") return void 0;
150
+ const root = data;
151
+ const sub = root.subscriber && typeof root.subscriber === "object" ? root.subscriber : root;
152
+ const raw = sub.accountId ?? sub.account_id ?? sub.account;
153
+ if (raw === void 0 || raw === null) return void 0;
154
+ return positiveAccountId(raw);
155
+ }
156
+ function isManagedSubscriberDenied(auth, subscriberData) {
157
+ const ids = tenantAccountIds(auth);
158
+ if (ids === void 0) return false;
159
+ const subAccountId = subscriberAccountId(subscriberData);
160
+ return subAccountId === void 0 || !ids.includes(subAccountId);
161
+ }
162
+ async function assertManagedIccidAccess(baseUrl2, auth, iccid) {
163
+ const ids = tenantAccountIds(auth);
164
+ if (ids === void 0) return { ok: true };
165
+ if (ids.length === 0) return { ok: false, error: "Subscriber not found" };
166
+ try {
167
+ const client2 = new OcsClient(baseUrl2, auth.token);
168
+ const data = await client2.call("getSingleSubscriber", { iccid });
169
+ if (isManagedSubscriberDenied(auth, data)) {
170
+ return { ok: false, error: "Subscriber not found" };
171
+ }
172
+ return { ok: true };
173
+ } catch (err9) {
174
+ return {
175
+ ok: false,
176
+ error: err9 instanceof Error ? err9.message : String(err9)
177
+ };
178
+ }
179
+ }
180
+
94
181
  // ../../packages/carrier-ai/dist/index.js
95
182
  import { AwsClient } from "aws4fetch";
96
183
  var AI_TIER_POLICY = {
@@ -1211,8 +1298,19 @@ var DESTRUCTIVE_TOOLS = /* @__PURE__ */ new Set([
1211
1298
  // S6 live-smoke gap fill (CAR-105)
1212
1299
  "change_network_profile_of_location_zone"
1213
1300
  ]);
1301
+ function assignPackageRefusal(args) {
1302
+ const hasIccid = typeof args.iccid === "string" && args.iccid.trim().length > 0;
1303
+ const conflict = hasIccid && (args.account_for_subs !== void 0 || args.business_reference !== void 0);
1304
+ const invalidKey = !hasIccid && (typeof args.request_key !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$/.test(args.request_key));
1305
+ if (conflict || invalidKey) return { isError: true, content: [{ type: "text", text: JSON.stringify({
1306
+ code: conflict ? "INVALID_ASSIGN_PACKAGE_INPUT" : "IDEMPOTENCY_KEY_REQUIRED",
1307
+ message: conflict ? "iccid and inventory/business reference are mutually exclusive. No changes made." : "Inventory requires a stable request_key (1\u2013200 characters: letters, digits, underscore, dot, colon, hyphen). No changes made.",
1308
+ request_key: args.request_key
1309
+ }) }] };
1310
+ return void 0;
1311
+ }
1214
1312
  function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler, options) {
1215
- return async (args) => {
1313
+ return async (args, extra) => {
1216
1314
  const start = Date.now();
1217
1315
  const isDryRun = args.dry_run === true;
1218
1316
  if (!ctx.props.scope.includes(requiredScope)) {
@@ -1256,6 +1354,11 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler, options)
1256
1354
  ]
1257
1355
  };
1258
1356
  }
1357
+ const refusal = options?.validateArgs?.(args);
1358
+ if (refusal) {
1359
+ ctx.audit({ tool_name: toolName, ocs_method: ocsMethod, status: "error", dry_run: isDryRun, duration_ms: 0 });
1360
+ return refusal;
1361
+ }
1259
1362
  if (isDryRun && DESTRUCTIVE_TOOLS.has(toolName)) {
1260
1363
  ctx.audit({
1261
1364
  tool_name: toolName,
@@ -1276,8 +1379,9 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler, options)
1276
1379
  }
1277
1380
  let result2;
1278
1381
  try {
1279
- const token2 = await ctx.getUserToken(ctx.props.sub);
1280
- result2 = await handler(args, token2);
1382
+ const skipToken = typeof options?.skipUserToken === "function" ? options.skipUserToken(args) : options?.skipUserToken;
1383
+ const token2 = skipToken ? "" : await ctx.getUserToken(ctx.props.sub);
1384
+ result2 = extra === void 0 ? await handler(args, token2) : await handler(args, token2, extra);
1281
1385
  } catch (err9) {
1282
1386
  try {
1283
1387
  Sentry.captureException(err9, {
@@ -1931,16 +2035,21 @@ function registerAllTools(server2, ctx) {
1931
2035
  server2.registerTool(
1932
2036
  "assign_package",
1933
2037
  {
1934
- title: "Assign Package to Subscriber",
1935
- description: "Use this to assign a one-time prepaid data/voice package to a subscriber from an existing template. The package is active immediately (or at first usage, depending on template settings). Params: `iccid` (subscriber identifier), `packageTemplateId` (integer from `list_package_templates`), `account_for_subs` (integer, optional \u2014 when provided instead of resolving a subscriber, OCS auto-selects a free eSIM from that account and assigns the package; this is the bulk auto-provisioning path). Returns: created package record with `packageId`, `startDate`, `endDate`, and allowances. Do NOT use this for packages that should auto-renew \u2014 use `assign_recurring_package` instead. Do NOT use this to provision a new subscriber end-to-end \u2014 consider `provision_esim_wizard` for a guided flow with dry-run preview.",
1936
- inputSchema: {
1937
- iccid: z.string().describe("The subscriber ICCID"),
1938
- packageTemplateId: z.number().describe("The package template ID to assign"),
1939
- account_for_subs: z.number().optional().describe(
1940
- "Account ID \u2014 when provided, OCS auto-selects a free eSIM from this account for bulk provisioning"
1941
- ),
2038
+ title: "Assign Package or Issue From Inventory",
2039
+ description: "Assign a one-time prepaid package to an existing subscriber using iccid + packageTemplateId. Inventory issuance uses authenticated Carrier API POST /v1/subscribers/issue-from-inventory with request_key as X-Idempotency-Key. API authorization and its durable ledger are authoritative. Keep the same key after unknown outcomes; never automatically issue with a new key. Do not send both iccid and account_for_subs. Use assign_recurring_package for auto-renew.",
2040
+ inputSchema: z.object({
2041
+ iccid: z.string().optional().describe("Existing subscriber ICCID"),
2042
+ packageTemplateId: z.number().int().positive().describe("The package template ID to assign"),
2043
+ account_for_subs: z.number().int().positive().optional().describe("Inventory account sent as API accountId"),
2044
+ request_key: z.string().max(200).regex(/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/).optional().describe("Required stable inventory key; preserve after unknown outcomes"),
2045
+ startDate: z.string().optional(),
2046
+ endDate: z.string().optional(),
2047
+ validityPeriod: z.number().int().positive().optional(),
2048
+ business_reference: z.object({ kind: z.literal("storefront_order"), id: z.string().min(1), line: z.literal("esim"), verb: z.literal("issue") }).strict().optional().describe("API-authorized existing order reference; not proof of ownership"),
1942
2049
  ...DRY_RUN_FIELD
1943
- },
2050
+ }).refine((d) => !((d.account_for_subs !== void 0 || d.business_reference !== void 0) && typeof d.iccid === "string" && d.iccid.trim().length > 0), {
2051
+ message: "iccid and account_for_subs are mutually exclusive \u2014 choose one path"
2052
+ }),
1944
2053
  annotations: { destructiveHint: true }
1945
2054
  },
1946
2055
  wrapHandler(
@@ -1948,17 +2057,34 @@ function registerAllTools(server2, ctx) {
1948
2057
  "affectPackageToSubscriber",
1949
2058
  TOOL_SCOPES["assign_package"],
1950
2059
  ctx,
1951
- async ({ iccid, packageTemplateId, account_for_subs }, token2) => {
1952
- if (account_for_subs !== void 0) {
1953
- return ocsCall(ctx.env, token2, "affectPackageToSubscriber", {
1954
- packageTemplateId,
1955
- accountForSubs: account_for_subs
1956
- });
1957
- }
2060
+ async (args, token2, extra) => {
2061
+ const { iccid, packageTemplateId } = args;
2062
+ if (typeof iccid !== "string" || !iccid.trim()) return issueInventory(ctx, args, extra);
2063
+ const access = await assertManagedIccidAccess(
2064
+ ctx.env.CARRIER_OCS_BASE_URL,
2065
+ { ...ctx.managedAuth, token: token2 },
2066
+ iccid
2067
+ );
2068
+ if (!access.ok) throw new Error(access.error);
1958
2069
  return ocsCall(ctx.env, token2, "affectPackageToSubscriber", {
1959
2070
  subscriber: { iccid },
1960
2071
  packageTemplateId
1961
2072
  });
2073
+ },
2074
+ {
2075
+ validateArgs: assignPackageRefusal,
2076
+ skipUserToken: (args) => typeof args.iccid !== "string" || !args.iccid.trim(),
2077
+ dryRunPayload: (args) => typeof args.iccid === "string" && args.iccid.trim() ? { subscriber: { iccid: args.iccid }, packageTemplateId: args.packageTemplateId } : {
2078
+ method: "POST",
2079
+ path: "/v1/subscribers/issue-from-inventory",
2080
+ headers: { "X-Idempotency-Key": args.request_key },
2081
+ body: {
2082
+ packageTemplateId: args.packageTemplateId,
2083
+ ...args.account_for_subs !== void 0 ? { accountId: args.account_for_subs } : {},
2084
+ ...Object.fromEntries(["startDate", "endDate", "validityPeriod"].filter((k) => args[k] !== void 0).map((k) => [k, args[k]])),
2085
+ ...args.business_reference !== void 0 ? { businessReference: args.business_reference } : {}
2086
+ }
2087
+ }
1962
2088
  }
1963
2089
  )
1964
2090
  );
@@ -6419,6 +6545,9 @@ function denyUnlessScoped(props2, toolName, scopes) {
6419
6545
 
6420
6546
  // src/wallet-tools.ts
6421
6547
  var PACKS = [
6548
+ { id: "pack_25", eurCents: 2500, bonusPct: 0 },
6549
+ { id: "pack_50", eurCents: 5e3, bonusPct: 0 },
6550
+ { id: "pack_100", eurCents: 1e4, bonusPct: 0 },
6422
6551
  { id: "pack_500", eurCents: 5e4, bonusPct: 0 },
6423
6552
  { id: "pack_1000", eurCents: 1e5, bonusPct: 3 },
6424
6553
  { id: "pack_2500", eurCents: 25e4, bonusPct: 6 },
@@ -6673,10 +6802,10 @@ function registerWalletTools(server2, ctx) {
6673
6802
  "wallet_topup_checkout",
6674
6803
  {
6675
6804
  title: "Wallet Top-up Checkout",
6676
- description: "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.",
6805
+ description: "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_25`/`pack_50`/`pack_100`/`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.",
6677
6806
  inputSchema: {
6678
6807
  pack: z7.string().optional().describe(
6679
- "Pack id (pack_500/pack_1000/pack_2500/pack_5000) or exact EUR-cents amount. Omit to list packs."
6808
+ "Pack id (pack_25/pack_50/pack_100/pack_500/pack_1000/pack_2500/pack_5000) or exact EUR-cents amount. Omit to list packs."
6680
6809
  )
6681
6810
  },
6682
6811
  /**
@@ -10586,45 +10715,6 @@ function buildExamples(toolName) {
10586
10715
  // src/list-recent-ocs-events.ts
10587
10716
  import { z as z16 } from "zod";
10588
10717
 
10589
- // src/ocs-scoping.ts
10590
- function managedOwnAccountId(auth) {
10591
- if (!auth.managed || auth.accountId === void 0) return void 0;
10592
- const n = Number(auth.accountId);
10593
- return Number.isFinite(n) ? n : void 0;
10594
- }
10595
- function subscriberAccountId(data) {
10596
- if (!data || typeof data !== "object") return void 0;
10597
- const root = data;
10598
- const sub = root.subscriber && typeof root.subscriber === "object" ? root.subscriber : root;
10599
- const raw = sub.accountId ?? sub.account_id ?? sub.account;
10600
- if (raw === void 0 || raw === null) return void 0;
10601
- const n = Number(raw);
10602
- return Number.isFinite(n) ? n : void 0;
10603
- }
10604
- function isManagedSubscriberDenied(auth, subscriberData) {
10605
- const ownId = managedOwnAccountId(auth);
10606
- if (ownId === void 0) return false;
10607
- const subAccountId = subscriberAccountId(subscriberData);
10608
- if (subAccountId === void 0) return true;
10609
- return subAccountId !== ownId;
10610
- }
10611
- async function assertManagedIccidAccess(baseUrl2, auth, iccid) {
10612
- if (managedOwnAccountId(auth) === void 0) return { ok: true };
10613
- try {
10614
- const client2 = new OcsClient(baseUrl2, auth.token);
10615
- const data = await client2.call("getSingleSubscriber", { iccid });
10616
- if (isManagedSubscriberDenied(auth, data)) {
10617
- return { ok: false, error: "Subscriber not found" };
10618
- }
10619
- return { ok: true };
10620
- } catch (err9) {
10621
- return {
10622
- ok: false,
10623
- error: err9 instanceof Error ? err9.message : String(err9)
10624
- };
10625
- }
10626
- }
10627
-
10628
10718
  // ../../packages/ocs-spec/src/ocs-event-buffer-read.ts
10629
10719
  var LIFECYCLE_RESELLER_ID2 = 1170;
10630
10720
  function ocsEventRingBufferIccidFromDigitsReference(raw) {
@@ -12857,13 +12947,14 @@ function registerUiAgentGenericTools(server2, ctx, runtime) {
12857
12947
  // src/index.ts
12858
12948
  var baseUrl = process.env.CARRIER_OCS_BASE_URL ?? process.env.ESIMVAULT_BASE_URL ?? "https://ocs.esimvault.cloud";
12859
12949
  var token = process.env.CARRIER_OCS_API_TOKEN ?? process.env.ESIMVAULT_API_TOKEN;
12860
- if (!token) {
12950
+ if (!token && !process.env.CARRIER_API_TOKEN) {
12861
12951
  console.error(
12862
- "ERROR: CARRIER_OCS_API_TOKEN (or legacy ESIMVAULT_API_TOKEN) must be set."
12952
+ "ERROR: Set CARRIER_API_TOKEN for durable inventory or CARRIER_OCS_API_TOKEN for explicit OCS tools."
12863
12953
  );
12864
12954
  process.exit(1);
12865
12955
  }
12866
12956
  var stdioEnv = {
12957
+ CARRIER_API_BASE_URL: process.env.CARRIER_API_BASE_URL ?? "https://api.carrier.llc",
12867
12958
  CARRIER_OCS_BASE_URL: baseUrl,
12868
12959
  // Empty on purpose. rotate_credentials reads this and throws a clear
12869
12960
  // message rather than pretending a local install can rotate the vault.
@@ -12931,13 +13022,17 @@ var props = {
12931
13022
  };
12932
13023
  var audit = (_row) => {
12933
13024
  };
12934
- var getUserToken = async (_sub) => token;
13025
+ var getUserToken = async (_sub) => {
13026
+ if (!token) throw new Error("CARRIER_OCS_API_TOKEN is required for explicit OCS tools");
13027
+ return token;
13028
+ };
12935
13029
  var toolCtx = {
12936
13030
  env: stdioEnv,
12937
13031
  props,
12938
13032
  managedAuth: {},
12939
13033
  audit,
12940
- getUserToken
13034
+ getUserToken,
13035
+ getApiToken: async () => process.env.CARRIER_API_TOKEN || void 0
12941
13036
  };
12942
13037
  var server = new McpServer(
12943
13038
  { name: "carrier-mcp", version: CARRIER_VERSION },