@absolutejs/mcp 0.15.0 → 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,18 @@ 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
+
15
+ ## 0.15.1 — 2026-09-11
16
+
17
+ ### Fixed
18
+
19
+ - **Build and test before publishing so checkout exports are present in the distributed artifact**
20
+
9
21
  ## 0.15.0 — 2026-09-11
10
22
 
11
23
  ### Added
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,26 @@
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
+ },
15
+ {
16
+ "version": "0.15.1",
17
+ "date": "2026-09-11",
18
+ "changes": [
19
+ {
20
+ "kind": "fixed",
21
+ "summary": "Build and test before publishing so checkout exports are present in the distributed artifact"
22
+ }
23
+ ]
24
+ },
5
25
  {
6
26
  "version": "0.15.0",
7
27
  "date": "2026-09-11",
package/dist/commerce.js CHANGED
@@ -59,6 +59,313 @@ var createCreditBalanceTool = (options) => ({
59
59
  };
60
60
  }
61
61
  });
62
+ // src/checkoutTools.ts
63
+ var record = (input) => typeof input === "object" && input !== null && !Array.isArray(input);
64
+ var createCheckoutHandoffTool = (options) => ({
65
+ annotations: {
66
+ readOnlyHint: false,
67
+ destructiveHint: false,
68
+ openWorldHint: true,
69
+ title: "Open credit checkout"
70
+ },
71
+ commerce: { action: "external_checkout", categories: ["usage_credits"] },
72
+ description: "Prepare a short-lived credit-purchase link on the service website. The user reviews and explicitly pays there. Never send card data in chat. Does not charge a card or create a subscription.",
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: { productId: { type: "string", minLength: 1, maxLength: 128 } },
76
+ required: ["productId"],
77
+ additionalProperties: false
78
+ },
79
+ handler: async (input) => {
80
+ if (!record(input) || Object.keys(input).length !== 1 || typeof input.productId !== "string" || !input.productId || input.productId.length > 128)
81
+ throw new Error("A productId is required");
82
+ const result = await options.issue(input.productId);
83
+ const url = new URL(result.url);
84
+ if (url.protocol !== "https:" || url.origin !== new URL(options.origin).origin || url.username || url.password || !Number.isFinite(Date.parse(result.expiresAt)))
85
+ throw new Error("Invalid checkout handoff");
86
+ return {
87
+ content: [
88
+ {
89
+ type: "text",
90
+ text: `Review and pay on ${url.hostname}: ${url.href}. Expires ${result.expiresAt}. No payment has been made.`
91
+ }
92
+ ],
93
+ structuredContent: {
94
+ purchaseId: result.purchaseId,
95
+ url: url.href,
96
+ expiresAt: result.expiresAt
97
+ }
98
+ };
99
+ }
100
+ });
101
+ var createPurchaseStatusTool = (options) => ({
102
+ annotations: {
103
+ readOnlyHint: true,
104
+ destructiveHint: false,
105
+ openWorldHint: false
106
+ },
107
+ commerce: { action: "entitlement_status", categories: ["usage_credits"] },
108
+ description: "Read this account's credit-purchase status. Available at zero balance. Does not initiate or retry payment.",
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: {
112
+ purchaseId: { type: "string", minLength: 1, maxLength: 128 }
113
+ },
114
+ required: ["purchaseId"],
115
+ additionalProperties: false
116
+ },
117
+ handler: async (input) => {
118
+ if (!record(input) || Object.keys(input).length !== 1 || typeof input.purchaseId !== "string" || !input.purchaseId || input.purchaseId.length > 128)
119
+ throw new Error("A purchaseId is required");
120
+ const value = await options.read(input.purchaseId);
121
+ if (!value)
122
+ return "No purchase with that ID exists for this account.";
123
+ if (value.purchaseId !== input.purchaseId || ![
124
+ "not_started",
125
+ "pending",
126
+ "approved",
127
+ "declined",
128
+ "refunded",
129
+ "reconciliation"
130
+ ].includes(value.status) || !Number.isSafeInteger(value.creditsGranted) || value.creditsGranted < 0)
131
+ throw new Error("Purchase status is unavailable");
132
+ const summary = {
133
+ purchaseId: value.purchaseId,
134
+ status: value.status,
135
+ creditsGranted: value.creditsGranted
136
+ };
137
+ return {
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: `Purchase ${summary.status}; ${summary.creditsGranted} credits granted.`
142
+ }
143
+ ],
144
+ structuredContent: summary
145
+ };
146
+ }
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
+ };
62
369
 
