@absolutejs/mcp 0.15.1 → 0.16.0

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/CHANGELOG.md CHANGED
@@ -6,6 +6,12 @@ This file is generated by `absolute-changelog` from the entries in
6
6
  `changelog/`. Edit an entry, not this file — and add new ones under
7
7
  `changelog/unreleased/`.
8
8
 
9
+ ## 0.16.0 — 2026-09-11
10
+
11
+ ### Added
12
+
13
+ - **Add account-bound billing status, paginated receipts, bounded usage and reviewed billing-management links**
14
+
9
15
  ## 0.15.1 — 2026-09-11
10
16
 
11
17
  ### Fixed
package/README.md CHANGED
@@ -431,3 +431,5 @@ on the Change Date.
431
431
  ## Secure credit checkout
432
432
 
433
433
  `createCheckoutHandoffTool` and `createPurchaseStatusTool` provide account-bound credit checkout and recovery contracts. The issuer must bind server pricing and identity; route these tools through the commerce guard. Checkout is classified as `external_checkout`, so restricted and unverified channels cannot discover or execute it. Status works at zero credits. Never pass card data in tool input. See [host rules](docs/commerce-host-rules.md).
434
+
435
+ `createBillingReportTools` binds billing status, paginated receipts, and bounded UTC usage reports to authenticated reader callbacks. These non-transactional tools remain available at zero balance. The shared billing projections discard payment/provider secrets and enforce reconciled usage breakdowns. `createBillingManagementTool` returns a fixed HTTPS browser page requiring normal browser authentication; because that page can initiate purchases, it retains `external_checkout` commerce classification. Never label a purchase-capable page informational to bypass host restrictions.
package/changelog.json CHANGED
@@ -2,6 +2,16 @@
2
2
  "contract": 1,
3
3
  "name": "@absolutejs/mcp",
4
4
  "releases": [
5
+ {
6
+ "version": "0.16.0",
7
+ "date": "2026-09-11",
8
+ "changes": [
9
+ {
10
+ "kind": "added",
11
+ "summary": "Add account-bound billing status, paginated receipts, bounded usage and reviewed billing-management links"
12
+ }
13
+ ]
14
+ },
5
15
  {
6
16
  "version": "0.15.1",
7
17
  "date": "2026-09-11",
package/dist/commerce.js CHANGED
@@ -145,6 +145,227 @@ var createPurchaseStatusTool = (options) => ({
145
145
  };
146
146
  }
147
147
  });
