@solvapay/mcp-core 0.1.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/LICENSE.md +21 -0
- package/README.md +80 -0
- package/dist/index.cjs +1397 -0
- package/dist/index.d.cts +1167 -0
- package/dist/index.d.ts +1167 -0
- package/dist/index.js +1337 -0
- package/package.json +56 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
DEFAULT_OAUTH_PATHS: () => DEFAULT_OAUTH_PATHS,
|
|
34
|
+
MCP_TOOL_NAMES: () => MCP_TOOL_NAMES,
|
|
35
|
+
McpBearerAuthError: () => McpBearerAuthError,
|
|
36
|
+
NARRATORS: () => NARRATORS,
|
|
37
|
+
OPEN_TOOL_FOR_VIEW: () => OPEN_TOOL_FOR_VIEW,
|
|
38
|
+
SOLVAPAY_DEFAULT_CSP: () => SOLVAPAY_DEFAULT_CSP,
|
|
39
|
+
SOLVAPAY_MCP_VIEW_KINDS: () => SOLVAPAY_MCP_VIEW_KINDS,
|
|
40
|
+
SOLVAPAY_OVERVIEW_MARKDOWN: () => SOLVAPAY_OVERVIEW_MARKDOWN,
|
|
41
|
+
SOLVAPAY_OVERVIEW_MIME_TYPE: () => SOLVAPAY_OVERVIEW_MIME_TYPE,
|
|
42
|
+
SOLVAPAY_OVERVIEW_URI: () => SOLVAPAY_OVERVIEW_URI,
|
|
43
|
+
TOOL_FOR_VIEW: () => TOOL_FOR_VIEW,
|
|
44
|
+
VIEW_FOR_OPEN_TOOL: () => VIEW_FOR_OPEN_TOOL,
|
|
45
|
+
VIEW_FOR_TOOL: () => VIEW_FOR_TOOL,
|
|
46
|
+
balanceSummary: () => balanceSummary,
|
|
47
|
+
buildAuthInfoFromBearer: () => buildAuthInfoFromBearer,
|
|
48
|
+
buildPayableHandler: () => buildPayableHandler,
|
|
49
|
+
buildPaywallUiMeta: () => buildPaywallUiMeta,
|
|
50
|
+
buildSolvaPayDescriptors: () => buildSolvaPayDescriptors,
|
|
51
|
+
buildSolvaPayPrompts: () => buildSolvaPayPrompts,
|
|
52
|
+
buildSolvaPayRequest: () => buildSolvaPayRequest,
|
|
53
|
+
createBuildBootstrapPayload: () => createBuildBootstrapPayload,
|
|
54
|
+
decodeJwtPayload: () => decodeJwtPayload,
|
|
55
|
+
defaultGetCustomerRef: () => defaultGetCustomerRef,
|
|
56
|
+
enrichPurchase: () => enrichPurchase,
|
|
57
|
+
extractBearerToken: () => extractBearerToken,
|
|
58
|
+
getCustomerRefFromBearerAuthHeader: () => getCustomerRefFromBearerAuthHeader,
|
|
59
|
+
getCustomerRefFromJwtPayload: () => getCustomerRefFromJwtPayload,
|
|
60
|
+
getOAuthAuthorizationServerResponse: () => getOAuthAuthorizationServerResponse,
|
|
61
|
+
getOAuthProtectedResourceResponse: () => getOAuthProtectedResourceResponse,
|
|
62
|
+
mergeCsp: () => mergeCsp,
|
|
63
|
+
narrateActivatePlan: () => narrateActivatePlan,
|
|
64
|
+
narrateManageAccount: () => narrateManageAccount,
|
|
65
|
+
narrateTopup: () => narrateTopup,
|
|
66
|
+
narrateUpgrade: () => narrateUpgrade,
|
|
67
|
+
narratedToolResult: () => narratedToolResult,
|
|
68
|
+
parseMode: () => parseMode,
|
|
69
|
+
paywallToolResult: () => paywallToolResult,
|
|
70
|
+
previewJson: () => previewJson,
|
|
71
|
+
resolveOAuthPaths: () => resolveOAuthPaths,
|
|
72
|
+
toolErrorResult: () => toolErrorResult,
|
|
73
|
+
toolResult: () => toolResult,
|
|
74
|
+
uiPlaceholder: () => uiPlaceholder,
|
|
75
|
+
withoutTrailingSlash: () => withoutTrailingSlash
|
|
76
|
+
});
|
|
77
|
+
module.exports = __toCommonJS(index_exports);
|
|
78
|
+
|
|
79
|
+
// src/tool-names.ts
|
|
80
|
+
var MCP_TOOL_NAMES = {
|
|
81
|
+
createPayment: "create_payment_intent",
|
|
82
|
+
processPayment: "process_payment",
|
|
83
|
+
createTopupPayment: "create_topup_payment_intent",
|
|
84
|
+
cancelRenewal: "cancel_renewal",
|
|
85
|
+
reactivateRenewal: "reactivate_renewal",
|
|
86
|
+
activatePlan: "activate_plan",
|
|
87
|
+
createCheckoutSession: "create_checkout_session",
|
|
88
|
+
createCustomerSession: "create_customer_session",
|
|
89
|
+
upgrade: "upgrade",
|
|
90
|
+
manageAccount: "manage_account",
|
|
91
|
+
topup: "topup"
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/types.ts
|
|
95
|
+
var SOLVAPAY_MCP_VIEW_KINDS = [
|
|
96
|
+
"checkout",
|
|
97
|
+
"account",
|
|
98
|
+
"topup",
|
|
99
|
+
"paywall",
|
|
100
|
+
"nudge"
|
|
101
|
+
];
|
|
102
|
+
var TOOL_FOR_VIEW = {
|
|
103
|
+
checkout: "upgrade",
|
|
104
|
+
account: "manage_account",
|
|
105
|
+
topup: "topup"
|
|
106
|
+
};
|
|
107
|
+
var VIEW_FOR_TOOL = Object.fromEntries(
|
|
108
|
+
Object.entries(TOOL_FOR_VIEW).map(([view, tool]) => [
|
|
109
|
+
tool,
|
|
110
|
+
view
|
|
111
|
+
])
|
|
112
|
+
);
|
|
113
|
+
var OPEN_TOOL_FOR_VIEW = TOOL_FOR_VIEW;
|
|
114
|
+
var VIEW_FOR_OPEN_TOOL = VIEW_FOR_TOOL;
|
|
115
|
+
|
|
116
|
+
// src/narrate.ts
|
|
117
|
+
var ZERO_DECIMAL = /* @__PURE__ */ new Set(["bif", "clp", "djf", "gnf", "jpy", "kmf", "krw", "mga", "pyg", "rwf", "ugx", "vnd", "vuv", "xaf", "xof", "xpf"]);
|
|
118
|
+
function formatMoney(amountMinor, currency) {
|
|
119
|
+
if (amountMinor == null || !currency) return null;
|
|
120
|
+
const zero = ZERO_DECIMAL.has(currency.toLowerCase());
|
|
121
|
+
const major = zero ? amountMinor : amountMinor / 100;
|
|
122
|
+
try {
|
|
123
|
+
return new Intl.NumberFormat("en-US", {
|
|
124
|
+
style: "currency",
|
|
125
|
+
currency: currency.toUpperCase(),
|
|
126
|
+
maximumFractionDigits: zero ? 0 : 2
|
|
127
|
+
}).format(major);
|
|
128
|
+
} catch {
|
|
129
|
+
return `${currency.toUpperCase()} ${major.toFixed(zero ? 0 : 2)}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function formatDate(iso) {
|
|
133
|
+
if (!iso) return null;
|
|
134
|
+
try {
|
|
135
|
+
return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function activePurchase(customer) {
|
|
141
|
+
const list = customer?.purchase?.purchases ?? [];
|
|
142
|
+
return list[0] ?? null;
|
|
143
|
+
}
|
|
144
|
+
function productName(data) {
|
|
145
|
+
const name = data.product?.name;
|
|
146
|
+
return typeof name === "string" && name ? name : "SolvaPay";
|
|
147
|
+
}
|
|
148
|
+
function balanceSummary(customer) {
|
|
149
|
+
const row = balanceRow(customer);
|
|
150
|
+
if (!row) return null;
|
|
151
|
+
return row.replace(/^Balance:\s*/, "");
|
|
152
|
+
}
|
|
153
|
+
function balanceRow(customer) {
|
|
154
|
+
if (!customer?.balance) return null;
|
|
155
|
+
const credits = customer.balance.credits ?? 0;
|
|
156
|
+
if (!credits && credits !== 0) return null;
|
|
157
|
+
const currency = customer.balance.displayCurrency;
|
|
158
|
+
const rate = customer.balance.displayExchangeRate ?? 1;
|
|
159
|
+
const majorMinor = currency ? Math.round(credits / rate) : null;
|
|
160
|
+
const money = formatMoney(majorMinor, currency ?? null);
|
|
161
|
+
const fmt = new Intl.NumberFormat("en-US").format(credits);
|
|
162
|
+
return money ? `Balance: ${fmt} credits (~${money})` : `Balance: ${fmt} credits`;
|
|
163
|
+
}
|
|
164
|
+
function commandsLine(commands) {
|
|
165
|
+
return `Commands: ${commands.map((c) => `\`/${c}\``).join(" ")}`;
|
|
166
|
+
}
|
|
167
|
+
function plansListLines(plans) {
|
|
168
|
+
return plans.map((p) => {
|
|
169
|
+
const name = p.name ?? "Plan";
|
|
170
|
+
const price = formatMoney(p.price, p.currency);
|
|
171
|
+
const cycle = p.billingCycle ? `/${p.billingCycle}` : "";
|
|
172
|
+
const type = p.planType === "free" ? "no payment required" : p.planType === "usage-based" ? "pay as you go" : p.planType === "trial" ? "trial" : "recurring";
|
|
173
|
+
if (price && p.planType !== "free") {
|
|
174
|
+
return `${name} \xB7 ${type} \xB7 ${price}${cycle}`;
|
|
175
|
+
}
|
|
176
|
+
return `${name} \xB7 ${type}`;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function hostedPortalLink(data) {
|
|
180
|
+
const url = data.portalUrl;
|
|
181
|
+
if (typeof url === "string" && /^https?:\/\//i.test(url)) {
|
|
182
|
+
return { uri: url, name: "Open hosted portal" };
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
function narrateManageAccount(data) {
|
|
187
|
+
const lines = [];
|
|
188
|
+
const customer = data.customer;
|
|
189
|
+
const active = activePurchase(customer);
|
|
190
|
+
const name = productName(data);
|
|
191
|
+
if (!active) {
|
|
192
|
+
lines.push(`**Welcome to ${name}**`);
|
|
193
|
+
lines.push("");
|
|
194
|
+
const plans = data.plans ?? [];
|
|
195
|
+
if (plans.length > 0) {
|
|
196
|
+
lines.push("No active plan. Plans available:");
|
|
197
|
+
lines.push(...plansListLines(plans));
|
|
198
|
+
} else {
|
|
199
|
+
lines.push("No active plan.");
|
|
200
|
+
}
|
|
201
|
+
lines.push("");
|
|
202
|
+
lines.push(commandsLine(["activate_plan", "upgrade"]));
|
|
203
|
+
} else {
|
|
204
|
+
lines.push(`**${name} \u2014 your account**`);
|
|
205
|
+
lines.push("");
|
|
206
|
+
const plan = active.planSnapshot;
|
|
207
|
+
if (plan) {
|
|
208
|
+
const planName = plan.name ?? "Plan";
|
|
209
|
+
const price = formatMoney(plan.price, plan.currency);
|
|
210
|
+
const cycle = plan.billingCycle ? `/${plan.billingCycle}` : "";
|
|
211
|
+
const end = formatDate(active.endDate);
|
|
212
|
+
const parts = [planName];
|
|
213
|
+
if (price && plan.planType !== "free") parts.push(`${price}${cycle}`);
|
|
214
|
+
if (end) parts.push(`renews ${end}`);
|
|
215
|
+
lines.push(`Plan: ${parts.join(" \xB7 ")}`);
|
|
216
|
+
}
|
|
217
|
+
const bal = balanceRow(customer);
|
|
218
|
+
if (bal) lines.push(bal);
|
|
219
|
+
lines.push("");
|
|
220
|
+
lines.push(commandsLine(["topup", "upgrade"]));
|
|
221
|
+
}
|
|
222
|
+
const links = [];
|
|
223
|
+
const portal = hostedPortalLink(data);
|
|
224
|
+
if (portal) links.push(portal);
|
|
225
|
+
return { text: lines.join("\n"), links };
|
|
226
|
+
}
|
|
227
|
+
function narrateUpgrade(data) {
|
|
228
|
+
const lines = [];
|
|
229
|
+
lines.push(`**Upgrade \u2014 ${productName(data)}**`);
|
|
230
|
+
lines.push("");
|
|
231
|
+
const plans = (data.plans ?? []).filter((p) => p.planType !== "free");
|
|
232
|
+
if (plans.length > 0) {
|
|
233
|
+
lines.push("Plans available:");
|
|
234
|
+
lines.push(...plansListLines(plans));
|
|
235
|
+
} else {
|
|
236
|
+
lines.push("No paid plans are configured on this product yet.");
|
|
237
|
+
}
|
|
238
|
+
lines.push("");
|
|
239
|
+
lines.push(commandsLine(["manage_account", "topup"]));
|
|
240
|
+
return { text: lines.join("\n") };
|
|
241
|
+
}
|
|
242
|
+
function narrateTopup(data) {
|
|
243
|
+
const lines = [];
|
|
244
|
+
lines.push(`**Top up \u2014 ${productName(data)}**`);
|
|
245
|
+
lines.push("");
|
|
246
|
+
const bal = balanceRow(data.customer);
|
|
247
|
+
if (bal) lines.push(bal);
|
|
248
|
+
const currency = data.customer?.balance?.displayCurrency ?? "USD";
|
|
249
|
+
const presets = [1e3, 2500, 5e3, 1e4].map((m) => formatMoney(m, currency)).filter(Boolean).join(" \xB7 ");
|
|
250
|
+
if (presets) lines.push(`Top-up presets: ${presets}`);
|
|
251
|
+
lines.push("");
|
|
252
|
+
lines.push(commandsLine(["manage_account"]));
|
|
253
|
+
return { text: lines.join("\n") };
|
|
254
|
+
}
|
|
255
|
+
function narrateActivatePlan(data) {
|
|
256
|
+
const lines = [];
|
|
257
|
+
lines.push(`**Activate a plan \u2014 ${productName(data)}**`);
|
|
258
|
+
lines.push("");
|
|
259
|
+
const plans = data.plans ?? [];
|
|
260
|
+
if (plans.length > 0) {
|
|
261
|
+
lines.push("Plans available:");
|
|
262
|
+
lines.push(...plansListLines(plans));
|
|
263
|
+
} else {
|
|
264
|
+
lines.push("No plans are configured on this product yet.");
|
|
265
|
+
}
|
|
266
|
+
lines.push("");
|
|
267
|
+
lines.push(commandsLine(["manage_account", "topup"]));
|
|
268
|
+
return { text: lines.join("\n") };
|
|
269
|
+
}
|
|
270
|
+
var NARRATORS = {
|
|
271
|
+
upgrade: narrateUpgrade,
|
|
272
|
+
manage_account: narrateManageAccount,
|
|
273
|
+
topup: narrateTopup,
|
|
274
|
+
activate_plan: narrateActivatePlan
|
|
275
|
+
};
|
|
276
|
+
var UI_OPENED_VERB = {
|
|
277
|
+
topup: (p) => `Opened ${p} top-up.`,
|
|
278
|
+
upgrade: (p) => `Opened ${p} upgrade.`,
|
|
279
|
+
manage_account: (p) => `Opened your ${p} account.`,
|
|
280
|
+
activate_plan: (p) => `Opened ${p} plan picker.`
|
|
281
|
+
};
|
|
282
|
+
function uiPlaceholder(tool, data) {
|
|
283
|
+
const name = productName(data);
|
|
284
|
+
const opened = UI_OPENED_VERB[tool](name);
|
|
285
|
+
const balance = balanceSummary(data.customer);
|
|
286
|
+
const parts = [opened];
|
|
287
|
+
if (balance) parts.push(`Balance: ${balance}.`);
|
|
288
|
+
parts.push("Pass `mode: 'text'` for a markdown summary.");
|
|
289
|
+
return parts.join(" ");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/helpers.ts
|
|
293
|
+
var ZERO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
|
|
294
|
+
"bif",
|
|
295
|
+
"clp",
|
|
296
|
+
"djf",
|
|
297
|
+
"gnf",
|
|
298
|
+
"jpy",
|
|
299
|
+
"kmf",
|
|
300
|
+
"krw",
|
|
301
|
+
"mga",
|
|
302
|
+
"pyg",
|
|
303
|
+
"rwf",
|
|
304
|
+
"ugx",
|
|
305
|
+
"vnd",
|
|
306
|
+
"vuv",
|
|
307
|
+
"xaf",
|
|
308
|
+
"xof",
|
|
309
|
+
"xpf"
|
|
310
|
+
]);
|
|
311
|
+
function formatMinorUnits(amountMinor, currency) {
|
|
312
|
+
if (amountMinor == null || !currency) return null;
|
|
313
|
+
const zeroDecimal = ZERO_DECIMAL_CURRENCIES.has(currency.toLowerCase());
|
|
314
|
+
const fractionDigits = zeroDecimal ? 0 : 2;
|
|
315
|
+
const major = zeroDecimal ? amountMinor : amountMinor / 100;
|
|
316
|
+
try {
|
|
317
|
+
return new Intl.NumberFormat("en-US", {
|
|
318
|
+
style: "currency",
|
|
319
|
+
currency: currency.toUpperCase(),
|
|
320
|
+
minimumFractionDigits: fractionDigits,
|
|
321
|
+
maximumFractionDigits: fractionDigits
|
|
322
|
+
}).format(major);
|
|
323
|
+
} catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function enrichPurchase(purchase) {
|
|
328
|
+
const amount = typeof purchase.amount === "number" ? purchase.amount : void 0;
|
|
329
|
+
const originalAmount = typeof purchase.originalAmount === "number" ? purchase.originalAmount : void 0;
|
|
330
|
+
const currency = typeof purchase.currency === "string" ? purchase.currency : void 0;
|
|
331
|
+
const priceDisplay = formatMinorUnits(originalAmount, currency) ?? formatMinorUnits(amount, "USD");
|
|
332
|
+
const priceUsdDisplay = currency && currency.toUpperCase() !== "USD" ? formatMinorUnits(amount, "USD") : null;
|
|
333
|
+
const planSnapshot = purchase.planSnapshot;
|
|
334
|
+
const enrichedPlanSnapshot = planSnapshot && typeof planSnapshot === "object" ? (() => {
|
|
335
|
+
const snap = planSnapshot;
|
|
336
|
+
const price = typeof snap.price === "number" ? snap.price : void 0;
|
|
337
|
+
const snapCurrency = typeof snap.currency === "string" ? snap.currency : void 0;
|
|
338
|
+
const snapPriceDisplay = formatMinorUnits(price, snapCurrency);
|
|
339
|
+
return snapPriceDisplay ? { ...snap, priceDisplay: snapPriceDisplay } : snap;
|
|
340
|
+
})() : planSnapshot;
|
|
341
|
+
return {
|
|
342
|
+
...purchase,
|
|
343
|
+
...priceDisplay ? { priceDisplay } : {},
|
|
344
|
+
...priceUsdDisplay ? { priceUsdDisplay } : {},
|
|
345
|
+
...enrichedPlanSnapshot !== void 0 ? { planSnapshot: enrichedPlanSnapshot } : {}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function defaultGetCustomerRef(extra) {
|
|
349
|
+
const fromExtra = extra?.authInfo?.extra?.customer_ref;
|
|
350
|
+
if (typeof fromExtra === "string" && fromExtra.trim()) {
|
|
351
|
+
return fromExtra.trim();
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
function buildSolvaPayRequest(extra, options = {}) {
|
|
356
|
+
const {
|
|
357
|
+
method = "GET",
|
|
358
|
+
query,
|
|
359
|
+
body,
|
|
360
|
+
getCustomerRef = defaultGetCustomerRef,
|
|
361
|
+
origin = "http://solvapay-mcp-server.local/"
|
|
362
|
+
} = options;
|
|
363
|
+
const url = new URL(origin);
|
|
364
|
+
if (query) {
|
|
365
|
+
for (const [key, value] of Object.entries(query)) {
|
|
366
|
+
if (value !== void 0) url.searchParams.set(key, value);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const headers = new Headers();
|
|
370
|
+
const customerRef = getCustomerRef(extra);
|
|
371
|
+
if (customerRef) {
|
|
372
|
+
headers.set("x-user-id", customerRef);
|
|
373
|
+
}
|
|
374
|
+
const init = { method, headers };
|
|
375
|
+
if (body !== void 0) {
|
|
376
|
+
headers.set("content-type", "application/json");
|
|
377
|
+
init.body = JSON.stringify(body);
|
|
378
|
+
}
|
|
379
|
+
return new Request(url, init);
|
|
380
|
+
}
|
|
381
|
+
function toolResult(data) {
|
|
382
|
+
return {
|
|
383
|
+
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
384
|
+
structuredContent: data
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function parseMode(raw) {
|
|
388
|
+
if (raw === "ui" || raw === "text" || raw === "auto") return raw;
|
|
389
|
+
return "ui";
|
|
390
|
+
}
|
|
391
|
+
function narratedToolResult(tool, data, mode = "ui", baseMeta = void 0) {
|
|
392
|
+
const narrator = NARRATORS[tool];
|
|
393
|
+
if (!narrator) {
|
|
394
|
+
const fallback = toolResult(data);
|
|
395
|
+
if (mode === "text" && baseMeta && "ui" in baseMeta) {
|
|
396
|
+
const { ui: _ui, ...rest } = baseMeta;
|
|
397
|
+
return { ...fallback, _meta: rest };
|
|
398
|
+
}
|
|
399
|
+
return baseMeta ? { ...fallback, _meta: baseMeta } : fallback;
|
|
400
|
+
}
|
|
401
|
+
const { text, links } = narrator(data);
|
|
402
|
+
const narratedBlock = {
|
|
403
|
+
type: "text",
|
|
404
|
+
text,
|
|
405
|
+
annotations: { audience: ["assistant"] }
|
|
406
|
+
};
|
|
407
|
+
const resourceLinkBlocks = (links ?? []).map((l) => ({
|
|
408
|
+
type: "resource_link",
|
|
409
|
+
uri: l.uri,
|
|
410
|
+
name: l.name,
|
|
411
|
+
annotations: { audience: ["user"] }
|
|
412
|
+
// `resource_link` isn't in the structural content union we use for
|
|
413
|
+
// `SolvaPayCallToolResult`, but the official SDK accepts it — we
|
|
414
|
+
// cast at the boundary to keep the local type narrow while still
|
|
415
|
+
// shipping the enrichment.
|
|
416
|
+
}));
|
|
417
|
+
const placeholderBlock = {
|
|
418
|
+
type: "text",
|
|
419
|
+
text: uiPlaceholder(tool, data)
|
|
420
|
+
};
|
|
421
|
+
const content = mode === "ui" ? [placeholderBlock] : mode === "text" ? [narratedBlock, ...resourceLinkBlocks] : [narratedBlock, ...resourceLinkBlocks];
|
|
422
|
+
const meta = mode === "text" && baseMeta && "ui" in baseMeta ? Object.fromEntries(Object.entries(baseMeta).filter(([k]) => k !== "ui")) : baseMeta;
|
|
423
|
+
return {
|
|
424
|
+
content,
|
|
425
|
+
structuredContent: data,
|
|
426
|
+
...meta ? { _meta: meta } : {}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function toolErrorResult(error) {
|
|
430
|
+
return {
|
|
431
|
+
isError: true,
|
|
432
|
+
content: [{ type: "text", text: JSON.stringify(error) }],
|
|
433
|
+
structuredContent: error
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function previewJson(value, max = 400) {
|
|
437
|
+
try {
|
|
438
|
+
const json = JSON.stringify(value);
|
|
439
|
+
if (!json) return String(value);
|
|
440
|
+
return json.length > max ? `${json.slice(0, max)}\u2026(+${json.length - max} chars)` : json;
|
|
441
|
+
} catch {
|
|
442
|
+
return String(value);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/paywall-meta.ts
|
|
447
|
+
function buildPaywallUiMeta(input) {
|
|
448
|
+
return {
|
|
449
|
+
ui: {
|
|
450
|
+
resourceUri: input.resourceUri
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/paywallToolResult.ts
|
|
456
|
+
var import_server = require("@solvapay/server");
|
|
457
|
+
async function paywallToolResult(errOrGate, ctx) {
|
|
458
|
+
const paywallContent = errOrGate instanceof import_server.PaywallError ? errOrGate.structuredContent : errOrGate;
|
|
459
|
+
const narrationText = errOrGate instanceof import_server.PaywallError ? errOrGate.message : paywallContent.message;
|
|
460
|
+
let structuredContent;
|
|
461
|
+
if (ctx.buildBootstrap) {
|
|
462
|
+
const bootstrap = await ctx.buildBootstrap("paywall", ctx.extra, {
|
|
463
|
+
paywall: paywallContent
|
|
464
|
+
});
|
|
465
|
+
structuredContent = bootstrap;
|
|
466
|
+
} else {
|
|
467
|
+
structuredContent = paywallContent;
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
// Deliberately `false`: paywall is a user-actionable gate, not a
|
|
471
|
+
// tool failure. Hosts short-circuit on `isError: true` and render
|
|
472
|
+
// the error text instead of opening the UI resource advertised by
|
|
473
|
+
// `_meta.ui`, which meant the paywall widget never appeared on
|
|
474
|
+
// MCPJam / Claude Desktop / ChatGPT Apps. The `structuredContent`
|
|
475
|
+
// and `content[0].text` still carry the gate reason so the LLM
|
|
476
|
+
// can narrate it, while `_meta.ui` triggers the widget.
|
|
477
|
+
isError: false,
|
|
478
|
+
content: [{ type: "text", text: narrationText }],
|
|
479
|
+
structuredContent,
|
|
480
|
+
_meta: buildPaywallUiMeta({ resourceUri: ctx.resourceUri })
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/csp.ts
|
|
485
|
+
var SOLVAPAY_DEFAULT_CSP = {
|
|
486
|
+
resourceDomains: [
|
|
487
|
+
"https://js.stripe.com",
|
|
488
|
+
"https://*.stripe.com",
|
|
489
|
+
"https://b.stripecdn.com"
|
|
490
|
+
],
|
|
491
|
+
connectDomains: [
|
|
492
|
+
"https://api.stripe.com",
|
|
493
|
+
"https://m.stripe.com",
|
|
494
|
+
"https://r.stripe.com",
|
|
495
|
+
"https://q.stripe.com",
|
|
496
|
+
"https://errors.stripe.com"
|
|
497
|
+
],
|
|
498
|
+
frameDomains: ["https://js.stripe.com", "https://hooks.stripe.com"]
|
|
499
|
+
};
|
|
500
|
+
function mergeCsp(overrides) {
|
|
501
|
+
if (!overrides) return SOLVAPAY_DEFAULT_CSP;
|
|
502
|
+
const merge = (base, extra) => extra && extra.length ? Array.from(/* @__PURE__ */ new Set([...base, ...extra])) : base;
|
|
503
|
+
return {
|
|
504
|
+
resourceDomains: merge(SOLVAPAY_DEFAULT_CSP.resourceDomains, overrides.resourceDomains),
|
|
505
|
+
connectDomains: merge(SOLVAPAY_DEFAULT_CSP.connectDomains, overrides.connectDomains),
|
|
506
|
+
frameDomains: merge(SOLVAPAY_DEFAULT_CSP.frameDomains, overrides.frameDomains)
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/descriptors.ts
|
|
511
|
+
var import_server3 = require("@solvapay/server");
|
|
512
|
+
var import_zod = require("zod");
|
|
513
|
+
|
|
514
|
+
// src/bootstrap-payload.ts
|
|
515
|
+
var import_server2 = require("@solvapay/server");
|
|
516
|
+
var okOrNull = (result) => (0, import_server2.isErrorResult)(result) ? null : result;
|
|
517
|
+
function createBuildBootstrapPayload(options) {
|
|
518
|
+
const {
|
|
519
|
+
solvaPay,
|
|
520
|
+
productRef,
|
|
521
|
+
publicBaseUrl,
|
|
522
|
+
getCustomerRef = defaultGetCustomerRef
|
|
523
|
+
} = options;
|
|
524
|
+
const fetchPublishableKey = async () => {
|
|
525
|
+
try {
|
|
526
|
+
const platform = await solvaPay.apiClient.getPlatformConfig?.();
|
|
527
|
+
return platform?.stripePublishableKey ?? null;
|
|
528
|
+
} catch {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
const buildRequest = (extra) => buildSolvaPayRequest(extra, { getCustomerRef });
|
|
533
|
+
const productQueryRequest = () => buildSolvaPayRequest(void 0, { query: { productRef }, getCustomerRef: () => null });
|
|
534
|
+
return async (view, extra, extras = {}) => {
|
|
535
|
+
const customerRef = getCustomerRef(extra);
|
|
536
|
+
const wrapError = (promise) => promise.catch((err) => ({
|
|
537
|
+
error: err instanceof Error ? err.message : String(err),
|
|
538
|
+
status: 500
|
|
539
|
+
}));
|
|
540
|
+
const unauthenticated = () => Promise.resolve({ error: "unauthenticated", status: 401 });
|
|
541
|
+
const [
|
|
542
|
+
stripePublishableKey,
|
|
543
|
+
merchantResult,
|
|
544
|
+
productResult,
|
|
545
|
+
plansResult,
|
|
546
|
+
purchaseResult,
|
|
547
|
+
paymentMethodResult,
|
|
548
|
+
balanceResult,
|
|
549
|
+
usageResult
|
|
550
|
+
] = await Promise.all([
|
|
551
|
+
fetchPublishableKey(),
|
|
552
|
+
(0, import_server2.getMerchantCore)(buildRequest(void 0), { solvaPay }),
|
|
553
|
+
(0, import_server2.getProductCore)(productQueryRequest(), { solvaPay }),
|
|
554
|
+
wrapError((0, import_server2.listPlansCore)(productQueryRequest(), { solvaPay })),
|
|
555
|
+
customerRef ? wrapError((0, import_server2.checkPurchaseCore)(buildRequest(extra), { solvaPay })) : unauthenticated(),
|
|
556
|
+
customerRef ? wrapError((0, import_server2.getPaymentMethodCore)(buildRequest(extra), { solvaPay })) : unauthenticated(),
|
|
557
|
+
customerRef ? wrapError((0, import_server2.getCustomerBalanceCore)(buildRequest(extra), { solvaPay })) : unauthenticated(),
|
|
558
|
+
customerRef ? wrapError((0, import_server2.getUsageCore)(buildRequest(extra), { solvaPay })) : unauthenticated()
|
|
559
|
+
]);
|
|
560
|
+
if ((0, import_server2.isErrorResult)(merchantResult)) {
|
|
561
|
+
throw new Error(`bootstrap: merchant lookup failed: ${merchantResult.error}`);
|
|
562
|
+
}
|
|
563
|
+
if ((0, import_server2.isErrorResult)(productResult)) {
|
|
564
|
+
throw new Error(`bootstrap: product lookup failed: ${productResult.error}`);
|
|
565
|
+
}
|
|
566
|
+
const plans = (0, import_server2.isErrorResult)(plansResult) ? [] : plansResult.plans;
|
|
567
|
+
const purchase = okOrNull(purchaseResult);
|
|
568
|
+
const enrichedPurchase = purchase ? {
|
|
569
|
+
...purchase,
|
|
570
|
+
purchases: purchase.purchases.map(
|
|
571
|
+
(p) => enrichPurchase(p)
|
|
572
|
+
)
|
|
573
|
+
} : null;
|
|
574
|
+
const customer = customerRef ? {
|
|
575
|
+
ref: customerRef,
|
|
576
|
+
purchase: enrichedPurchase,
|
|
577
|
+
paymentMethod: okOrNull(paymentMethodResult),
|
|
578
|
+
balance: okOrNull(balanceResult),
|
|
579
|
+
usage: okOrNull(usageResult)
|
|
580
|
+
} : null;
|
|
581
|
+
const payload = {
|
|
582
|
+
view,
|
|
583
|
+
productRef,
|
|
584
|
+
stripePublishableKey,
|
|
585
|
+
returnUrl: publicBaseUrl,
|
|
586
|
+
merchant: merchantResult,
|
|
587
|
+
product: productResult,
|
|
588
|
+
plans,
|
|
589
|
+
customer
|
|
590
|
+
};
|
|
591
|
+
if (extras.paywall) payload.paywall = extras.paywall;
|
|
592
|
+
return payload;
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/resources/overview.ts
|
|
597
|
+
var SOLVAPAY_OVERVIEW_URI = "docs://solvapay/overview.md";
|
|
598
|
+
var SOLVAPAY_OVERVIEW_MIME_TYPE = "text/markdown";
|
|
599
|
+
var SOLVAPAY_OVERVIEW_MARKDOWN = `# SolvaPay MCP server \u2014 overview
|
|
600
|
+
|
|
601
|
+
This MCP server connects a SolvaPay-protected product to your host so users can
|
|
602
|
+
manage their plan, usage, and billing **without leaving the chat**. It is
|
|
603
|
+
dual-audience: every tool returns a UI bootstrap for hosts that render MCP UI
|
|
604
|
+
resources (\`basic-host\`, Claude Desktop, ChatGPT, etc.) and a markdown summary
|
|
605
|
+
with clickable URLs for text-only hosts.
|
|
606
|
+
|
|
607
|
+
## What the user can do
|
|
608
|
+
|
|
609
|
+
- **Upgrade** \u2014 start or change a paid plan. \`/upgrade\` opens the embedded
|
|
610
|
+
checkout (Stripe Elements inline) on UI hosts and returns a hosted-checkout
|
|
611
|
+
URL on text hosts.
|
|
612
|
+
- **Manage account** \u2014 current plan, balance, usage, payment method, cancel
|
|
613
|
+
or reactivate auto-renewal. Credits + usage are folded inline into the
|
|
614
|
+
account view. \`/manage_account\`.
|
|
615
|
+
- **Top up credits** \u2014 add SolvaPay credits for usage-based plans.
|
|
616
|
+
\`/topup\`.
|
|
617
|
+
- **Activate plan** \u2014 pick a plan from the list or activate a specific plan
|
|
618
|
+
by \`planRef\`. Free plans activate immediately, usage-based plans activate
|
|
619
|
+
when balance covers the configured usage, paid plans return the embedded
|
|
620
|
+
checkout or a hosted-checkout URL. \`/activate_plan\`.
|
|
621
|
+
|
|
622
|
+
## How it fits together
|
|
623
|
+
|
|
624
|
+
Each intent tool returns a full \`BootstrapPayload\` (merchant + product + plans
|
|
625
|
+
+ customer snapshot) so the embedded UI never fires per-view read calls. When
|
|
626
|
+
a paywalled data tool hits the usage limit, its response carries the same
|
|
627
|
+
payload with \`view: "paywall"\`, letting the host open the UI directly on the
|
|
628
|
+
paywall screen \u2014 no second tool call required.
|
|
629
|
+
|
|
630
|
+
Auth is handled by the SolvaPay OAuth bridge (see \`createMcpOAuthBridge\`). The
|
|
631
|
+
bridge injects \`customer_ref\` onto every authenticated request; tools that
|
|
632
|
+
need an authenticated caller return \`Unauthorized\` when it is missing.
|
|
633
|
+
|
|
634
|
+
## Also see
|
|
635
|
+
|
|
636
|
+
- \`docs://solvapay/overview.md\` (this resource) \u2014 agent-facing narration.
|
|
637
|
+
- \`ui://<app>/mcp-app.html\` \u2014 the embedded UI shell.
|
|
638
|
+
- \`tools/list\` + \`prompts/list\` \u2014 programmatic discovery of the intent tools
|
|
639
|
+
and their slash-command shortcuts.
|
|
640
|
+
- Documentation: https://docs.solvapay.com/sdks/typescript/guides/mcp-app.
|
|
641
|
+
`;
|
|
642
|
+
|
|
643
|
+
// src/descriptors.ts
|
|
644
|
+
function deriveIcons(branding) {
|
|
645
|
+
if (!branding) return void 0;
|
|
646
|
+
const assets = [];
|
|
647
|
+
if (branding.iconUrl) {
|
|
648
|
+
assets.push({ src: branding.iconUrl, sizes: ["any", "512x512"] });
|
|
649
|
+
} else if (branding.logoUrl) {
|
|
650
|
+
assets.push({ src: branding.logoUrl });
|
|
651
|
+
}
|
|
652
|
+
return assets.length > 0 ? assets : void 0;
|
|
653
|
+
}
|
|
654
|
+
var solvapayTool = (hints) => ({ openWorldHint: true, ...hints });
|
|
655
|
+
var INTENT_TOOL_ANNOTATIONS = {
|
|
656
|
+
account: solvapayTool({ readOnlyHint: true, idempotentHint: true }),
|
|
657
|
+
topup: solvapayTool({ destructiveHint: true }),
|
|
658
|
+
checkout: solvapayTool({ destructiveHint: true })
|
|
659
|
+
};
|
|
660
|
+
var DEFAULT_VIEWS = [...SOLVAPAY_MCP_VIEW_KINDS];
|
|
661
|
+
function buildSolvaPayDescriptors(options) {
|
|
662
|
+
const {
|
|
663
|
+
solvaPay,
|
|
664
|
+
productRef,
|
|
665
|
+
resourceUri,
|
|
666
|
+
htmlPath,
|
|
667
|
+
readHtml,
|
|
668
|
+
publicBaseUrl,
|
|
669
|
+
views = DEFAULT_VIEWS,
|
|
670
|
+
csp,
|
|
671
|
+
getCustomerRef = defaultGetCustomerRef,
|
|
672
|
+
onToolCall,
|
|
673
|
+
onToolResult,
|
|
674
|
+
branding
|
|
675
|
+
} = options;
|
|
676
|
+
const toolIcons = deriveIcons(branding);
|
|
677
|
+
if (!/^https?:\/\//i.test(publicBaseUrl)) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
"buildSolvaPayDescriptors: publicBaseUrl must be an http(s) URL (Stripe confirmPayment rejects `ui://`)."
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
if (!htmlPath && !readHtml) {
|
|
683
|
+
throw new Error(
|
|
684
|
+
"buildSolvaPayDescriptors: either `htmlPath` (node) or `readHtml` (edge) must be provided."
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
const toolMeta = { ui: { resourceUri } };
|
|
688
|
+
const uiToolMeta = { ...toolMeta, audience: "ui" };
|
|
689
|
+
const enabledViews = new Set(views);
|
|
690
|
+
const tools = [];
|
|
691
|
+
const pushTool = (descriptor) => {
|
|
692
|
+
tools.push(toolIcons ? { ...descriptor, icons: toolIcons } : descriptor);
|
|
693
|
+
};
|
|
694
|
+
const UI_ONLY_PREFIX = "UI-only; agents should prefer `upgrade` / `manage_account` / `activate_plan`. ";
|
|
695
|
+
const buildRequest = (extra, init = {}) => buildSolvaPayRequest(extra, { ...init, getCustomerRef });
|
|
696
|
+
const requireCustomerRef = (extra) => {
|
|
697
|
+
const ref = getCustomerRef(extra);
|
|
698
|
+
if (!ref) {
|
|
699
|
+
return toolErrorResult({
|
|
700
|
+
error: "Unauthorized",
|
|
701
|
+
status: 401,
|
|
702
|
+
details: "customer_ref missing from MCP auth context"
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
return ref;
|
|
706
|
+
};
|
|
707
|
+
const trace = async (name, args, extra, handler) => {
|
|
708
|
+
const started = Date.now();
|
|
709
|
+
onToolCall?.(name, args, extra);
|
|
710
|
+
try {
|
|
711
|
+
const result = await handler();
|
|
712
|
+
if (onToolResult) onToolResult(name, result, { durationMs: Date.now() - started });
|
|
713
|
+
return result;
|
|
714
|
+
} catch (err) {
|
|
715
|
+
const errorResult = toolErrorResult({
|
|
716
|
+
error: err instanceof Error ? err.message : String(err),
|
|
717
|
+
status: 500,
|
|
718
|
+
details: previewJson(err)
|
|
719
|
+
});
|
|
720
|
+
if (onToolResult) onToolResult(name, errorResult, { durationMs: Date.now() - started });
|
|
721
|
+
return errorResult;
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
const buildBootstrapPayload = createBuildBootstrapPayload({
|
|
725
|
+
solvaPay,
|
|
726
|
+
productRef,
|
|
727
|
+
publicBaseUrl,
|
|
728
|
+
getCustomerRef
|
|
729
|
+
});
|
|
730
|
+
const pushIntentTool = (view, title, description) => {
|
|
731
|
+
if (!enabledViews.has(view)) return;
|
|
732
|
+
const name = TOOL_FOR_VIEW[view];
|
|
733
|
+
pushTool({
|
|
734
|
+
name,
|
|
735
|
+
title,
|
|
736
|
+
description,
|
|
737
|
+
// Every intent tool accepts an optional `mode` so users /
|
|
738
|
+
// agents on any host can opt into text-only responses (or
|
|
739
|
+
// suppress the narrated markdown when they know the host is
|
|
740
|
+
// rendering the UI iframe). Default `'auto'` emits both.
|
|
741
|
+
inputSchema: { mode: import_zod.z.enum(["ui", "text", "auto"]).optional() },
|
|
742
|
+
meta: toolMeta,
|
|
743
|
+
annotations: INTENT_TOOL_ANNOTATIONS[view],
|
|
744
|
+
handler: async (args, extra) => trace(name, args, extra, async () => {
|
|
745
|
+
const mode = parseMode(args.mode);
|
|
746
|
+
const data = await buildBootstrapPayload(view, extra);
|
|
747
|
+
return narratedToolResult(name, data, mode, toolMeta);
|
|
748
|
+
})
|
|
749
|
+
});
|
|
750
|
+
};
|
|
751
|
+
const MODE_HINT = " By default renders the UI iframe with a one-line placeholder; pass `mode: 'text'` for a markdown-only summary on CLI / text-only hosts, or `mode: 'auto'` to include both.";
|
|
752
|
+
pushIntentTool(
|
|
753
|
+
"checkout",
|
|
754
|
+
"Upgrade plan",
|
|
755
|
+
"Start or change a paid plan for the current customer. On UI hosts this opens the embedded checkout; on text hosts returns a markdown summary with a checkout URL. Also available: manage_account (current plan + cancel/reactivate), activate_plan (pick or activate a specific plan), topup (add credits)." + MODE_HINT
|
|
756
|
+
);
|
|
757
|
+
pushIntentTool(
|
|
758
|
+
"account",
|
|
759
|
+
"Manage account",
|
|
760
|
+
"Show or manage the current customer's SolvaPay account: plan, balance, usage, payment method, cancel/reactivate auto-renewal. On UI hosts this opens the embedded account view; on text hosts returns a markdown summary. Also available: upgrade (start/change a paid plan), activate_plan (pick or activate), topup (add credits)." + MODE_HINT
|
|
761
|
+
);
|
|
762
|
+
pushIntentTool(
|
|
763
|
+
"topup",
|
|
764
|
+
"Top up credits",
|
|
765
|
+
"Add SolvaPay credits for the current customer. On UI hosts this opens the embedded top-up flow; on text hosts returns a markdown summary with a top-up URL. Also available: manage_account (current plan + balance + usage), upgrade (switch to a recurring plan)." + MODE_HINT
|
|
766
|
+
);
|
|
767
|
+
pushTool({
|
|
768
|
+
name: MCP_TOOL_NAMES.createCheckoutSession,
|
|
769
|
+
description: UI_ONLY_PREFIX + "Create a SolvaPay hosted checkout session and return its URL. The UI opens this URL in a new tab when Stripe Elements is blocked by the host sandbox.",
|
|
770
|
+
inputSchema: {
|
|
771
|
+
planRef: import_zod.z.string().optional(),
|
|
772
|
+
productRef: import_zod.z.string().optional()
|
|
773
|
+
},
|
|
774
|
+
meta: uiToolMeta,
|
|
775
|
+
annotations: solvapayTool({}),
|
|
776
|
+
handler: async (args, extra) => trace(MCP_TOOL_NAMES.createCheckoutSession, args, extra, async () => {
|
|
777
|
+
const auth = requireCustomerRef(extra);
|
|
778
|
+
if (typeof auth !== "string") return auth;
|
|
779
|
+
const effectiveProduct = typeof args.productRef === "string" && args.productRef ? args.productRef : productRef;
|
|
780
|
+
const planRef = typeof args.planRef === "string" && args.planRef ? args.planRef : void 0;
|
|
781
|
+
const result = await (0, import_server3.createCheckoutSessionCore)(
|
|
782
|
+
buildRequest(extra, { method: "POST" }),
|
|
783
|
+
{ productRef: effectiveProduct, planRef },
|
|
784
|
+
{ solvaPay }
|
|
785
|
+
);
|
|
786
|
+
if ((0, import_server3.isErrorResult)(result)) return toolErrorResult(result);
|
|
787
|
+
return toolResult(result);
|
|
788
|
+
})
|
|
789
|
+
});
|
|
790
|
+
pushTool({
|
|
791
|
+
name: MCP_TOOL_NAMES.createPayment,
|
|
792
|
+
description: UI_ONLY_PREFIX + "Create a Stripe payment intent for the authenticated customer to purchase a plan. Returns { clientSecret, publishableKey, accountId?, customerRef } for confirmation with Stripe Elements in the app UI.",
|
|
793
|
+
inputSchema: {
|
|
794
|
+
planRef: import_zod.z.string(),
|
|
795
|
+
productRef: import_zod.z.string()
|
|
796
|
+
},
|
|
797
|
+
meta: uiToolMeta,
|
|
798
|
+
annotations: solvapayTool({}),
|
|
799
|
+
handler: async (args, extra) => trace(MCP_TOOL_NAMES.createPayment, args, extra, async () => {
|
|
800
|
+
const auth = requireCustomerRef(extra);
|
|
801
|
+
if (typeof auth !== "string") return auth;
|
|
802
|
+
const planRef = typeof args.planRef === "string" ? args.planRef : "";
|
|
803
|
+
const effectiveProduct = typeof args.productRef === "string" && args.productRef ? args.productRef : productRef;
|
|
804
|
+
const result = await (0, import_server3.createPaymentIntentCore)(
|
|
805
|
+
buildRequest(extra, { method: "POST" }),
|
|
806
|
+
{ planRef, productRef: effectiveProduct },
|
|
807
|
+
{ solvaPay }
|
|
808
|
+
);
|
|
809
|
+
if ((0, import_server3.isErrorResult)(result)) return toolErrorResult(result);
|
|
810
|
+
return toolResult(result);
|
|
811
|
+
})
|
|
812
|
+
});
|
|
813
|
+
pushTool({
|
|
814
|
+
name: MCP_TOOL_NAMES.processPayment,
|
|
815
|
+
description: UI_ONLY_PREFIX + "Process a Stripe payment intent after client-side confirmation and create the SolvaPay purchase. Call after confirmPayment resolves to short-circuit webhook latency.",
|
|
816
|
+
inputSchema: {
|
|
817
|
+
paymentIntentId: import_zod.z.string(),
|
|
818
|
+
productRef: import_zod.z.string(),
|
|
819
|
+
planRef: import_zod.z.string().optional()
|
|
820
|
+
},
|
|
821
|
+
meta: uiToolMeta,
|
|
822
|
+
annotations: solvapayTool({ destructiveHint: true }),
|
|
823
|
+
handler: async (args, extra) => trace(MCP_TOOL_NAMES.processPayment, args, extra, async () => {
|
|
824
|
+
const auth = requireCustomerRef(extra);
|
|
825
|
+
if (typeof auth !== "string") return auth;
|
|
826
|
+
const paymentIntentId = typeof args.paymentIntentId === "string" ? args.paymentIntentId : "";
|
|
827
|
+
const effectiveProduct = typeof args.productRef === "string" && args.productRef ? args.productRef : productRef;
|
|
828
|
+
const planRef = typeof args.planRef === "string" && args.planRef ? args.planRef : void 0;
|
|
829
|
+
const result = await (0, import_server3.processPaymentIntentCore)(
|
|
830
|
+
buildRequest(extra, { method: "POST" }),
|
|
831
|
+
{ paymentIntentId, productRef: effectiveProduct, planRef },
|
|
832
|
+
{ solvaPay }
|
|
833
|
+
);
|
|
834
|
+
if ((0, import_server3.isErrorResult)(result)) return toolErrorResult(result);
|
|
835
|
+
return toolResult(result);
|
|
836
|
+
})
|
|
837
|
+
});
|
|
838
|
+
pushTool({
|
|
839
|
+
name: MCP_TOOL_NAMES.createCustomerSession,
|
|
840
|
+
description: UI_ONLY_PREFIX + "Create a SolvaPay hosted customer portal session and return its URL. Used to let a paid customer manage or cancel their purchase in a new tab.",
|
|
841
|
+
inputSchema: {},
|
|
842
|
+
meta: uiToolMeta,
|
|
843
|
+
annotations: solvapayTool({ readOnlyHint: true, idempotentHint: true }),
|
|
844
|
+
handler: async (args, extra) => trace(MCP_TOOL_NAMES.createCustomerSession, args, extra, async () => {
|
|
845
|
+
const auth = requireCustomerRef(extra);
|
|
846
|
+
if (typeof auth !== "string") return auth;
|
|
847
|
+
const result = await (0, import_server3.createCustomerSessionCore)(buildRequest(extra, { method: "POST" }), {
|
|
848
|
+
solvaPay
|
|
849
|
+
});
|
|
850
|
+
if ((0, import_server3.isErrorResult)(result)) return toolErrorResult(result);
|
|
851
|
+
return toolResult(result);
|
|
852
|
+
})
|
|
853
|
+
});
|
|
854
|
+
pushTool({
|
|
855
|
+
name: MCP_TOOL_NAMES.createTopupPayment,
|
|
856
|
+
description: UI_ONLY_PREFIX + "Create a Stripe payment intent for a credit top-up. Credits are recorded by the SolvaPay webhook after confirmation.",
|
|
857
|
+
inputSchema: {
|
|
858
|
+
amount: import_zod.z.number().int().positive(),
|
|
859
|
+
currency: import_zod.z.string(),
|
|
860
|
+
description: import_zod.z.string().optional()
|
|
861
|
+
},
|
|
862
|
+
meta: uiToolMeta,
|
|
863
|
+
annotations: solvapayTool({}),
|
|
864
|
+
handler: async (args, extra) => trace(MCP_TOOL_NAMES.createTopupPayment, args, extra, async () => {
|
|
865
|
+
const auth = requireCustomerRef(extra);
|
|
866
|
+
if (typeof auth !== "string") return auth;
|
|
867
|
+
const amount = typeof args.amount === "number" ? args.amount : 0;
|
|
868
|
+
const currency = typeof args.currency === "string" ? args.currency : "";
|
|
869
|
+
const description = typeof args.description === "string" ? args.description : void 0;
|
|
870
|
+
const result = await (0, import_server3.createTopupPaymentIntentCore)(
|
|
871
|
+
buildRequest(extra, { method: "POST" }),
|
|
872
|
+
{ amount, currency, description },
|
|
873
|
+
{ solvaPay }
|
|
874
|
+
);
|
|
875
|
+
if ((0, import_server3.isErrorResult)(result)) return toolErrorResult(result);
|
|
876
|
+
return toolResult(result);
|
|
877
|
+
})
|
|
878
|
+
});
|
|
879
|
+
pushTool({
|
|
880
|
+
name: MCP_TOOL_NAMES.cancelRenewal,
|
|
881
|
+
description: UI_ONLY_PREFIX + "Cancel the auto-renewal on an active purchase. Backend keeps access until the current period ends.",
|
|
882
|
+
inputSchema: {
|
|
883
|
+
purchaseRef: import_zod.z.string(),
|
|
884
|
+
reason: import_zod.z.string().optional()
|
|
885
|
+
},
|
|
886
|
+
meta: uiToolMeta,
|
|
887
|
+
annotations: solvapayTool({ destructiveHint: true, idempotentHint: true }),
|
|
888
|
+
handler: async (args, extra) => trace(MCP_TOOL_NAMES.cancelRenewal, args, extra, async () => {
|
|
889
|
+
const auth = requireCustomerRef(extra);
|
|
890
|
+
if (typeof auth !== "string") return auth;
|
|
891
|
+
const purchaseRef = typeof args.purchaseRef === "string" ? args.purchaseRef : "";
|
|
892
|
+
const reason = typeof args.reason === "string" ? args.reason : void 0;
|
|
893
|
+
const result = await (0, import_server3.cancelPurchaseCore)(
|
|
894
|
+
buildRequest(extra, { method: "POST" }),
|
|
895
|
+
{ purchaseRef, reason },
|
|
896
|
+
{ solvaPay }
|
|
897
|
+
);
|
|
898
|
+
if ((0, import_server3.isErrorResult)(result)) return toolErrorResult(result);
|
|
899
|
+
return toolResult(result);
|
|
900
|
+
})
|
|
901
|
+
});
|
|
902
|
+
pushTool({
|
|
903
|
+
name: MCP_TOOL_NAMES.reactivateRenewal,
|
|
904
|
+
description: UI_ONLY_PREFIX + "Undo a pending cancellation so auto-renewal resumes. Only valid while the purchase is still active and its end date hasn't passed.",
|
|
905
|
+
inputSchema: { purchaseRef: import_zod.z.string() },
|
|
906
|
+
meta: uiToolMeta,
|
|
907
|
+
annotations: solvapayTool({ idempotentHint: true }),
|
|
908
|
+
handler: async (args, extra) => trace(MCP_TOOL_NAMES.reactivateRenewal, args, extra, async () => {
|
|
909
|
+
const auth = requireCustomerRef(extra);
|
|
910
|
+
if (typeof auth !== "string") return auth;
|
|
911
|
+
const purchaseRef = typeof args.purchaseRef === "string" ? args.purchaseRef : "";
|
|
912
|
+
const result = await (0, import_server3.reactivatePurchaseCore)(
|
|
913
|
+
buildRequest(extra, { method: "POST" }),
|
|
914
|
+
{ purchaseRef },
|
|
915
|
+
{ solvaPay }
|
|
916
|
+
);
|
|
917
|
+
if ((0, import_server3.isErrorResult)(result)) return toolErrorResult(result);
|
|
918
|
+
return toolResult(result);
|
|
919
|
+
})
|
|
920
|
+
});
|
|
921
|
+
pushTool({
|
|
922
|
+
name: MCP_TOOL_NAMES.activatePlan,
|
|
923
|
+
title: "Activate plan",
|
|
924
|
+
description: "Activate a plan for the current customer. With a `planRef`: free plans activate immediately; usage-based plans activate when the balance covers the configured usage; paid plans return a markdown checkout link on text hosts or open the embedded checkout on UI hosts. Without a `planRef`: returns the available plans so the customer can pick \u2014 UI hosts render the embedded checkout picker, text hosts see a plans list. Also available: upgrade (direct to checkout), manage_account (current plan + usage), topup (add credits)." + MODE_HINT,
|
|
925
|
+
inputSchema: {
|
|
926
|
+
productRef: import_zod.z.string().optional(),
|
|
927
|
+
planRef: import_zod.z.string().optional(),
|
|
928
|
+
mode: import_zod.z.enum(["ui", "text", "auto"]).optional()
|
|
929
|
+
},
|
|
930
|
+
meta: toolMeta,
|
|
931
|
+
annotations: solvapayTool({}),
|
|
932
|
+
handler: async (args, extra) => trace(MCP_TOOL_NAMES.activatePlan, args, extra, async () => {
|
|
933
|
+
const effectiveProduct = typeof args.productRef === "string" && args.productRef ? args.productRef : productRef;
|
|
934
|
+
const planRef = typeof args.planRef === "string" && args.planRef ? args.planRef : void 0;
|
|
935
|
+
const mode = parseMode(args.mode);
|
|
936
|
+
if (!planRef) {
|
|
937
|
+
if (!enabledViews.has("checkout")) {
|
|
938
|
+
return toolErrorResult({
|
|
939
|
+
error: "activate_plan requires a planRef on this server",
|
|
940
|
+
status: 400,
|
|
941
|
+
details: 'The checkout view (where the plan picker lives) is not enabled on this server. Pass `planRef` to activate a specific plan, or re-enable the "checkout" view via the `views` option.'
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
return narratedToolResult(
|
|
945
|
+
MCP_TOOL_NAMES.activatePlan,
|
|
946
|
+
await buildBootstrapPayload("checkout", extra),
|
|
947
|
+
mode,
|
|
948
|
+
toolMeta
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
const auth = requireCustomerRef(extra);
|
|
952
|
+
if (typeof auth !== "string") return auth;
|
|
953
|
+
const result = await (0, import_server3.activatePlanCore)(
|
|
954
|
+
buildRequest(extra, { method: "POST" }),
|
|
955
|
+
{ productRef: effectiveProduct, planRef },
|
|
956
|
+
{ solvaPay }
|
|
957
|
+
);
|
|
958
|
+
if ((0, import_server3.isErrorResult)(result)) return toolErrorResult(result);
|
|
959
|
+
return toolResult(result);
|
|
960
|
+
})
|
|
961
|
+
});
|
|
962
|
+
const resolvedCsp = mergeCsp(csp);
|
|
963
|
+
const resource = {
|
|
964
|
+
uri: resourceUri,
|
|
965
|
+
mimeType: "text/html;profile=mcp-app",
|
|
966
|
+
csp: resolvedCsp,
|
|
967
|
+
readHtml: readHtml ? readHtml : async () => {
|
|
968
|
+
const fs = await import("fs/promises");
|
|
969
|
+
return fs.readFile(htmlPath, "utf-8");
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
const prompts = buildSolvaPayPrompts({ enabledViews });
|
|
973
|
+
const docsResources = [
|
|
974
|
+
{
|
|
975
|
+
uri: SOLVAPAY_OVERVIEW_URI,
|
|
976
|
+
name: "SolvaPay MCP \u2014 overview",
|
|
977
|
+
title: "SolvaPay overview",
|
|
978
|
+
description: 'Agent-facing "start here" doc \u2014 explains the five intent tools, dual-audience fallback, and auth model before any tool is called.',
|
|
979
|
+
mimeType: SOLVAPAY_OVERVIEW_MIME_TYPE,
|
|
980
|
+
readBody: () => SOLVAPAY_OVERVIEW_MARKDOWN
|
|
981
|
+
}
|
|
982
|
+
];
|
|
983
|
+
return { tools, resource, prompts, docsResources, buildBootstrapPayload };
|
|
984
|
+
}
|
|
985
|
+
function buildSolvaPayPrompts(options = {}) {
|
|
986
|
+
const enabled = options.enabledViews ?? new Set(DEFAULT_VIEWS);
|
|
987
|
+
const prompts = [];
|
|
988
|
+
const userMessage = (text) => ({
|
|
989
|
+
messages: [{ role: "user", content: { type: "text", text } }]
|
|
990
|
+
});
|
|
991
|
+
if (enabled.has("checkout")) {
|
|
992
|
+
prompts.push({
|
|
993
|
+
name: MCP_TOOL_NAMES.upgrade,
|
|
994
|
+
title: "Upgrade plan",
|
|
995
|
+
description: "Start or change a paid plan for the current customer.",
|
|
996
|
+
argsSchema: { planRef: import_zod.z.string().optional() },
|
|
997
|
+
handler: async ({ planRef }) => userMessage(
|
|
998
|
+
typeof planRef === "string" && planRef ? `Activate plan ${planRef} for me.` : "Show me the upgrade options for my SolvaPay account."
|
|
999
|
+
)
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
if (enabled.has("account")) {
|
|
1003
|
+
prompts.push({
|
|
1004
|
+
name: MCP_TOOL_NAMES.manageAccount,
|
|
1005
|
+
title: "Manage account",
|
|
1006
|
+
description: "Show the current plan, balance, payment method, and cancel/reactivate controls for the current customer.",
|
|
1007
|
+
handler: async () => userMessage("Show me my SolvaPay account.")
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
if (enabled.has("topup")) {
|
|
1011
|
+
prompts.push({
|
|
1012
|
+
name: MCP_TOOL_NAMES.topup,
|
|
1013
|
+
title: "Top up credits",
|
|
1014
|
+
description: "Add SolvaPay credits to the current customer.",
|
|
1015
|
+
argsSchema: { amount: import_zod.z.string().optional() },
|
|
1016
|
+
handler: async ({ amount }) => userMessage(
|
|
1017
|
+
typeof amount === "string" && amount ? `Top up my SolvaPay credits by ${amount}.` : "I want to top up my SolvaPay credits."
|
|
1018
|
+
)
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
if (enabled.has("checkout")) {
|
|
1022
|
+
prompts.push({
|
|
1023
|
+
name: MCP_TOOL_NAMES.activatePlan,
|
|
1024
|
+
title: "Activate plan",
|
|
1025
|
+
description: "Pick a plan to activate, or activate a specific plan by ref.",
|
|
1026
|
+
argsSchema: { planRef: import_zod.z.string().optional() },
|
|
1027
|
+
handler: async ({ planRef }) => userMessage(
|
|
1028
|
+
typeof planRef === "string" && planRef ? `Activate plan ${planRef} on my SolvaPay account.` : "What plans can I activate on my SolvaPay account?"
|
|
1029
|
+
)
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
return prompts;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/payable-handler.ts
|
|
1036
|
+
var import_server5 = require("@solvapay/server");
|
|
1037
|
+
|
|
1038
|
+
// src/response-context.ts
|
|
1039
|
+
var import_server4 = require("@solvapay/server");
|
|
1040
|
+
|
|
1041
|
+
// src/response-envelope.ts
|
|
1042
|
+
function isResponseResult(value) {
|
|
1043
|
+
return typeof value === "object" && value !== null && value.__solvapayResponse === true;
|
|
1044
|
+
}
|
|
1045
|
+
function assertResponseResult(value) {
|
|
1046
|
+
if (!isResponseResult(value)) {
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
"SolvaPay: registerPayable handler returned a raw value. Handlers must return ctx.respond(data, options?). If you believe you did, this is an internal bug \u2014 please file an issue at https://github.com/solvapay/solvapay-sdk/issues."
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
return value;
|
|
1052
|
+
}
|
|
1053
|
+
function makeResponseResult(data, options, emittedBlocks) {
|
|
1054
|
+
return {
|
|
1055
|
+
__solvapayResponse: true,
|
|
1056
|
+
data,
|
|
1057
|
+
...options !== void 0 ? { options } : {},
|
|
1058
|
+
...emittedBlocks.length > 0 ? { emittedBlocks: [...emittedBlocks] } : {}
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/response-context.ts
|
|
1063
|
+
function synthesizePlanStub(limits) {
|
|
1064
|
+
if (!limits?.plan) return null;
|
|
1065
|
+
return { reference: limits.plan };
|
|
1066
|
+
}
|
|
1067
|
+
function snapshotFromLimits(params) {
|
|
1068
|
+
const { customerRef, limits, plan, refresh } = params;
|
|
1069
|
+
return {
|
|
1070
|
+
ref: customerRef,
|
|
1071
|
+
balance: limits?.creditBalance ?? 0,
|
|
1072
|
+
remaining: limits?.remaining ?? null,
|
|
1073
|
+
withinLimits: limits?.withinLimits ?? true,
|
|
1074
|
+
plan,
|
|
1075
|
+
fresh: refresh
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function buildResponseContext(params) {
|
|
1079
|
+
const { customerRef, limits, product, bootstrapProduct, solvaPay } = params;
|
|
1080
|
+
const bootstrapPlan = params.bootstrapPlan ?? synthesizePlanStub(limits);
|
|
1081
|
+
const emittedBlocks = [];
|
|
1082
|
+
const abortController = new AbortController();
|
|
1083
|
+
const freshSnapshot = async () => {
|
|
1084
|
+
const freshLimits = await solvaPay.checkLimits({
|
|
1085
|
+
customerRef,
|
|
1086
|
+
productRef: product,
|
|
1087
|
+
meterName: "requests"
|
|
1088
|
+
});
|
|
1089
|
+
return snapshotFromLimits({
|
|
1090
|
+
customerRef,
|
|
1091
|
+
limits: freshLimits,
|
|
1092
|
+
plan: bootstrapPlan,
|
|
1093
|
+
refresh: freshSnapshot
|
|
1094
|
+
});
|
|
1095
|
+
};
|
|
1096
|
+
const customer = snapshotFromLimits({
|
|
1097
|
+
customerRef,
|
|
1098
|
+
limits,
|
|
1099
|
+
plan: bootstrapPlan,
|
|
1100
|
+
refresh: freshSnapshot
|
|
1101
|
+
});
|
|
1102
|
+
const product_ = bootstrapProduct ?? {
|
|
1103
|
+
reference: product,
|
|
1104
|
+
name: product
|
|
1105
|
+
};
|
|
1106
|
+
function respond(data, options) {
|
|
1107
|
+
return makeResponseResult(data, options, emittedBlocks);
|
|
1108
|
+
}
|
|
1109
|
+
function gate(reason) {
|
|
1110
|
+
const message = reason ?? "Payment required";
|
|
1111
|
+
const structuredContent = {
|
|
1112
|
+
kind: "payment_required",
|
|
1113
|
+
product,
|
|
1114
|
+
checkoutUrl: "",
|
|
1115
|
+
message
|
|
1116
|
+
};
|
|
1117
|
+
throw new import_server4.PaywallError(message, structuredContent);
|
|
1118
|
+
}
|
|
1119
|
+
const ctx = {
|
|
1120
|
+
customer,
|
|
1121
|
+
product: product_,
|
|
1122
|
+
respond,
|
|
1123
|
+
gate,
|
|
1124
|
+
// ——————————————————————————————————————————————————————————————
|
|
1125
|
+
// Reserved surface — V1 stubs, V1.1 real.
|
|
1126
|
+
// ——————————————————————————————————————————————————————————————
|
|
1127
|
+
emit: async (block) => {
|
|
1128
|
+
emittedBlocks.push(block);
|
|
1129
|
+
},
|
|
1130
|
+
progress: async () => {
|
|
1131
|
+
},
|
|
1132
|
+
progressRaw: async () => {
|
|
1133
|
+
},
|
|
1134
|
+
signal: abortController.signal
|
|
1135
|
+
};
|
|
1136
|
+
return { ctx, emittedBlocks };
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/payable-handler.ts
|
|
1140
|
+
function buildPayableHandler(solvaPay, ctx, handler) {
|
|
1141
|
+
const { product, resourceUri, buildBootstrap, getCustomerRef } = ctx;
|
|
1142
|
+
const wrappedBusinessLogic = async (args, handlerContext) => {
|
|
1143
|
+
const limits = handlerContext?.limits ?? null;
|
|
1144
|
+
const customerRef = handlerContext?.customerRef ?? "";
|
|
1145
|
+
const { ctx: responseCtx } = buildResponseContext({
|
|
1146
|
+
customerRef,
|
|
1147
|
+
limits,
|
|
1148
|
+
product,
|
|
1149
|
+
solvaPay
|
|
1150
|
+
});
|
|
1151
|
+
return handler(args, responseCtx);
|
|
1152
|
+
};
|
|
1153
|
+
const protectedHandler = solvaPay.payable({ product, getCustomerRef }).mcp(wrappedBusinessLogic);
|
|
1154
|
+
return async (args, extra) => {
|
|
1155
|
+
const result = await protectedHandler(args, extra);
|
|
1156
|
+
if ((0, import_server5.isPaywallStructuredContent)(result.structuredContent)) {
|
|
1157
|
+
const existingMeta = typeof result._meta === "object" && result._meta !== null ? result._meta : {};
|
|
1158
|
+
const gateContent = result.structuredContent;
|
|
1159
|
+
const structuredContent = buildBootstrap ? await buildBootstrap("paywall", extra, {
|
|
1160
|
+
paywall: gateContent
|
|
1161
|
+
}) : result.structuredContent;
|
|
1162
|
+
return {
|
|
1163
|
+
...result,
|
|
1164
|
+
isError: false,
|
|
1165
|
+
structuredContent,
|
|
1166
|
+
_meta: {
|
|
1167
|
+
...existingMeta,
|
|
1168
|
+
...buildPaywallUiMeta({ resourceUri })
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
if (result.isError) {
|
|
1173
|
+
return result;
|
|
1174
|
+
}
|
|
1175
|
+
const envelope = assertResponseResult(result.structuredContent);
|
|
1176
|
+
return unwrapResponseEnvelope(
|
|
1177
|
+
result,
|
|
1178
|
+
envelope,
|
|
1179
|
+
resourceUri,
|
|
1180
|
+
buildBootstrap,
|
|
1181
|
+
extra
|
|
1182
|
+
);
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
async function unwrapResponseEnvelope(adapterResult, envelope, resourceUri, buildBootstrap, extra) {
|
|
1186
|
+
const { data, options, emittedBlocks } = envelope;
|
|
1187
|
+
const textOverride = options?.text;
|
|
1188
|
+
const nudge = options?.nudge;
|
|
1189
|
+
const primaryText = typeof textOverride === "string" ? textOverride : JSON.stringify(data);
|
|
1190
|
+
const content = [
|
|
1191
|
+
...emittedBlocks ?? [],
|
|
1192
|
+
{ type: "text", text: primaryText }
|
|
1193
|
+
];
|
|
1194
|
+
const existingMeta = typeof adapterResult._meta === "object" && adapterResult._meta !== null ? adapterResult._meta : {};
|
|
1195
|
+
if (nudge) {
|
|
1196
|
+
const structuredContent = buildBootstrap ? {
|
|
1197
|
+
...await buildBootstrap("nudge", extra),
|
|
1198
|
+
nudge,
|
|
1199
|
+
data
|
|
1200
|
+
} : data;
|
|
1201
|
+
return {
|
|
1202
|
+
...adapterResult,
|
|
1203
|
+
content,
|
|
1204
|
+
structuredContent,
|
|
1205
|
+
_meta: {
|
|
1206
|
+
...existingMeta,
|
|
1207
|
+
...buildNudgeUiMeta({ resourceUri, nudge })
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
return {
|
|
1212
|
+
...adapterResult,
|
|
1213
|
+
content,
|
|
1214
|
+
structuredContent: data,
|
|
1215
|
+
...Object.keys(existingMeta).length > 0 ? { _meta: existingMeta } : {}
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
function buildNudgeUiMeta(input) {
|
|
1219
|
+
return { ui: { resourceUri: input.resourceUri, nudge: input.nudge } };
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// src/oauth-discovery.ts
|
|
1223
|
+
var DEFAULT_OAUTH_PATHS = {
|
|
1224
|
+
register: "/oauth/register",
|
|
1225
|
+
authorize: "/oauth/authorize",
|
|
1226
|
+
token: "/oauth/token",
|
|
1227
|
+
revoke: "/oauth/revoke"
|
|
1228
|
+
};
|
|
1229
|
+
function withoutTrailingSlash(value) {
|
|
1230
|
+
return value.replace(/\/$/, "");
|
|
1231
|
+
}
|
|
1232
|
+
function resolveOAuthPaths(paths = {}) {
|
|
1233
|
+
return { ...DEFAULT_OAUTH_PATHS, ...paths };
|
|
1234
|
+
}
|
|
1235
|
+
function getOAuthProtectedResourceResponse(publicBaseUrl) {
|
|
1236
|
+
const resource = withoutTrailingSlash(publicBaseUrl);
|
|
1237
|
+
return {
|
|
1238
|
+
resource,
|
|
1239
|
+
authorization_servers: [resource],
|
|
1240
|
+
scopes_supported: ["openid", "profile", "email"]
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
function getOAuthAuthorizationServerResponse({
|
|
1244
|
+
publicBaseUrl,
|
|
1245
|
+
paths
|
|
1246
|
+
}) {
|
|
1247
|
+
const base = withoutTrailingSlash(publicBaseUrl);
|
|
1248
|
+
const p = resolveOAuthPaths(paths);
|
|
1249
|
+
return {
|
|
1250
|
+
issuer: base,
|
|
1251
|
+
authorization_endpoint: `${base}${p.authorize}`,
|
|
1252
|
+
token_endpoint: `${base}${p.token}`,
|
|
1253
|
+
registration_endpoint: `${base}${p.register}`,
|
|
1254
|
+
revocation_endpoint: `${base}${p.revoke}`,
|
|
1255
|
+
token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
1256
|
+
response_types_supported: ["code"],
|
|
1257
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
1258
|
+
scopes_supported: ["openid", "profile", "email"],
|
|
1259
|
+
code_challenge_methods_supported: ["S256"]
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// src/bearer.ts
|
|
1264
|
+
var McpBearerAuthError = class extends Error {
|
|
1265
|
+
constructor(message) {
|
|
1266
|
+
super(message);
|
|
1267
|
+
this.name = "McpBearerAuthError";
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
function base64UrlDecode(input) {
|
|
1271
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
1272
|
+
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
1273
|
+
const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
|
|
1274
|
+
return new TextDecoder().decode(bytes);
|
|
1275
|
+
}
|
|
1276
|
+
function extractBearerToken(authorization) {
|
|
1277
|
+
if (!authorization) return null;
|
|
1278
|
+
if (!authorization.startsWith("Bearer ")) return null;
|
|
1279
|
+
return authorization.slice(7).trim() || null;
|
|
1280
|
+
}
|
|
1281
|
+
function decodeJwtPayload(token) {
|
|
1282
|
+
const parts = token.split(".");
|
|
1283
|
+
if (parts.length < 2) {
|
|
1284
|
+
throw new McpBearerAuthError("Invalid JWT format");
|
|
1285
|
+
}
|
|
1286
|
+
try {
|
|
1287
|
+
const payloadText = base64UrlDecode(parts[1]);
|
|
1288
|
+
const payload = JSON.parse(payloadText);
|
|
1289
|
+
return payload;
|
|
1290
|
+
} catch {
|
|
1291
|
+
throw new McpBearerAuthError("Invalid JWT payload");
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
function getCustomerRefFromJwtPayload(payload, options = {}) {
|
|
1295
|
+
const claimPriority = options.claimPriority || ["customerRef", "customer_ref", "sub"];
|
|
1296
|
+
for (const claim of claimPriority) {
|
|
1297
|
+
const value = payload[claim];
|
|
1298
|
+
if (typeof value === "string" && value.trim()) {
|
|
1299
|
+
return value.trim();
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
throw new McpBearerAuthError(
|
|
1303
|
+
`No customer reference claim found (checked: ${claimPriority.join(", ")})`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
function getCustomerRefFromBearerAuthHeader(authorization, options = {}) {
|
|
1307
|
+
const token = extractBearerToken(authorization);
|
|
1308
|
+
if (!token) {
|
|
1309
|
+
throw new McpBearerAuthError("Missing bearer token");
|
|
1310
|
+
}
|
|
1311
|
+
const payload = decodeJwtPayload(token);
|
|
1312
|
+
return getCustomerRefFromJwtPayload(payload, options);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/auth-bridge.ts
|
|
1316
|
+
function getClientId(payload, explicitClientId) {
|
|
1317
|
+
if (explicitClientId) return explicitClientId;
|
|
1318
|
+
const payloadClientId = typeof payload.client_id === "string" && payload.client_id || typeof payload.azp === "string" && payload.azp || typeof payload.aud === "string" && payload.aud || null;
|
|
1319
|
+
return payloadClientId || "solvapay-mcp-client";
|
|
1320
|
+
}
|
|
1321
|
+
function getScopes(payload, defaultScopes) {
|
|
1322
|
+
if (Array.isArray(payload.scp)) {
|
|
1323
|
+
return payload.scp.filter((scope) => typeof scope === "string");
|
|
1324
|
+
}
|
|
1325
|
+
if (typeof payload.scope === "string" && payload.scope.trim()) {
|
|
1326
|
+
return payload.scope.split(/\s+/).map((scope) => scope.trim()).filter(Boolean);
|
|
1327
|
+
}
|
|
1328
|
+
return defaultScopes;
|
|
1329
|
+
}
|
|
1330
|
+
function getExpiresAt(payload) {
|
|
1331
|
+
return typeof payload.exp === "number" ? payload.exp : void 0;
|
|
1332
|
+
}
|
|
1333
|
+
function buildAuthInfoFromBearer(authorization, options = {}) {
|
|
1334
|
+
const token = extractBearerToken(authorization);
|
|
1335
|
+
if (!token) return null;
|
|
1336
|
+
const payload = decodeJwtPayload(token);
|
|
1337
|
+
const customerRef = getCustomerRefFromJwtPayload(payload, options);
|
|
1338
|
+
const clientId = getClientId(payload, options.clientId);
|
|
1339
|
+
const scopes = getScopes(payload, options.defaultScopes || []);
|
|
1340
|
+
const expiresAt = getExpiresAt(payload);
|
|
1341
|
+
return {
|
|
1342
|
+
token,
|
|
1343
|
+
clientId,
|
|
1344
|
+
scopes,
|
|
1345
|
+
expiresAt,
|
|
1346
|
+
extra: {
|
|
1347
|
+
customer_ref: customerRef,
|
|
1348
|
+
...options.includePayload ? { payload } : {}
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1353
|
+
0 && (module.exports = {
|
|
1354
|
+
DEFAULT_OAUTH_PATHS,
|
|
1355
|
+
MCP_TOOL_NAMES,
|
|
1356
|
+
McpBearerAuthError,
|
|
1357
|
+
NARRATORS,
|
|
1358
|
+
OPEN_TOOL_FOR_VIEW,
|
|
1359
|
+
SOLVAPAY_DEFAULT_CSP,
|
|
1360
|
+
SOLVAPAY_MCP_VIEW_KINDS,
|
|
1361
|
+
SOLVAPAY_OVERVIEW_MARKDOWN,
|
|
1362
|
+
SOLVAPAY_OVERVIEW_MIME_TYPE,
|
|
1363
|
+
SOLVAPAY_OVERVIEW_URI,
|
|
1364
|
+
TOOL_FOR_VIEW,
|
|
1365
|
+
VIEW_FOR_OPEN_TOOL,
|
|
1366
|
+
VIEW_FOR_TOOL,
|
|
1367
|
+
balanceSummary,
|
|
1368
|
+
buildAuthInfoFromBearer,
|
|
1369
|
+
buildPayableHandler,
|
|
1370
|
+
buildPaywallUiMeta,
|
|
1371
|
+
buildSolvaPayDescriptors,
|
|
1372
|
+
buildSolvaPayPrompts,
|
|
1373
|
+
buildSolvaPayRequest,
|
|
1374
|
+
createBuildBootstrapPayload,
|
|
1375
|
+
decodeJwtPayload,
|
|
1376
|
+
defaultGetCustomerRef,
|
|
1377
|
+
enrichPurchase,
|
|
1378
|
+
extractBearerToken,
|
|
1379
|
+
getCustomerRefFromBearerAuthHeader,
|
|
1380
|
+
getCustomerRefFromJwtPayload,
|
|
1381
|
+
getOAuthAuthorizationServerResponse,
|
|
1382
|
+
getOAuthProtectedResourceResponse,
|
|
1383
|
+
mergeCsp,
|
|
1384
|
+
narrateActivatePlan,
|
|
1385
|
+
narrateManageAccount,
|
|
1386
|
+
narrateTopup,
|
|
1387
|
+
narrateUpgrade,
|
|
1388
|
+
narratedToolResult,
|
|
1389
|
+
parseMode,
|
|
1390
|
+
paywallToolResult,
|
|
1391
|
+
previewJson,
|
|
1392
|
+
resolveOAuthPaths,
|
|
1393
|
+
toolErrorResult,
|
|
1394
|
+
toolResult,
|
|
1395
|
+
uiPlaceholder,
|
|
1396
|
+
withoutTrailingSlash
|
|
1397
|
+
});
|