63
370
  // src/commerce.ts
64
371
  var COMMERCE_POLICY_VERSION = "2026-09-10.1";
@@ -168,6 +475,10 @@ var evaluateCommerce = (req, context, now = new Date) => {
168
475
  export {
169
476
  COMMERCE_POLICY_SOURCES,
170
477
  COMMERCE_POLICY_VERSION,
478
+ createBillingManagementTool,
479
+ createBillingReportTools,
480
+ createCheckoutHandoffTool,
171
481
  createCreditBalanceTool,
482
+ createPurchaseStatusTool,
172
483
  evaluateCommerce
173
484
  };
package/dist/index.js CHANGED
@@ -59,6 +59,313 @@ var createCreditBalanceTool = (options) => ({
59
59
  };
60
60
  }
61
61
  });
62
+ // src/checkoutTools.ts
63
+ var record = (input) => typeof input === "object" && input !== null && !Array.isArray(input);
64
+ var createCheckoutHandoffTool = (options) => ({
65
+ annotations: {
66
+ readOnlyHint: false,
67
+ destructiveHint: false,
68
+ openWorldHint: true,
69
+ title: "Open credit checkout"
70
+ },
71
+ commerce: { action: "external_checkout", categories: ["usage_credits"] },
72
+ description: "Prepare a short-lived credit-purchase link on the service website. The user reviews and explicitly pays there. Never send card data in chat. Does not charge a card or create a subscription.",
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: { productId: { type: "string", minLength: 1, maxLength: 128 } },
76
+ required: ["productId"],
77
+ additionalProperties: false
78
+ },
79
+ handler: async (input) => {
80
+ if (!record(input) || Object.keys(input).length !== 1 || typeof input.productId !== "string" || !input.productId || input.productId.length > 128)
81
+ throw new Error("A productId is required");
82
+ const result = await options.issue(input.productId);
83
+ const url = new URL(result.url);
84
+ if (url.protocol !== "https:" || url.origin !== new URL(options.origin).origin || url.username || url.password || !Number.isFinite(Date.parse(result.expiresAt)))
85
+ throw new Error("Invalid checkout handoff");
86
+ return {
87
+ content: [
88
+ {
89
+ type: "text",
90
+ text: `Review and pay on ${url.hostname}: ${url.href}. Expires ${result.expiresAt}. No payment has been made.`
91
+ }
92
+ ],
93
+ structuredContent: {
94
+ purchaseId: result.purchaseId,
95
+ url: url.href,
96
+ expiresAt: result.expiresAt
97
+ }
98
+ };
99
+ }
100
+ });
101
+ var createPurchaseStatusTool = (options) => ({
102
+ annotations: {
103
+ readOnlyHint: true,
104
+ destructiveHint: false,
105
+ openWorldHint: false
106
+ },
107
+ commerce: { action: "entitlement_status", categories: ["usage_credits"] },
108
+ description: "Read this account's credit-purchase status. Available at zero balance. Does not initiate or retry payment.",
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: {
112
+ purchaseId: { type: "string", minLength: 1, maxLength: 128 }
113
+ },
114
+ required: ["purchaseId"],
115
+ additionalProperties: false
116
+ },
117
+ handler: async (input) => {
118
+ if (!record(input) || Object.keys(input).length !== 1 || typeof input.purchaseId !== "string" || !input.purchaseId || input.purchaseId.length > 128)
119
+ throw new Error("A purchaseId is required");
120
+ const value = await options.read(input.purchaseId);
121
+ if (!value)
122
+ return "No purchase with that ID exists for this account.";
123
+ if (value.purchaseId !== input.purchaseId || ![
124
+ "not_started",
125
+ "pending",
126
+ "approved",
127
+ "declined",
128
+ "refunded",
129
+ "reconciliation"
130
+ ].includes(value.status) || !Number.isSafeInteger(value.creditsGranted) || value.creditsGranted < 0)
131
+ throw new Error("Purchase status is unavailable");
132
+ const summary = {
133
+ purchaseId: value.purchaseId,
134
+ status: value.status,
135
+ creditsGranted: value.creditsGranted
136
+ };
137
+ return {
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: `Purchase ${summary.status}; ${summary.creditsGranted} credits granted.`
142
+ }
143
+ ],
144
+ structuredContent: summary
145
+ };
146
+ }
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
+ };
62
369
 