148
+ // node_modules/@absolutejs/billing/dist/reports.js
149
+ var DAY_MS = 86400000;
150
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
151
+ var record2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
152
+ var only = (input, keys) => {
153
+ if (!record2(input) || Object.keys(input).some((key) => !keys.includes(key)))
154
+ throw new Error("Invalid report input");
155
+ return input;
156
+ };
157
+ var integer = (value) => {
158
+ if (!Number.isSafeInteger(value) || value < 0)
159
+ throw new Error("Invalid report amount");
160
+ return value;
161
+ };
162
+ var iso = (value) => {
163
+ if (!Number.isFinite(Date.parse(value)) || new Date(value).toISOString() !== value)
164
+ throw new Error("Invalid report timestamp");
165
+ return value;
166
+ };
167
+ var day = (value) => {
168
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value) || new Date(value + "T00:00:00.000Z").toISOString().slice(0, 10) !== value)
169
+ throw new Error("Expected a UTC calendar date");
170
+ return value;
171
+ };
172
+ var parseUsageRange = (input, now = new Date) => {
173
+ const args = only(input, ["from", "to"]);
174
+ const tomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) + DAY_MS);
175
+ const to = day(args.to ?? tomorrow.toISOString().slice(0, 10));
176
+ const from = day(args.from ?? new Date(Date.parse(to) - 30 * DAY_MS).toISOString().slice(0, 10));
177
+ const duration = Date.parse(to) - Date.parse(from);
178
+ if (duration <= 0 || duration > 90 * DAY_MS || Date.parse(to) > tomorrow.getTime())
179
+ throw new Error("Choose a range of 1\u201390 UTC days, ending no later than tomorrow");
180
+ return { from, to };
181
+ };
182
+ var parseReceiptPage = (input) => {
183
+ const args = only(input, ["limit", "cursor"]);
184
+ const limit = args.limit ?? 20;
185
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > 50)
186
+ throw new Error("Receipt limit must be 1\u201350");
187
+ if (args.cursor === undefined || args.cursor === null)
188
+ return { limit, cursor: null };
189
+ if (typeof args.cursor !== "string" || args.cursor.length > 256 || !/^[A-Za-z0-9_-]+$/.test(args.cursor))
190
+ throw new Error("Invalid receipt cursor");
191
+ const decoded = JSON.parse(Buffer.from(args.cursor, "base64url").toString("utf8"));
192
+ if (!record2(decoded) || typeof decoded.at !== "string" || typeof decoded.id !== "string" || !UUID.test(decoded.id))
193
+ throw new Error("Invalid receipt cursor");
194
+ return { limit, cursor: { at: iso(decoded.at), id: decoded.id } };
195
+ };
196
+ var projectBillingStatus = (value) => ({
197
+ portalAccess: value.portalAccess === true,
198
+ subscription: value.subscription ? {
199
+ status: String(value.subscription.status).slice(0, 64),
200
+ renewsAt: value.subscription.renewsAt === null ? null : iso(value.subscription.renewsAt),
201
+ cancelAtPeriodEnd: value.subscription.cancelAtPeriodEnd === true
202
+ } : null,
203
+ credits: {
204
+ remaining: integer(value.credits.remaining),
205
+ reserved: integer(value.credits.reserved),
206
+ purchased: integer(value.credits.purchased),
207
+ promotional: integer(value.credits.promotional),
208
+ debt: integer(value.credits.debt)
209
+ },
210
+ automaticRefill: false
211
+ });
212
+ var projectReceiptPage = (value, limit) => {
213
+ if (value.receipts.length > limit)
214
+ throw new Error("Receipt page exceeds its limit");
215
+ if (value.nextCursor !== null)
216
+ parseReceiptPage({ cursor: value.nextCursor });
217
+ return {
218
+ receipts: value.receipts.map((row) => {
219
+ if (!UUID.test(row.id) || !/^[A-Z]{3}$/.test(row.currency) || !["credit_purchase", "initial", "plan_change", "renewal"].includes(row.source) || !["paid", "partially_refunded", "refunded"].includes(row.status) || row.refundedAmountCents > row.amountCents)
220
+ throw new Error("Invalid receipt");
221
+ return {
222
+ id: row.id,
223
+ issuedAt: iso(row.issuedAt),
224
+ currency: row.currency,
225
+ amountCents: integer(row.amountCents),
226
+ refundedAmountCents: integer(row.refundedAmountCents),
227
+ source: row.source,
228
+ status: row.status
229
+ };
230
+ }),
231
+ nextCursor: value.nextCursor
232
+ };
233
+ };
234
+ var projectUsageReport = (value, range) => {
235
+ if (value.from !== range.from || value.to !== range.to || value.byDay.length > 90 || value.byFeature.length > 21)
236
+ throw new Error("Invalid usage report bounds");
237
+ const byDay = value.byDay.map((row) => ({
238
+ day: day(row.day),
239
+ credits: integer(row.credits),
240
+ events: integer(row.events)
241
+ }));
242
+ const byFeature = value.byFeature.map((row) => ({
243
+ feature: row.feature.slice(0, 128),
244
+ credits: integer(row.credits),
245
+ events: integer(row.events)
246
+ }));
247
+ const credits = integer(value.creditsConsumed), events = integer(value.events);
248
+ for (const rows of [byDay, byFeature])
249
+ if (rows.reduce((sum, row) => sum + row.credits, 0) !== credits || rows.reduce((sum, row) => sum + row.events, 0) !== events)
250
+ throw new Error("Usage breakdown does not reconcile");
251
+ if (new Set(byDay.map((row) => row.day)).size !== byDay.length || byDay.some((row) => row.day < range.from || row.day >= range.to))
252
+ throw new Error("Invalid usage days");
253
+ return {
254
+ from: range.from,
255
+ to: range.to,
256
+ creditsConsumed: credits,
257
+ events,
258
+ byDay,
259
+ byFeature
260
+ };
261
+ };
262
+
263
+ // src/billingTools.ts
264
+ var result = (text, data) => ({
265
+ content: [{ type: "text", text }],
266
+ structuredContent: { ...data }
267
+ });
268
+ var empty = (input) => {
269
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).length)
270
+ throw new Error("This tool takes no arguments");
271
+ };
272
+ var createBillingReportTools = (readers) => ({
273
+ get_billing_status: {
274
+ annotations: {
275
+ readOnlyHint: true,
276
+ destructiveHint: false,
277
+ openWorldHint: false
278
+ },
279
+ commerce: {
280
+ action: "entitlement_status",
281
+ categories: ["usage_credits", "subscription"]
282
+ },
283
+ description: "Read subscription and service-credit status for this account, including reserved credits and debt. Does not charge, renew or cancel anything. Available at zero balance.",
284
+ inputSchema: {
285
+ type: "object",
286
+ properties: {},
287
+ additionalProperties: false
288
+ },
289
+ handler: async (input) => {
290
+ empty(input);
291
+ const status = projectBillingStatus(await readers.status());
292
+ return result(`${status.credits.remaining} service credits available, ${status.credits.reserved} reserved. Portal access: ${status.portalAccess ? "enabled" : "not enabled"}. Automatic refill is off.`, status);
293
+ }
294
+ },
295
+ list_receipts: {
296
+ annotations: {
297
+ readOnlyHint: true,
298
+ destructiveHint: false,
299
+ openWorldHint: false
300
+ },
301
+ commerce: {
302
+ action: "entitlement_status",
303
+ categories: ["usage_credits", "subscription"]
304
+ },
305
+ description: "List this account's payment receipts, newest first, with original amounts and refunds recorded to date. Amounts are currency minor units, not consumed credits. Follow nextCursor for more. No payment credentials or provider references are exposed.",
306
+ inputSchema: {
307
+ type: "object",
308
+ properties: {
309
+ limit: { type: "integer", minimum: 1, maximum: 50, default: 20 },
310
+ cursor: { type: ["string", "null"], maxLength: 256 }
311
+ },
312
+ additionalProperties: false
313
+ },
314
+ handler: async (input) => {
315
+ const request = parseReceiptPage(input);
316
+ const page = projectReceiptPage(await readers.receipts(request), request.limit);
317
+ return result(`${page.receipts.length} receipts.${page.nextCursor ? " More receipts are available using nextCursor." : ""}`, page);
318
+ }
319
+ },
320
+ get_usage_report: {
321
+ annotations: {
322
+ readOnlyHint: true,
323
+ destructiveHint: false,
324
+ openWorldHint: false
325
+ },
326
+ commerce: { action: "entitlement_status", categories: ["usage_credits"] },
327
+ description: "Read recorded service-credit consumption by UTC day and feature. from is inclusive and to is exclusive; at most 90 days, default last 30 days including today. Credits consumed are not dollars paid or the assistant's own tokens. Does not estimate ROI or expose internal provider costs.",
328
+ inputSchema: {
329
+ type: "object",
330
+ properties: {
331
+ from: { type: "string", format: "date" },
332
+ to: { type: "string", format: "date" }
333
+ },
334
+ additionalProperties: false
335
+ },
336
+ handler: async (input) => {
337
+ const range = parseUsageRange(input, readers.now?.());
338
+ const usage = projectUsageReport(await readers.usage(range), range);
339
+ return result(`${usage.creditsConsumed} service credits across ${usage.events} recorded events, ${range.from} inclusive to ${range.to} exclusive (UTC).`, usage);
340
+ }
341
+ }
342
+ });
343
+ var createBillingManagementTool = (destination) => {
344
+ const url = new URL(destination);
345
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash)
346
+ throw new Error("Billing management requires a fixed HTTPS page URL");
347
+ return {
348
+ annotations: {
349
+ readOnlyHint: true,
350
+ destructiveHint: false,
351
+ openWorldHint: true
352
+ },
353
+ commerce: {
354
+ action: "external_checkout",
355
+ categories: ["usage_credits", "subscription"]
356
+ },
357
+ description: "Open the service's authenticated billing page to buy credits, review receipts, or manage an existing subscription. Uses a matching browser login; may require sign-in. This link does not grant a login session or authorize any payment.",
358
+ inputSchema: {
359
+ type: "object",
360
+ properties: {},
361
+ additionalProperties: false
362
+ },
363
+ handler: async (input) => {
364
+ empty(input);
365
+ return result(`Manage billing on ${url.hostname}: ${url.href}. Sign in if needed; changes require your confirmation.`, { url: url.href, requiresBrowserAuthentication: true });
366
+ }
367
+ };
368
+ };
148
369
 
