@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 +12 -0
- package/README.md +2 -0
- package/changelog.json +20 -0
- package/dist/commerce.js +311 -0
- package/dist/index.js +364 -53
- package/dist/manifest.js +3 -1
- package/dist/src/billingTools.d.ts +30 -0
- package/dist/src/checkoutTools.d.ts +19 -0
- package/dist/src/commerce.d.ts +2 -0
- package/dist/src/index.d.ts +2 -0
- package/package.json +4 -3
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,
|
|
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
|
|
322
|
-
await respond(message.id, request.mode === "url" &&
|
|
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
|
|
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(
|
|
416
|
-
protocolVersion =
|
|
722
|
+
if (isRecord(result2) && typeof result2.protocolVersion === "string") {
|
|
723
|
+
protocolVersion = result2.protocolVersion;
|
|
417
724
|
}
|
|
418
725
|
await notify("notifications/initialized");
|
|
419
|
-
return isRecord(
|
|
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
|
|
427
|
-
const tools = isRecord(
|
|
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(
|
|
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
|
|
446
|
-
if (isRecord(
|
|
447
|
-
return
|
|
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
|
|
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(
|
|
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
|
|
469
|
-
if (!isRecord(
|
|
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
|
|
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
|
|
479
|
-
if (isRecord(
|
|
480
|
-
collected.push(...
|
|
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(
|
|
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
|
|
494
|
-
if (isRecord(
|
|
495
|
-
collected.push(...
|
|
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(
|
|
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
|
|
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 (
|
|
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:
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1440
|
+
result2 = executed.result;
|
|
1134
1441
|
}
|
|
1135
1442
|
}
|
|
1136
|
-
ok =
|
|
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
|
|
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:
|
|
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
|
|
1638
|
+
const result2 = task.result ?? {
|
|
1332
1639
|
content: [],
|
|
1333
1640
|
isError: task.status !== "completed"
|
|
1334
1641
|
};
|
|
1335
1642
|
return rpcResult(id, {
|
|
1336
|
-
...
|
|
1643
|
+
...result2,
|
|
1337
1644
|
_meta: {
|
|
1338
|
-
...isRecord(
|
|
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
|
|
1465
|
-
const 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(
|
|
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
|
|
1921
|
-
if (
|
|
1922
|
-
return
|
|
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
|
|
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 =
|
|
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: [
|
|
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;
|
package/dist/src/commerce.d.ts
CHANGED
|
@@ -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";
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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.
|
|
76
|
+
"version": "0.16.0"
|
|
76
77
|
}
|