63
370
  // src/commerce.ts
64
371
  var COMMERCE_POLICY_VERSION = "2026-09-10.1";
@@ -287,7 +594,7 @@ var createMcpClient = (options) => {
287
594
  let sessionId = null;
288
595
  let nextId = 1;
289
596
  const authorizationHeaders = (method) => options.authorization?.headers({ method, url: options.url }) ?? {};
290
- const respond = async (id, result) => {
597
+ const respond = async (id, result2) => {
291
598
  const headers = {
292
599
  "content-type": "application/json",
293
600
  "mcp-protocol-version": protocolVersion,
@@ -297,7 +604,7 @@ var createMcpClient = (options) => {
297
604
  if (sessionId !== null)
298
605
  headers["mcp-session-id"] = sessionId;
299
606
  await doFetch(options.url, {
300
- body: JSON.stringify({ id, jsonrpc: "2.0", result }),
607
+ body: JSON.stringify({ id, jsonrpc: "2.0", result: result2 }),
301
608
  headers,
302
609
  method: "POST"
303
610
  }).catch(() => {
@@ -318,8 +625,8 @@ var createMcpClient = (options) => {
318
625
  mode: "form",
319
626
  requestedSchema: isRecord(params.requestedSchema) ? params.requestedSchema : {}
320
627
  };
321
- const result = options.onElicit ? await options.onElicit(request) : { action: "decline" };
322
- 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);
323
630
  };
324
631
  const rpc = async (method, params) => {
325
632
  const controller = new AbortController;
@@ -404,7 +711,7 @@ var createMcpClient = (options) => {
404
711
  });
405
712
  };
406
713
  const initialize = async () => {
407
- const result = await rpc("initialize", {
714
+ const result2 = await rpc("initialize", {
408
715
  capabilities: options.onElicit ? { elicitation: { form: {}, url: {} } } : {},
409
716
  clientInfo: options.clientInfo ?? {
410
717
  name: "@absolutejs/mcp",
@@ -412,19 +719,19 @@ var createMcpClient = (options) => {
412
719
  },
413
720
  protocolVersion
414
721
  });
415
- if (isRecord(result) && typeof result.protocolVersion === "string") {
416
- protocolVersion = result.protocolVersion;
722
+ if (isRecord(result2) && typeof result2.protocolVersion === "string") {
723
+ protocolVersion = result2.protocolVersion;
417
724
  }
418
725
  await notify("notifications/initialized");
419
- return isRecord(result) ? result : {};
726
+ return isRecord(result2) ? result2 : {};
420
727
  };
421
728
  const MAX_LIST_PAGES = 40;
422
729
  const listTools = async () => {
423
730
  const collected = [];
424
731
  let cursor;
425
732
  for (let page = 0;page < MAX_LIST_PAGES; page += 1) {
426
- const result = await rpc("tools/list", cursor === undefined ? undefined : { cursor });
427
- 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 : [];
428
735
  collected.push(...tools.filter(isRecord).map((tool) => ({
429
736
  annotations: isRecord(tool.annotations) ? tool.annotations : undefined,
430
737
  coaz: typeof tool.coaz === "boolean" ? tool.coaz : undefined,
@@ -434,7 +741,7 @@ var createMcpClient = (options) => {
434
741
  outputSchema: isRecord(tool.outputSchema) ? tool.outputSchema : undefined,
435
742
  taskSupport: isRecord(tool.execution) && (tool.execution.taskSupport === "forbidden" || tool.execution.taskSupport === "optional" || tool.execution.taskSupport === "required") ? tool.execution.taskSupport : undefined
436
743
  })));
437
- const next = isRecord(result) && typeof result.nextCursor === "string" ? result.nextCursor : undefined;
744
+ const next = isRecord(result2) && typeof result2.nextCursor === "string" ? result2.nextCursor : undefined;
438
745
  if (next === undefined)
439
746
  break;
440
747
  cursor = next;
@@ -442,9 +749,9 @@ var createMcpClient = (options) => {
442
749
  return collected;
443
750
  };
444
751
  const callTool = async (name, args) => {
445
- const result = await rpc("tools/call", { arguments: args ?? {}, name });
446
- if (isRecord(result) && Array.isArray(result.content)) {
447
- return result;
752
+ const result2 = await rpc("tools/call", { arguments: args ?? {}, name });
753
+ if (isRecord(result2) && Array.isArray(result2.content)) {
754
+ return result2;
448
755
  }
449
756
  return { content: [], isError: false };
450
757
  };
@@ -455,31 +762,31 @@ var createMcpClient = (options) => {
455
762
  return value;
456
763
  };
457
764
  const callToolAsTask = async (name, args, options2 = {}) => {
458
- const result = await rpc("tools/call", {
765
+ const result2 = await rpc("tools/call", {
459
766
  arguments: args ?? {},
460
767
  name,
461
768
  task: options2.ttl === undefined ? {} : { ttl: options2.ttl }
462
769
  });
463
- return taskFrom(isRecord(result) ? result.task : undefined);
770
+ return taskFrom(isRecord(result2) ? result2.task : undefined);
464
771
  };
465
772
  const getTask = async (taskId) => taskFrom(await rpc("tasks/get", { taskId }));
466
773
  const cancelTask = async (taskId) => taskFrom(await rpc("tasks/cancel", { taskId }));
467
774
  const getTaskResult = async (taskId) => {
468
- const result = await rpc("tasks/result", { taskId });
469
- if (!isRecord(result) || !Array.isArray(result.content)) {
775
+ const result2 = await rpc("tasks/result", { taskId });
776
+ if (!isRecord(result2) || !Array.isArray(result2.content)) {
470
777
  throw new McpClientError("Malformed MCP task result");
471
778
  }
472
- return result;
779
+ return result2;
473
780
  };
474
781
  const listTasks = async () => {
475
782
  const collected = [];
476
783
  let cursor;
477
784
  for (let page = 0;page < MAX_LIST_PAGES; page += 1) {
478
- const result = await rpc("tasks/list", cursor === undefined ? undefined : { cursor });
479
- if (isRecord(result) && Array.isArray(result.tasks)) {
480
- 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));
481
788
  }
482
- const next = isRecord(result) && typeof result.nextCursor === "string" ? result.nextCursor : undefined;
789
+ const next = isRecord(result2) && typeof result2.nextCursor === "string" ? result2.nextCursor : undefined;
483
790
  if (next === undefined)
484
791
  break;
485
792
  cursor = next;
@@ -490,11 +797,11 @@ var createMcpClient = (options) => {
490
797
  const collected = [];
491
798
  let cursor;
492
799
  for (let page = 0;page < MAX_LIST_PAGES; page += 1) {
493
- const result = await rpc("resources/list", cursor === undefined ? undefined : { cursor });
494
- if (isRecord(result) && Array.isArray(result.resources)) {
495
- 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);
496
803
  }
497
- const next = isRecord(result) && typeof result.nextCursor === "string" ? result.nextCursor : undefined;
804
+ const next = isRecord(result2) && typeof result2.nextCursor === "string" ? result2.nextCursor : undefined;
498
805
  if (next === undefined)
499
806
  break;
500
807
  cursor = next;
@@ -763,17 +1070,17 @@ var createMcpOAuthProvider = (options) => {
763
1070
  redirectUri: options.redirectUri,
764
1071
  scopes
765
1072
  });
766
- const result = await options.onAuthorize({
1073
+ const result2 = await options.onAuthorize({
767
1074
  authorizationUrl: request.authorizationUrl,
768
1075
  state: request.state,
769
1076
  scopes,
770
1077
  resource: found.resource.resource
771
1078
  });
772
- if (result.state !== request.state)
1079
+ if (result2.state !== request.state)
773
1080
  throw new Error("OAuth state mismatch");
774
1081
  const params = new URLSearchParams({
775
1082
  grant_type: "authorization_code",
776
- code: result.code,
1083
+ code: result2.code,
777
1084
  client_id: options.clientId,
778
1085
  redirect_uri: options.redirectUri,
779
1086
  code_verifier: request.codeVerifier,
@@ -813,7 +1120,7 @@ var HTTP_METHOD_NOT_ALLOWED = 405;
813
1120
  var jsonHeaders = {
814
1121
  "content-type": "application/json"
815
1122
  };
816
- 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 }), {
817
1124
  headers: jsonHeaders
818
1125
  });
819
1126
  var rpcError = (id, code, message, data) => new Response(JSON.stringify({
@@ -877,7 +1184,7 @@ var createMemoryMcpTaskStore = () => {
877
1184
  };
878
1185
  };
879
1186
  var publicMcpTask = ({ authorizationKey, ...task }) => {
880
- const { error, inputRequests, pollIntervalMs, result, ttlMs, ...rest } = task;
1187
+ const { error, inputRequests, pollIntervalMs, result: result2, ttlMs, ...rest } = task;
881
1188
  return {
882
1189
  ...rest,
883
1190
  ...pollIntervalMs === undefined ? {} : { pollInterval: pollIntervalMs },
@@ -1056,12 +1363,12 @@ var runTool = async (config, caller, scopes, id, name, args, meta, tool, context
1056
1363
  };
1057
1364
  return normalizeResult(await tool.handler(args, context));
1058
1365
  };
1059
- let result;
1366
+ let result2;
1060
1367
  const eligibility = await commerceDecision(config, caller, name, tool);
1061
1368
  if (eligibility)
1062
1369
  meta.commerceDecision = eligibility;
1063
1370
  if (eligibility?.allowed === false) {
1064
- result = {
1371
+ result2 = {
1065
1372
  content: [
1066
1373
  {
1067
1374
  type: "text",
@@ -1075,7 +1382,7 @@ var runTool = async (config, caller, scopes, id, name, args, meta, tool, context
1075
1382
  }
1076
1383
  };
1077
1384
  } else if (tool.authorization === undefined) {
1078
- result = await invoke();
1385
+ result2 = await invoke();
1079
1386
  } else {
1080
1387
  const agency = config.agency;
1081
1388
  if (agency === undefined)
@@ -1107,7 +1414,7 @@ var runTool = async (config, caller, scopes, id, name, args, meta, tool, context
1107
1414
  meta.agencyActionId = requested.action.actionId;
1108
1415
  meta.agencyDecisionId = requested.decision.decisionId;
1109
1416
  if (requested.decision.kind === "deny") {
1110
- result = {
1417
+ result2 = {
1111
1418
  content: [
1112
1419
  {
1113
1420
  text: requested.decision.requestable ? `Action requires approval (${requested.action.actionId})` : `Action denied: ${requested.decision.reason}`,
@@ -1130,11 +1437,11 @@ var runTool = async (config, caller, scopes, id, name, args, meta, tool, context
1130
1437
  });
1131
1438
  meta.agencyLeaseId = lease.leaseId;
1132
1439
  meta.agencyReceiptId = executed.receipt.receiptId;
1133
- result = executed.result;
1440
+ result2 = executed.result;
1134
1441
  }
1135
1442
  }
1136
- ok = result.isError !== true;
1137
- payload = { id, jsonrpc: "2.0", result };
1443
+ ok = result2.isError !== true;
1444
+ payload = { id, jsonrpc: "2.0", result: result2 };
1138
1445
  } catch (error) {
1139
1446
  const detail = error instanceof Error ? error.message : "unknown error";
1140
1447
  payload = {
@@ -1261,10 +1568,10 @@ var toolsCall = async (config, caller, scopes, id, params, context) => {
1261
1568
  await tasks.store.save(task);
1262
1569
  setTimeout(() => {
1263
1570
  runTool(config, caller, scopes, id, name, args, meta, tool, noElicit).then(async (payload2) => {
1264
- 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 };
1265
1572
  await tasks.store.update(task.taskId, {
1266
- result,
1267
- status: result.isError === true ? "failed" : "completed"
1573
+ result: result2,
1574
+ status: result2.isError === true ? "failed" : "completed"
1268
1575
  });
1269
1576
  }).catch(async (error) => {
1270
1577
  await tasks.store.update(task.taskId, {
@@ -1328,14 +1635,14 @@ var tasksResult = async (config, caller, id, params, signal) => {
1328
1635
  if (task.error !== undefined) {
1329
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");
1330
1637
  }
1331
- const result = task.result ?? {
1638
+ const result2 = task.result ?? {
1332
1639
  content: [],
1333
1640
  isError: task.status !== "completed"
1334
1641
  };
1335
1642
  return rpcResult(id, {
1336
- ...result,
1643
+ ...result2,
1337
1644
  _meta: {
1338
- ...isRecord(result._meta) ? result._meta : {},
1645
+ ...isRecord(result2._meta) ? result2._meta : {},
1339
1646
  "io.modelcontextprotocol/related-task": { taskId: task.taskId }
1340
1647
  }
1341
1648
  });
@@ -1461,11 +1768,11 @@ var elicitAnswer = async (message, context) => {
1461
1768
  const requestId = typeof message.id === "string" ? message.id : null;
1462
1769
  if (!requestId || !context.sessions)
1463
1770
  return notificationAck();
1464
- const result = isRecord(message.result) ? message.result : null;
1465
- const action = result?.action;
1771
+ const result2 = isRecord(message.result) ? message.result : null;
1772
+ const action = result2?.action;
1466
1773
  const answer = action === "accept" ? {
1467
1774
  action: "accept",
1468
- content: isRecord(result?.content) ? result.content : {}
1775
+ content: isRecord(result2?.content) ? result2.content : {}
1469
1776
  } : action === "decline" ? { action: "decline" } : { action: "cancel" };
1470
1777
  await context.sessions.resolveElicit({
1471
1778
  requestId,
@@ -1917,9 +2224,9 @@ var createPostgresMcpTaskStore = ({
1917
2224
  update: async (taskId, update) => {
1918
2225
  const updatedAt = now().toISOString();
1919
2226
  const data = { ...update, lastUpdatedAt: updatedAt };
1920
- 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)]);
1921
- if (result.rows[0] !== undefined)
1922
- 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;
1923
2230
  return (await client.query(`SELECT data FROM ${ns}.tasks WHERE task_id = $1`, [taskId])).rows[0]?.data ?? null;
1924
2231
  }
1925
2232
  };
@@ -1951,12 +2258,12 @@ var createPostgresMcpSessionStore = ({
1951
2258
  },
1952
2259
  get: async (id) => {
1953
2260
  const current = now();
1954
- 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`, [
1955
2262
  id,
1956
2263
  current.toISOString(),
1957
2264
  new Date(current.getTime() + ttlMs).toISOString()
1958
2265
  ]);
1959
- const row = result.rows[0];
2266
+ const row = result2.rows[0];
1960
2267
  return row === undefined ? null : {
1961
2268
  canElicit: row.can_elicit,
1962
2269
  canElicitUrl: row.can_elicit_url
@@ -2008,6 +2315,9 @@ export {
2008
2315
  MCP_LATEST_PROTOCOL_VERSION,
2009
2316
  McpClientError,
2010
2317
  budgetedMcpTool,
2318
+ createBillingManagementTool,
2319
+ createBillingReportTools,
2320
+ createCheckoutHandoffTool,
2011
2321
  createCreditBalanceTool,
2012
2322
  createMcpAuthorizationRequest,
2013
2323
  createMcpClient,
@@ -2017,6 +2327,7 @@ export {
2017
2327
  createMemoryMcpTaskStore,
2018
2328
  createPostgresMcpSessionStore,
2019
2329
  createPostgresMcpTaskStore,
2330
+ createPurchaseStatusTool,
2020
2331
  createSessionRegistry,
2021
2332
  discoverMcpAuthorization,
2022
2333
  dispatchMcp,
package/dist/manifest.js CHANGED
@@ -2861,7 +2861,9 @@ var manifest = defineManifest()({
2861
2861
  tagline: "Let AI assistants connect to your site and use its tools."
2862
2862
  },
2863
2863
  requires: {
2864
- peers: [{ name: "elysia", range: "^2.0.0-beta.6", reason: "plugin host" }]
2864
+ peers: [
2865
+ { name: "elysia", range: "^2.0.0-beta.6", reason: "plugin host" }
2866
+ ]
2865
2867
  },
2866
2868
  settings: Type.Object({
2867
2869
  instructions: Type.Optional(Type.String({
@@ -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
+ };
@@ -0,0 +1,19 @@
1
+ import type { McpTool } from "./types";
2
+ export type McpPurchaseStatus = {
3
+ purchaseId: string;
4
+ status: "not_started" | "pending" | "approved" | "declined" | "refunded" | "reconciliation";
5
+ creditsGranted: number;
6
+ };
7
+ /** The issuer is bound to the authenticated account and canonical server pricing.
8
+ * Register only through the commerce guard. This tool never takes payment data. */
9
+ export declare const createCheckoutHandoffTool: (options: {
10
+ origin: string;
11
+ issue: (productId: string) => Promise<{
12
+ purchaseId: string;
13
+ url: string;
14
+ expiresAt: string;
15
+ }>;
16
+ }) => McpTool;
17
+ export declare const createPurchaseStatusTool: (options: {
18
+ read: (purchaseId: string) => Promise<McpPurchaseStatus | null>;
19
+ }) => McpTool;
@@ -48,3 +48,5 @@ export type CommerceDecision = {
48
48
  * Unverified paths need explicit, fresh server-side review evidence. */
49
49
  export declare const evaluateCommerce: (req: CommerceRequirement, context: CommerceContext, now?: Date) => CommerceDecision;
50
50
  export { createCreditBalanceTool, type McpCreditBalance } from "./creditStatus";
51
+ export { createCheckoutHandoffTool, createPurchaseStatusTool, type McpPurchaseStatus, } from "./checkoutTools";
52
+ export { createBillingReportTools, createBillingManagementTool, } from "./billingTools";
@@ -45,3 +45,5 @@ export type { McpAgencyOptions, McpAudioContent, McpElicitAnswer, McpElicitation
45
45
  export * from "./commerce";
46
46
  export { createCreditBalanceTool, type McpCreditBalance } from "./creditStatus";
47
47
  export { budgetedMcpTool, type McpCreditWorkRequest } from "./budgetedTool";
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": {
@@ -68,9 +69,9 @@
68
69
  "release": "bun run format && bun run test && bun run build && bun publish",
69
70
  "test": "bun test",
70
71
  "typecheck": "tsc --noEmit --project tsconfig.json",
71
- "check:package": "absolute-changelog check",
72
+ "check:package": "bun run typecheck && bun run test && bun run build && absolute-changelog check",
72
73
  "prepublishOnly": "bun run check:package"
73
74
  },
74
75
  "types": "./dist/src/index.d.ts",
75
- "version": "0.15.0"
76
+ "version": "0.16.0"
76
77
  }