149
370
  // src/commerce.ts
150
371
  var COMMERCE_POLICY_VERSION = "2026-09-10.1";
@@ -254,6 +475,8 @@ var evaluateCommerce = (req, context, now = new Date) => {
254
475
  export {
255
476
  COMMERCE_POLICY_SOURCES,
256
477
  COMMERCE_POLICY_VERSION,
478
+ createBillingManagementTool,
479
+ createBillingReportTools,
257
480
  createCheckoutHandoffTool,
258
481
  createCreditBalanceTool,
259
482
  createPurchaseStatusTool,
package/dist/index.js CHANGED
@@ -145,6 +145,227 @@ var createPurchaseStatusTool = (options) => ({
145
145
  };
146
146
  }
147
147
  });
148
+ // node_modules/@absolutejs/billing/dist/reports.js
149
+ var DAY_MS = 86400000;
150
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
151
+ var record2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
152
+ var only = (input, keys) => {
153
+ if (!record2(input) || Object.keys(input).some((key) => !keys.includes(key)))
154
+ throw new Error("Invalid report input");
155
+ return input;
156
+ };
157
+ var integer = (value) => {
158
+ if (!Number.isSafeInteger(value) || value < 0)
159
+ throw new Error("Invalid report amount");
160
+ return value;
161
+ };
162
+ var iso = (value) => {
163
+ if (!Number.isFinite(Date.parse(value)) || new Date(value).toISOString() !== value)
164
+ throw new Error("Invalid report timestamp");
165
+ return value;
166
+ };
167
+ var day = (value) => {
168
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value) || new Date(value + "T00:00:00.000Z").toISOString().slice(0, 10) !== value)
169
+ throw new Error("Expected a UTC calendar date");
170
+ return value;
171
+ };
172
+ var parseUsageRange = (input, now = new Date) => {
173
+ const args = only(input, ["from", "to"]);
174
+ const tomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) + DAY_MS);
175
+ const to = day(args.to ?? tomorrow.toISOString().slice(0, 10));
176
+ const from = day(args.from ?? new Date(Date.parse(to) - 30 * DAY_MS).toISOString().slice(0, 10));
177
+ const duration = Date.parse(to) - Date.parse(from);
178
+ if (duration <= 0 || duration > 90 * DAY_MS || Date.parse(to) > tomorrow.getTime())
179
+ throw new Error("Choose a range of 1\u201390 UTC days, ending no later than tomorrow");
180
+ return { from, to };
181
+ };
182
+ var parseReceiptPage = (input) => {
183
+ const args = only(input, ["limit", "cursor"]);
184
+ const limit = args.limit ?? 20;
185
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > 50)
186
+ throw new Error("Receipt limit must be 1\u201350");
187
+ if (args.cursor === undefined || args.cursor === null)
188
+ return { limit, cursor: null };
189
+ if (typeof args.cursor !== "string" || args.cursor.length > 256 || !/^[A-Za-z0-9_-]+$/.test(args.cursor))
190
+ throw new Error("Invalid receipt cursor");
191
+ const decoded = JSON.parse(Buffer.from(args.cursor, "base64url").toString("utf8"));
192
+ if (!record2(decoded) || typeof decoded.at !== "string" || typeof decoded.id !== "string" || !UUID.test(decoded.id))
193
+ throw new Error("Invalid receipt cursor");
194
+ return { limit, cursor: { at: iso(decoded.at), id: decoded.id } };
195
+ };
196
+ var projectBillingStatus = (value) => ({
197
+ portalAccess: value.portalAccess === true,
198
+ subscription: value.subscription ? {
199
+ status: String(value.subscription.status).slice(0, 64),
200
+ renewsAt: value.subscription.renewsAt === null ? null : iso(value.subscription.renewsAt),
201
+ cancelAtPeriodEnd: value.subscription.cancelAtPeriodEnd === true
202
+ } : null,
203
+ credits: {
204
+ remaining: integer(value.credits.remaining),
205
+ reserved: integer(value.credits.reserved),
206
+ purchased: integer(value.credits.purchased),
207
+ promotional: integer(value.credits.promotional),
208
+ debt: integer(value.credits.debt)
209
+ },
210
+ automaticRefill: false
211
+ });
212
+ var projectReceiptPage = (value, limit) => {
213
+ if (value.receipts.length > limit)
214
+ throw new Error("Receipt page exceeds its limit");
215
+ if (value.nextCursor !== null)
216
+ parseReceiptPage({ cursor: value.nextCursor });
217
+ return {
218
+ receipts: value.receipts.map((row) => {
219
+ if (!UUID.test(row.id) || !/^[A-Z]{3}$/.test(row.currency) || !["credit_purchase", "initial", "plan_change", "renewal"].includes(row.source) || !["paid", "partially_refunded", "refunded"].includes(row.status) || row.refundedAmountCents > row.amountCents)
220
+ throw new Error("Invalid receipt");
221
+ return {
222
+ id: row.id,
223
+ issuedAt: iso(row.issuedAt),
224
+ currency: row.currency,
225
+ amountCents: integer(row.amountCents),
226
+ refundedAmountCents: integer(row.refundedAmountCents),
227
+ source: row.source,
228
+ status: row.status
229
+ };
230
+ }),
231
+ nextCursor: value.nextCursor
232
+ };
233
+ };
234
+ var projectUsageReport = (value, range) => {
235
+ if (value.from !== range.from || value.to !== range.to || value.byDay.length > 90 || value.byFeature.length > 21)
236
+ throw new Error("Invalid usage report bounds");
237
+ const byDay = value.byDay.map((row) => ({
238
+ day: day(row.day),
239
+ credits: integer(row.credits),
240
+ events: integer(row.events)
241
+ }));
242
+ const byFeature = value.byFeature.map((row) => ({
243
+ feature: row.feature.slice(0, 128),
244
+ credits: integer(row.credits),
245
+ events: integer(row.events)
246
+ }));
247
+ const credits = integer(value.creditsConsumed), events = integer(value.events);
248
+ for (const rows of [byDay, byFeature])
249
+ if (rows.reduce((sum, row) => sum + row.credits, 0) !== credits || rows.reduce((sum, row) => sum + row.events, 0) !== events)
250
+ throw new Error("Usage breakdown does not reconcile");
251
+ if (new Set(byDay.map((row) => row.day)).size !== byDay.length || byDay.some((row) => row.day < range.from || row.day >= range.to))
252
+ throw new Error("Invalid usage days");
253
+ return {
254
+ from: range.from,
255
+ to: range.to,
256
+ creditsConsumed: credits,
257
+ events,
258
+ byDay,
259
+ byFeature
260
+ };
261
+ };
262
+
263
+ // src/billingTools.ts
264
+ var result = (text, data) => ({
265
+ content: [{ type: "text", text }],
266
+ structuredContent: { ...data }
267
+ });
268
+ var empty = (input) => {
269
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).length)
270
+ throw new Error("This tool takes no arguments");
271
+ };
272
+ var createBillingReportTools = (readers) => ({
273
+ get_billing_status: {
274
+ annotations: {
275
+ readOnlyHint: true,
276
+ destructiveHint: false,
277
+ openWorldHint: false
278
+ },
279
+ commerce: {
280
+ action: "entitlement_status",
281
+ categories: ["usage_credits", "subscription"]
282
+ },
283
+ description: "Read subscription and service-credit status for this account, including reserved credits and debt. Does not charge, renew or cancel anything. Available at zero balance.",
284
+ inputSchema: {
285
+ type: "object",
286
+ properties: {},
287
+ additionalProperties: false
288
+ },
289
+ handler: async (input) => {
290
+ empty(input);
291
+ const status = projectBillingStatus(await readers.status());
292
+ return result(`${status.credits.remaining} service credits available, ${status.credits.reserved} reserved. Portal access: ${status.portalAccess ? "enabled" : "not enabled"}. Automatic refill is off.`, status);
293
+ }
294
+ },
295
+ list_receipts: {
296
+ annotations: {
297
+ readOnlyHint: true,
298
+ destructiveHint: false,
299
+ openWorldHint: false
300
+ },
301
+ commerce: {
302
+ action: "entitlement_status",
303
+ categories: ["usage_credits", "subscription"]
304
+ },
305
+ description: "List this account's payment receipts, newest first, with original amounts and refunds recorded to date. Amounts are currency minor units, not consumed credits. Follow nextCursor for more. No payment credentials or provider references are exposed.",
306
+ inputSchema: {
307
+ type: "object",
308
+ properties: {
309
+ limit: { type: "integer", minimum: 1, maximum: 50, default: 20 },
310
+ cursor: { type: ["string", "null"], maxLength: 256 }
311
+ },
312
+ additionalProperties: false
313
+ },
314
+ handler: async (input) => {
315
+ const request = parseReceiptPage(input);
316
+ const page = projectReceiptPage(await readers.receipts(request), request.limit);
317
+ return result(`${page.receipts.length} receipts.${page.nextCursor ? " More receipts are available using nextCursor." : ""}`, page);
318
+ }
319
+ },
320
+ get_usage_report: {
321
+ annotations: {
322
+ readOnlyHint: true,
323
+ destructiveHint: false,
324
+ openWorldHint: false
325
+ },
326
+ commerce: { action: "entitlement_status", categories: ["usage_credits"] },
327
+ description: "Read recorded service-credit consumption by UTC day and feature. from is inclusive and to is exclusive; at most 90 days, default last 30 days including today. Credits consumed are not dollars paid or the assistant's own tokens. Does not estimate ROI or expose internal provider costs.",
328
+ inputSchema: {
329
+ type: "object",
330
+ properties: {
331
+ from: { type: "string", format: "date" },
332
+ to: { type: "string", format: "date" }
333
+ },
334
+ additionalProperties: false
335
+ },
336
+ handler: async (input) => {
337
+ const range = parseUsageRange(input, readers.now?.());
338
+ const usage = projectUsageReport(await readers.usage(range), range);
339
+ return result(`${usage.creditsConsumed} service credits across ${usage.events} recorded events, ${range.from} inclusive to ${range.to} exclusive (UTC).`, usage);
340
+ }
341
+ }
342
+ });
343
+ var createBillingManagementTool = (destination) => {
344
+ const url = new URL(destination);
345
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash)
346
+ throw new Error("Billing management requires a fixed HTTPS page URL");
347
+ return {
348
+ annotations: {
349
+ readOnlyHint: true,
350
+ destructiveHint: false,
351
+ openWorldHint: true
352
+ },
353
+ commerce: {
354
+ action: "external_checkout",
355
+ categories: ["usage_credits", "subscription"]
356
+ },
357
+ description: "Open the service's authenticated billing page to buy credits, review receipts, or manage an existing subscription. Uses a matching browser login; may require sign-in. This link does not grant a login session or authorize any payment.",
358
+ inputSchema: {
359
+ type: "object",
360
+ properties: {},
361
+ additionalProperties: false
362
+ },
363
+ handler: async (input) => {
364
+ empty(input);
365
+ return result(`Manage billing on ${url.hostname}: ${url.href}. Sign in if needed; changes require your confirmation.`, { url: url.href, requiresBrowserAuthentication: true });
366
+ }
367
+ };
368
+ };
148
369
 
149
370
  // src/commerce.ts
150
371
  var COMMERCE_POLICY_VERSION = "2026-09-10.1";
@@ -373,7 +594,7 @@ var createMcpClient = (options) => {
373
594
  let sessionId = null;
374
595
  let nextId = 1;
375
596
  const authorizationHeaders = (method) => options.authorization?.headers({ method, url: options.url }) ?? {};
376
- const respond = async (id, result) => {
597
+ const respond = async (id, result2) => {
377
598
  const headers = {
378
599
  "content-type": "application/json",
379
600
  "mcp-protocol-version": protocolVersion,
@@ -383,7 +604,7 @@ var createMcpClient = (options) => {
383
604
  if (sessionId !== null)
384
605
  headers["mcp-session-id"] = sessionId;
385
606
  await doFetch(options.url, {
386
- body: JSON.stringify({ id, jsonrpc: "2.0", result }),
607
+ body: JSON.stringify({ id, jsonrpc: "2.0", result: result2 }),
387
608
  headers,
388
609
  method: "POST"
389
610
  }).catch(() => {
@@ -404,8 +625,8 @@ var createMcpClient = (options) => {
404
625
  mode: "form",
405
626
  requestedSchema: isRecord(params.requestedSchema) ? params.requestedSchema : {}
406
627
  };
407
- const result = options.onElicit ? await options.onElicit(request) : { action: "decline" };
408
- await respond(message.id, request.mode === "url" && result.action === "accept" ? { action: "accept" } : result);
628
+ const result2 = options.onElicit ? await options.onElicit(request) : { action: "decline" };
629
+ await respond(message.id, request.mode === "url" && result2.action === "accept" ? { action: "accept" } : result2);
409
630
  };
410
631
  const rpc = async (method, params) => {
411
632
  const controller = new AbortController;
@@ -490,7 +711,7 @@ var createMcpClient = (options) => {
490
711
  });
491
712
  };
492
713
  const initialize = async () => {
493
- const result = await rpc("initialize", {
714
+ const result2 = await rpc("initialize", {
494
715
  capabilities: options.onElicit ? { elicitation: { form: {}, url: {} } } : {},
495
716
  clientInfo: options.clientInfo ?? {
496
717
  name: "@absolutejs/mcp",
@@ -498,19 +719,19 @@ var createMcpClient = (options) => {
498
719
  },
499
720
  protocolVersion
500
721
  });
501
- if (isRecord(result) && typeof result.protocolVersion === "string") {
502
- protocolVersion = result.protocolVersion;
722
+ if (isRecord(result2) && typeof result2.protocolVersion === "string") {
723
+ protocolVersion = result2.protocolVersion;
503
724
  }
504
725
  await notify("notifications/initialized");
505
- return isRecord(result) ? result : {};
726
+ return isRecord(result2) ? result2 : {};
506
727
  };
507
728
  const MAX_LIST_PAGES = 40;
508
729
  const listTools = async () => {
509
730
  const collected = [];
510
731
  let cursor;
511
732
  for (let page = 0;page < MAX_LIST_PAGES; page += 1) {
512
- const result = await rpc("tools/list", cursor === undefined ? undefined : { cursor });
513
- const tools = isRecord(result) && Array.isArray(result.tools) ? result.tools : [];
733
+ const result2 = await rpc("tools/list", cursor === undefined ? undefined : { cursor });
734
+ const tools = isRecord(result2) && Array.isArray(result2.tools) ? result2.tools : [];
514
735
  collected.push(...tools.filter(isRecord).map((tool) => ({
515
736
  annotations: isRecord(tool.annotations) ? tool.annotations : undefined,
516
737
  coaz: typeof tool.coaz === "boolean" ? tool.coaz : undefined,
@@ -520,7 +741,7 @@ var createMcpClient = (options) => {
520
741
  outputSchema: isRecord(tool.outputSchema) ? tool.outputSchema : undefined,
521
742
  taskSupport: isRecord(tool.execution) && (tool.execution.taskSupport === "forbidden" || tool.execution.taskSupport === "optional" || tool.execution.taskSupport === "required") ? tool.execution.taskSupport : undefined
522
743
  })));
523
- const next = isRecord(result) && typeof result.nextCursor === "string" ? result.nextCursor : undefined;
744
+ const next = isRecord(result2) && typeof result2.nextCursor === "string" ? result2.nextCursor : undefined;
524
745
  if (next === undefined)
525
746
  break;
526
747
  cursor = next;
@@ -528,9 +749,9 @@ var createMcpClient = (options) => {
528
749
  return collected;
529
750
  };
530
751
  const callTool = async (name, args) => {
531
- const result = await rpc("tools/call", { arguments: args ?? {}, name });
532
- if (isRecord(result) && Array.isArray(result.content)) {
533
- return result;
752
+ const result2 = await rpc("tools/call", { arguments: args ?? {}, name });
753
+ if (isRecord(result2) && Array.isArray(result2.content)) {
754
+ return result2;
534
755
  }
535
756
  return { content: [], isError: false };
536
757
  };
@@ -541,31 +762,31 @@ var createMcpClient = (options) => {
541
762
  return value;
542
763
  };
543
764
  const callToolAsTask = async (name, args, options2 = {}) => {
544
- const result = await rpc("tools/call", {
765
+ const result2 = await rpc("tools/call", {
545
766
  arguments: args ?? {},
546
767
  name,
547
768
  task: options2.ttl === undefined ? {} : { ttl: options2.ttl }
548
769
  });
549
- return taskFrom(isRecord(result) ? result.task : undefined);
770
+ return taskFrom(isRecord(result2) ? result2.task : undefined);
550
771
  };
551
772
  const getTask = async (taskId) => taskFrom(await rpc("tasks/get", { taskId }));
552
773
  const cancelTask = async (taskId) => taskFrom(await rpc("tasks/cancel", { taskId }));
553
774
  const getTaskResult = async (taskId) => {
554
- const result = await rpc("tasks/result", { taskId });
555
- if (!isRecord(result) || !Array.isArray(result.content)) {
775
+ const result2 = await rpc("tasks/result", { taskId });
776
+ if (!isRecord(result2) || !Array.isArray(result2.content)) {
556
777
  throw new McpClientError("Malformed MCP task result");
557
778
  }
558
- return result;
779
+ return result2;
559
780
  };
560
781
  const listTasks = async () => {
561
782
  const collected = [];
562
783
  let cursor;
563
784
  for (let page = 0;page < MAX_LIST_PAGES; page += 1) {
564
- const result = await rpc("tasks/list", cursor === undefined ? undefined : { cursor });
565
- if (isRecord(result) && Array.isArray(result.tasks)) {
566
- collected.push(...result.tasks.map(taskFrom));
785
+ const result2 = await rpc("tasks/list", cursor === undefined ? undefined : { cursor });
786
+ if (isRecord(result2) && Array.isArray(result2.tasks)) {
787
+ collected.push(...result2.tasks.map(taskFrom));
567
788
  }
568
- const next = isRecord(result) && typeof result.nextCursor === "string" ? result.nextCursor : undefined;
789
+ const next = isRecord(result2) && typeof result2.nextCursor === "string" ? result2.nextCursor : undefined;
569
790
  if (next === undefined)
570
791
  break;
571
792
  cursor = next;
@@ -576,11 +797,11 @@ var createMcpClient = (options) => {
576
797
  const collected = [];
577
798
  let cursor;
578
799
  for (let page = 0;page < MAX_LIST_PAGES; page += 1) {
579
- const result = await rpc("resources/list", cursor === undefined ? undefined : { cursor });
580
- if (isRecord(result) && Array.isArray(result.resources)) {
581
- collected.push(...result.resources);
800
+ const result2 = await rpc("resources/list", cursor === undefined ? undefined : { cursor });
801
+ if (isRecord(result2) && Array.isArray(result2.resources)) {
802
+ collected.push(...result2.resources);
582
803
  }
583
- const next = isRecord(result) && typeof result.nextCursor === "string" ? result.nextCursor : undefined;
804
+ const next = isRecord(result2) && typeof result2.nextCursor === "string" ? result2.nextCursor : undefined;
584
805
  if (next === undefined)
585
806
  break;
586
807
  cursor = next;
@@ -849,17 +1070,17 @@ var createMcpOAuthProvider = (options) => {
849
1070
  redirectUri: options.redirectUri,
850
1071
  scopes
851
1072
  });
852
- const result = await options.onAuthorize({
1073
+ const result2 = await options.onAuthorize({
853
1074
  authorizationUrl: request.authorizationUrl,
854
1075
  state: request.state,
855
1076
  scopes,
856
1077
  resource: found.resource.resource
857
1078
  });
858
- if (result.state !== request.state)
1079
+ if (result2.state !== request.state)
859
1080
  throw new Error("OAuth state mismatch");
860
1081
  const params = new URLSearchParams({
861
1082
  grant_type: "authorization_code",
862
- code: result.code,
1083
+ code: result2.code,
863
1084
  client_id: options.clientId,
864
1085
  redirect_uri: options.redirectUri,
865
1086
  code_verifier: request.codeVerifier,
@@ -899,7 +1120,7 @@ var HTTP_METHOD_NOT_ALLOWED = 405;
899
1120
  var jsonHeaders = {
900
1121
  "content-type": "application/json"
901
1122
  };
902
- var rpcResult = (id, result) => new Response(JSON.stringify({ id, jsonrpc: "2.0", result }), {
1123
+ var rpcResult = (id, result2) => new Response(JSON.stringify({ id, jsonrpc: "2.0", result: result2 }), {
903
1124
  headers: jsonHeaders
904
1125
  });
905
1126
  var rpcError = (id, code, message, data) => new Response(JSON.stringify({
@@ -963,7 +1184,7 @@ var createMemoryMcpTaskStore = () => {
963
1184
  };
964
1185
  };
965
1186
  var publicMcpTask = ({ authorizationKey, ...task }) => {
966
- const { error, inputRequests, pollIntervalMs, result, ttlMs, ...rest } = task;
1187
+ const { error, inputRequests, pollIntervalMs, result: result2, ttlMs, ...rest } = task;
967
1188
  return {
968
1189
  ...rest,
969
1190
  ...pollIntervalMs === undefined ? {} : { pollInterval: pollIntervalMs },
@@ -1142,12 +1363,12 @@ var runTool = async (config, caller, scopes, id, name, args, meta, tool, context
1142
1363
  };
1143
1364
  return normalizeResult(await tool.handler(args, context));
1144
1365
  };
1145
- let result;
1366
+ let result2;
1146
1367
  const eligibility = await commerceDecision(config, caller, name, tool);
1147
1368
  if (eligibility)
1148
1369
  meta.commerceDecision = eligibility;
1149
1370
  if (eligibility?.allowed === false) {
1150
- result = {
1371
+ result2 = {
1151
1372
  content: [
1152
1373
  {
1153
1374
  type: "text",
@@ -1161,7 +1382,7 @@ var runTool = async (config, caller, scopes, id, name, args, meta, tool, context
1161
1382
  }
1162
1383
  };
1163
1384
  } else if (tool.authorization === undefined) {
1164
- result = await invoke();
1385
+ result2 = await invoke();
1165
1386
  } else {
1166
1387
  const agency = config.agency;
1167
1388
  if (agency === undefined)
@@ -1193,7 +1414,7 @@ var runTool = async (config, caller, scopes, id, name, args, meta, tool, context
1193
1414
  meta.agencyActionId = requested.action.actionId;
1194
1415
  meta.agencyDecisionId = requested.decision.decisionId;
1195
1416
  if (requested.decision.kind === "deny") {
1196
- result = {
1417
+ result2 = {
1197
1418
  content: [
1198
1419
  {
1199
1420
  text: requested.decision.requestable ? `Action requires approval (${requested.action.actionId})` : `Action denied: ${requested.decision.reason}`,
@@ -1216,11 +1437,11 @@ var runTool = async (config, caller, scopes, id, name, args, meta, tool, context
1216
1437
  });
1217
1438
  meta.agencyLeaseId = lease.leaseId;
1218
1439
  meta.agencyReceiptId = executed.receipt.receiptId;
1219
- result = executed.result;
1440
+ result2 = executed.result;
1220
1441
  }
1221
1442
  }
1222
- ok = result.isError !== true;
1223
- payload = { id, jsonrpc: "2.0", result };
1443
+ ok = result2.isError !== true;
1444
+ payload = { id, jsonrpc: "2.0", result: result2 };
1224
1445
  } catch (error) {
1225
1446
  const detail = error instanceof Error ? error.message : "unknown error";
1226
1447
  payload = {
@@ -1347,10 +1568,10 @@ var toolsCall = async (config, caller, scopes, id, params, context) => {
1347
1568
  await tasks.store.save(task);
1348
1569
  setTimeout(() => {
1349
1570
  runTool(config, caller, scopes, id, name, args, meta, tool, noElicit).then(async (payload2) => {
1350
- const result = isRecord(payload2) && isRecord(payload2.result) ? payload2.result : { content: [], isError: true };
1571
+ const result2 = isRecord(payload2) && isRecord(payload2.result) ? payload2.result : { content: [], isError: true };
1351
1572
  await tasks.store.update(task.taskId, {
1352
- result,
1353
- status: result.isError === true ? "failed" : "completed"
1573
+ result: result2,
1574
+ status: result2.isError === true ? "failed" : "completed"
1354
1575
  });
1355
1576
  }).catch(async (error) => {
1356
1577
  await tasks.store.update(task.taskId, {
@@ -1414,14 +1635,14 @@ var tasksResult = async (config, caller, id, params, signal) => {
1414
1635
  if (task.error !== undefined) {
1415
1636
  return rpcError(id, typeof task.error.code === "number" ? task.error.code : JSONRPC_INTERNAL_ERROR, typeof task.error.message === "string" ? task.error.message : "Task failed");
1416
1637
  }
1417
- const result = task.result ?? {
1638
+ const result2 = task.result ?? {
1418
1639
  content: [],
1419
1640
  isError: task.status !== "completed"
1420
1641
  };
1421
1642
  return rpcResult(id, {
1422
- ...result,
1643
+ ...result2,
1423
1644
  _meta: {
1424
- ...isRecord(result._meta) ? result._meta : {},
1645
+ ...isRecord(result2._meta) ? result2._meta : {},
1425
1646
  "io.modelcontextprotocol/related-task": { taskId: task.taskId }
1426
1647
  }
1427
1648
  });
@@ -1547,11 +1768,11 @@ var elicitAnswer = async (message, context) => {
1547
1768
  const requestId = typeof message.id === "string" ? message.id : null;
1548
1769
  if (!requestId || !context.sessions)
1549
1770
  return notificationAck();
1550
- const result = isRecord(message.result) ? message.result : null;
1551
- const action = result?.action;
1771
+ const result2 = isRecord(message.result) ? message.result : null;
1772
+ const action = result2?.action;
1552
1773
  const answer = action === "accept" ? {
1553
1774
  action: "accept",
1554
- content: isRecord(result?.content) ? result.content : {}
1775
+ content: isRecord(result2?.content) ? result2.content : {}
1555
1776
  } : action === "decline" ? { action: "decline" } : { action: "cancel" };
1556
1777
  await context.sessions.resolveElicit({
1557
1778
  requestId,
@@ -2003,9 +2224,9 @@ var createPostgresMcpTaskStore = ({
2003
2224
  update: async (taskId, update) => {
2004
2225
  const updatedAt = now().toISOString();
2005
2226
  const data = { ...update, lastUpdatedAt: updatedAt };
2006
- const result = await client.query(`UPDATE ${ns}.tasks SET status = COALESCE($2::text, status), updated_at = $3::timestamptz, data = data || $4::jsonb WHERE task_id = $1 AND status NOT IN ('cancelled','completed','failed') RETURNING data`, [taskId, update.status ?? null, updatedAt, JSON.stringify(data)]);
2007
- if (result.rows[0] !== undefined)
2008
- return result.rows[0].data;
2227
+ const result2 = await client.query(`UPDATE ${ns}.tasks SET status = COALESCE($2::text, status), updated_at = $3::timestamptz, data = data || $4::jsonb WHERE task_id = $1 AND status NOT IN ('cancelled','completed','failed') RETURNING data`, [taskId, update.status ?? null, updatedAt, JSON.stringify(data)]);
2228
+ if (result2.rows[0] !== undefined)
2229
+ return result2.rows[0].data;
2009
2230
  return (await client.query(`SELECT data FROM ${ns}.tasks WHERE task_id = $1`, [taskId])).rows[0]?.data ?? null;
2010
2231
  }
2011
2232
  };
@@ -2037,12 +2258,12 @@ var createPostgresMcpSessionStore = ({
2037
2258
  },
2038
2259
  get: async (id) => {
2039
2260
  const current = now();
2040
- const result = await client.query(`UPDATE ${ns}.sessions SET last_seen_at = $2::timestamptz, expires_at = $3::timestamptz WHERE session_id = $1 AND expires_at > $2::timestamptz RETURNING can_elicit, can_elicit_url`, [
2261
+ const result2 = await client.query(`UPDATE ${ns}.sessions SET last_seen_at = $2::timestamptz, expires_at = $3::timestamptz WHERE session_id = $1 AND expires_at > $2::timestamptz RETURNING can_elicit, can_elicit_url`, [
2041
2262
  id,
2042
2263
  current.toISOString(),
2043
2264
  new Date(current.getTime() + ttlMs).toISOString()
2044
2265
  ]);
2045
- const row = result.rows[0];
2266
+ const row = result2.rows[0];
2046
2267
  return row === undefined ? null : {
2047
2268
  canElicit: row.can_elicit,
2048
2269
  canElicitUrl: row.can_elicit_url
@@ -2094,6 +2315,8 @@ export {
2094
2315
  MCP_LATEST_PROTOCOL_VERSION,
2095
2316
  McpClientError,
2096
2317
  budgetedMcpTool,
2318
+ createBillingManagementTool,
2319
+ createBillingReportTools,
2097
2320
  createCheckoutHandoffTool,
2098
2321
  createCreditBalanceTool,
2099
2322
  createMcpAuthorizationRequest,
@@ -0,0 +1,30 @@
1
+ import { type BillingStatus, type ReceiptPage, type ReceiptPageRequest, type UsageRange, type CustomerUsageReport } from "@absolutejs/billing/reports";
2
+ import type { McpToolRegistry, McpToolResult } from "./types";
3
+ /** Bind each reader to the authenticated account. Keep these reads outside
4
+ * paid-work gates; never include account IDs or provider credentials in input. */
5
+ export declare const createBillingReportTools: (readers: {
6
+ status: () => Promise<BillingStatus>;
7
+ receipts: (request: ReceiptPageRequest) => Promise<ReceiptPage>;
8
+ usage: (range: UsageRange) => Promise<CustomerUsageReport>;
9
+ now?: () => Date;
10
+ }) => McpToolRegistry;
11
+ /** This destination can initiate purchases, so it must retain the checkout
12
+ * classification. A generic billing-page label cannot bypass host rules. */
13
+ export declare const createBillingManagementTool: (destination: string) => {
14
+ annotations: {
15
+ readOnlyHint: boolean;
16
+ destructiveHint: boolean;
17
+ openWorldHint: boolean;
18
+ };
19
+ commerce: {
20
+ action: "external_checkout";
21
+ categories: ("subscription" | "usage_credits")[];
22
+ };
23
+ description: string;
24
+ inputSchema: {
25
+ type: string;
26
+ properties: {};
27
+ additionalProperties: boolean;
28
+ };
29
+ handler: (input: unknown) => Promise<McpToolResult>;
30
+ };
@@ -49,3 +49,4 @@ export type CommerceDecision = {
49
49
  export declare const evaluateCommerce: (req: CommerceRequirement, context: CommerceContext, now?: Date) => CommerceDecision;
50
50
  export { createCreditBalanceTool, type McpCreditBalance } from "./creditStatus";
51
51
  export { createCheckoutHandoffTool, createPurchaseStatusTool, type McpPurchaseStatus, } from "./checkoutTools";
52
+ export { createBillingReportTools, createBillingManagementTool, } from "./billingTools";
@@ -46,3 +46,4 @@ export * from "./commerce";
46
46
  export { createCreditBalanceTool, type McpCreditBalance } from "./creditStatus";
47
47
  export { budgetedMcpTool, type McpCreditWorkRequest } from "./budgetedTool";
48
48
  export { createCheckoutHandoffTool, createPurchaseStatusTool, type McpPurchaseStatus, } from "./checkoutTools";
49
+ export { createBillingReportTools, createBillingManagementTool, } from "./billingTools";
package/package.json CHANGED
@@ -17,7 +17,8 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@absolutejs/manifest": "^0.9.0",
20
- "@sinclair/typebox": "^0.34.0"
20
+ "@sinclair/typebox": "^0.34.0",
21
+ "@absolutejs/billing": "^0.9.0"
21
22
  },
22
23
  "license": "BUSL-1.1",
23
24
  "absolutejs": {
@@ -72,5 +73,5 @@
72
73
  "prepublishOnly": "bun run check:package"
73
74
  },
74
75
  "types": "./dist/src/index.d.ts",
75
- "version": "0.15.1"
76
+ "version": "0.16.0"
76
77
  }